Repository navigation
Description
Surfaced by #8087's implementation (PR #9099) and filed rather than decided there. ⛔ Deliberately not held against #9099, which delivers its ruled scope in full — this is a bound on the ruling that was measured after it, not a defect in it.
Filed unassigned and without a domain:* label: the two candidate answers land in different lanes (see below), so routing is triage's call.
The measurement
SandboxError carries a metadata app's own .code across the QuickJS boundary on purpose. From sandbox/error-passthrough.test.ts (#7867), verbatim:
"Author-thrown structured errors get the same treatment; nothing here is objectql-specific"
domains/actions.ts then serves that error through errorFromThrown, which puts declaredCode — the producer's own string, un-narrowed — onto the wire. domains/actions-validation-envelope.test.ts pins that end to end.
So this door's error.code has a limb whose vocabulary is written by tenants, at runtime.
Why this is a decision and not a bug
The 2026-08-12 ruling on #8087 chose option B: keep the verbatim spelling, register the producers, delivered as a gate. That is correct and now shipped — the gate found 7 unregistered platform producers (handed to #8846) and will fail on an unswept one added later.
But registration cannot close this limb, by construction: ERROR_CODE_LEDGER would have to enumerate strings that do not exist when CI runs. No amount of sweeping reaches it.
error.code open), and it should not be read as reopening that. Option C was rejected, correctly: the closure is load-bearing for every platform producer, and the 7 rows the gate just found are exactly what it catches. What is open is narrower — what the contract should say about the author-authored limb specifically, which nobody has ruled on because nobody had measured it.
⭐ DUPLICATE is the pinned witness to this limb and was deliberately not re-spelled in #9099 — re-spelling it would have deleted the only evidence in the repo that the limb is open. It should stay unregistered and unrewritten until this is ruled. That also revises #8087's premise on its third named code: DUPLICATE is neither "genuinely wrong" nor "merely unregistered".
The two available answers
- A — narrow the sandbox boundary. Author-thrown codes get narrowed (or namespaced, e.g. a reserved prefix) before reaching
error.code, so the wire vocabulary stays closed. Costs: it changes what a metadata app can put on the wire, which is a published capability Action-body writes have no not-found gate:ctx.api.object().update()against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867 deliberately granted; and it is a behaviour change for existing apps. - B — declare a second, non-ledger vocabulary for author-thrown codes. ADR-0112 states that
error.codeis closed for platform producers and that the sandbox limb is deliberately open, with the boundary named and pinned. Costs: the envelope's "closed vocabulary" property becomes conditional, and every consumer must learn which door and which limb it is reading.
Both are contract-shaped, which is why neither the dev nor the PM took them.
Four-prism block
- Platform long-term coherence. B makes an existing truth explicit; A makes the stated invariant actually hold everywhere. A is the cleaner invariant, B is the more honest description of a capability we already shipped. The real question is whether "closed" was ever meant to survive a tenant-extensible surface.
- Measured business pull. No reported consumer breakage. The limb is reachable today by any metadata app throwing a structured error — so it is live, not theoretical, but nothing measured says an operator or client has tripped on it. Neither option is urgent; the cost of deferring is that the ledger's guarantee is quietly weaker than its own note claims.
- AI-agent error-resistance. This is the axis that actually discriminates. Today an agent reading ADR-0112 concludes
error.codeis closed and may write a consumer that switches exhaustively on it — which is wrong at exactly one door, and wrong in a way no gate will tell it about. Both A and B fix that; silence does not. Whichever is chosen, the ADR text is the deliverable, not just the code. - Startup scope discipline. A removes a capability that was deliberately granted (Action-body writes have no not-found gate:
ctx.api.object().update()against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867) — that is a retirement decision, not a cleanup. B adds no code and no declared surface, only prose and a pin. B is the cheaper honest move; A is the more expensive correct-if-you-want-closure move.
Recommendation: B, on the grounds that the capability in #7867 was deliberate and product-shaped, and retiring it to preserve a property nobody has yet needed at that door would be paying a real cost for a theoretical one. But prism 1 genuinely cuts both ways and this is a contract shape — recording the recommendation, not taking it.
Routing note for triage
Landing differs by option: A lands in the sandbox/actions path (domain:cli, possibly domain:engine-core); B lands in ADR-0112 plus packages/spec prose (domain:spec, and ADRs are maintainer-merged either way). Suggest leaving domain:* off until it is ruled, since the ruling picks the lane.
Related: #8087 (the ruling and its gate) · PR #9099 (where this was measured) · #8846 (the ledger half) · #7867 (where the passthrough was granted) · #9098 (a different, unrelated hole at the REST door).
Activity
os-zhuang commentedon Aug 16, 2026
Maintainer ruling (2026-08-16, recorded by PM session
session_01NYgmGheCzM6NrHZN436Cxf). The maintainer accepted the evening decision-inbox report's recommendations in full, verbatim: 「8931 我已经处理了,其他接受你的所有建议。」 For this card the accepted recommendation was a variant of Option A: align the actions door with the REST door's demote semantics — not the reject/namespace form of A, and not B's two-vocabulary declaration:error.codestays a closed vocabulary at every door. An author-thrown code that is not anErrorCodeenum member is demoted todeclaredCodeat the actions door, exactly as the REST mapper (resolveThrownHttpError) already does — one consistent rule platform-wide, and consistent with the hotcrm#1075 ruling (apps branch on enum codes; app-specific spellings ridedeclaredCode).ctx.api.object().update()against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867 capability is preserved: the author's code still crosses the sandbox and still reaches the wire — it lands indeclaredCodeinstead oferror.code.error.codeis closed everywhere anddeclaredCodeis the open, author-authored channel, with the boundary named and pinned. AI seats draft; the ADR PR is maintainer-merged per standing rule.DUPLICATE(the pinned witness) gets re-homed under this rule rather than registered.error.codetoday. If real consumers exist, this is a behaviour change with a blast radius — stop and bring the measurement back here before landing.Routing per the card's own note: implementation lands in the sandbox/actions path;
domain:*left for triage now that the lane is picked. Label flippedneeds-user-decision→pm:queuein the same stroke.Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Triage routing per the 13:03Z ruling (the ruled shape — demote at the actions door, mirroring
resolveThrownHttpError— lands in the runtime sandbox/actions path):domain:cli. Type Bug (ADR-0112 declareserror.codeclosed; the limb violates the declaration on the wire today).Dispatch constraints for the executing seat, from the ruling verbatim:
error.codetoday; real consumers ⇒ stop and bring the measurement back before landing.DUPLICATEis re-homed under the demote rule, ⛔ not registered (also fenced on spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846).model: claude-fable-5per the standing tiering clause.Triage: this comment comes from the triage seat Routine; not a claim.
Generated by Claude Code
feat(spec): register the dispatcher conformance gate's reported error…
os-project-manager commentedon Aug 17, 2026
Claiming for implementation per the 2026-08-16 13:03Z maintainer ruling (demote non-enum author codes to
declaredCodeat the actions door; ADR-0112 prose deliverable; binding consumer-measurement precondition).session_01Y26DJEHSBhhAQ6wwfsHNzaclaude/issue-9106-demote-author-codesAssignee untouched — PM holds the claim.
Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 17, 2026
Binding-precondition measurement (session
session_01Y26DJEHSBhhAQ6wwfsHNza): no existing consumer of the actions door branches on author-authored strings inerror.code. Empty result, search proven — proceeding to land.Method (two passes, because the first pass was proven insufficient):
x.code === 'LIT', reversed,.includes,switch/case) over@objectstack/client,client-react,examples/,packages/qa, and the whole objectui checkout, classifying every literal against the 288-member closed union (53StandardErrorCode+ 235 ledger rows, parsed from spec source). Proof-of-search on a known-present neighbour failed: objectui'sRecordAttachmentsPanelbranch ishas('ATTACHMENT_DOWNLOAD_DENIED')— helper-mediated, invisible to comparison patterns. So pass 1 alone was discarded as evidence.DEV_ENV_PLAN_LOCKED,ANALYTICS_NOT_INSTALLED). Every hit hand-classified:DEV_ENV_LIMIT,DEV_ENV_PLAN_LOCKED,PRODUCTION_ENV_LIMIT) — cloud control-plane env routes; the pre-cloud#948 wire shape carried undeclarederrorsiblings no framework builder emits, so these ride cloud's own sender, not this repo's dispatcher exits. Not the actions door, not tenant-authored.data-objectstack:ANALYTICS_NOT_INSTALLED,ANALYTICS_QUERY_REJECTED,APPROVAL_LOOKUP_FAILED,UNSUPPORTED_OPERATION,CONNECTION_ERROR, …) — constructed client-side, never read off the wire'serror.code.ECONNREFUSED,ENOENT,NETWORK_ERROR,EADDRINUSE) and config-map constants — not error.code reads.DUPLICATE(the only in-repo witness value) across objectui, examples, client, dogfood, docs.useConsoleActionRuntime+actionErrorDetail+ ActionRunner extract onlyerror.messagefor toasts — noerror.codebranch on the/actionsroute at all. Published docs teach branching on the closed catalog only.Named residual (could not be scanned): the
objectstack-ai/cloudrepo is not accessible to this session. Evidence it is unaffected: cloud's consumer-branched codes ride its own control-plane sender (above), and any coded throw transiting the framework's REST door has been narrowed there since #8016 with no cloud breakage on record.File surface for this claim (beyond
domains/actions.ts, since the ruling's "closed at every door" requires all three dispatcher-door exits that lift a thrown.code):packages/types/src/thrown-http-error.ts(prose + helper),packages/runtime/src/http-dispatcher.ts,packages/runtime/src/dispatcher-plugin.ts,packages/runtime/src/endpoint-executor.ts,packages/runtime/src/dispatcher-error-vocabulary.ts,scripts/check-dispatcher-error-vocabulary.mjs(prose),packages/spec/src/api/contract.zod.ts(+error-code-ledger.zod.tsprose) + regenerated spec artifacts, the pinned tests, a changeset — plus a separate ADR-0112 amendment PR (maintainer-merged). ⛔ Not touchingpackages/rest/src/error-response.ts(#9098's claim).Generated by Claude Code
Generated by Claude Code
fix(runtime): demote non-enum author codes to declaredCode at the dis…
docs(adr): ADR-0112 amendment — error.code is closed at every door, d…
docs(adr): point the #9106 amendment's scope note at the live card (#…
chore(spec): regenerate authorable surface + reference docs after mer…
docs(adr): anchor the ADR-0112 closed-vocabulary demote rule at its d…
2 remaining items
feat(spec): register the dispatcher conformance gate's reported error…
fix(runtime): demote non-enum author-thrown codes to `declaredCode` —…
fix(rest): narrow the flat door's thrown `code` to the declared vocab…
docs(adr-0112): record the #9232 ruling — the flat door narrows too
docs(adr): ADR-0112 amendment — `error.code` is closed at every door,…
fix(rest): narrow the flat door's thrown `code` to the declared ADR-0…
fix(runtime): restate pending-registration verdict doc comment in post-…
fix(types): let sendError's `extra` carry `declaredCode`
fix(rest): carry the demoted producer code on the package door's decl…
test(runtime): declare the §4 unregistered producer spelling to check…
fix(types): let sendError's `extra` carry `declaredCode` (objectstack…
fix(rest): the package door carries the demoted producer code on `dec…
fix(runtime): carry the producer's `userMessage` to the wire at the d…
test(spec): automation test titles state each cited decision in words…