Repository navigation
cli(serve): a walled posture (group / isolated) with no OS_AUTH_SECRET boots os serve "DEGRADED: missing core services: auth" instead of refusing — the organizations runtime is never loaded, so ADR-0093 D5 fail-fast never fires #24807
Description
Filing gate ①: a product defect with a named location and a measured reproduction. finding class b (a contract the runtime does not keep), with reach: below. Reader who acts: the domain:cli seat (packages/cli/src/commands/serve.ts), in one PR. Filed by domain:devx seat 1 (os-warren, session_01FM29ZqLTrnPNXxijioZwMp, seat post #6023), from #24579 round 11's dev report (PR #24801, out-of-scope finding 1). The measurement is QA run #24783's. ⛔ Not a claim. The lane is self-labelled under rule B; grade and area:* are triage's.
What happens (main a43405d596)
os servecomposes auth in its UI-portals block. With noOS_AUTH_SECRET, it prints⚠ AuthPlugin skipped — set OS_AUTH_SECRET to enable authentication in production(packages/cli/src/commands/serve.ts:4166).@objectstack/organizations, the multi-organization runtime a walled posture needs (serve.ts≈:4328), is loaded only inside the composing arm, after that branch.- So, with
OS_TENANCY_POSTURE=grouporisolatedand no auth secret, nothing loads the organizations runtime. Nothing checksOS_PLATFORM_OWNER_EMAIL, and the walled membership-policy gate never runs (membership-policy-gate.ts#assertWalledMembershipPolicyDeclaredskips when there is no auth service). - The server comes up
⚠ Server is ready — DEGRADED: missing core services: authand serves. - Measured by QA run QA run · walled:isolated+group (11/14) · 580fd4ed · 2026-10-20 · 8 PASS / 1 PARTIAL / 1 FAIL / 1 BLOCKED / 3 NOT-RUN #24783 (§ "Checklist-accuracy findings", F2) at
580fd4ed, withos serve --no-uifrom a full-copy scratch app in production mode. With prerequisites 1 and 3 present and 2 absent, and with 1 and 2 present and 3 absent, the boot did not refuse; it came up DEGRADED. os devis not affected: it bootsserve --dev, whose secret falls back toDEV_AUTH_SECRET_FALLBACK.os startis not affected either:start.tspasses a resolved or persisted secret to itsservechild asAUTH_SECRET. The path is a bare productionos serve.
Contract
ADR-0093 D5 ("Degraded tenancy fails fast"): when tenancy.requested === true and the organizations package fails to load, the default is to refuse to boot. "A deployment that asked for tenant isolation and cannot have it must not serve traffic pretending otherwise — this is ADR-0049 applied to deployment configuration." The escape hatch is OS_ALLOW_DEGRADED_TENANCY=1.
The open reading, for the lane. D5's trigger names a failed load, and here the load is never attempted. Whether D5 covers "never attempted", or a separate refusal is owed, is the domain:cli seat's call (with an ADR note if D5's wording must widen; Prime Directive 13). Either way, a walled request that serves without its wall is the state D5 forbids.
Not measured
Whether the DEGRADED server answers data requests, and with what, has not been measured. With no auth service, the expected answer is a 401 everywhere, but no run has read it. The defect is that the boot does not refuse, whatever the server then answers.
Premises, each with a re-check
git grep -n "AuthPlugin skipped — set OS_AUTH_SECRET" origin/main -- packages/cli/src/commands/serve.ts→ 1 hit, expected before the fix.git grep -n "@objectstack/organizations" origin/main -- packages/cli/src/commands/serve.ts→ hits after that line only. Read the arm to confirm the load is inside the composing branch.- Control:
git grep -n "AUTH_SECRET: authSecret" origin/main -- packages/cli/src/commands/start.ts→ 1 hit (os startsupplies a secret).
Wanted
A bare os serve under a walled posture with no auth secret refuses to boot, exit non-zero, with an actionable message naming OS_AUTH_SECRET. It honours OS_ALLOW_DEGRADED_TENANCY=1 if the lane rules the escape hatch applies. Add a test pinning both. single and os dev are unchanged.
Dedupe
MCP search_issues, objectstack, open and closed: "walled posture os serve no OS_AUTH_SECRET AuthPlugin skipped DEGRADED organizations runtime not loaded fail fast ADR-0093 D5", 34 hits.
- None covers this path.
- The nearest are collaboration: authorization / tenancy-wall defect in record-discussion mention notifications under a walled posture (18.0 round-3 walled pass) — detail withheld pending maintainer #24785 (a different walled defect, in mention notifications), ADR-0131 D9 on a walled control plane: three framework writers land NULL organization_id rows on every sign-up (the self-registration grant, the verification mail, activity about org-less records) #24343 (NULL writers on a walled control plane), and cli: REST's direct-mount registrars, the runtime dispatcher plugin's declared-endpoint fallback and cloud-connection's raw-app plugins never consult the ADR-0069 authentication-policy gate #23190 / auth-gate parity: the remediation allow-list exempts the whole auth namespace, and the platform-admin mounts under it judge the caller without the ADR-0069 posture, so a held platform admin may reach user administration (read from source, not yet measured) #23335 (closed; auth-gate parity, not boot).
Dedupe words: walled posture no auth secret degraded boot; AuthPlugin skipped organizations not loaded; ADR-0093 D5 never attempted.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 20, 2026
Blocked-by: #24583
Serial note ·
domain:cliseat 2 (#22648) ·os-justin· sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T10:11Z · ⛔ not a claimWhy this waits. The epic lane's open PR #24845 (#24583, 18.x step 7 part 7b-3,
os-steve) adds a boot refusal topackages/cli/src/commands/serve.ts. Read on its head:A production
os servewhose stack enables auth but resolved no secret is refused before any plugin starts. The message namesOS_AUTH_SECRETand theos devfallback. That refusal is posture-blind. So this card's measured case (a bare productionos serve --no-uiundergroup/isolatedwith no auth secret, which came upDEGRADED) falls inside it. Building a second refusal on the same file now would duplicate that PR's rule and collide with it.Re-check when PR #24845 lands:
main.completedagainst that PR.OS_ALLOW_DEGRADED_TENANCY.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Unblocked ·
domain:cliseat 2 (#22648) ·os-justin· sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T12:07Z · ⛔ not a claimThe blocker named in
6288576973has landed: the epic lane's PR #24845 (#24583, part 7b-3) is merged.mainnow carries the refusal atpackages/cli/src/commands/serve.ts:4688:The warning at
:4166("AuthPlugin skipped — set OS_AUTH_SECRET …") still prints before it.What the next dispatch measures first (verify-or-fix):
main, then QA run QA run · walled:isolated+group (11/14) · 580fd4ed · 2026-10-20 · 8 PASS / 1 PARTIAL / 1 FAIL / 1 BLOCKED / 3 NOT-RUN #24783's two prerequisite combinations (bare productionos serve --no-ui, walled posture, no auth secret).OS_AUTH_SECRET, this card closescompletedagainst PR feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the__global__counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) #24845, with the measurement quoted.DEGRADED, the remainder is fixed here, along with the open reading on ADR-0093 D5 andOS_ALLOW_DEGRADED_TENANCY. One example is a stack whoseauthtier is off under a walled posture, whichauthCompositionmay not report asno-secret.Back to
pm:queue. This seat dispatches it when a slot frees.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 10 ·
domain:cliseat 2 · p2 · verify-or-fix (unblocked6290478642after PR #24845 landed) · claimed under exception 2 (filer-labelled; the lane fits the table, and the body carries the filing gate's three answers) · 2026-10-20T12:23ZSession:
session_01LmjU2x1faLZ39DVTFyVca6Account:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24807-walled-serve-no-secretWorktree:
objectstack-issue-24807Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main3fbef41b4; line numbers move; stop on a breach and explain it in the report):main, a bare productionos serve --no-uiunderOS_TENANCY_POSTURE=groupandisolated, with no auth secret, in the card's two prerequisite combinations (QA run QA run · walled:isolated+group (11/14) · 580fd4ed · 2026-10-20 · 8 PASS / 1 PARTIAL / 1 FAIL / 1 BLOCKED / 3 NOT-RUN #24783). PR feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the__global__counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) #24845's refusal (packages/cli/src/commands/serve.ts:4688,flags.server && !authComposition.composes && reason === 'no-secret') may already refuse every one of them. If it does, there is no code change: the dev reports the measurement, and this seat closes the cardcompletedagainst PR feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the__global__counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) #24845.DEGRADED: the fix is inpackages/cli/src/commands/serve.ts. A walled posture that cannot load its organizations runtime refuses to boot with an actionable message namingOS_AUTH_SECRET, andOS_ALLOW_DEGRADED_TENANCY=1is honoured if ADR-0093 D5's escape hatch applies. That goes with pins for both.singleandos devare unchanged.resolvePlatformAuthCompositionand PR feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the__global__counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) #24845's refusal text (the epic's); the organizations runtime itself;membership-policy-gate.ts; ADR text (if D5's wording must widen, that is reported, not edited);packages/spec/**.Container & model:
S,mode:subagent,model: default tier(dispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing)
Basis for the Clause-② line: if code changes, a boot that came up
DEGRADEDis refused, which narrows whatos serveadmits and widens nothing. Any code change owes oneCONTRACT_REVIEW_TIERreview of record before the queue. A measurement-only outcome owes none.Responsibility: this lane's CLI door (
packages/cliserve). The platform path that may already cover it is PR feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the__global__counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) #24845'sno-secretrefusal, which is measured first. It is reachable by any bare productionos serveunder a walled posture without a secret.Thread-read: 6290478642
Serial constraints cleared:
packages/cli/src/commands/serve.ts. All 10 open PRs were read in this act. PR feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the__global__counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) #24845 (serve.ts) has merged.migrate apply, the boot guard's birth half) and security: the CORS doors #24738 does not reach keep the old default (@objectstack/hono'screateHonoApp, loopback beside a configured list, all-host patterns) — family closure, detail withheld under the disclosure ruling #24809 (CORS doors,plugin-hono-server,adapters/hono,plugin-auth), are disjoint.objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24807,
"status": "done",
"branch": "claude/issue-24807-walled-serve-no-secret",
"pr": null,
"session": "session_01LmjU2x1faLZ39DVTFyVca6 — this run's harness-stamped id (subagent = parent's)",
"premise_still_valid": false,
"summary": "Outcome (A), already covered. On main 3fbef41 (no change to packages/cli/src, packages/core/src/stack-auth.ts, organizations, plugin-auth or plugin-hono-server between it and origin/main b167644), PR #24845's no-secret refusal (serve.ts:4688) refuses all four of the card's combinations. Each is a bare production
os serve --no-uifrom a full scratch copy of examples/app-showcase, declaring @objectstack/organizations, with no OS_AUTH_SECRET, AUTH_SECRET, BETTER_AUTH_SECRET or NODE_ENV, under group and isolated, with QA run #24783's two prerequisite combinations: (1) the app declares @objectstack/organizations, (2) OS_PLATFORM_OWNER_EMAIL, (3) OS_AUTH_MEMBERSHIP_POLICY. Each combination exits 1, never listens, never creates its database file, and refuses with a message naming OS_AUTH_SECRET.singlewith no secret gets the same refusal (it is posture-blind).os devunder walled isolated with no secret still boots through its development fallback: Server is ready, not DEGRADED, with Auth and Organizations composed. One door remains:--no-serverwith a stack-supplied HonoServerPlugin under a walled posture with no secret listens and comes up DEGRADED. Both server-on refusals are gated on flags.server. It is reported below as a finding and an open question, not fixed: the predicate is PR #24845's (a hold-off), and the dispatch's A/B rule is decided by the card's four combinations.","tests": "Build (under scripts/pm/os-verify-lock.sh, slot issue-24807-dev): NODE_OPTIONS=--max-old-space-size=4096 pnpm turbo run build --concurrency=2 --filter='@objectstack/example-showcase^...' --filter='@objectstack/organizations...' gave 'Tasks: 64 successful, 64 total', 'Cached: 0 cached', 'VERDICT command-exit 0', held 7m07s. Scratch app: a tar copy of examples/app-showcase (without node_modules, .objectstack or dist); every workspace dependency rewritten to link: the worktree package, plus '@objectstack/organizations': link to packages/plugins/organizations; pnpm install --ignore-workspace --prefer-offline exit 0. Per run: a fresh OS_HOME and OS_DATABASE_URL=file:RUN/data.db; OS_SECRET_KEY set to a locally generated 32-byte hex (a production serve needs a stable key; QA's scratch app had one persisted by earlier os dev boots), never echoed, and grep -c of its value over every run log = 0. Listen probe: lsof -iTCP:PORT -sTCP:LISTEN, polled every 200 ms. Its controls: a node listener on 48901 read 'yes', an idle 48902 read 'no', and 48901 after close read 'no'. Card combinations,
node packages/cli/bin/run.js serve --no-ui -p PORT: [group, 1+3 present, 2 absent] exit=1 ever_listened=no ready_line=no dbfile=absent; [group, 1+2, 3 absent] exit=1 no no absent; [isolated, 1+3] exit=1 no no absent; [isolated, 1+2] exit=1 no no absent. Each prints first '⚠ AuthPlugin skipped — set OS_AUTH_SECRET to enable authentication in production', then '✗ This stack enables auth, but no auth secret is set, so AuthPlugin was not composed and no caller can authenticate …' and '• set OS_AUTH_SECRET (a long random value, identical across restarts and nodes), e.g. OS_AUTH_SECRET=$(openssl rand -hex 32);'. single (OS_TENANCY_POSTURE=single, no secret): exit=1, never listened, the same refusal. os dev (run.js dev -p 48851, isolated, prerequisites 1+2+3, no secret, no OS_SECRET_KEY): '✓ Server is ready', 'Tenancy: isolated', Auth and Organizations in the plugin list, listened=yes, GET /api/v1/health 200, GET /api/v1/data/showcase_task 401 UNAUTHENTICATED; its listener child 31259 was recorded and the port was freed after the kill. Positive control (group, 1+2+3, with OS_AUTH_SECRET): '✓ Server is ready', 'Tenancy: group', Organizations loaded, listened=yes, health 200, data 401. Branch probes (group, 1+3, no secret): OS_ALLOW_DEGRADED_TENANCY=1 gives exit=1 with the same no-secret refusal (the escape hatch does not pass it); --preset minimal (auth tier off) gives exit=1 with A1's '✗ This stack mounts no auth, so no caller can authenticate …', never listened; --no-server on the stock showcase gives '⚠ Server is ready — DEGRADED: missing core services: auth' and 'Tenancy: group' with no listener (kernel only); --no-server with a scratch config adding a stack HonoServerPlugin gives listened=yes, '⚠ Server is ready — DEGRADED: missing core services: auth', 'Tenancy: group', 'Plugins: 45 loaded' without Organizations, GET /api/v1/data/showcase_task 401 UNAUTHENTICATED, GET /api/v1/health 404 ENDPOINT_NOT_FOUND and 'Seeds: not run this boot (multi-tenant-replay)'; adding --preset minimal to that gives listened=yes, the same DEGRADED line, and data 401. Premise checks at 3fbef41: 'AuthPlugin skipped — set OS_AUTH_SECRET' has 1 hit (serve.ts:4166); "authComposition.reason === 'no-secret'" has 1 hit (serve.ts:4688); the organizations load (Serve.ORGANIZATIONS_RUNTIME_PKG, serve.ts:4380) sits inside the composing arm; the control 'AUTH_SECRET: authSecret' has 1 hit in start.ts:455; the PR #24845 merge df8bb40 is an ancestor of origin/main (merge-base --is-ancestor exit 0). No ablation: no behaviour changed.","gates": "No diff, so no local gate subset is owed.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackexit=2: 'this branch changes nothing against origin/main (merge base 3fbef41) — nothing to derive'. No PR, so there are no check-runs to cite. pnpm lint was not run: the dispatch owes it only in outcome (B), on a final commit, and none exists.","mcp_calls": "0 — no MCP GitHub tool used (reads went through gh api REST GETs)",
"api_writes": "1 — this os-dev-report comment on #24807 (POST /repos//issues/24807/comments), sent through scripts/pm/post-stamped.mjs on the fleet-write relay; no git push, no pr_create",
"open_questions": [
{
"question": "The remaining walled door, measured on 3fbef41:
os serve CONFIG --no-ui --no-server, whose stack mounts its own HonoServerPlugin, under OS_TENANCY_POSTURE=group with no auth secret. It listens and comes up '⚠ Server is ready — DEGRADED: missing core services: auth', and Organizations is never loaded. Data answers 401 and no seed runs. Does #24807 close on PR #24845, or does this door keep a remainder? Axes. Business need, measured: no example, content/docs page or skill mounts HonoServerPlugin in anos servestack; the docs mount it only in programmatic kernels that also compose AuthPlugin. So the door has zero pull, and its harm reading is 401 with no write. Long-term soundness: the true predicate for serving traffic is a composed HTTP transport, which serve already computes (configHasHonoServer, resolveHttpServerComposition); flags.server is the inexact stand-in in both #24845's refusal and A1's. AI-error prevention: #24845's refusal text prescribesrun with --no-serveras a remedy, and an AI whose stack mounts its own server would follow it into a served walled boot with the wall off. Startup focus: no new gate either way.","options": [
"A — close #24807 completed against PR #24845 with this measurement; record the --no-server plus stack-server door as zero-pull in the close comment; no card",
"B — file the door as a sub-issue of #24807 for the epic lane: key #24845's and A1's refusals on a composed HTTP transport instead of flags.server (one condition each, posture-blind, no new message; it touches #24845's code, a hold-off for this lane)",
"C — a CLI-lane walled-only refusal under the 'never attempted' reading of ADR-0093 D5: refuse whenever a walled posture composes no auth, --no-server included, honouring OS_ALLOW_DEGRADED_TENANCY=1. It is a second message beside #24845's, it also refuses the kernel-only walled boot that serves nothing, and D5's wording (organizations 'fails to load') would need an ADR note"
],
"recommendation": "A now, with B named as the shape if a puller appears. The business axis measures zero pull and a 401/no-write harm. B is the long-term-correct predicate at its one site and closes the AI-follows-the-remedy path structurally. C widens D5 and duplicates a message for no measured need."
}
],
"out_of_scope_findings": [
"class: b · reach: public door measured —
node packages/cli/bin/run.js serve hono-variant.config.ts --no-ui --no-server -p 48861(the stock showcase plus a stack HonoServerPlugin, the shape serve.ts's resolveHttpServerComposition docblock documents), under OS_TENANCY_POSTURE=group, OS_AUTH_MEMBERSHIP_POLICY=invite-only and no auth secret, listens and prints '⚠ Server is ready — DEGRADED: missing core services: auth' and 'Tenancy: group'; Organizations is absent from the 45 plugins, and GET /api/v1/data/showcase_task answers 401 UNAUTHENTICATED; the same holds with --preset minimal · contract: ADR-0093 D5 'A deployment that asked for tenant isolation and cannot have it must not serve traffic pretending otherwise', and #24807's Wanted · Seam: spec:none (env OS_TENANCY_POSTURE, ADR-0093 D5) → runtime:packages/cli/src/commands/serve.ts refusals gated onflags.server(no-secret :4688, no-auth A1 :4666) | consumer: none · producer: none found (zero pull) · routing: inside #24807's Wanted ⇒ a sub-issue of #24807 if the seat keeps it (open_questions option B), else noted in the close comment · dedupe words: walled no-server stack HonoServerPlugin degraded; flags.server gate no-secret refusal; ADR-0093 D5 never attempted; walled kernel-only boot no auth secret","carrier: none · noted, not filed — on a walled posture with no secret,
os serve --no-serveron the stock showcase (no stack server) boots kernel-only, '⚠ Server is ready — DEGRADED: missing core services: auth', with no listener. This is PR #24845's stated design ('--no-server (kernel only) still boots with the warning'), and it serves no traffic"],
"line_budget": "0 lines changed; no governed, skills/** or ratcheted file touched",
"files_changed": [],
"deviations": [
"Rule 1's empty-branch push was not made. The dispatch's outcome (A) budget is no branch push, and no edit was ever made, so the rule's 'before any edit' trigger never arose. The local branch, created by
git worktree addand never pushed, is removed with the worktree after this report is posted; nothing reached the remote.","The first listen probe used
ss, which is absent in this container ('ss: command not found'), so the first round's ever_listened=no readings were not measurements. They were discarded, and all four combinations were re-run with the lsof probe after its positive and negative controls. One first-round run also hit 'Port 47814 is already in use' (not this session's process) and was re-run on a free port.","OS_SECRET_KEY (a locally generated 32-byte hex, never printed) was set on the production serve runs so that LocalCryptoProvider's key refusal could not pre-empt the auth branch. QA run #24783 had a persisted key file from earlier os dev boots instead; RUNNER.md's walled-boot entry names both as equivalent."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Already covered by PR #24845 (
df8bb40a0) · closing this carddomain:cliseat 2 ·os-justin· sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T12:52Z · claim6290756214→ report6291169262(measurement only, no branch pushed, no PR)main3fbef41b4os serveunder a walled posture with no auth secret refuses to boot, exit non-zerogroupandisolated, each with QA run #24783's two prerequisite sets) exit 1. None listens, and none creates its database file. The refusal is PR #24845'sno-secretarm (serve.ts:4688)OS_AUTH_SECRET✗ This stack enables auth, but no auth secret is set …andset OS_AUTH_SECRET …packages/cli/test/serve-no-auth-secret-refusal.integration.test.tspins the refusal and its control. The predicate does not read the posture, so a walled-only copy would pin the same line twice; none is addedOS_ALLOW_DEGRADED_TENANCY=1os devunchangedsingleunchangedsinglewith no secret too. That is the epic's ruled design, and this lane holds it offADR-0093 D5 needs no note: the refusal fires before the organizations load is reached, so "never attempted" no longer occurs on this path.
Remainder, noted and not filed:
os serve --no-serverwith a stack config that mounts its ownHonoServerPlugin, under a walled posture and no secret, still listensDEGRADED, and data answers 401. That is not a bareos serve, and the measured pull is zero: no example, docs page or skill mountsHonoServerPluginin anos servestack. If a puller appears, the fix is to key #24845's refusal and A1's no-auth refusal (:4666) on a composed HTTP transport rather thanflags.server. That is the epic lane's code. The call is put to the maintainer.Generated by Claude Code