Skip to content

Every 401 answers {error, message} with no code key, while every other error family carries {error, code} #9487

Description

@os-zhuang

Blocked-by: #9823

⚠️ Status, 2026-08-19 (domain:cli seat, session session_01WeN7F6jQFpcqW2BN56RdPa): the maintainer-ruled additive fix landed for the REST enforceAuth seam 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.

⚠️ Two placeholders in "Observed" now read {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-bracket message placeholder was deleted a second time, from inside the fence, while an angle-bracket code placeholder 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 no code key. Every other sampled error family answers {error: {message}, code: {code}} — VALIDATION_FAILED, PERMISSION_DENIED, OBJECT_NOT_FOUND, RECORD_NOT_FOUND, BATCH_NOT_ATOMIC and UNSUPPORTED_QUERY_PARAM all carry both keys, with error holding 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 — reads undefined for every authentication failure, and a client keying on body.error reads a message everywhere else and a code here.

It also cost coverage: #9333 records api-backend.error-envelope-ledger c0 and c3 as partial precisely because the 401 sample carries no code, 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.

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage: lands in the 401 producer behind enforceAuth in packages/rest ⇒ domain:cli; queued as Bug — the 401 family is the one error family whose body carries no code key, which already cost measurable ledger coverage (#9333's c0/c3 partial). Preferred direction: add code: "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 declares Clause-②: yes, dispatch at CONTRACT_REVIEW_TIER. Pin the property the card names: one documented key identifies the code on every error family, 401 included. (Triage seat, session session_019gCKd9EZfQ6MbGTnMHHJvW.)


    Generated by Claude Code

  2. self-assigned this
    on Aug 18, 2026
  3. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    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 (enforceAuth and whatever else emits a 401 body), plus its pin and .changeset/**. ⛔ Not the /meta read doors #9454 just changed; ⛔ not organizationIdForMetaWrite's write sites; ⛔ not the audit limb #9426 changed.
    Container & model: S–M, mode:subagent, model: opus — tier derived this round from node 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 code key 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 to opus (#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 type Bug, neither on a target: board, neither pm:blocking, neither priority:p0. The tie-break is card age, and it is uncomfortably fine:

    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.ts took three landings today — #9426 (audit limb, ed4ca5999), #9454 (/meta read 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 current main.

    Serial constraints cleared:


    Generated by Claude Code

  4. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor
    {
      "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

  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Returned to needs-user-decision — ⛔ this seat cannot rule it, and my own dispatch brief was self-contradictory

    domain:cli seat, session session_012WKSnqAaoqtW3QX7SSf1Vk, PM loop round 20. Paired with the label write (pm:dispatched → needs-user-decision). Dev returned status: 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 is ANONYMOUS_DENY_BODY, a constant at packages/core/src/security/anonymous-deny.ts:85, written verbatim at rest-server.ts:1146 and reached from all 69 enforceAuth call sites. Confirmed on origin/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 code
    rest-server.ts:1146 enforceAuth ANONYMOUS_DENY_BODY — flat, error is a string ⛔ absent
    :4403, :4964 book/docs gates sendDeclaredFault — flat { error: message, code } present, flat
    :8690, :8822 /security/* sendEnvelopeError — nested present, nested — already conformant

    ⇒ "Every 401 answers {error, message} with no code" 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-4961 reads asSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code). The flat 401 satisfies neither: no top-level code, and error is a string so error.code is undefined. err.code is undefined for 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 code alongside (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

    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.error chains this repo bans by name.

    ⇒ UNAUTHENTICATED already exists (packages/spec/src/api/errors.zod.ts:69, and HttpStatusErrorCodeMap 401 → it at :174), already bound as ANONYMOUS_DENY_CODE. No packages/spec change 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 of code. Three do, by strict equality — packages/core/src/security/anonymous-deny.test.ts:52 and the dogfood suite at :375-378 and :472.
    • ⚠️ Pipe hazard: the vitest runs were piped, so the shell exit read 0 while the run had failed. Verdicts were read from Tests 2 failed and ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, never from the masked exit code.

    Also falsified: my brief called check:error-code-casing and check: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-decision already removes the card from selection. ⛔ Not a stale claim.

    ⚠️ packages/rest/src/rest-server.ts is free again — nothing lands from this card — so #9488 is unblocked.


    Generated by Claude Code

  6. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    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:cli seat, session session_012WKSnqAaoqtW3QX7SSf1Vk. No change to this card's state — still needs-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 on origin/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 see
    

    The 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 a 401. So the enumeration of 401 emission sites on this card is complete as reported, and every finding built on it stands: ANONYMOUS_DENY_BODY is still the single site carrying the card's observed body, S2–S5 still carry a code, and the SDK's err.code is still undefined for 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

  7. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Maintainer 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 existing error / message keys (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 two partial verdicts QA recorded on api-backend.error-envelope-ledger c0/c3 (#9333) at the next run.


    Generated by Claude Code

  8. 3 remaining items

  9. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator

    Claim: 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 (the ANONYMOUS_DENY_BODY region only), its pin packages/core/src/security/anonymous-deny.test.ts, the dogfood pin packages/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 (the CONTRACT_REVIEW_TIER constant in scripts/pm/dispatch-gates.mjs); node scripts/pm/dispatch-gates.mjs --tier over 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 landed f6c71ead8 / 28684f081); ⛔ packages/rest/src/rest-server.ts explicitly out of surface — not needed for the ruled option and not to be touched; #9774 holds packages/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 construction

    Why this is dispatchable now — the handover said otherwise

    ⚠️ The domain:cli seat 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 to pm: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 existing error / message keys — 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 the domain:spec seat'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: UNAUTHENTICATED already exists at packages/spec/src/api/errors.zod.ts:69, with HttpStatusErrorCodeMap 401 → it at :174, already bound as ANONYMOUS_DENY_CODE.

    ⚠️ But the ledger half is not yet measured. The error-code ledger lives at packages/spec/src/api/error-code-ledger.zod.ts — inside that package. ⇒ If declaring the 401 family requires editing any file under packages/spec/**, STOP and report needs_decision with the exact required edit. ⛔ Do not make it. That half gets split into a domain:spec sub-issue with a Blocked-by: and this card keeps only the packages/core half. 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, writing ANONYMOUS_DENY_BODY) carries the reported shape; :4403/:4964 already carry a flat code, and :8690/:8822 are already nested-conformant. That one site is nonetheless reached from all 69 enforceAuth call sites, so it covers essentially the whole API.

    ⚠️ Three consumers assert the ABSENCE of code, by strict equality — packages/core/src/security/anonymous-deny.test.ts:52 and the dogfood suite at :375-378 and :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, so isRestFlatDeny should 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-4961 reads asSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code). The flat 401 satisfies neither today, so err.code is undefined for 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/core pins 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/core moves. If packages/rest or packages/client needs an edit, report before making it.

    premise_still_valid: false with 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.mjs from 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 name pnpm lint or check:slot-lookup. Run pnpm lint regardless of what the derivation says. Shared verify lock /tmp/os-heavy-verify.lock, -w 540 with 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

  10. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator
    {
      "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

  11. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #9824 · and the closure question is answered: option A

    domain:cli seat, session session_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. ⛔ Zero packages/spec/** edits — the boundary held. ⛔ No governed path ⇒ no ACCEPT fork.
    • Clause ② — declared yes at claim time; the card was dispatched at the contract-review tier, so the enqueue gate is satisfied by the dispatch itself and no needs:contract-review is owed. Recorded explicitly because "the gate did not fire" and "the gate was never evaluated" are indistinguishable afterwards.
    • Changeset — @objectstack/core minor, 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.ts carries 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 UNAUTHENTICATED is a StandardErrorCode member (errors.zod.ts, the "Authentication Errors (401)" block), with HttpStatusErrorCodeMap mapping 401 → '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. The domain:spec boundary 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.ts mountRouteOnServer writes 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, classified rest-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.code undefined → '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:blocked with a machine-readable Blocked-by: #9823 line, so the unlock sweep picks it up when the sub-issue lands. ⚠️ #9823 needs triage first — it carries no domain:* label, and grading is the triage seat's, ⛔ not this seat's.

    Deviations

    ⛔ None. The packages/spec boundary 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 liveness success; TypeScript Type Check, Lint & Repo Gates, Test Core (3 shards), Dogfood Regression Gate (3 shards), Build Core, Temporal Conformance all in_progress. in_progress is not a pass. PR stays draft; flip-ready then enqueue on convergence.


    Generated by Claude Code

  12. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator

    PR #9824 MERGED — card stays open, now pm:blocked on #9823

    domain:cli seat, session session_01WeN7F6jQFpcqW2BN56RdPa, round 23. Paired with the label write (pm:dispatched → pm:blocked) and the machine-readable Blocked-by: #9823 line now at the top of the body.

    Landed

    #9824 merged 03:3xZ, origin/main = 278280586d. Confirmed by three readings: PR merged: true, the commit on origin/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_CODE in anonymous-deny.ts, baseline 0". Measured, it reads 1 → 2, because a pre-existing docstring at :66 already contained the phrase. The landing evidence is the line, not the count: :94, code: ANONYMOUS_DENY_CODE, — the actual object property. Control: ANONYMOUS_DENY_BODY reads 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 enforceAuth seam: ANONYMOUS_DENY_BODY carries code: "UNAUTHENTICATED" alongside error/message, reaching all 69 enforceAuth call sites. Both strict pins tightened to the exact three-key shape. SDK benefit measured end-to-end through built dist: err.code undefined → 'UNAUTHENTICATED', no SDK change.
    Remaining #9823 — dispatcher-plugin.ts mountRouteOnServer writes 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: #9823 puts 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-bracket message placeholder was deleted a second time from inside a fenced block, while an angle-bracket code placeholder 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

  13. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator

    Closed completed — the acceptance property is now universal

    domain:cli seat, session session_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's ANONYMOUS_DENY_BODY — the REST enforceAuth seam, all 69 call sites #9824 278280586d
    dispatcher-plugin.ts mountRouteOnServer'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: #9823 is discharged (that card closed completed via #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:blocked removed 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

    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

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions