Repository navigation
Every 401 answers {error, message} with no code key, while every other error family carries {error, code} #9487
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage: lands in the 401 producer behind
enforceAuthinpackages/rest⇒domain:cli; queued as Bug — the 401 family is the one error family whose body carries nocodekey, which already cost measurable ledger coverage (#9333's c0/c3partial). Preferred direction: addcode: "UNAUTHENTICATED"alongside the existing keys (additive, aligns the outlier family to the{error, code}shape the other six teach) rather than declaring the divergence in the ledger — declaring an exception would make every client carry a special case forever. Clause-② flag: the fix touches a public wire shape (adds a key to every 401 body) — claim declaresClause-②: yes, dispatch atCONTRACT_REVIEW_TIER. Pin the property the card names: one documented key identifies the code on every error family, 401 included. (Triage seat, sessionsession_019gCKd9EZfQ6MbGTnMHHJvW.)
Generated by Claude Code
Claim: PM loop round 20
Session:session_012WKSnqAaoqtW3QX7SSf1Vk
Branch:claude/issue-9487-401-error-code-key
Worktree:objectstack-issue-9487
Domain:domain:cli
File surface (region-level, ⛔ not line numbers — this file has taken three landings today and every line number has moved):packages/rest/src/rest-server.ts— the 401-emitting auth-refusal region only (enforceAuthand whatever else emits a 401 body), plus its pin and.changeset/**. ⛔ Not the/metaread doors #9454 just changed; ⛔ notorganizationIdForMetaWrite's write sites; ⛔ not the audit limb #9426 changed.
Container & model:S–M,mode:subagent,model: opus— tier derived this round fromnode scripts/pm/dispatch-gates.mjs --tier packages/rest/src/rest-server.ts: no path-derived mandate (floor sonnet · default opus · ceiling fable), ⛔ not recalled.Clause-② and the downgrade, stated precisely. Adding a
codekey to every 401 body changes the shape of a published error family.⚠️ That is Clause-② territory judged from content, and it would ordinarily be fable-mandatory. This card is one of the five the maintainer named in the one-time downgrade toopus(#9446, #9416, #9462, #9488, #9487) — which is precisely why that authorisation exists. ⇒opus, authorised, ⛔ does not spread beyond those five, ⛔ reverts to default when Fable recovers.Why this card and not #9488
#9488 and #9487 both land in
packages/rest/src/rest-server.ts, so they are hard serial against each other — same file, and the maintainer's exemption explicitly kept same-file serialisation. Only one could go.Under the lane's total order they tie at every level: both
pm:queue, both typeBug, neither on atarget:board, neitherpm:blocking, neitherpriority:p0. The tie-break is card age, and it is uncomfortably fine:- Every 401 answers
{error, message}with nocodekey, while every other error family carries{error, code}#9487 — created2026-08-18T04:09:08Z GET /api/v1/meta/<unknown-type>answers 200 with an empty collection while the write door refuses the same type #9488 — created2026-08-18T04:09:19Z
Eleven seconds. I am taking #9487 because that is what the rule says, and stating the margin rather than dressing it up as a judgement — an eleven-second age gap is not a reason, it is a tie-breaker doing its job. #9488 goes next round, unless something outranks it by then. ⛔ It is not blocked and ⛔ not on hold; it is queued behind a same-file sibling.
Premise check
⚠️ rest-server.tstook three landings today — #9426 (audit limb,ed4ca5999), #9454 (/metaread doors,b6c769019), and it is the same file #9488 waits on. Any line number in this card's body is stale by construction. The dispatch declares surface by symbol and region and instructs re-derivation on the currentmain.Serial constraints cleared:
- Same file —
rest-server.tsis clear as ofb6c769019: Org-overridable metadata (view,dashboard) is accepted with a 200state:'active'receipt but served by no read door #9454 merged (verified two readings plus probeorganizationIdForMetaRead, 0 → 9 occurrences across 3 files).GET /api/v1/meta/<unknown-type>answers 200 with an empty collection while the write door refuses the same type #9488 is the one card still waiting on this file and is deliberately not dispatched. - Same package — parallel authoring permitted under the 2026-08-18 exemption; four replacement rules carried into the dispatch.
- Dispatched alongside
os init -t appscaffold does not compile on CLI 17.0.0 — the template's own object trips thesecurity-owd-unsetauthor-time rule #9666 (packages/cli/src/commands/init.ts) — different package, different file, no read coupling.
Generated by Claude Code
- Every 401 answers
{ "issue": 9487, "status": "needs_decision", "branch": "claude/issue-9487-401-error-code-key", "pr": null, "premise_still_valid": true, "summary": "The defect is real and I reproduced its cause, but the PM's file-surface premise is falsified: the 401 body is NOT local to rest-server.ts. It is ANONYMOUS_DENY_BODY, a constant in packages/core/src/security/anonymous-deny.ts:85, written verbatim by enforceAuth at rest-server.ts:1146 — the single site reached from all 69 enforceAuth call sites, i.e. every /data, /meta and /reports family the card names. Its flat shape is not an accident: three packages independently document it as one of exactly TWO live, ADR-0112-amendment-sanctioned (#4007, 2026-07-30) envelopes, and all three assign its retirement to the envelope-convergence line, which is the still-open tracking card #9559 — whose own text records that whether the flat position is RETIRED or PERMANENTLY ACCEPTED is 'the open question ADR-0112 D5 leaves'. I did not write the fix, because ZONE 1 (1) and ZONE 1 (2) are in direct conflict here and I measured the conflict rather than guessing: the nested envelope ZONE 1 (1) prescribes is, for THIS family, the third dialect the repo has an executable classifier to reject (packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts declares a closed two-family world and fails when a body satisfies neither), so landing it requires rewriting that classifier — which is the gate-weakening ZONE 1 (2) forbids. The issue-comment triage ruling (add code alongside, flat) and the dispatch ruling (converge onto the nested shape) select two different architectures and cannot both be honoured; the choice is ADR-0112 D5's open question and belongs to the maintainer. Measured consumer harm, which both options fix: packages/client/src/index.ts:4959-4961 reads errorBody?.code then errorBody?.error?.code, and the REST 401 satisfies NEITHER, so err.code is undefined for every authentication failure through the official SDK. No out-of-scope cards filed: everything found is already owned by an open card (see out_of_scope_findings).", "enumerated_401_emission_sites": { "measured_on": "origin/main b6c769019 (card line numbers are stale by construction; these are re-derived)", "grep_shapes_used": [ "grep -nE '(^|[^0-9])401([^0-9]|$)' packages/rest/src/rest-server.ts — literal statuses, digit-bounded so 4010/1401 cannot match", "grep -nE '\\.status\\s*\\??\\s*\\(' packages/rest/src/rest-server.ts — OPTIONAL-CALL TOLERANT: the '\\??' between the member name and '(' matches both '.status(' and '.status?.(', which is the shape the brief warned about; the obviously-non-401 literal statuses were then filtered out and every remaining dynamic-status site resolved by hand", "grep -nE 'enforceAuth\\s*\\??\\s*[(<]' packages/rest/src/rest-server.ts — same optional-call-tolerant shape for the gate's call sites (69 hits)", "grep -rn 'ANONYMOUS_DENY_BODY|ANONYMOUS_DENY_STATUS' --include=*.ts packages/ — cross-package, which is what exposed that the body lives in packages/core" ], "sites": [ { "id": "S1", "where": "packages/rest/src/rest-server.ts:1146, in enforceAuth's anonymous-deny arm", "emits": "res.status(ANONYMOUS_DENY_STATUS).json(ANONYMOUS_DENY_BODY) => { error: 'UNAUTHENTICATED', message: '...' }", "code_key": "ABSENT — this is the card's site, and the only site whose body matches the card's observed sample", "reach": "the 69 enforceAuth call sites: all of /data, /data/:id, /meta/*, /reports, /security, ...", "note": "the body is an IDENTIFIER, so it is invisible to both check:route-envelope dialect counters" }, { "id": "S2", "where": "packages/rest/src/rest-server.ts:4403, the book/docs audience gate", "emits": "sendDeclaredFault(res, { code: 'UNAUTHENTICATED', message: 'This documentation requires sign-in', status: 401 }) => flat { error: the-human-message, code }", "code_key": "present, FLAT position; body.error.code still reads undefined" }, { "id": "S3", "where": "packages/rest/src/rest-server.ts:4964, the second book door", "emits": "identical to S2", "code_key": "present, FLAT position" }, { "id": "S4", "where": "packages/rest/src/rest-server.ts:8690, /security/explain", "emits": "respondError(res, 401, 'UNAUTHORIZED', ...) -> sendEnvelopeError -> { success: false, error: { code, message } }", "code_key": "present, NESTED — already conformant" }, { "id": "S5", "where": "packages/rest/src/rest-server.ts:8822, /security/my-delegable-scope", "emits": "respondError(res, 401, 'UNAUTHORIZED', ...) -> sendEnvelopeError", "code_key": "present, NESTED — already conformant" } ], "dynamic_status_sites_checked_and_excluded": [ "denial.status (rest-server.ts:1209, 10138) — apiAccessDenialFromEnable emits 404/405 only", "prep.status (6724, 6818) — import-prepare.ts emits 400/413 only", "mapped.status (6314, 6373, 6428, 6490, 6568, 6615, 7651, 8143, 8320) — mapDataError has no 401 arm; grep for 401 in error-response.ts returns zero. A 401 can only appear as passthrough of a producer-DECLARED status, which no auth-refusal site does", "envelopeStatus (8502) — analytics 4xx passthrough of a producer-declared envelope" ], "zone2_assumption_result": "CONFIRMED that there is more than one site and that they do not all go through one helper — but REFINED in a way that makes the card smaller than it reads: only S1 carries the card's observed body. S2-S5 already carry a code. So 'every 401 has no code key' is really 'the anonymous-deny 401 has no code key' — which is nevertheless the 401 that covers essentially the whole API." }, "ratchet_pair": { "gate": "pnpm check:route-envelope, packages/rest/src/rest-server.ts, dialectOnly entry owned by ratchet #9559", "before": "stringError 44, siblingCode 69 — measured on origin/main b6c769019", "after_ablated_nested_change": "stringError 44, siblingCode 69 — UNMOVED", "reading": "the nested literal fires neither counter (its top-level 'error' is an object, and there is no top-level 'code'), and the site it replaces was an identifier, also invisible to both. So ZONE 1 (1) is satisfied on the letter — no ratchet ticks up — but nothing ticks DOWN either: this change banks no ratchet progress. The flat sibling-code literal the triage comment prefers WOULD tick siblingCode 69 -> 70 if written at the .json() call site; written instead into the packages/core constant it would leave the counter at 69 while putting the same dialect on the wire, which I flag as counter-gaming rather than propose." }, "registered_error_code": { "code": "UNAUTHENTICATED", "declared_at": "packages/spec/src/api/errors.zod.ts:69 (StandardErrorCode enum), plus HttpStatusErrorCodeMap 401 -> 'UNAUTHENTICATED' at packages/spec/src/api/errors.zod.ts:174", "already_bound": "packages/core/src/security/anonymous-deny.ts:39 exports it as ANONYMOUS_DENY_CODE", "spec_change_needed": false, "zone1_3_result": "SATISFIED — a standard member already exists, no synonym minted, packages/spec untouched", "related": "S4/S5 emit 'UNAUTHORIZED', which is a REGISTERED and explicitly WAIVED grandfathered synonym of UNAUTHENTICATED (packages/spec/src/api/error-code-ledger.zod.ts:752-757, 'Wire value kept; consolidation deferred per #8211'). Known and owned, not a new finding." }, "consumer_sweep": { "query": "grep -rnE \"(toBe|toEqual|toContain)\\(\\s*['\\\"]UNAUTHENTICATED['\\\"]\" --include=*.ts . | grep -v node_modules => 28 hits", "control": "same regex with PERMISSION_DENIED => 114 hits, proving the shape matches (a zero here would have meant a broken query reading as a clean sweep)", "broken_first_attempt_reported_because_this_is_the_trap": "my first query was \\.error[^.]{0,40}(toBe|toEqual)\\( ... which returned ZERO for the subject AND zero for the control, because [^.] excluded the '.' inside ').toBe('. The control is the only reason I caught it; the subject query alone exited 1 and would have read as 'no consumer depends on this'.", "result": { "read_the_code_out_of_TOP_LEVEL_error (would break under a nested move)": [ "packages/rest/src/rest-meta-auth.test.ts:49 — expect(state.body?.error).toBe('UNAUTHENTICATED')", "packages/rest/src/security-routes.test.ts:99 — expect(res.body.error).toBe('UNAUTHENTICATED')" ], "read error.code (other seams, unaffected)": "the other 26 hits, all in packages/runtime, packages/cloud-connection, packages/services/service-datasource and the dispatcher-side dogfood suites", "official_SDK": "packages/client/src/index.ts:4959-4961 — errorCode = asSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code). The REST 401 satisfies NEITHER, so err.code is undefined on every authentication failure. Its own comment names the two live spots and says 'the flat shape's retirement belongs to the envelope-convergence line (#3843)'. This is the measured business cost, and BOTH candidate fixes remove it with no SDK change." }, "zone2_assumption_FALSIFIED": "'no consumer asserts the ABSENCE of code on a 401' is false. Three assert it by STRICT equality: packages/core/src/security/anonymous-deny.test.ts:52 (expect(ANONYMOUS_DENY_BODY).toEqual({ error: 'UNAUTHENTICATED', message: expect.any(String) })); packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts:375-378 (expect(body).toEqual({ error: 'UNAUTHENTICATED', message: '...' })); and the same file:472 (expect(flat).toEqual(ANONYMOUS_DENY_BODY))." }, "the_blocker_reported_not_weakened": { "gate": "packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts (#5632), a required 3-shard gate per its own comment", "what_it_declares": "a CLOSED two-family world for anonymous 401s. isRestFlatDeny requires typeof body.error === 'string' AND no 'success' key; isDispatcherWrapperDeny requires body.success === false AND error.httpStatus to be a number. declaredFamiliesOf(body) must equal exactly the seam's declared family, and its own comment states that an empty result means 'a third dialect reached the wire'.", "consequence": "the nested { error: { code, message } } that ZONE 1 (1) prescribes satisfies NEITHER predicate — for THIS family it IS the third dialect the classifier exists to reject. sendEnvelopeError's { success: false, error: { code, message } } also fails isDispatcherWrapperDeny (no httpStatus). The only nested shape that is not a third dialect is the full dispatcher wrapper, and that flips the seam's family, which the DENIED_SEAMS rows declare red as 'this seam changed family'. Every nested option therefore requires rewriting the classifier — i.e. weakening the gate ZONE 1 (2) forbids weakening.", "why_the_9426_precedent_does_not_transfer": "PR #9637 / #9426 converged a /meta 501 — a site with no cross-package constant, no two-envelope declaration and no dogfood classifier over it. Its commit message is explicit that it chose nested to avoid ticking the ratchet. That reasoning is sound for a 501; the anonymous-deny 401 additionally carries an ADR-0112 amendment and an executable family classifier, which the 501 did not." }, "reverse_verification": { "method": "committed a throwaway ablation on the branch (never pushed), measured, then git reset --hard origin/main; git diff origin/main is empty and rest-server.ts:1146 is restored byte-identically", "prediction_written_before_running": "(1) rest-meta-auth.test.ts:49 and security-routes.test.ts:99 go RED because body.error becomes an object where a string is pinned; (2) core anonymous-deny.test.ts stays GREEN because it pins the CONSTANT, which the ablation does not touch; (3) the ratchet pair stays 44/69 because the nested literal fires neither counter", "actual": { "packages/rest": "Test Files 2 failed (2) | Tests 2 failed | 17 passed (19). Both failures: 'AssertionError: expected { code: 'UNAUTHENTICATED', ...(1) } to be 'UNAUTHENTICATED' // Object.is equality' — the reasons match the prediction, not merely the totals", "packages/core": "Test Files 1 passed (1) | Tests 8 passed (8) — green as predicted, for the predicted reason", "ratchets": "stringError 44, siblingCode 69 — unmoved as predicted", "baselines_for_comparison": "before the ablation, on b6c769019: rest 19 passed (19), core 8 passed (8), ratchets 44/69" }, "not_measured": "the dogfood suite showcase-anonymous-deny-surfaces.dogfood.test.ts was NOT executed — it needs a booted showcase. Its verdict in 'the_blocker' above is READ FROM THE PREDICATE SOURCE, not run, and is declared not-measured. The two executed rest failures do independently establish the load-bearing fact it turns on (body.error stops being a string).", "pipe_hazard_noted": "the vitest runs were piped, so the shell EXIT was 0 while the run had failed; the verdict above is read from 'Tests 2 failed' and ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL in the output, never from the masked exit code" }, "gate_union": { "derivation": "node scripts/pm/dispatch-gates.mjs run against the CANDIDATE change surface (rest-server.ts, core/security/anonymous-deny.ts + its test, the dogfood suite, the two rest pins, a changeset) => 18 families matched, 31 undetermined, 61 silent", "matched": [ "pnpm check:authz-resolver", "pnpm check:changeset-gate-self-tests", "pnpm check:cross-package-test-inputs", "pnpm check:dispatcher-error-vocabulary", "pnpm --filter @objectstack/spec run check:empty-state", "pnpm check:kernel-hook-pairs", "pnpm --filter @objectstack/spec run check:liveness", "pnpm check:objectui-changeset", "pnpm check:route-envelope", "pnpm --filter @objectstack/spec run check:strictness-ledger", "pnpm check:test-source-alias", "pnpm check:type-source-resolution", "pnpm --filter @objectstack/spec run check:variant-docs", "node scripts/check-adr-0087-registration.mjs", "node scripts/check-changeset-no-major.mjs", "node scripts/check-cross-package-test-inputs.mjs", "node scripts/check-empty-changeset.mjs", "node scripts/docs-audit/check-affected-docs.mjs" ], "convention_triggered": "adds/edits a test file => check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:engine-double-contract, check:where-matcher", "brief_assumption_falsified": "the brief called check:error-code-casing and check:error-status-conformance 'near-certain'. Neither matched this path set — they are in the undetermined/silent residue. Reported per the instruction not to rely on the brief's list.", "actually_run": "ONLY check:route-envelope, twice (before and under the ablation): green both times, 44/69 both times. Every other family in the union is NOT RUN — there is no diff to run them against, and a derived list is not a result." }, "open_questions": [ { "question": "ADR-0112 D5's open question, which #9559 names as its own exit condition, has to be ruled before #9487 can land: is the REST seam's FLAT 401 position retired, or permanently accepted? The issue-comment triage ruling (add `code` alongside — flat) and the dispatch ZONE 1 (1) ruling (converge onto the nested ADR-0112 position) select opposite answers, and no implementation satisfies both.", "options": [ "A — ACCEPT the flat position for this family, and fix the card additively: add code: 'UNAUTHENTICATED' to ANONYMOUS_DENY_BODY (packages/core/src/security/anonymous-deny.ts:85), update the two strict pins (core anonymous-deny.test.ts:52, dogfood :376), changeset. Cost: touches packages/core (outside the declared surface) and adds one more instance of a dialect that may later be retired. Benefit: purely ADDITIVE — no existing consumer breaks, the official SDK starts reporting err.code with no SDK change (it reads errorBody?.code FIRST), the outlier family matches the other six TODAY, and the dogfood family classifier stays valid (isRestFlatDeny tolerates extra keys). Small: 1 constant, 2 pins, 1 changeset. Reversible when the convergence lands.", "B — RETIRE the flat position, converging this seam onto the nested/wrapper envelope. This is #9559's assignment executed on its highest-traffic seam. Cost: a BREAKING wire change on a published error family; requires rewriting the #5632 two-family classifier (the gate ZONE 1 (2) forbids weakening), retiring ANONYMOUS_DENY_BODY and its docstring in packages/core, correcting the matching docstrings in packages/runtime/src/endpoint-policy.ts and packages/client/src/index.ts, updating the published 401 shape in content/docs, and updating the two rest pins I measured going red. Surface spans packages/core + packages/rest + packages/qa/dogfood + docs — far outside this card's declared 'rest-server.ts 401 region plus a pin'. Benefit: it is the correct end state and banks real convergence.", "C — Land nothing on #9487 now; re-file it as an increment of #9559 once the D5 ruling exists, and let the ruling decide whether that increment is A-shaped or B-shaped." ], "recommendation": "C, and if the maintainer wants a fix this round, A — never B on this card. Three axes. REAL BUSINESS NEED: the measured cost is err.code === undefined on every 401 through the official SDK; A removes it entirely, with zero consumer breakage and zero SDK change, so the business need does not require B. LONG-TERM SOUNDNESS FOR THIS PROJECT: B is the correct end state, but whether to take it is precisely the question ADR-0112 D5 left open and #9559 was filed to own; executing it on one seam from a card scoped to one region pre-empts a maintainer ruling and, mid-transition, leaves THREE live 401 positions instead of two — strictly worse than either endpoint. Startup-focus also argues against paying a breaking-wire-change cost for a repositioning that delivers no capability. HARD TO GET WRONG FOR AI AUTHORS: this is the axis that most favours B in the abstract — two live positions are what invite the tolerant `body.error?.code ?? body.error` chains this repo bans by name — but the same axis condemns B done piecemeal, because a half-converged family is exactly what makes a tolerant chain feel necessary. A keeps the count at two and makes the outlier consistent; B done properly, in one card, with the classifier and the docs moved together, takes it to one. The axes therefore agree on the sequencing: rule D5 first, then land the whole convergence as one reviewed change, and do not let a point fix decide a contract question by accident." }, { "question": "Harness/dispatch conflict to flag (nothing was pushed, so nothing shipped): the brief says no model identifier in any pushed artifact and to keep `Co-Authored-By: Claude`, but the harness default commit trailer is `Co-Authored-By: Claude` followed by a model name, which contains one.", "options": ["Follow the brief and write `Co-Authored-By: Claude`", "Follow the harness default"], "recommendation": "Follow the brief, as instructed, and flag it here — which is what this entry does. No commit was pushed on this card, so no artifact carries either spelling." } ], "out_of_scope_findings": [ "NONE FILED, deliberately, per 'search before filing' and 'attach, do not scatter'. Everything found is already owned by an open card: (a) S2/S3 — the two book-audience 401s emit the flat { error: the-human-message, code } dialect, so body.error.code reads undefined there too; that is exactly the convergence #9559 owns, and error-response.ts's own docstring states that routing sendDeclaredFault through the envelope writer would also re-open the #5437 5xx-prose leak class, so it is explicitly NOT a drive-by. (b) S4/S5 emit code 'UNAUTHORIZED' on a 401 where the platform standard is 'UNAUTHENTICATED'; that is a registered, waived, grandfathered synonym with consolidation deferred per #8211. (c) The flat bodies flowing through sendDeclaredFault are counted by NEITHER file's route-envelope ratchet (the literal is passed to a helper, and the helper's own .json() takes an identifier) — this is stated openly in the gate's own comments and in the error-response.ts table entry, so it is documented, not drift. Filing any of these would duplicate an existing open card." ] }Generated by Claude Code
Generated by Claude Code
Returned to
needs-user-decision— ⛔ this seat cannot rule it, and my own dispatch brief was self-contradictorydomain:cliseat, sessionsession_012WKSnqAaoqtW3QX7SSf1Vk, PM loop round 20. Paired with the label write (pm:dispatched→needs-user-decision). Dev returnedstatus: needs_decision, no PR, zero commits — it stopped rather than guessing, which is the correct outcome.⛔ My error first: the two Zone 1 rulings I issued cannot both be satisfied
My brief said (①) converge onto the sibling ADR-0112 envelope, and (②) never weaken a gate. On this family those are mutually exclusive, and I did not know it when I wrote them.
I verified the mechanism myself rather than taking the report.
packages/core/src/security/anonymous-deny.ts, in its own doc comment:"Both shapes are pinned against a real booted showcase by
packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts, which classifies every anonymous 401 into exactly one of the two families and fails on a third dialect or on a seam that changes family."⇒ For this family the nested envelope ① prescribes is the third dialect that classifier exists to reject. Landing it requires rewriting the classifier — the gate-weakening ② forbids. The dev measured the conflict and stopped; it did not pick the ruling it preferred.
⛔ My file surface was also wrong
I declared
packages/rest/src/rest-server.ts, "the 401-emitting auth-refusal region only". The body is not local to that file. It isANONYMOUS_DENY_BODY, a constant atpackages/core/src/security/anonymous-deny.ts:85, written verbatim atrest-server.ts:1146and reached from all 69enforceAuthcall sites. Confirmed onorigin/main.The enumeration, and a refinement of the card's own title
Five 401 emission sites on current
main, re-derived (⛔ not the card's stale line numbers):site shape coderest-server.ts:1146enforceAuthANONYMOUS_DENY_BODY— flat,erroris a string⛔ absent :4403,:4964book/docs gatessendDeclaredFault— flat{ error: message, code }present, flat :8690,:8822/security/*sendEnvelopeError— nestedpresent, nested — already conformant ⇒ "Every 401 answers
{error, message}with nocode" is more precisely "the anonymous-deny 401 does" — which is nonetheless the 401 covering essentially the whole API.The measured cost — and both candidate fixes remove it
packages/client/src/index.ts:4959-4961readsasSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code). The flat 401 satisfies neither: no top-levelcode, anderroris a string soerror.codeisundefined.err.codeisundefinedfor every authentication failure through the official SDK. Verified by reading both the SDK and the constant.⛔ Why this is not mine to adjudicate
The real question is ADR-0112 D5's open question — is the REST seam's flat 401 position retired or permanently accepted? — which the tracking card #9559 names as its own exit condition. Two rulings on this card already select opposite answers: the triage comment says add
codealongside (flat); my dispatch said converge (nested).That is an ADR-class contract decision on a published error family, and option B is a breaking wire change. ⛔ Human floor. This seat does not rule it, and ⛔ must not let a point fix settle a contract question by accident.
The options, as measured by the dev
- A — accept the flat position for this family, fix additively. Add
code: 'UNAUTHENTICATED'toANONYMOUS_DENY_BODY; update the two strict pins; changeset. Purely additive: no consumer breaks, the SDK starts reportingerr.codewith no SDK change (it readserrorBody?.codefirst), the classifier stays valid (isRestFlatDenytolerates extra keys). Touchespackages/core. Reversible when convergence lands. - B — retire the flat position, converge this seam. The correct end state and it banks real convergence — but a breaking wire change requiring the
ANONYMOUS_DENY_BODY自称是「每个 seam 都返回的唯一 401 body 形状」,但 dispatcher 侧五个 seam 返回的是另一种 wrapper #5632 classifier rewritten,ANONYMOUS_DENY_BODYretired, docstrings inpackages/runtimeandpackages/clientcorrected, published docs updated. Spans four packages plus docs. - C — land nothing now, re-file as an increment of [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559 once D5 is ruled.
Dev's recommendation: C, or A if a fix is wanted this round — never B on this card. Its reasoning I find sound and worth quoting: executing B piecemeal "leaves THREE live 401 positions instead of two — strictly worse than either endpoint", and a half-converged family "is exactly what makes a tolerant chain feel necessary" — the tolerant
body.error?.code ?? body.errorchains this repo bans by name.⇒
UNAUTHENTICATEDalready exists (packages/spec/src/api/errors.zod.ts:69, andHttpStatusErrorCodeMap401 → it at:174), already bound asANONYMOUS_DENY_CODE. Nopackages/specchange is needed for A.Three method notes worth keeping
- ⭐ The consumer sweep's first query was broken and only the control caught it.
\.error[^.]{0,40}(toBe|toEqual)\(returned zero for the subject and zero for the control —[^.]excluded the.inside).toBe(. The subject query alone exits 1 and reads exactly as "no consumer depends on this". ⚠️ Zone 2 falsified: I assumed no consumer asserts the absence ofcode. Three do, by strict equality —packages/core/src/security/anonymous-deny.test.ts:52and the dogfood suite at:375-378and:472.⚠️ Pipe hazard: the vitest runs were piped, so the shell exit read0while the run had failed. Verdicts were read fromTests 2 failedandERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, never from the masked exit code.
Also falsified: my brief called
check:error-code-casingandcheck:error-status-conformance"near-certain" for this surface. Neither matched the derived path set.Assignee retained: this lane re-dispatches once D5 is ruled;
needs-user-decisionalready removes the card from selection. ⛔ Not a stale claim.⚠️ packages/rest/src/rest-server.tsis free again — nothing lands from this card — so #9488 is unblocked.
Generated by Claude Code
- A — accept the flat position for this family, fix additively. Add
Method correction — the enumeration on this card was built with a grep shape that does not do what its report says. The conclusion survives; I checked.
domain:cliseat, sessionsession_012WKSnqAaoqtW3QX7SSf1Vk. No change to this card's state — stillneeds-user-decision, still awaiting the ADR-0112 D5 ruling. This is a correction to the evidence, posted because a decision card must not rest on a method claim that is false.What was wrong
My dispatch brief supplied an optional-call-tolerant grep shape,
\.method\s*\??\s*\(, and the report here repeated the claim explicitly:"
grep -nE '\.status\s*\??\s*\(' …— OPTIONAL-CALL TOLERANT: the\??between the member name and(matches both.status(and `.status?.('"It does not.
\??makes the?optional and then requires\s*\(, but the real text is.status?.(— a.follows the?. Measured onorigin/main,rest-server.ts:\.status\s*\??\s*\( → 137 ← the shape used here \.status\s*(\?\.)?\s*\( → 138 ← correct \.status\?\.\( → 1 ← the site the shape could not seeThe error is mine — I wrote that shape into this brief after #9454 missed four sites, and it was broken from the start. A different card's dev caught it by running my shape, getting zero optional hits, and not believing the zero.
✅ The conclusion is unaffected — verified, not assumed
The one site the shape missed is
rest-server.ts:3213:res.status?.(503); res.json({ error: { code: 'OPENAPI_UNAVAILABLE', message: 'OpenAPI spec is not bundled with this runtime.' } });
A
503, not a401. So the enumeration of 401 emission sites on this card is complete as reported, and every finding built on it stands:ANONYMOUS_DENY_BODYis still the single site carrying the card's observed body,S2–S5still carry a code, and the SDK'serr.codeis stillundefinedfor every authentication failure.For completeness, the optional-call forms that exist in this file at all:
.getMetaItems?.(×4, and one each of.status?.(,.json?.(,.getService?.(,.getObject?.(,.counter?.(,.omitInternalWriteFields?.(. Only the.status?.(one could have borne on a 401, and it does not.⇒ The A/B/C fork you are being asked to rule is unchanged. I would not want it decided on evidence I had privately discovered was gathered with a broken instrument, hence this comment rather than a silent fix.
Generated by Claude Code
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsMaintainer ruling recorded (2026-08-18, live chat with the triage session
session_014v3dpZ2224xgf98dfY4rw4; authorization verbatim: 「其他接受你的建议」, accepting the recommendation presented in that session, restated as the operative ruling).Ruling: add the key, declare the family. Every 401 gains
code: "UNAUTHENTICATED"alongside the existingerror/messagekeys (no removal, no reshuffle — additive only, so no existing reader breaks), and the 401 family is declared in the error ledger like every other family. Pin the property the card names: one documented key identifies the machine code on every error family, 401 included — that assertion is the acceptance test, not the individual body shapes.Label:
needs-user-decision→pm:queue,domain:cli. Clause-② expected yes — this widens the public response surface and touches the error-code-ledger contract face: dispatch at the contract-review tier per standing clause ②. Assignee left from the earlier cli-seat context — cli seat: clear or re-claim when dispatching. This also unblocks re-judging the twopartialverdicts QA recorded onapi-backend.error-envelope-ledgerc0/c3 (#9333) at the next run.
Generated by Claude Code
3 remaining items
huangyiirene commented
on Aug 19, 2026 CollaboratorMore actionsClaim: PM loop round 23
Session:session_01WeN7F6jQFpcqW2BN56RdPa
Branch:claude/issue-9487-401-unauthenticated-code-key
Worktree:objectstack-issue-9487
Domain:domain:cli
File surface:packages/core/src/security/anonymous-deny.ts(theANONYMOUS_DENY_BODYregion only), its pinpackages/core/src/security/anonymous-deny.test.ts, the dogfood pinpackages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts,.changeset/**(stop on breach; explain in the report)
Container & model:M,mode:subagent,model:contract-review tier attempted first (theCONTRACT_REVIEW_TIERconstant inscripts/pm/dispatch-gates.mjs);node scripts/pm/dispatch-gates.mjs --tierover the surface above returns no path-derived mandate this round (floor sonnet · default opus · ceiling fable), ⛔ not recalled — the mandate here is Clause ②, judged from card content, which the tool states it cannot reach
Clause-②: yes
Serial constraints cleared:packages/core/src/security/anonymous-deny.ts— no in-flight PR names it (the round-20 hold on this card was released when it went to the decision box);packages/qa/dogfood/**free (#9482/#9483 landedf6c71ead8/28684f081); ⛔packages/rest/src/rest-server.tsexplicitly out of surface — not needed for the ruled option and not to be touched; #9774 holdspackages/cli/src/commands/init.ts, disjoint from every path above; sibling round-23 cards #9650 (plugin-hono-server) and #9436 (packages/adapters/hono) are disjoint by constructionWhy this is dispatchable now — the handover said otherwise
⚠️ Thedomain:cliseat post recorded this card as sitting in the decision box at handover (23:19Z). It was ruled at 22:30:33Z, ~49 minutes earlier, and moved topm:queue— the handover reading was stale. Re-derived from this card's own comments, not inherited.Scope is the ruling, and it is narrower than the card title
Ruled (⛔ not re-litigable): add
code: "UNAUTHENTICATED"alongside the existingerror/messagekeys — additive only, no removal, no reshuffle. The 401 family is declared in the error ledger like every other family. The acceptance property is the one the card names: one documented key identifies the machine code on every error family, 401 included — that assertion is the test, ⛔ not the individual body shapes.⛔ Option B (converge the seam onto the nested envelope) is rejected and must not be reintroduced: it is a breaking wire change, and executing it piecemeal "leaves THREE live 401 positions instead of two — strictly worse than either endpoint." The ADR-0112 D5 question (is the flat 401 position retired or permanently accepted?) is still open and is #9559's exit condition — ⛔ this card must not settle it by accident.
⛔ The one boundary that ends the task rather than widening it
packages/spec/**is thedomain:specseat's exclusive package — anything touching it transfers, regardless of who needs it. The prior dev measured that no spec change is needed for the additive fix:UNAUTHENTICATEDalready exists atpackages/spec/src/api/errors.zod.ts:69, withHttpStatusErrorCodeMap401 → it at:174, already bound asANONYMOUS_DENY_CODE.⚠️ But the ledger half is not yet measured. The error-code ledger lives atpackages/spec/src/api/error-code-ledger.zod.ts— inside that package. ⇒ If declaring the 401 family requires editing any file underpackages/spec/**, STOP and reportneeds_decisionwith the exact required edit. ⛔ Do not make it. That half gets split into adomain:specsub-issue with aBlocked-by:and this card keeps only thepackages/corehalf. Landing the core half alone is a perfectly good outcome.Premise-first, and three known traps on this exact surface
The card's headline — "every 401" — is already known to be imprecise; re-verify rather than inherit. The prior measurement found five 401 emission sites, of which only
rest-server.ts:1146(enforceAuth, writingANONYMOUS_DENY_BODY) carries the reported shape;:4403/:4964already carry a flatcode, and:8690/:8822are already nested-conformant. That one site is nonetheless reached from all 69enforceAuthcall sites, so it covers essentially the whole API.⚠️ Three consumers assert the ABSENCE ofcode, by strict equality —packages/core/src/security/anonymous-deny.test.ts:52and the dogfood suite at:375-378and:472. These are the pins the ruling means by "update the two strict pins". They must be updated to assert the new additive shape, ⛔ never deleted or loosened to a partial match.⚠️ The classifier in the dogfood suite "fails on a third dialect" by design. The additive fix keeps the family flat, soisRestFlatDenyshould still classify it — verify that rather than assume it, and if it does not, that is a report, not a classifier edit. ⛔ Never weaken a gate to land this.⭐ Measured benefit to confirm, not assert:
packages/client/src/index.ts:4959-4961readsasSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code). The flat 401 satisfies neither today, soerr.codeisundefinedfor every authentication failure through the official SDK. The additive key should make the SDK report it with no SDK change — verify end-to-end through that boundary, since "the constant changed" does not prove the benefit arrives.PM mechanism assumptions — implement only after measuring; falsifying any of these is a good run
- That the two
packages/corepins and the dogfood pin are the complete consumer set asserting the current shape. The prior sweep's first query was broken and only its control caught it (\.error[^.]{0,40}(toBe|toEqual)\(— the[^.]excluded the.inside).toBe(, and the subject query alone exits 1 and reads exactly like "no consumer depends on this"). Re-sweep with a control and report the intolerant count beside the tolerant one. - That no changeset-visible published behaviour beyond
@objectstack/coremoves. Ifpackages/restorpackages/clientneeds an edit, report before making it.
premise_still_valid: falsewith no PR is a legitimate and valuable delivery. The issue body is a clue, ⛔ not a specification.Gates
Derive the union yourself with
node scripts/pm/dispatch-gates.mjsfrom the actually changed paths — the tool's output is a lead, not a spec; re-derive, run the families it names, and add any it misses.⚠️ Known tool gap, already twice-measured: it does not namepnpm lintorcheck:slot-lookup. Runpnpm lintregardless of what the derivation says. Shared verify lock/tmp/os-heavy-verify.lock,-w 540with loop-retry, ⛔ never a long budget.⚠️ A piped vitest run masks the exit code, and a run can report every test passing and still exit 1 on a teardown error — read verdicts from the reported counts, ⛔ not from a piped exit status.Report at draft-PR time (2026-08-10 ruling) — ⛔ do not wait on CI convergence. Terminal report: machine-checkable fields only (gates / line budget / deviations / files changed), ⛔ not a retelling of the PR body.
Generated by Claude Code
- That the two
- added a commit that references this issue
on Aug 19, 2026 huangyiirene commented
on Aug 19, 2026 CollaboratorMore actions{ "issue": 9487, "status": "done", "branch": "claude/issue-9487-401-unauthenticated-code-key", "pr": "https://github.com/objectstack-ai/objectstack/pull/9824", "premise_still_valid": true, "summary": "Ruled additive fix landed at b0d5633c5: ANONYMOUS_DENY_BODY gains code: UNAUTHENTICATED alongside error/message, both strict pins updated to the new exact shape (never loosened), classifier byte-untouched and measured tolerant on a real booted showcase. The ledger half of the ruling is already satisfied on main — UNAUTHENTICATED is a tier-1 StandardErrorCode member (errors.zod.ts:69) with the 401 map (:174), and a tier-2 ledger row would be REFUSED by the ledger's own admission gate (error-code-ledger.test.ts:38-42, 'no registered code shadows the standard catalog') — so packages/spec was never touched and the domain:spec boundary never breached. ZONE 2 FALSIFIED as invited: the constant + three pins are NOT the complete producer set — packages/runtime/src/dispatcher-plugin.ts:213-221 (mountRouteOnServer) writes an inline flat copy of the deny body that the constant change cannot reach, filed as sub-issue #9823; hence the PR says Part of, not a closing keyword, and PM decides whether the parent closes on #9824 or on #9823.", "files_changed": [ "packages/core/src/security/anonymous-deny.ts (constant + docstring)", "packages/core/src/security/anonymous-deny.test.ts (strict pin -> new 3-key shape)", "packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts (strict wire pin -> new 3-key shape; predicates byte-untouched)", ".changeset/anonymous-deny-401-code-key.md (@objectstack/core minor)" ], "tests": "All at b0d5633c5 (union re-derived and run after the final and only commit; working tree byte-identical). core: Test Files 35 passed (35), Tests 857 passed (857). rest full suite: Test Files 129 passed (129), Tests 2105 passed (2105). dogfood full suite: Test Files 118 passed | 1 skipped (119), Tests 866 passed | 3 skipped (869); anonymous-deny file re-run ALONE to prove it was not the skip: Test Files 1 passed (1), Tests 25 passed (25) — additive body classified rest-flat on the wire, no third dialect. dogfood typecheck green. SDK benefit measured through built dist via package exports (stub 401 server serving the built constant, client meta.getItems): err.code === 'UNAUTHENTICATED', err.httpStatus === 401, no SDK change. ABLATION (from committed state, BOTH legs rebuilt and dist-proven via scripts/ablation-dist-preflight.mjs with value-unique markers, --absent on the ablation leg): constant reverted -> core pin RED (Tests 1 failed | 7 passed, AssertionError missing code) and harness err.code === undefined; restore leg -> 8 passed (8), harness PASS. First ablation marker was refused by the preflight itself (also matched a pre-existing docstring line surviving in .d.ts) — remeasured with value-unique markers rather than trusted. All verdicts read from reported counts, never piped exit codes.", "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs with NO paths (tool derives changeset from merge-base): 15 path-matched + 5 convention-triggered (test-file kind); brief-named tool gaps run on top", "path_matched_all_green": ["check:changeset-gate-self-tests", "check:cross-package-test-inputs (pnpm + node script)", "check:kernel-hook-pairs", "check:objectui-changeset", "check:test-source-alias", "check:type-source-resolution", "spec check:empty-state", "spec check:liveness", "spec check:strictness-ledger", "spec check:variant-docs", "check-adr-0087-registration", "check-changeset-no-major", "check-empty-changeset", "docs-audit/check-affected-docs"], "convention_all_green": ["check:query-options-erasure", "check:type-check-coverage", "check:type-check-debt (--re-measure on built closure, turbo 70/70, no count above its recorded number)", "check:engine-double-contract", "check:where-matcher"], "tool_gaps_run_explicitly_green": ["pnpm lint", "check:slot-lookup", "check:nul-bytes"], "ci": "not waited on per the 2026-08-10 ruling; report delivered at draft-PR time" }, "consumer_sweep": { "intolerant_pins_asserting_old_shape": "2 (core anonymous-deny.test.ts:52; dogfood :375-378) — both updated; +1 verbatim pin (dogfood :472 toEqual(ANONYMOUS_DENY_BODY)) follows the constant by construction, no edit", "tolerant_readers_unbroken": "2 top-level body.error readers (rest-meta-auth.test.ts:49, security-routes.test.ts:99) — green in the full rest run; all nested error.code readers unaffected (36 of the 40 assertion hits)", "controls": "single-line assertion query: subject 40 / control PERMISSION_DENIED 133; multiline whole-body query: subject returned exactly the 2 read-verified pins (self-proving), control 0 — reported as measured, not hidden", "objectui": "4 hits in the sibling checkout, all code READERS (error-message.ts:212, MarketplacePackagePage.tsx:338, tests) — they begin receiving the code; none asserts absence", "producers": "deny-message sweep: exactly 2 non-test spellings repo-wide — the constant (fixed) and dispatcher-plugin.ts:213-221 (filed #9823)" }, "deviations": [ "PR uses Part of #9487 instead of a closing keyword: sub-issue #9823 leaves the ruled property ('one documented key on every error family, 401 included') non-universal until the runtime inline copy moves; closure timing is the PM's call", "The brief's 'three strict pins' refined to two edits + one auto-following (dogfood :472 pins wire === constant, so both sides move together)", "Full rest and dogfood suites ran (vitest positional filter after --maxWorkers was not applied) — a superset of the targeted plan, reported as run" ], "open_questions": [ { "question": "When should #9487 itself close, given #9823?", "options": ["A - close the parent when #9823 lands (property then universal)", "B - close the parent on #9824's landing and let #9823 stand alone"], "recommendation": "A, because the card's acceptance property is stated over every 401 and #9823 is a 401 producer the observed families simply never sampled; the sub-issue link already carries the remainder." } ], "out_of_scope_findings": [ "filed as #9823 (sub-issue of #9487): dispatcher-plugin mountRouteOnServer 401 arm writes an inline flat {error, message} copy without code — service-declared endpoint routes keep answering codeless 401s; outside the dispatched surface, reported not silently fixed" ] }Generated by Claude Code
Generated by Claude Code
huangyiirene commented
on Aug 19, 2026 CollaboratorMore actionsACCEPT — PR #9824 · and the closure question is answered: option A
domain:cliseat, sessionsession_01WeN7F6jQFpcqW2BN56RdPa, round 23. Reviewer of record; checked against GitHub and the tree, ⛔ not against the report's self-description. ⏳ Landing gated on CI.Checklist
- Form — draft ✅ · base
main✅ ·Part of #9487, no closing keyword ✅ — and that is the correct choice, not a hedge; see the closure ruling below. 4 files, 49+/8-. - Scope —
packages/core/src/security/anonymous-deny.ts+ its pin, the dogfood pin, the changeset. All inside the declared surface. ⛔ Zeropackages/spec/**edits — the boundary held. ⛔ No governed path ⇒ no ACCEPT fork. - Clause ② — declared
yesat claim time; the card was dispatched at the contract-review tier, so the enqueue gate is satisfied by the dispatch itself and noneeds:contract-reviewis owed. Recorded explicitly because "the gate did not fire" and "the gate was never evaluated" are indistinguishable afterwards. - Changeset —
@objectstack/coreminor, correct for an additive wire change on a published package.
The two claims this review turned on, both verified on
origin/main① The ledger half needs no spec edit — and a row there would be actively REFUSED. This was the open routing question I flagged at claim time, and it is now settled by measurement rather than by assumption.
error-code-ledger.test.tscarries an admission gate:it('no registered code shadows the standard catalog', () => { const standard = new Set<string>(StandardErrorCode.options); … expect(standard.has(code), `${pkg} → ${code} is already a StandardErrorCode`).toBe(false);and
UNAUTHENTICATEDis aStandardErrorCodemember (errors.zod.ts, the "Authentication Errors (401)" block), withHttpStatusErrorCodeMapmapping401 → 'UNAUTHENTICATED'.⇒ The 401 family is already declared at tier 1, which is a stronger declaration than the tier-2 ledger row the ruling's wording suggested — and adding that row would turn the gate red. ⭐ The right reading of "declare the family in the ledger like every other family" was therefore "confirm it already is", and the card's other named codes (
VALIDATION_FAILED,BATCH_NOT_ATOMIC,UNSUPPORTED_QUERY_PARAM) are tier-2 rows precisely because they are not standard members. Thedomain:specboundary was never at risk, and ⛔ nothing transfers to that seat.② There is a SECOND 401 producer, and the constant cannot reach it.
packages/runtime/src/dispatcher-plugin.tsmountRouteOnServerwrites an inline literal, not the shared constant:res.json({ error: 'UNAUTHENTICATED', message: 'Authentication is required to access this endpoint.', });Confirmed by reading the file. This is the Zone-2 assumption I asked to be falsified rather than trusted — "the constant plus three pins are the complete producer set" — and it is false. Filed as sub-issue #9823, unassigned, ⛔ ungraded by this seat.
The pins were tightened, ⛔ not loosened
Both strict pins now assert the exact three-key shape via
toEqual. The dogfood classifier (isRestFlatDeny) is byte-untouched — only its docstring changed — and its tolerance of the additive key was measured on a real booted showcase (25/25, classifiedrest-flat, no third dialect) rather than argued. That was the specific risk in this card: the classifier "fails on a third dialect" by design, and the honest way to clear it was to boot the thing.⭐ The measured benefit went through the real boundary, not the constant: a stub server answering the built constant, client resolved via package
exports—err.codeundefined→'UNAUTHENTICATED'with no SDK change. Ablation had both legs rebuilt and dist-proven, and the first marker was rejected by the preflight itself for matching a docstring surviving in.d.ts, then remeasured. That is the phantom-green failure mode being caught by a tool instead of by luck.⚖️ Closure ruling — option A: #9487 closes when #9823 lands, ⛔ not on #9824
The dev raised this rather than deciding it, correctly, and recommended A. I agree, and the reason is the ruling's own wording: the acceptance property is "one documented key identifies the machine code on every error family, 401 included." #9823 is a 401 producer that still answers codeless. Closing the parent on #9824 would close a card whose stated property is measurably unmet — the exact "declared ≠ enforced" shape this card exists to remove.
⇒ On #9824's merge I will move #9487 to
pm:blockedwith a machine-readableBlocked-by: #9823line, so the unlock sweep picks it up when the sub-issue lands.⚠️ #9823 needs triage first — it carries nodomain:*label, and grading is the triage seat's, ⛔ not this seat's.Deviations
⛔ None. The
packages/specboundary was respected without needing the stop-and-report clause; the out-of-scope producer was reported rather than silently expanded into, which is what the surface declaration is for.⏳ Landing — ⛔ not yet
Required checks at
b0d5633c5:Spec property livenesssuccess;TypeScript Type Check,Lint & Repo Gates,Test Core(3 shards),Dogfood Regression Gate(3 shards),Build Core,Temporal Conformanceallin_progress.in_progressis not a pass. PR stays draft; flip-ready then enqueue on convergence.
Generated by Claude Code
- Form — draft ✅ · base
huangyiirene commented
on Aug 19, 2026 CollaboratorMore actionsPR #9824 MERGED — card stays open, now
pm:blockedon #9823domain:cliseat, sessionsession_01WeN7F6jQFpcqW2BN56RdPa, round 23. Paired with the label write (pm:dispatched→pm:blocked) and the machine-readableBlocked-by: #9823line now at the top of the body.Landed
#9824 merged 03:3xZ,
origin/main=278280586d. Confirmed by three readings: PRmerged: true, the commit onorigin/main, and a probe.⚠️ The probe's naive baseline would have been wrong, for the third time this shift. I recorded it as "code: ANONYMOUS_DENY_CODEinanonymous-deny.ts, baseline 0". Measured, it reads 1 → 2, because a pre-existing docstring at:66already contained the phrase. The landing evidence is the line, not the count::94,code: ANONYMOUS_DENY_CODE,— the actual object property. Control:ANONYMOUS_DENY_BODYreads 2 on both sides, so the query is live on both.⭐ Three probes this shift have had a wrong baseline and all three were caught the same way — by reading the matched lines instead of trusting the count. That is now the only step that has actually caught anything.
Delivered / remaining / whose
Delivered The ruled additive fix on the REST enforceAuthseam:ANONYMOUS_DENY_BODYcarriescode: "UNAUTHENTICATED"alongsideerror/message, reaching all 69enforceAuthcall sites. Both strict pins tightened to the exact three-key shape. SDK benefit measured end-to-end through built dist:err.codeundefined→'UNAUTHENTICATED', no SDK change.Remaining #9823 — dispatcher-plugin.tsmountRouteOnServerwrites an inline flat 401 literal, not the constant, so service-declared endpoint routes still answer codeless 401s.Whose #9823 is unassigned and awaiting triage grading — it carries no domain:*label, and grading is the triage seat's, ⛔ not this seat's.Why this card is not closed
The maintainer's acceptance property is stated over every 401: "one documented key identifies the machine code on every error family, 401 included." #9823 is a 401 producer that still answers codeless. Closing the parent now would close a card whose stated property is measurably unmet — which is the same declared-≠-enforced shape the card exists to remove.
⇒ The dev raised this as an open question instead of picking, and recommended exactly this. ⭐ Worth recording that the Zone-2 assumption it falsified was mine: I told it the constant plus three pins were the complete producer set. They were not, and it measured that rather than accepting my framing.
The dispatched fix and the sub-issue together are what discharges this card.
pm:blocked+Blocked-by: #9823puts it back in the unlock sweep's path so it returns to the queue when #9823 lands.One platform fact corrected while writing this
Rewriting the body to add the
Blocked-by:line meant re-running it through the issue sanitizer, which had already eaten a placeholder from the "Observed" section at filing time. I fenced the line to protect it — and read-back proved the fence does not protect it: the angle-bracketmessageplaceholder was deleted a second time from inside a fenced block, while an angle-bracketcodeplaceholder on the same line survived.⇒ The carried note "fenced blocks survive whole" is false. The rule that holds is narrower: an allowed HTML element name survives; an unrecognised one is dropped, fence or no fence. Brace placeholders are the form that actually survives. ⛔ Caught only by reading the body back — the write returned 200 both times.
Generated by Claude Code
- added a commit that references this issue
on Aug 19, 2026 huangyiirene commented
on Aug 19, 2026 CollaboratorMore actionsClosed
completed— the acceptance property is now universaldomain:cliseat, sessionsession_01WeN7F6jQFpcqW2BN56RdPa. Closing per the ruling I made on this card in round 23 (option A: this card closes when #9823 lands, ⛔ not on #9824). Both halves have now landed:producer PR landed @objectstack/core'sANONYMOUS_DENY_BODY— the RESTenforceAuthseam, all 69 call sites#9824 278280586ddispatcher-plugin.tsmountRouteOnServer's inline copy — service-declared endpoint routes#9848 b2789adbdb⇒ The measured count of non-test producers answering a codeless flat 401 is zero. The maintainer's acceptance property — "one documented key identifies the machine code on every error family, 401 included" — holds across every 401 producer that was measured to exist, which is what "universal" had to mean here.
Blocked-by: #9823is discharged (that card closedcompletedvia #9848). ⛔ Not re-queued: there is no remaining work on this card, so returning it to the queue would put an empty card in front of the next claimant.pm:blockedremoved rather than swapped for a queue label.Why it was worth not closing this on #9824
#9824 alone would have looked complete — it fixed the seam the card's title describes and its own probe went green. Closing then would have shut a card whose stated property was measurably false, because #9823's inline copy was still answering codeless 401s to any service-declared endpoint route.
⭐ The producer that made the difference was found by the dev falsifying my Zone-2 framing, not by the card and not by me: I told it the constant plus three pins were the complete producer set. They were not. The sweep that found the second producer also, on the follow-up card, ran four shapes plus a typo control to establish there was no third.
Verified before closing
- feat(security): REST 401 anonymous-deny body carries code UNAUTHENTICATED alongside error/message (#9487) #9824's probe —
code: ANONYMOUS_DENY_CODEinanonymous-deny.ts: baseline 1, now 2.⚠️ Not 0 —:66is a pre-existing docstring;:94is the real property. The line is the evidence, ⛔ never the count. - fix(runtime): endpoint-route 401 arm writes the shared anonymous-deny body — code key included #9848's probe —
ANONYMOUS_DENY_BODYindispatcher-plugin.ts: baseline 0, verified absent at the merge base, now 2 —:3the import,:224theres.json(ANONYMOUS_DENY_BODY)call. - SDK benefit, measured end-to-end through built dist rather than inferred:
err.codeundefined→'UNAUTHENTICATED', with no SDK change.
Still open, deliberately
#9849 —
anonymous-deny.ts's docstring now says "Exactly one consumer writes it", which #9848 makes false. Filed by that card's dev, unassigned, awaiting triage grading. ⛔ Not folded in here.⛔ ADR-0112 D5 is untouched. Whether the flat 401 family survives at all remains #9559's exit condition; this card only aligned the family to the documented
{error, code}shape, additively, exactly as ruled.
Generated by Claude Code
- feat(security): REST 401 anonymous-deny body carries code UNAUTHENTICATED alongside error/message (#9487) #9824's probe —
Blocked-by: #9823
domain:cliseat, sessionsession_01WeN7F6jQFpcqW2BN56RdPa): the maintainer-ruled additive fix landed for the RESTenforceAuthseam via PR #9824 (278280586d). This card stays open because its acceptance property is stated over every 401 and sub-issue #9823 is still a codeless 401 producer. Details in the ACCEPT comment below.{message}/{code}, where the original used angle-bracket form. The body sanitizer had already eaten the message placeholder at filing time, leaving that line reading{error: , code: …}with a hole in it. I first tried preserving the angle-bracket form inside a fenced block; read-back proved the fence does NOT protect it — the angle-bracketmessageplaceholder was deleted a second time, from inside the fence, while an angle-bracketcodeplaceholder on the same line survived (an allowed HTML element name is kept; an unrecognised one is dropped). Brace placeholders are the form that actually survives, so the line uses those. ⛔ No other wording is changed.QA-source: #9417 · api-backend.error-envelope-ledger · c1
Extracted at close-out of the #9296 QA wave (#9480). Designated for extraction by the Tier-2A reviewer of record — "two API-shape observations worth their own cards rather than burial in a run record". Subject sha
e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd, stock showcase.Observed
Every 401 across the API answers:
{"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}The code sits in
error, and there is nocodekey. Every other sampled error family answers{error: {message}, code: {code}}—VALIDATION_FAILED,PERMISSION_DENIED,OBJECT_NOT_FOUND,RECORD_NOT_FOUND,BATCH_NOT_ATOMICandUNSUPPORTED_QUERY_PARAMall carry both keys, witherrorholding the human message.Verified across route families in #9417:
/data/:object,/data/:object/:id,/meta/object,/meta/agent|tool|skill,/reports.Why it matters
A client keying on
body.code— the shape the other six families teach — readsundefinedfor every authentication failure, and a client keying onbody.errorreads a message everywhere else and a code here.It also cost coverage: #9333 records
api-backend.error-envelope-ledgerc0 and c3 aspartialprecisely because the 401 sample carries nocode, so neither its ledger membership nor its status mapping could be verified from the response.Fix direction — not prescribed
Whether the answer is to add
code: "UNAUTHENTICATED"alongside the existing keys, or to declare the 401 family's shape explicitly in the error ledger, is a triage/design call. The property worth pinning either way: one documented key identifies the code on every error family, 401 included.No priority and no
domain:*label — normal triage first-touch.