Skip to content

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

Closed

Description

@objectstack-fleet

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 serve composes auth in its UI-portals block. With no OS_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=group or isolated and no auth secret, nothing loads the organizations runtime. Nothing checks OS_PLATFORM_OWNER_EMAIL, and the walled membership-policy gate never runs (membership-policy-gate.ts#assertWalledMembershipPolicyDeclared skips when there is no auth service).
  • The server comes up ⚠ Server is ready — DEGRADED: missing core services: auth and 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, with os serve --no-ui from 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 dev is not affected: it boots serve --dev, whose secret falls back to DEV_AUTH_SECRET_FALLBACK. os start is not affected either: start.ts passes a resolved or persisted secret to its serve child as AUTH_SECRET. The path is a bare production os 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 start supplies 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.

Dedupe words: walled posture no auth secret degraded boot; AuthPlugin skipped organizations not loaded; ADR-0093 D5 never attempted.


Generated by Claude Code

Activity

  1. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: #24583
    Serial note · domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T10:11Z · ⛔ not a claim

    Why this waits. The epic lane's open PR #24845 (#24583, 18.x step 7 part 7b-3, os-steve) adds a boot refusal to packages/cli/src/commands/serve.ts. Read on its head:

    if (flags.server && !authComposition.composes && authComposition.reason === 'no-secret') { throw … }
    

    A production os serve whose stack enables auth but resolved no secret is refused before any plugin starts. The message names OS_AUTH_SECRET and the os dev fallback. That refusal is posture-blind. So this card's measured case (a bare production os serve --no-ui under group / isolated with no auth secret, which came up DEGRADED) 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:


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked · domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T12:07Z · ⛔ not a claim

    The blocker named in 6288576973 has landed: the epic lane's PR #24845 (#24583, part 7b-3) is merged. main now carries the refusal at packages/cli/src/commands/serve.ts:4688:

    if (flags.server && !authComposition.composes && authComposition.reason === 'no-secret') { throw … }
    

    The warning at :4166 ("AuthPlugin skipped — set OS_AUTH_SECRET …") still prints before it.

    What the next dispatch measures first (verify-or-fix):

    Back to pm:queue. This seat dispatches it when a slot frees.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 10 · domain:cli seat 2 · p2 · verify-or-fix (unblocked 6290478642 after 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:23Z
    Session: session_01LmjU2x1faLZ39DVTFyVca6
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24807-walled-serve-no-secret
    Worktree: objectstack-issue-24807
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main 3fbef41b4; line numbers move; stop on a breach and explain it in the report):

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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-ui from 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. single with no secret gets the same refusal (it is posture-blind). os dev under 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-server with 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/objectstack exit=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 an os serve stack; 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 prescribes run with --no-server as 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 on flags.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-server on 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 add and 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

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Already covered by PR #24845 (df8bb40a0) · closing this card

    domain:cli seat 2 · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T12:52Z · claim 6290756214 → report 6291169262 (measurement only, no branch pushed, no PR)

    Wanted Reading on main 3fbef41b4
    A bare os serve under a walled posture with no auth secret refuses to boot, exit non-zero All four of the card's combinations (group and isolated, each with QA run #24783's two prerequisite sets) exit 1. None listens, and none creates its database file. The refusal is PR #24845's no-secret arm (serve.ts:4688)
    The message names OS_AUTH_SECRET Each run prints ✗ This stack enables auth, but no auth secret is set … and set OS_AUTH_SECRET …
    A test pins it PR #24845's packages/cli/test/serve-no-auth-secret-refusal.integration.test.ts pins 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 added
    OS_ALLOW_DEGRADED_TENANCY=1 Ruled not to apply. ADR-0093 D5's hatch admits a deployment that runs without its organizations runtime. It does not admit one where no caller can authenticate at all, and that is the boot refused here. Measured: the hatch does not pass the refusal
    os dev unchanged It boots through its development secret fallback: ready, not DEGRADED, Auth and Organizations composed
    single unchanged A deliberate difference from the card's wording: PR #24845 refuses single with no secret too. That is the epic's ruled design, and this lane holds it off

    ADR-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-server with a stack config that mounts its own HonoServerPlugin, under a walled posture and no secret, still listens DEGRADED, and data answers 401. That is not a bare os serve, and the measured pull is zero: no example, docs page or skill mounts HonoServerPlugin in an os serve stack. 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 than flags.server. That is the epic lane's code. The call is put to the maintainer.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions