Repository navigation
Commit a77c2e9
fix(cli, service-settings, console): the official image refuses to start without its database and secrets; administrators see what a production server keeps on its own disk (#30720)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DUcNL9fhimxNPTrujEyeSTy)_
Fixes #30120
Clause-②: no
Metadata-protocol: none
Skills-docs: owed as a follow-up, not edited here —
`skills/objectstack-platform/references/operations.md` (the
Docker/Kubernetes row and "Common ops pitfalls") does not say the
official image now refuses to start without `OS_DATABASE_URL`,
`OS_AUTH_SECRET` and `OS_SECRET_KEY`; every `docker run` example under
`content/docs/**` already sets all three, and
`content/docs/deployment/self-hosting.mdx` + `docker/README.md` are
updated here
## 维护者速读
-
改了什么:官方容器镜像在没有显式设置数据库地址、登录密钥、数据加密密钥这三项时拒绝启动,点名缺哪几项,并写明已经在用自动生成值的部署怎样先把原值从旧容器里取出来;其他方式的
`os start` 照常启动,只是在生产模式下把「放在本机磁盘上的东西」打印在启动日志里;管理员在控制台首页和「设置 ›
系统概览」顶部看到一条常驻提示(可一键复制要设的变量,可隐藏,隐藏写进审计日志),普通成员看不到。
- 为什么改:#30120(裁决 A3)。镜像文档说这三项必须注入,但代码从不检查:容器里的业务数据是一个 SQLite
文件,登录密钥和加密密钥也在容器磁盘上自动生成,换一次容器就全部丢失,而管理员平时只看控制台、不看日志。
-
风险与回滚:已经在用镜像且没设这三项的部署,升级后会启动失败——拒绝信息逐项写明原值在旧容器的哪个文件里(真实容器里验证过取出原值后能照常启动、不会重新生成密钥),也可设
`OS_DEPLOYMENT_DURABILITY_CHECK_ENABLED=false`
暂时回到旧行为;提示内容只经设置服务的管理员入口给出,公共配置里没有。另外,控制台首屏英文包本 head 实测 60,540 / 60,000
gz(提示文案 +633 字节),要等 #30640 把应用市场文案移出首屏后再合并 main 才会转绿,上限不抬。回滚即还原本 PR。
- 先看:`packages/cli/src/commands/start.ts` 的拒启分支(`durabilityCheck ===
'on'`)和写给服务进程的
`OS_INTERNAL_DEPLOYMENT_FINDINGS`;`packages/services/service-settings/src/deployment-notice.ts`
的 `hide`(指纹比对后才写审计行);`settings-routes.ts` 两条新路由的能力校验
`refuseNoticeCaller`。
- 两分钟验证:先 `pnpm --filter @objectstack/cli build`,再在空目录里运行
`OS_DEPLOYMENT_DURABILITY_CHECK_ENABLED=true HOME=$(mktemp -d) node
<repo>/packages/cli/bin/run.js start`,预期退出码 1,提示「OS_DATABASE_URL,
OS_AUTH_SECRET and OS_SECRET_KEY are not set」并列出从旧容器取值的三个文件。
- 需要你做的:#30714(受管的工作流一行改动)需要 os-zhuang 或 hotlong 批准,并先于本 PR 合入。
## What changes
**The official image refuses a start it would lose (`packages/cli`,
`docker/Dockerfile`).**
- `docker/Dockerfile` sets `OS_DEPLOYMENT_DURABILITY_CHECK_ENABLED=true`
in its `ENV`. `NODE_ENV` cannot be the signal: `os start` sets
`production` itself on every boot, including the zero-config laptop
trial (measured, see Verification).
- With the switch on, `os start` exits 1 before it opens a database or
writes a secret, unless `OS_DATABASE_URL`, `OS_AUTH_SECRET` and
`OS_SECRET_KEY` are all set explicitly. It names exactly the missing
ones and says how to set each. An explicit
`OS_DATABASE_URL=file:/path/on/a/volume/app.db` passes, so SQLite on a
kept volume stays a choice.
- The refusal also says how a deployment that already ran on generated
values carries them over: `docker exec <container> cat <file>` for
`auth-secret`, `dev-crypto-key` and `data/objectstack.db` under the home
directory, marking which of those files this container holds.
- A switch value other than `true`/`false`/`1`/`0` (or empty) is
refused, not read as off.
**Every other `os start` keeps booting, and says what it keeps on its
own disk.**
- In production mode, when the database is the defaulted SQLite file, or
the sign-in secret or the data key was generated on this disk, the boot
prints one block naming each file and the variable that moves it, and
says administrators see the same in the Console.
- The same facts go to the server process on an internal, CLI-owned
environment channel (`OS_INTERNAL_DEPLOYMENT_FINDINGS`, JSON).
`service-settings` spells the name in its own module: no package entry
gains an export.
**The administrator's notice (`packages/services/service-settings`).**
- `GET /api/settings/deployment/notice` and `POST
/api/settings/deployment/notice/hide`, mounted through the settings
family door (`mountFamilyRoute`: held-session gate, anonymous floor
`401`, authz outage `503`), for callers holding
`manage_platform_settings`. A member gets `403 PERMISSION_DENIED` and
the door never reads the process. Both rows are in
`settings-route-ledger.ts`.
- The state: `production`, the findings (`database_local_file`,
`auth_secret_generated`, `data_key_file` at high severity with their
variable and file; `file_storage_local` and `email_not_configured` at
low severity) and a `fingerprint` (the finding ids). Outside production
mode the list is empty. The email line reads
`IEmailService.isConfigured()`, the signal #30107 added; storage reads
the `storage.adapter` setting.
- The hide names the fingerprint the administrator read. It writes one
`config_change` row on `sys_audit_log` through the same single insert
site the deployment config audit uses (`config-change-audit.ts`), with
`metadata.event = 'deployment_notice.hide'`, the fingerprint and the
findings. It is refused `409 RESOURCE_CONFLICT` when the notice changed
since it was read (the answer carries the current state), when there is
nothing to hide, and when no audit log is mounted (`details.reason:
'no_audit_log'`): a hide that cannot be audited does not happen.
- The state is not on `/api/v1/auth/config` or `/api/v1/runtime/config`
(pinned end to end).
**The Console (`ui/packages/app-shell`, `ui/packages/i18n`).**
- `DeploymentNotice` on the home page (both the empty and the populated
layout) and at the top of Setup's System Overview dashboard. It is drawn
for a resolved workspace administrator only; a member's browser never
sends the request. No dialog.
- "Copy the settings" puts an env-file block on the clipboard: each high
finding's variable, with a comment saying what value it takes, and for a
generated value, which file to carry the current value over from.
- "Hide" posts the fingerprint; on success the hide is remembered per
user in this browser (`localStorage`, keyed by the fingerprint, so a new
finding brings the notice back). The home page then draws nothing;
System Overview keeps one small line with "Show again". A deployment
with no audit log says the notice cannot be hidden.
- 18 keys under `deploymentNotice` in all ten language packs.
**Docs.** `content/docs/deployment/self-hosting.mdx` gets a "When the
three are not set" section (the refusal, the `file:` pass, carrying
values over, the boot block, the notice and its hide) and corrects the
`OS_AUTH_SECRET` row (`os start` generates it into `auth-secret` under
the home directory). `docker/README.md` states the refusal, the
carry-over and the switch.
## Why
`docker/README.md` declared the three variables required and nothing
enforced it: the unchanged image boots with none of them, keeps the
business data in a SQLite file in the container layer and mints both
secrets on the container's disk (measured below). Replacing the
container loses the data, every sign-in and every saved credential. The
ruling (A3, 6634240777) refuses that in the image, keeps every other `os
start` booting, and tells the administrator, who reads the Console
rather than the log.
## Tests
- `packages/cli/src/commands/start-deployment-durability.test.ts` (20):
the switch's values, which variables count as missing and in what order,
`file:` URL parsing, the refusal and boot-block text, the channel write
and removal.
- `packages/cli/test/start-deployment-durability.integration.test.ts`
(8, real `os start` processes): refusal with none set; with only the
database missing (names only `OS_DATABASE_URL`); an invalid switch; an
explicit `file:` database plus both secrets boots with no block; the
defaulted boot prints the block; the administrator reads the three high
findings plus the two low ones; a member (created by the admin) is
refused `403` with no server path in the answer; neither public config
carries a finding, with a non-vacuity control.
-
`packages/services/service-settings/src/deployment-notice.routes.test.ts`
(19): findings and their order, the CLI's key-file channel, a malformed
channel, low-severity lines, outside production; admin `200`, member
`403` for read and hide with no env read and no insert, anonymous `401`,
no door `503`; the audit row's shape and system context, `409` with no
ledger / no engine / a changed fingerprint / nothing to hide, `400`
without a fingerprint, `500` when the insert throws.
- Pins updated for two more routes:
`settings-routes.anonymous-floor.pin.test.ts`,
`settings-routes.auth-gate.pin.test.ts` (six door members),
`settings-routes.authz-outage-relay.test.ts`.
-
`ui/packages/app-shell/src/console/deployment-notice/__tests__/DeploymentNotice.test.tsx`
(12): admin draws, member never requests, production-only, copy text,
hide paths (hidden, no audit log, changed, failed), the per-fingerprint
hide, System Overview's line and "Show again".
- **Ablations** (each restored the forbidden behaviour on disk, watched
the pin go red, then restored; full output in
`/mnt/project-files/server-lane-seat3/30120/ablations.md`):
<details><summary>A1–A9</summary>
| # | Mutation | Red |
|---|---|---|
| A1 | capability check removed from the notice routes | 2 / 19 (member
read and hide) |
| A2 | the hide writes no audit row | 2 / 19 |
| A3 | the no-ledger refusal removed | 1 / 19 |
| A4 | the notice speaks outside production | 1 / 19 |
| A5 | the fingerprint check removed | 1 / 19 |
| A6 | the Console's admin gate removed | 1 / 12 |
| A7 | the hide not keyed by fingerprint | 1 / 12 |
| A8 | the image refusal removed (dist rebuilt) | 2 / 8 integration |
| A9 | every production start refuses, switch or not | 4 / 8 integration
|
</details>
## Acceptance notes
- **Needs #30714 first.** `.github/workflows/scaffold-e2e.yml` runs the
image built from the checkout with only `OS_SECRET_KEY` and
`OS_AUTH_SECRET`; with the refusal it would go red. The one line (`-e
OS_DATABASE_URL="file:/tmp/e2e.db"`) is governed, so it rides #30714
(`Refs #30120`), which lands before this PR.
- **The eager `en` catalogue.** `check:eager-closure` measures
`i18n-locale-en` at 60,540 gz on this head against its 60,000 ceiling
(over by 540). The notice's 18 keys are +633 gz of that chunk (the built
chunk with and without the block: 60,440 / 59,807 by `gzip -9`), and
`main` had ~95 bytes left (#30588). The ceiling is not raised here:
#30640 moves the marketplace copy into a lazy namespace (its reading
57,003), and this branch merges `main` after it lands. Every other chunk
and the aggregate are inside their ceilings (aggregate 2,158.4 KB /
2,431.6 KB).
- **What counts as explicit.** A database from `--database`,
`OS_DATABASE_URL` or a datasource the project config declares; an auth
secret from `--auth-secret` or `OS_AUTH_SECRET`; their legacy aliases
too. `OS_DEV_CRYPTO_KEY` does not count for `OS_SECRET_KEY`: it is the
dev key the refusal exists to keep out of a container.
- **The image check runs only where the switch is on.** A custom image
built `FROM node` that runs `os start` gets the boot block and the
notice, not the refusal.
- **The boot block names the three disk-bound facts only.** Local
uploads and no email service are on the admin notice at low severity,
and the existing storage warning already prints at boot.
- **The hide is per administrator, per browser,** plus a mandatory audit
row; there is no server-side "hidden" setting (a settings namespace
would need a hub card and nav copy outside this claim). Another
administrator, or the same one in another browser, still sees the
notice.
- **An un-enforced settings context is not capability-gated,** the same
as every other settings route (`ctx.enforced`).
- **The data key finding reads the local crypto provider's tiers** (the
CLI's key-file channel first, then the persisted key file). A deployment
that registers a non-local `ICryptoProvider` and also leaves
`OS_SECRET_KEY` unset would still be told about the local file.
- **Upgrade note.** The changeset tells an image deployment on defaults
to carry `auth-secret`, `dev-crypto-key` and the database file over
before upgrading; the refusal says the same at the moment it matters. No
BREAKING marker: the published packages' surfaces are unchanged.
## Verification
**Measured on `main` `47c457b2` first**
(`/mnt/project-files/server-lane-seat3/30120/measure-main.md`): `os
start` with `NODE_ENV=production` and none of the three set boots,
writes `auth-secret`, `dev-crypto-key` and `data/objectstack.db` under
the home directory, and answers health `200`. The official image built
from `docker/Dockerfile` (published CLI 18.3.0) boots the same way with
none set: the ruling's premise holds.
**Real container, this branch**
(`/mnt/project-files/server-lane-seat3/30120/container-verify.md`).
Image `objectstack:30120-branch`: this branch's `docker/Dockerfile`
built, then the published CLI replaced by this branch's CLI and its
workspace dependencies (`pnpm --filter @objectstack/cli deploy --prod
--legacy`), so `os start` in the container is the code under review.
<details><summary>Container readings</summary>
1. None of the three set: exit 1, naming all three, with the carry-over
lines.
2. Explicit `OS_DATABASE_URL=file:/data/app.db` on a mounted volume plus
both secrets: health `200` after ~10 s, no block, `app.db` on the
volume.
3. Upgrade: the published 18.3.0 image ran on a volume with none set
(home holds `auth-secret`, `data/objectstack.db`, `dev-crypto-key`);
this branch's image on the same volume refuses and marks all three files
"(present in this container)"; with the carried-over values set it
boots, prints no block, and the crypto provider mints no new key.
4. Switch `false`, none set: boots and prints the block; the first
registrant's `GET /api/settings/deployment/notice` lists the five
findings with their files; the hide answers `200`; anonymous `401`;
`/api/v1/auth/config` names no finding. The hide's `sys_audit_log` row,
read from the container's database: `action: config_change`, `user_id` =
`actor` = the administrator, `object_name`/`record_id`/`tenant_id` null,
`metadata.event: deployment_notice.hide` with the fingerprint and
findings.
Not verified in the container: the Console UI, because the deploy bundle
carries no built Console (`ConsoleNotBuilt`); the component tests above
cover it.
</details>
**Local commands** (exit codes captured before any pipe):
- `pnpm --filter @objectstack/cli exec vitest run
src/commands/start-deployment-durability.test.ts`: 20 passed.
- `pnpm --filter @objectstack/cli exec vitest run --project integration
test/start-deployment-durability.integration.test.ts`: 8 passed.
- `pnpm --filter @objectstack/service-settings exec vitest run`: 70
files, 1227 tests passed (the notice routes and the three updated pins
among them).
- `pnpm lint`: exit 0.
- `pnpm --dir ui exec vitest run
packages/app-shell/src/console/deployment-notice`: 12 passed.
- `pnpm --dir ui/apps/console exec vite build` then `pnpm --dir ui run
check:eager-closure`: exit 1 on `i18n-locale-en` only (see Acceptance
notes).
- The derived gate battery (`node scripts/pm/dispatch-gates.mjs
--commands`, plus `pnpm lint`) and its `--ran` reconciliation: in the
os-dev report on #30120.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` untouched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request. (The governed workflow line is #30714, `Refs #30120`, at the
seat's direction.)
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 87fc795 commit a77c2e9
45 files changed
Lines changed: 2813 additions & 35 deletions
File tree
- .changeset
- content/docs/deployment
- docker
- packages
- cli
- src/commands
- test
- services/service-settings/src
- scripts
- ui/packages
- app-shell/src
- console
- deployment-notice
- __tests__
- home
- __tests__
- views
- i18n/src/locales
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
48 | 82 | | |
49 | 83 | | |
50 | 84 | | |
| |||
252 | 286 | | |
253 | 287 | | |
254 | 288 | | |
255 | | - | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
256 | 292 | | |
257 | 293 | | |
258 | 294 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
107 | 115 | | |
108 | 116 | | |
109 | | - | |
| 117 | + | |
| 118 | + | |
110 | 119 | | |
111 | 120 | | |
112 | 121 | | |
| |||
115 | 124 | | |
116 | 125 | | |
117 | 126 | | |
118 | | - | |
| 127 | + | |
| 128 | + | |
119 | 129 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
97 | | - | |
98 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
99 | 110 | | |
100 | 111 | | |
101 | 112 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
0 commit comments