You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(auth)!: refuse to boot multi-replica without an explicit OS_AUTH_SECRET (#3814 F3e) #31600
epic:#3814#1 · os-zhuang · session_01QUs3BrVu5TqwtmuhsjdEvM
Filing gate: ③ maintainer-dispatched task (child of #3814). Maintainer, (2026-11-08, 16:25 UTC): 「继续,现在派第二批」 (gate 1 after the card 0 inventory, cloud#3868 comment 6723322368).
Parent: #3814 (rulings R0-R6 on comment 6722625146).
Problem (smoke, card 0)
With OS_SECRET_KEY set but no OS_AUTH_SECRET, a third replica booted and minted /data/auth-secret on its own volume; the session cookie from r1 was UNAUTHENTICATED on r3. OS_SECRET_KEY is already refused when missing (LocalCryptoProvider); the auth secret is not. The kit's install.sh and the Helm secret carry it, so only hand-rolled deploys are exposed.
Do
When replicas>1 is declared on the Redis driver and OS_AUTH_SECRET is unset, refuse the boot with one line naming the variable and how to generate it, mirroring the data-key refusal.
Single node keeps auto-minting exactly as today.
Tests: declared multi-replica + Redis + no auth secret = refused with the message; single node = minted as today.
Rules that bind every #3814 child (verbatim rulings on the parent)
R1 peer round-robin. R2 Redis required for multi-replica only. R3 「同意:每张卡都只在"声明多副本并接上 Redis 驱动"时才生效」: the single-node default path is unchanged, multi-replica behaviour switches on only behind declared replicas>1 AND a connected Redis cluster driver, the split-brain guard is never weakened, and the deploy README "multi-node unsupported" line stays until the final gate.
Done when
PR merged; a multi-replica boot without the auth secret refuses; single node unchanged.
Claim: PM loop round 1 · #31600 · 2026-11-08T21:34Z · feature (p2; #3814 F3e)
Session: session_01ADHkmPF1skfu2kx89BEXZi
Account: os-steve
Branch: claude/project-thread-0ndnrp (the card thread's harness-pinned branch; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-31600
Domain: domain:server
Seat: domain:server#1
File surface: the auth-secret mint site (plugin-auth / runtime, measured in step 0), its tests, a changeset (stop on breach; explain in the report)
Container & model: S/M, mode:thread, model: default tier, effort: high
Clause-②: no (narrowing)
Metadata-protocol: none
Responsibility: n/a — the multi-replica path is new under #3814
Thread-read: none
Premise: holds (read at 5d1382a5: no refusal reads OS_AUTH_SECRET on a declared multi-replica boot)
Serial constraints cleared: it reuses PR #31624's multi-replica predicate (merged). Seat 4's #31599 / #31602 share that predicate but not this file. R3 binds: single node keeps auto-minting.
Order: standard order + server-seat1/31600-delta.md (project files).
{
"issue": 31600,
"status": "done",
"branch": "claude/project-thread-0ndnrp",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31718",
"session": "session_01ADHkmNwSCrFC9AxLfZsBxf",
"premise_still_valid": true,
"summary": "os start now refuses, before generating anything, to mint its own sign-in secret when the deployment declares several replicas (OS_EXPECT_MULTI_NODE=true / OS_CLUSTER_REPLICAS>1) over the redis cluster driver and no OS_AUTH_SECRET is set: exit 1 on one line naming OS_AUTH_SECRET and openssl rand -hex 32, no value in it. Step 0: the only mint site is os start (readOrCreateAuthSecret, home/auth-secret), not plugin-auth or runtime; on base fc4c3072 the OS_EXPECT_MULTI_NODE=true + redis path booted and minted (narrowing measured). The predicate is the shared multi-replica rule in its existing pre-boot form, sharesCloudBindingCredential(env, OS_CLUSTER_DRIVER) from @objectstack/cloud-connection (a CLI dependency already), lazily imported; single node and replicas over any other driver are unchanged. Three doc sentences that said os start always generates a secret are corrected.",
"tests": "head c96d6e73. cli vitest start-deployment-durability.test.ts + test/start-deployment-durability.integration.test.ts + serve-cluster-host-resolution.test.ts: 3 files 67 passed (VERDICT command-exit 0). CLI unit tier: 405 files 5728 passed (VERDICT 0). cli typecheck VERDICT 0. Ablation (refusal return made false, marker ABLATION_31600 via ablation-replace.mjs): anchor 1->0, blob e9d80387->d55df8e4, rebuilt, dist-preflight marker in 1 built file; 2 failed/33 passed (unit 'refuses when several replicas are declared over the redis driver' expected false to be true; spawn 'OS_AUTH_SECRET unset: exit 1' got the split-brain guard instead); restored blob==HEAD, git diff HEAD empty; rebuilt, --absent over 904 files, clean tree, 35/35. Real Redis (7.0.15) probes: head no secret -> exit 1 + line + 0 files; head secret set -> redis driver + redis cache + ready, 0 files; two head replicas, one secret, shared sqlite -> r1 cookie reads the session on r1 and r2; differing secrets -> r2 does not.",
"gates": "dispatch-gates --commands on c96d6e73: 99 derived, all 99 exit 0 (check:type-check-debt first exit 3 = lock queue-timeout behind dogfood, then exit 0 under the lock); --ran: 99 run, 0 NOT-MEASURED (DERIVED zero). check-adr-0087-registration: 1 declared-breaking changeset, [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription). Lint: cli has no lint script; npx eslint --no-inline-config packages/cli exit 0. CI check-runs: in_progress at report time (not awaited).",
"dogfood_battery": "c96d6e73 · VERDICT command-exit 0 (held 32m17s) · Test Files 440 passed, 3 skipped (443) · Tests 3652 passed, 41 skipped (3693) · no failure",
"mcp_calls": "5 — reads only: mcp__github__issue_read x3 (31600 get, get_comments x2), mcp__github__pull_request_read x2 (31624 get, 31718 get)",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (relay pr_create, PR 31718, assignees os-steve, read-back 12186 bytes identical); this os-dev-report comment via post-stamped.mjs. git push is not a REST write.",
"open_questions": [
{
"question": "The pre-boot predicate is read from cloud-connection's sharesCloudBindingCredential, a function named for the cloud binding credential. Keep it, or lift a neutrally named pre-boot form?",
"options": [
"A keep: the same rule (declaresMultiNode AND redis), already the one pre-boot form, no new dependency or export",
"B lift a neutral pre-boot predicate (e.g. in service-cluster) and re-point cloud-connection and os start at it; the CLI still cannot import a cluster package directly (serve-cluster-host-resolution.test.ts), so it needs a re-export from a package the CLI declares"
],
"recommendation": "A, because B adds a public export (a widening) for a naming concern only; revisit if seat 4's #31599/#31602 need the same pre-boot form."
}
],
"security_findings": "withheld",
"out_of_scope_findings": [
"class: b · Seam: runtime:service-cluster checkMultiNodeAllowed (no gate registered) -> runtime:cli serve.ts gate call · contract: multi-node-gate.ts says a DECLARED multi-node topology with no gate registered is refused outright, but serve.ts asks the gate with Number(OS_CLUSTER_REPLICAS) only, so a topology declared by OS_EXPECT_MULTI_NODE=true (declaresMultiNode's other spelling) reaches the gate as undeclared and is allowed · reach: os start on open core with OS_EXPECT_MULTI_NODE=true OS_CLUSTER_DRIVER=redis booted the redis driver multi-node with no gate (measured on fc4c3072 and c96d6e73), while OS_CLUSTER_REPLICAS=2 is denied and then refused by the split-brain guard · dedupe words: multi-node gate, OS_EXPECT_MULTI_NODE, checkMultiNodeAllowed, fail closed, declared topology",
"carrier: none · noted, not filed"
],
"files_changed": [
".changeset/31600-multi-replica-auth-secret-refusal.md",
"content/docs/deployment/cli.mdx",
"content/docs/deployment/self-hosting.mdx",
"packages/cli/src/commands/start.ts",
"packages/cli/src/commands/start-deployment-durability.test.ts",
"packages/cli/test/start-deployment-durability.integration.test.ts"
],
"line_budget": "+163 / -4 over 6 files (S/M card)",
"deviations": [
"File surface: the claim named plugin-auth / runtime; step 0 measured the only mint site in packages/cli/src/commands/start.ts, so the change and its tests live there.",
"Docs outside the claimed surface: cli.mdx (2 places) and self-hosting.mdx (1) said os start always generates a secret; this change made that false, so they are corrected in the same PR.",
"A first head (fd977a59) declared @objectstack/service-cluster on the CLI; serve-cluster-host-resolution.test.ts refused it and the dependency was dropped (net zero in package.json and pnpm-lock.yaml). The first ablation ran on that implementation; the reported ablation is the second, on the final code.",
"A dogfood run started on the stale head fd977a59 was stopped (own PIDs) and re-run once on the final head.",
"check:type-check-debt answered exit 3 (lock queue-timeout) in the gate sweep and was re-run under the lock: exit 0.",
"The cli package has no lint script: lint = root eslint on packages/cli with --no-inline-config."
]
}
Activity
objectstack-fleet commentedon Nov 8, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: confirmed ·
domain:server· p2 ·pm:queue· 2026-11-08T17:01Z⛔ Not a claim.
domain:server. The fix is in objectstack packages only.lockRecord,客户端只能假定「有 pending 就是锁死」 #3814 child, maintainer-directed batch.domain:devxanddomain:serveras separate lanes, or merge them? Measured load and boundary friction, 2026-11-08 #31424 rule 2).Generated by Claude Code
objectstack-fleet commentedon Nov 8, 2026
domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi
Claim: PM loop round 1 · #31600 · 2026-11-08T21:34Z · feature (p2; #3814 F3e)
Session:
session_01ADHkmPF1skfu2kx89BEXZiAccount:
os-steveBranch:
claude/project-thread-0ndnrp(the card thread's harness-pinned branch; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-31600Domain:
domain:serverSeat:
domain:server#1File surface: the auth-secret mint site (plugin-auth / runtime, measured in step 0), its tests, a changeset (stop on breach; explain in the report)
Container & model:
S/M,mode:thread,model: default tier,effort: highClause-②: no (narrowing)
Metadata-protocol: none
Responsibility: n/a — the multi-replica path is new under #3814
Thread-read: none
Premise: holds (read at
5d1382a5: no refusal readsOS_AUTH_SECRETon a declared multi-replica boot)Serial constraints cleared: it reuses PR #31624's multi-replica predicate (merged). Seat 4's #31599 / #31602 share that predicate but not this file. R3 binds: single node keeps auto-minting.
Order: standard order +
server-seat1/31600-delta.md(project files).objectstack-fleet commentedon Nov 8, 2026
domain:server#1 · os-steve · session_01ADHkmNwSCrFC9AxLfZsBxf
os-dev-report
{ "issue": 31600, "status": "done", "branch": "claude/project-thread-0ndnrp", "pr": "https://github.com/objectstack-ai/objectstack/pull/31718", "session": "session_01ADHkmNwSCrFC9AxLfZsBxf", "premise_still_valid": true, "summary": "os start now refuses, before generating anything, to mint its own sign-in secret when the deployment declares several replicas (OS_EXPECT_MULTI_NODE=true / OS_CLUSTER_REPLICAS>1) over the redis cluster driver and no OS_AUTH_SECRET is set: exit 1 on one line naming OS_AUTH_SECRET and openssl rand -hex 32, no value in it. Step 0: the only mint site is os start (readOrCreateAuthSecret, home/auth-secret), not plugin-auth or runtime; on base fc4c3072 the OS_EXPECT_MULTI_NODE=true + redis path booted and minted (narrowing measured). The predicate is the shared multi-replica rule in its existing pre-boot form, sharesCloudBindingCredential(env, OS_CLUSTER_DRIVER) from @objectstack/cloud-connection (a CLI dependency already), lazily imported; single node and replicas over any other driver are unchanged. Three doc sentences that said os start always generates a secret are corrected.", "tests": "head c96d6e73. cli vitest start-deployment-durability.test.ts + test/start-deployment-durability.integration.test.ts + serve-cluster-host-resolution.test.ts: 3 files 67 passed (VERDICT command-exit 0). CLI unit tier: 405 files 5728 passed (VERDICT 0). cli typecheck VERDICT 0. Ablation (refusal return made false, marker ABLATION_31600 via ablation-replace.mjs): anchor 1->0, blob e9d80387->d55df8e4, rebuilt, dist-preflight marker in 1 built file; 2 failed/33 passed (unit 'refuses when several replicas are declared over the redis driver' expected false to be true; spawn 'OS_AUTH_SECRET unset: exit 1' got the split-brain guard instead); restored blob==HEAD, git diff HEAD empty; rebuilt, --absent over 904 files, clean tree, 35/35. Real Redis (7.0.15) probes: head no secret -> exit 1 + line + 0 files; head secret set -> redis driver + redis cache + ready, 0 files; two head replicas, one secret, shared sqlite -> r1 cookie reads the session on r1 and r2; differing secrets -> r2 does not.", "gates": "dispatch-gates --commands on c96d6e73: 99 derived, all 99 exit 0 (check:type-check-debt first exit 3 = lock queue-timeout behind dogfood, then exit 0 under the lock); --ran: 99 run, 0 NOT-MEASURED (DERIVED zero). check-adr-0087-registration: 1 declared-breaking changeset, [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription). Lint: cli has no lint script; npx eslint --no-inline-config packages/cli exit 0. CI check-runs: in_progress at report time (not awaited).", "dogfood_battery": "c96d6e73 · VERDICT command-exit 0 (held 32m17s) · Test Files 440 passed, 3 skipped (443) · Tests 3652 passed, 41 skipped (3693) · no failure", "mcp_calls": "5 — reads only: mcp__github__issue_read x3 (31600 get, get_comments x2), mcp__github__pull_request_read x2 (31624 get, 31718 get)", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (relay pr_create, PR 31718, assignees os-steve, read-back 12186 bytes identical); this os-dev-report comment via post-stamped.mjs. git push is not a REST write.", "open_questions": [ { "question": "The pre-boot predicate is read from cloud-connection's sharesCloudBindingCredential, a function named for the cloud binding credential. Keep it, or lift a neutrally named pre-boot form?", "options": [ "A keep: the same rule (declaresMultiNode AND redis), already the one pre-boot form, no new dependency or export", "B lift a neutral pre-boot predicate (e.g. in service-cluster) and re-point cloud-connection and os start at it; the CLI still cannot import a cluster package directly (serve-cluster-host-resolution.test.ts), so it needs a re-export from a package the CLI declares" ], "recommendation": "A, because B adds a public export (a widening) for a naming concern only; revisit if seat 4's #31599/#31602 need the same pre-boot form." } ], "security_findings": "withheld", "out_of_scope_findings": [ "class: b · Seam: runtime:service-cluster checkMultiNodeAllowed (no gate registered) -> runtime:cli serve.ts gate call · contract: multi-node-gate.ts says a DECLARED multi-node topology with no gate registered is refused outright, but serve.ts asks the gate with Number(OS_CLUSTER_REPLICAS) only, so a topology declared by OS_EXPECT_MULTI_NODE=true (declaresMultiNode's other spelling) reaches the gate as undeclared and is allowed · reach: os start on open core with OS_EXPECT_MULTI_NODE=true OS_CLUSTER_DRIVER=redis booted the redis driver multi-node with no gate (measured on fc4c3072 and c96d6e73), while OS_CLUSTER_REPLICAS=2 is denied and then refused by the split-brain guard · dedupe words: multi-node gate, OS_EXPECT_MULTI_NODE, checkMultiNodeAllowed, fail closed, declared topology", "carrier: none · noted, not filed" ], "files_changed": [ ".changeset/31600-multi-replica-auth-secret-refusal.md", "content/docs/deployment/cli.mdx", "content/docs/deployment/self-hosting.mdx", "packages/cli/src/commands/start.ts", "packages/cli/src/commands/start-deployment-durability.test.ts", "packages/cli/test/start-deployment-durability.integration.test.ts" ], "line_budget": "+163 / -4 over 6 files (S/M card)", "deviations": [ "File surface: the claim named plugin-auth / runtime; step 0 measured the only mint site in packages/cli/src/commands/start.ts, so the change and its tests live there.", "Docs outside the claimed surface: cli.mdx (2 places) and self-hosting.mdx (1) said os start always generates a secret; this change made that false, so they are corrected in the same PR.", "A first head (fd977a59) declared @objectstack/service-cluster on the CLI; serve-cluster-host-resolution.test.ts refused it and the dependency was dropped (net zero in package.json and pnpm-lock.yaml). The first ablation ran on that implementation; the reported ablation is the second, on the final code.", "A dogfood run started on the stale head fd977a59 was stopped (own PIDs) and re-run once on the final head.", "check:type-check-debt answered exit 3 (lock queue-timeout) in the gate sweep and was re-run under the lock: exit 0.", "The cli package has no lint script: lint = root eslint on packages/cli with --no-inline-config." ] }Stamped 2026-11-08T23:22Z.
Generated by Claude Code