Repository navigation
feat(cli)!: a declared multi-replica os start on the redis cluster driver refuses to boot without OS_AUTH_SECRET, instead of minting its own sign-in secret - #31718
Conversation
…r refuses to mint its own sign-in secret Claude-Session: https://claude.ai/code/session_01ADHkmNwSCrFC9AxLfZsBxf Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmNwSCrFC9AxLfZsBxf Co-authored-by: Claude <noreply@anthropic.com>
…multi-replica redis deployment Claude-Session: https://claude.ai/code/session_01ADHkmNwSCrFC9AxLfZsBxf Co-authored-by: Claude <noreply@anthropic.com>
…not a new cluster dependency The CLI declares no cluster package of its own: those ship with the host app. Claude-Session: https://claude.ai/code/session_01ADHkmNwSCrFC9AxLfZsBxf Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmNwSCrFC9AxLfZsBxf Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 35 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 fa423033eb67127b5b9509ee0095181ac84b30f8 && git checkout fa423033eb67127b5b9509ee0095181ac84b30f8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 35a9c6941c18eb5597dedd8e27b518050851bdd5 d6fe923526713c5cd161c0e86c2b79eff7aa8b80 && git checkout -B drift-repro 35a9c6941c18eb5597dedd8e27b518050851bdd5 && git merge --no-ff d6fe923526713c5cd161c0e86c2b79eff7aa8b80
node scripts/docs-audit/affected-docs.mjs --json 35a9c6941c18eb5597dedd8e27b518050851bdd5
|
|
domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi Contract-review-claim: session_01ADHkmPF1skfu2kx89BEXZi · head c96d6e7 · 2026-11-08T23:23Z |
|
domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi Contract reviewServed-tier: Face: narrowing ( Check-runs on the head at review time: every completed gate-carrying run is ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings, graded: nit — barrel load on the single-node path (start.ts); nit — predicate naming coupling (③.1, rec A kept); escalation to the seat, not a finding on this diff — the Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi 席位 ACCEPT · head
|
The two legs that reach the serve child read ~4 s locally, under vitest's default 5 s per test; on a loaded CI runner (Test Core 1/8 on c96d6e7) the memory-driver CONTROL leg timed out at 5000 ms. Each leg now carries the spawnSync bound (120 s) plus 10 s. Claude-Session: https://claude.ai/code/session_01ADHkmNwSCrFC9AxLfZsBxf Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi 席位 ACCEPT · 增量 · head
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42540685668 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Requested by Steve · project thread
Fixes #31600
Clause-②: no (narrowing) — measured: on base
fc4c3072anos startdeclaring several replicas (OS_EXPECT_MULTI_NODE=true) over the redis cluster driver, withOS_SECRET_KEYset and noOS_AUTH_SECRET, booted (redis cluster driver and redis cache adapter registered,Press Ctrl+C to stop) and generated{home}/auth-secret; on head the same start exits 1 on the refusal line and generates nothing.Metadata-protocol: none
Skills-docs: updated here —
content/docs/deployment/cli.mdx(the--auth-secretrow and the Authentication paragraph) andcontent/docs/deployment/self-hosting.mdx(theOS_AUTH_SECRETrow) no longer sayos startalways generates a secret;skills/**says nothing aboutos startgenerating one on a multi-replica deployment (its onlyOS_AUTH_SECRETtext is the image's durability refusal, unchanged).维护者速读
os start在"声明多副本(OS_EXPECT_MULTI_NODE=true或OS_CLUSTER_REPLICAS>1)且集群驱动是 Redis"时,如果没设OS_AUTH_SECRET,不再自己生成登录密钥,而是直接拒绝启动,打印一行:缺哪个变量、为什么、怎么生成(openssl rand -hex 32),所有副本设同一个值。{home}/auth-secret,任何驱动都一样;声明多副本但驱动不是 Redis 的维持原样(R3:两个条件同时成立才生效)。会被拒绝的只有"多副本 + Redis + 没设密钥"这一种配置,修法就是设密钥。回滚 = revert 本 PR。packages/cli/src/commands/start.ts生成密钥之前新加的那一段判断与declaresReplicasSharingState;② 同文件的拒绝文案MULTI_REPLICA_AUTH_SECRET_REFUSAL;③packages/cli/test/start-deployment-durability.integration.test.ts新增的三条真实os start用例。pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/start-deployment-durability.test.ts test/start-deployment-durability.integration.test.ts(需先 build CLI),预期 35 passed:其中多副本 + Redis + 无密钥的os start退出码 1、输出那一行、家目录里没有auth-secret。What changes
os startrefuses to generate a sign-in secret for a declared multi-replica deployment on the redis cluster driver. Where the operator set no secret (--auth-secret,OS_AUTH_SECRETor its legacy names unset or blank),os startnow asks, before it generates anything, whether the deployment runs replicas that share state. If it does, it exits 1 on one line, in the data key refusal's shape:The line names the variable and never a value. The refusal writes no secret, so a replica that hits it leaves nothing on its volume.
Where it lives, measured in step 0. The only place a sign-in secret is generated is
os startitself (readOrCreateAuthSecret,{home}/auth-secret; the image's home is the/data/auth-secretof the card).os servenever generates one outside development: plugin-auth refuses a production boot withoutOS_AUTH_SECRET. The data key's missing-key refusal lives in theservechild (LocalCryptoProvider), andos startalready declines to opt that key into self-minting whenOS_CLUSTER_DRIVERis set. The secret is generated in the parent before the child starts, so the refusal belongs in the parent, at the same point as the image's durability check, which it follows.The predicate is not a second copy. It is the rule every multi-replica change keeps (cloud#3814 R3): replicas declared (
declaresMultiNode, the split-brain guard's reader) AND therediscluster driver. PR #31624 applies it inplanCacheAdapterto the registered cluster service's driver; its one pre-boot form already exists assharesCloudBindingCredential(env, OS_CLUSTER_DRIVER)in@objectstack/cloud-connection(it trims the driver the way theservechild does before composing the cluster service).os startasks that function, through a lazy import of a package the CLI already declares. A first attempt declared@objectstack/service-clusteron the CLI to importdeclaresMultiNodedirectly;serve-cluster-host-resolution.test.tsrefuses that, because cluster packages ship with the host app and the open-core CLI takes no static dependency on them, so the dependency was dropped.Docs.
content/docs/deployment/cli.mdx(the--auth-secretrow and the Authentication paragraph) andcontent/docs/deployment/self-hosting.mdx(theOS_AUTH_SECRETrow) saidos startalways generates a secret; they now name the one topology where it refuses instead.Tests
packages/cli/src/commands/start-deployment-durability.test.ts, new top-leveldescribe: the decision on real environments — refused forOS_CLUSTER_REPLICAS=3and forOS_EXPECT_MULTI_NODE=true(with a padded' redis ') over redis; CONTROL single node (no declaration, redis alone, replicas1) not refused; CONTROL replicas declared overmemoryor no driver not refused; the line is one line, starts with the pinned first sentence, namesOS_AUTH_SECRETand the generator. The file also gains three module-top side-effect imports (@objectstack/cloud-connection,@objectstack/runtime,@objectstack/service-settings):check:test-source-aliasasks every edited test file that reaches a lazy import in its source to pay the load during collection. Forced test edit, named here.packages/cli/test/start-deployment-durability.integration.test.ts, newdescribeat the shipped entry (bin/run.jsoverdist/), no Redis needed because the door is inos start: (1) replicas declared over redis, no secret → exit 1, stderr carries the line, noauth-secretanywhere under home or cwd; (2) the secret set → the door passes (banner printed after it), nothing generated; (3) CONTROL replicas declared overmemory→ the secret is generated exactly as before (.objectstack/auth-secret). The single-node CONTROL is the file's existing leg 3, which reads the generatedauth-secretfile. Forced test edit, named here:baseEnvnow also clearsOS_EXPECT_MULTI_NODE,OS_CLUSTER_REPLICAS,OS_ALLOW_MEMORY_CLUSTER_MULTINODEandOS_REDIS_URL, so an inherited declaration cannot reach the existing legs; thetest-budgetline is re-read (26.9 s, budget 40 unchanged).Acceptance notes
os startonly. A deployment that runsos servedirectly already has to setOS_AUTH_SECRET(plugin-auth refuses production without it), so it never generated a per-replica secret.OS_AUTH_SECRETis refused by that check first (it lists every missing variable); this line is reached only without the switch or with the secret check passed.OS_CLUSTER_REPLICAS> 1 and no multi-node gate registered, theservechild falls back to the memory driver and the split-brain guard refuses the boot anyway (measured on head with a shared secret set). That configuration now meets this line first when no secret is set; it never booted, so no boot is lost.OS_EXPECT_MULTI_NODE=trueover redis did boot on base and is the narrowed path.OS_AUTH_SECRETto one replica'sauth-secretcontents on every replica; the changeset says so.Verification
Local, on head
c96d6e73unless a line says otherwise. Every exit code below was captured before any pipe.Step 0 and the real-Redis readings (redis-server 7.0.15, a probe app declaring the two cluster packages)
fc4c3072,OS_EXPECT_MULTI_NODE=true OS_CLUSTER_DRIVER=redis OS_REDIS_URL=… OS_SECRET_KEY=…, no auth secret:ClusterServicePlugin: registered "redis" driver,CacheServicePlugin: registered redis cache adapter,Press Ctrl+C to stop;{home}/.objectstack/auth-secretgenerated.auth-secretfiles.OS_AUTH_SECRETset: redis driver and redis cache registered,Press Ctrl+C to stop, 0auth-secretfiles.OS_AUTH_SECRET: a cookie signed in on r1 reads the session on r1 and on r2. The same pair with a different secret each (what base generated): r1 reads it, r2 does not.Tests and the ablation
pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/start-deployment-durability.test.ts test/start-deployment-durability.integration.test.ts src/commands/serve-cluster-host-resolution.test.ts: 3 files, 67 passed (VERDICT command-exit 0).vitest run --project unit: 405 files, 5728 passed (VERDICT command-exit 0).pnpm --filter @objectstack/cli typecheck: VERDICT command-exit 0 (tsc and the test-typecheck ledger, 3 files / 28 errors / 6 pinned signatures unchanged).ABLATION_31600, throughscripts/ablation-replace.mjs): mutation landed (anchor 1 → 0, blobe9d80387→d55df8e4), CLI rebuilt,ablation-dist-preflightfound the marker in 1 built file; 2 failed / 33 passed — the unit caserefuses when several replicas are declared over the redis driver(expected false to be true) and the spawn caseOS_AUTH_SECRET unset: exit 1 …(the child went on to the split-brain guard instead). Restored: blob == HEAD,git diff HEADempty; rebuilt,--absentover 904 built files and a clean tree; 35/35 passed.Derived gates and lint
node scripts/pm/dispatch-gates.mjs --commandsderived 99 families onc96d6e73; all 99 ran and exited 0.check:type-check-debtfirst answered exit 3 (the lock was held by the dogfood run, queue-timeout, nothing measured) and exited 0 under the lock afterwards.--ranwith every exit code recorded:99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED (a DERIVED zero …). On the earlier headfd977a59(with the dropped dependency)check:test-source-aliaswas red and named the three lazy loads the edited test file reaches; the module-top imports fixed it.check-adr-0087-registration:1 declared-breaking changeset(s), each carrying an ADR-0087 disposition—[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).npx eslint --no-inline-config packages/cli(the package has no lint script): exit 0.Full dogfood (owed by the narrowing arm)
On
c96d6e73: the dependency closure built (pnpm --filter '@objectstack/dogfood^...' build, VERDICT command-exit 0, before the last two commits; the CLI and every package were rebuilt onc96d6e73byturbo run build --filter='./packages/**', VERDICT command-exit 0), thenpnpm --filter @objectstack/dogfood testunder the verify lock: VERDICT command-exit 0, Test Files 440 passed, 3 skipped (443); Tests 3652 passed, 41 skipped (3693). No failure.Generated by Claude Code