Repository navigation
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
Conversation
…otice routes Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
…n notice Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
📓 Docs Drift CheckThis PR changes 2 package(s): 54 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 17 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 41 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 834735e288ae701eaf02f35b1c1bf75760b3e0ce && git checkout 834735e288ae701eaf02f35b1c1bf75760b3e0ce
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 447226b222467709ed74ca99eaf808c178656b33 ea323f08882bce9ad174e9ea701bceb179cefd6f && git checkout -B drift-repro 447226b222467709ed74ca99eaf808c178656b33 && git merge --no-ff ea323f08882bce9ad174e9ea701bceb179cefd6f
node scripts/docs-audit/affected-docs.mjs --json 447226b222467709ed74ca99eaf808c178656b33
|
…tes drop the tracker id Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
…er source the page reads The administrator notice on the Console home reads its own state from the server, so the Home tests that render an admin reached a real socket and the network-escape guard failed them. Each one now stubs the notice module; its fetch, gate and copy stay pinned in DeploymentNotice.test.tsx. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd
Requested by Elon · project thread
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 withoutOS_DATABASE_URL,OS_AUTH_SECRETandOS_SECRET_KEY; everydocker runexample undercontent/docs/**already sets all three, andcontent/docs/deployment/self-hosting.mdx+docker/README.mdare updated here维护者速读
os start照常启动,只是在生产模式下把「放在本机磁盘上的东西」打印在启动日志里;管理员在控制台首页和「设置 › 系统概览」顶部看到一条常驻提示(可一键复制要设的变量,可隐藏,隐藏写进审计日志),普通成员看不到。OS_DEPLOYMENT_DURABILITY_CHECK_ENABLED=false暂时回到旧行为;提示内容只经设置服务的管理员入口给出,公共配置里没有。另外,控制台首屏英文包本 head 实测 60,540 / 60,000 gz(提示文案 +633 字节),要等 perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazymarketplacenamespace #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」并列出从旧容器取值的三个文件。What changes
The official image refuses a start it would lose (
packages/cli,docker/Dockerfile).docker/DockerfilesetsOS_DEPLOYMENT_DURABILITY_CHECK_ENABLED=truein itsENV.NODE_ENVcannot be the signal:os startsetsproductionitself on every boot, including the zero-config laptop trial (measured, see Verification).os startexits 1 before it opens a database or writes a secret, unlessOS_DATABASE_URL,OS_AUTH_SECRETandOS_SECRET_KEYare all set explicitly. It names exactly the missing ones and says how to set each. An explicitOS_DATABASE_URL=file:/path/on/a/volume/app.dbpasses, so SQLite on a kept volume stays a choice.docker exec <container> cat <file>forauth-secret,dev-crypto-keyanddata/objectstack.dbunder the home directory, marking which of those files this container holds.true/false/1/0(or empty) is refused, not read as off.Every other
os startkeeps booting, and says what it keeps on its own disk.OS_INTERNAL_DEPLOYMENT_FINDINGS, JSON).service-settingsspells the name in its own module: no package entry gains an export.The administrator's notice (
packages/services/service-settings).GET /api/settings/deployment/noticeandPOST /api/settings/deployment/notice/hide, mounted through the settings family door (mountFamilyRoute: held-session gate, anonymous floor401, authz outage503), for callers holdingmanage_platform_settings. A member gets403 PERMISSION_DENIEDand the door never reads the process. Both rows are insettings-route-ledger.ts.production, the findings (database_local_file,auth_secret_generated,data_key_fileat high severity with their variable and file;file_storage_localandemail_not_configuredat low severity) and afingerprint(the finding ids). Outside production mode the list is empty. The email line readsIEmailService.isConfigured(), the signal Forgot password says the reset link was sent on a deployment with no email service #30107 added; storage reads thestorage.adaptersetting.config_changerow onsys_audit_logthrough the same single insert site the deployment config audit uses (config-change-audit.ts), withmetadata.event = 'deployment_notice.hide', the fingerprint and the findings. It is refused409 RESOURCE_CONFLICTwhen 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./api/v1/auth/configor/api/v1/runtime/config(pinned end to end).The Console (
ui/packages/app-shell,ui/packages/i18n).DeploymentNoticeon 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.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.deploymentNoticein all ten language packs.Docs.
content/docs/deployment/self-hosting.mdxgets a "When the three are not set" section (the refusal, thefile:pass, carrying values over, the boot block, the notice and its hide) and corrects theOS_AUTH_SECRETrow (os startgenerates it intoauth-secretunder the home directory).docker/README.mdstates the refusal, the carry-over and the switch.Why
docker/README.mddeclared 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 otheros startbooting, 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, realos startprocesses): refusal with none set; with only the database missing (names onlyOS_DATABASE_URL); an invalid switch; an explicitfile: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 refused403with 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; admin200, member403for read and hide with no env read and no insert, anonymous401, no door503; the audit row's shape and system context,409with no ledger / no engine / a changed fingerprint / nothing to hide,400without a fingerprint,500when the insert throws.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"./mnt/project-files/server-lane-seat3/30120/ablations.md):A1–A9
Acceptance notes
.github/workflows/scaffold-e2e.ymlruns the image built from the checkout with onlyOS_SECRET_KEYandOS_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 ci(scaffold-e2e): give the image run an explicit database (Refs #30120) #30714 (Refs #30120), which lands before this PR.encatalogue.check:eager-closuremeasuresi18n-locale-enat 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 bygzip -9), andmainhad ~95 bytes left (Console first load: the English language pack has 63 bytes left under its size ceiling, so any card that adds English console text fails CI #30588). The ceiling is not raised here: perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazymarketplacenamespace #30640 moves the marketplace copy into a lazy namespace (its reading 57,003), and this branch mergesmainafter it lands. Every other chunk and the aggregate are inside their ceilings (aggregate 2,158.4 KB / 2,431.6 KB).--database,OS_DATABASE_URLor a datasource the project config declares; an auth secret from--auth-secretorOS_AUTH_SECRET; their legacy aliases too.OS_DEV_CRYPTO_KEYdoes not count forOS_SECRET_KEY: it is the dev key the refusal exists to keep out of a container.FROM nodethat runsos startgets the boot block and the notice, not the refusal.ctx.enforced).ICryptoProviderand also leavesOS_SECRET_KEYunset would still be told about the local file.auth-secret,dev-crypto-keyand 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
main47c457b2first (/mnt/project-files/server-lane-seat3/30120/measure-main.md):os startwithNODE_ENV=productionand none of the three set boots, writesauth-secret,dev-crypto-keyanddata/objectstack.dbunder the home directory, and answers health200. The official image built fromdocker/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). Imageobjectstack:30120-branch: this branch'sdocker/Dockerfilebuilt, then the published CLI replaced by this branch's CLI and its workspace dependencies (pnpm --filter @objectstack/cli deploy --prod --legacy), soos startin the container is the code under review.Container readings
OS_DATABASE_URL=file:/data/app.dbon a mounted volume plus both secrets: health200after ~10 s, no block,app.dbon the volume.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.false, none set: boots and prints the block; the first registrant'sGET /api/settings/deployment/noticelists the five findings with their files; the hide answers200; anonymous401;/api/v1/auth/confignames no finding. The hide'ssys_audit_logrow, read from the container's database:action: config_change,user_id=actor= the administrator,object_name/record_id/tenant_idnull,metadata.event: deployment_notice.hidewith 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.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 buildthenpnpm --dir ui run check:eager-closure: exit 1 oni18n-locale-enonly (see Acceptance notes).node scripts/pm/dispatch-gates.mjs --commands, pluspnpm lint) and its--ranreconciliation: in the os-dev report on [Decision] os start in production mode runs on a local SQLite file and self-generated secrets, with no warning #30120.Human Minimum
packages/specholds no runtime logic. (packages/specuntouched.)pnpm lintpasses.main, one pull request. (The governed workflow line is ci(scaffold-e2e): give the image run an explicit database (Refs #30120) #30714,Refs #30120, at the seat's direction.)packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cyhB3ePno23ywn1PQd