Skip to content

Endpoint-route 401 in dispatcher-plugin mountRouteOnServer writes an inline flat body with no code key — the one producer #9487's constant change does not reach #9823

Description

@huangyiirene

Parent: #9487 (found during that card's Zone-2 producer sweep; filed unassigned for triage). The #9487 maintainer ruling — add code: "UNAUTHENTICATED" alongside the existing error / message keys, additive only — is the authority for the fix shape here too.

Observed

packages/runtime/src/dispatcher-plugin.ts mountRouteOnServer (the 401 arm at lines 213–221 on main @ 4c260cda5) writes an inline copy of the flat anonymous-deny body:

res.status(401);
// ...
res.json({
    error: 'UNAUTHENTICATED',
    message: 'Authentication is required to access this endpoint.',
});

It does not reference ANONYMOUS_DENY_BODY, so the #9487 constant change does not reach it. A repo-wide sweep for the deny message finds exactly two non-test producers: the @objectstack/core constant (fixed by #9487's PR) and this inline copy.

Impact

Service-declared endpoint routes (RouteDefinition emitted via hooks, e.g. buildAIRoutes() — any route with auth not explicitly false) answer an anonymous caller 401 { error, message } with no code key. Through @objectstack/client, err.code stays undefined for those 401s — the exact defect #9487 fixed for the REST enforceAuth seam. #9487's acceptance property ("one documented key identifies the machine code on every error family, 401 included") is not universal until this site moves.

Fix shape (per the #9487 ruling — additive only)

The file already imports from @objectstack/core, and the shared constant exists precisely so seams cannot drift: replace the inline literal with res.json(ANONYMOUS_DENY_BODY) (plus ANONYMOUS_DENY_STATUS for the status), or minimally add code: 'UNAUTHENTICATED'. Additive only — no key removed or moved.

Notes for the implementer


Generated by Claude Code

Activity

  1. added theissue type on Aug 19, 2026
  2. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage: lands in packages/runtime/src/dispatcher-plugin.ts (mountRouteOnServer 401 arm) → domain:cli; type Bug (declared property of the #9487 ruling — "one documented key identifies the machine code on every error family, 401 included" — not yet universal). Queued and blocked on the parent's in-flight PR.

    Blocked-by: #9487

    Rationale: the fix references the @objectstack/core constant that #9487's PR is changing (ANONYMOUS_DENY_BODY); landing this before that merge either duplicates the additive change or races it. Filed deliberately outside #9487's PR by that card's own producer sweep, so the merge will NOT cover this site — on unlock, re-verify the 213–221 line range on the merged ref, then dispatch. Fix shape is inherited from the maintainer's #9487 ruling (additive code key only; envelope convergence stays with ADR-0112 D5 / #9559).

    Dispatch notes: pin this arm's exact body (the existing integration test asserts the OTHER path's nested shape — re-derive which path serves what before writing pins, per the card). Dispatch-side Clause-② declaration should mirror #9487's own (response-body key addition). Size/model suggestion: S, mode:subagent.


    Generated by Claude Code

  3. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor

    Claim: PM loop round 24
    Session: session_01WeN7F6jQFpcqW2BN56RdPa
    Branch: claude/issue-9823-endpoint-route-401-code-key
    Worktree: objectstack-issue-9823
    Domain: domain:cli
    File surface: packages/runtime/src/dispatcher-plugin.ts — the mountRouteOnServer 401 arm ONLY (the if (route.auth !== false && !user) block, :213-222 on the merged ref) — plus a pin for that arm's exact body and .changeset/** (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: contract-review tier attempted first (the CONTRACT_REVIEW_TIER constant in scripts/pm/dispatch-gates.mjs); node scripts/pm/dispatch-gates.mjs --tier returns no path-derived mandate, ⛔ not recalled — the mandate is Clause ②, judged from card content, which the tool states it cannot reach
    Clause-②: yes — mirrors #9487's own declaration (adding a key to a published response body), exactly as triage advised
    Serial constraints cleared: packages/runtime/src/dispatcher-plugin.ts — no in-flight PR names it; #9746 landed (152bff8fcd) and its seam is elsewhere in the file. ⚠️ This claim HOLDS that file — sibling card #9813 (envelope the two discovery bodies at :738 / :764) is deliberately not dispatched alongside this one: same file is hard serial under the maintainer's 2026-08-18 ruling 「放开同包,同文件仍硬串行」, and the regions being ~520 lines apart does not change that. #9805 (packages/rest/src/rest-server.ts) is disjoint

    Unblocked — and the unlock re-verified, ⛔ not inherited

    Triage set Blocked-by: #9487 because the fix references ANONYMOUS_DENY_BODY, which #9487's PR was changing at the time. That PR (#9824) merged at 278280586d. ⇒ The blocker is discharged.

    Triage's own instruction on unlock was "re-verify the 213–221 line range on the merged ref, then dispatch." Done, on origin/main at 152bff8fcd — the inline literal is still there, still bare:

    :218      error: 'UNAUTHENTICATED',
    :219      message: 'Authentication is required to access this endpoint.',
    

    ⭐ And the unlock sharpens the card rather than weakening it: now that ANONYMOUS_DENY_BODY carries code, this inline copy is visibly inconsistent with the constant it was copied from. Two seams, one intended shape, and only one of them moved.

    Scope — the ruling is inherited, ⛔ not re-litigable

    The maintainer's #9487 ruling is the authority for the fix shape here: add code: "UNAUTHENTICATED" alongside the existing error / message keys, additive only — no key removed, no key moved.

    ⛔ Do NOT converge envelopes here. Whether this arm should answer the flat family at all is ADR-0112 D5 territory and #9559 stays open. The additive key is the whole scope.

    PM mechanism assumptions — measure; falsifying them is a good run

    • That replacing the inline literal with the shared ANONYMOUS_DENY_BODY (plus ANONYMOUS_DENY_STATUS) is the right shape. The card notes dispatcher-plugin.ts already imports from @objectstack/core, and the constant exists precisely so seams cannot drift — but verify the import and that the constant's status/headers behaviour matches this arm before swapping. If the swap changes anything beyond adding code, report rather than proceeding: additive-only is the ruling.
    • That this arm is the last producer. The Every 401 answers {error, message} with no code key, while every other error family carries {error, code} #9487 sweep found exactly two non-test producers repo-wide. Re-run that sweep on the merged ref with a control and report the intolerant count beside the tolerant one — if a third exists, that is a finding, not something to absorb.

    ⚠️ Pins: re-derive which path serves what before writing them. dispatcher-plugin.endpoint-fallback.integration.test.ts asserts a nested body.error.code — i.e. it exercises the http-dispatcher path, not this arm. ⛔ Do not assume it covers you; add a pin for this arm's exact body. The dogfood two-family classifier tolerates the additive key (measured on a real booted showcase in #9487's PR, 25/25).

    Gates

    Re-derive with node scripts/pm/dispatch-gates.mjs from the actually changed paths. ⚠️ Known gap, now three confirmed instances (#9721 / #9700): it names neither pnpm lint nor check:slot-lookup. Run pnpm lint regardless. ⛔ Never weaken a gate. Read verdicts from reported counts, ⛔ never a piped exit code.

    Report at draft-PR time — ⛔ do not wait on CI convergence. ⚠️ Landing this makes #9487's acceptance property universal, so Fixes #9823 is right for this card, but say explicitly in the report whether you believe #9487 can then close — that is my call, not yours.


    Generated by Claude Code

  4. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor
    {
      "issue": 9823,
      "status": "done",
      "branch": "claude/issue-9823-endpoint-route-401-code-key",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9848",
      "premise_still_valid": true,
      "summary": "Ruled additive fix landed at 7dffa23303: the mountRouteOnServer 401 arm now writes @objectstack/core's ANONYMOUS_DENY_BODY / ANONYMOUS_DENY_STATUS verbatim (only wire delta: the code key — status 401 and message verified byte-identical before swapping), with a new pin spelling this arm's exact flat body literally; sibling card #9813's discovery bodies untouched. Both Zone-2 assumptions confirmed by measurement: the swap is exactly additive, and this arm was the last producer — sweep on the merged ref found 2 non-test producers by exact message and by tolerant prefix (core constant + this arm), 1 by inline error-literal (this arm), 0 on the typo control. Reachability sharpened during pin re-derivation: registerAIRoutes wildcards cover get/post/delete/put only, so PATCH routes under /ai/* and any non-/ai emitted path reach this arm unshadowed on the wire. On #9487: I believe it can now close once PR #9848 merges — the measured non-test producer count of codeless flat 401s drops to zero, making the ruled acceptance property universal — but that call is the PM's; no closing keyword was used for #9487 anywhere.",
      "tests": "Union run at final commit 7dffa23303 (no push since). New pin dispatcher-plugin.route-auth-deny-body.test.ts: 'Test Files 1 passed (1) / Tests 4 passed (4)'. Full @objectstack/runtime suite: 'Test Files 176 passed (176) / Tests 2621 passed (2621)'; pnpm --filter @objectstack/runtime typecheck exit 0. Reverse verification from the committed state, predicted direction red: git restore --source=origin/main on the source file gave 'Tests 2 failed | 2 passed (4)' — exactly the two body pins (AssertionError on the missing code key), behaviour tests stayed green; restored from HEAD, porcelain clean, 4/4 green again; no ablation rebuild owed — the pin imports ./dispatcher-plugin.js as same-package sibling source compiled by vitest, never through a dist. Derived gates via node scripts/pm/dispatch-gates.mjs with no hand-fed paths, all green with reported counts: adr-0087 ('adds no declared-breaking changeset'), changeset-no-major, empty-changeset, changeset-gate-self-tests (212+116 assertions), objectui-changeset, cross-package-test-inputs both spellings ('12 package(s) ... all declared'), affected-docs (242 cases), query-options-erasure ('240 site(s), no files added'), type-check-coverage (64/77), type-check-debt --re-measure after full packages-closure turbo build 70/70 ('none above its recorded number'), engine-double-contract ('321 pinned'), where-matcher ('none new'). Mandated pnpm lint: eslint exit 0. check:nul-bytes: 'OK (scanned 6280 text file(s))'.",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #9849: core anonymous-deny.ts docstring sentence 'Exactly one consumer writes it' reads false once this PR lands (this arm becomes the second verbatim writer) — one-sentence docs update, Blocked-by #9823, finding label, unassigned"]
    }

    Generated by Claude Code


    Generated by Claude Code

  5. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor

    ACCEPT — PR #9848

    domain:cli seat, session session_01WeN7F6jQFpcqW2BN56RdPa, round 24. Reviewer of record; checked against GitHub and the tree, ⛔ not against the report's self-description.

    ⛔ First, a correction I owe this dev

    I recorded that its report never arrived and probed it as a possible death. That was my error. The report was posted to #9823 at 04:45Z with the os-dev-report marker, read back and verified — exactly where the contract puts it. Only the terminal-message copy was lost, which the contract explicitly treats as the accelerator, not the record.

    ⭐ The rule I skipped is written down: 收集先扫 GitHub,标记评论在 = 报告完整,两处皆无才进探活. I checked one channel, found it empty, and jumped to the liveness step. ⚠️ And a second thing I had been over-reading: SendMessage returning "Resuming agent" does not mean the agent was dead — it means not currently running, which is equally true of an agent that finished correctly. It does not discriminate; the report on GitHub does.

    Checklist

    The three things I checked rather than accepted

    ① Additive-only was verified before the swap, not asserted after. ANONYMOUS_DENY_STATUS = 401 matches the arm's hardcoded status; ANONYMOUS_DENY_MESSAGE byte-identical to the inline message; the constant carries no header behaviour so the securityHeaders loop is untouched; the import already existed. ⇒ The only wire delta is code. That is the ruling, met exactly.

    ② The "last producer" claim carries four sweeps and a control. Exact-message grep → 2 non-test producers; tolerant prefix → same 2; inline error: 'UNAUTHENTICATED' literal → 1 (this arm; the other hit read as a core docstring, not a producer — read, not counted); typo control → 0. ⇒ No third producer.

    ③ The pin re-derived which path serves what, and the re-derivation found the discriminator. The existing dispatcher-plugin.endpoint-fallback.integration.test.ts asserts a nested body.error.code — the http-dispatcher wildcard path, a different 401 producer on the wrapped family. ⛔ It never covered this arm. The new pin drives the concrete hook-route mounts, and reachability is established rather than assumed: registerAIRoutes wildcards cover get/post/delete/put only, so a PATCH route reaches this arm unshadowed — which is why the test uses PATCH.

    ⭐ The pin spells every key literally, deliberately NOT via the constant, and a second test separately asserts the body equals the constant. That is the right split: a pin written only through the constant would follow it silently and never notice a drift, which is the exact defect this card is about.

    Out of scope, filed

    #9849 — anonymous-deny.ts's docstring says "Exactly one consumer writes it", which this PR makes false (a second verbatim writer appears). ⭐ Catching that your own change invalidates a sentence elsewhere is the kind of thing nothing mechanical checks.

    Verdict

    ACCEPT. All six required green at 7dffa23303. Flipping ready and enqueuing.

    ⇒ On merge, Fixes #9823 closes this card, and per my round-23 ruling (option A) #9487 closes too — the measured count of codeless flat-401 non-test producers reaches zero, making its acceptance property universal. The dev flagged that conclusion and correctly left the call to me.


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions