You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
authored
fix(rest): a sandboxed crash outranks the declared-code arm and answers the sanitised 500 UNCLASSIFIED_FAULT (#15071) (#17228)
* wip(rest): move the sandbox crash terminal above the code-gated arms
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
* wip(rest): flip the §4 pin, name the door residue, add the changeset
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
* fix(rest): grade the changeset minor and declare the wire break
The level axis (#16055) refuses a clause-\xe2\x91\xa1 PR that grades every package
it moves at patch. The declaration is the maintainer's (batch #27), so the
level was the wrong half: @objectstack/rest goes to minor, and the BREAKING
banner carries the breaking-ness the launch window keeps off the level.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
* docs(rest): retire the stale crash-divergence note on `armSentence`
Contract review F1: the "What this deliberately does NOT decide" block in
`armSentence`'s docblock still described the pre-#15071 world, and after this
PR every clause of it was false. It said a sandboxed CRASH reaches an arm and
is answered at the arm's own declared status, that the divergence against the
unwrap door's sanitised 500 is UNCHANGED, that the pin records it as an
accepted divergence, and that it "carries its own decision card" -- while the
card is this one and it has been executed: the crash terminal now sits above
the code-gated arms, so no crash reaches this function on either door.
Rewritten as a cross-reference rather than a second statement of the ruling.
`isSandboxCrash`'s own docblock carries the maintainer ruling, its fence and
its negative control; a file that states one rule twice is the drift this
finding is made of, so the block now points there and stops.
The second paragraph keeps the surviving divergence visible: what converged is
the no-declared-status case. A crash that DECLARED a 4xx still leaves
`resolveErrorResponse` at that status with the QuickJS wrapper, through a
passthrough this card did not touch -- pinned as an ACCEPTED DIVERGENCE in
`error-response-structured-arm-door-parity.test.ts`. The prose must not read as
"all divergence is gone", because it is not.
Comment lines only -- no executable byte moves. Proven at parser level: both
revisions re-printed with `removeComments: true` hash identically
(sha256 5daab82cdd23e0b93a1dfb420b2a9e3c83786975248504bdbf9907b321005fa7),
with a control leg that flips one identifier and is correctly rejected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
---------
Co-authored-by: Claude <pm@objectstack.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
fix(rest): a hook that crashes after declaring a code now answers 500 UNCLASSIFIED_FAULT instead of the declared status with the crash text (#15071)
6
+
7
+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves: no `packages/spec` key, no Zod schema, no object definition, no config field and no stored representation changes its name, its type or its optionality, so `objectstack migrate meta` has nothing to visit, `spec-changes.json` has nothing to project and the upgrade guide has no row to gain. What moves is the RESPONSE a published REST door gives for one input shape at request time, and this changeset ships no instructions for rewriting anything a consumer authored — there is no authored artifact to rewrite. The affected caller's remedy is not an edit but the truth: the hook crashed, and the 500 says so. The other four categories are closed on facts: `@objectstack/rest` publishes to npm (not `unpublished`); no ADR-0087 id is minted in this diff (not `registered`) and none pre-dates the base that would cover it (not `already-registered`); no named `path#Symbol` is a non-metadata runtime interface whose members moved — no exported declaration changes at all, `isSandboxCrash` being file-local and absent from the package entry (not `runtime-interface-only`), which is also why `type-surface-only` has no subject. -->
8
+
9
+
**BREAKING** — the answer this published door gives moves for existing inputs.
10
+
No export, signature or declared type changes; what changes is the response an
11
+
existing call observes, and a client branching on `error.code` for the affected
12
+
shape now falls to its 5xx path instead of its refusal path. Shipped as `minor`
13
+
under the launch-window convention (`major` is refused while the fixed group
14
+
versions in lockstep), so this banner — not the level — is the breaking-ness
15
+
signal.
16
+
17
+
**What changes for an operator.** A sandboxed hook or action body that declared a
18
+
refusal code and then CRASHED — `throw`-ing nothing, but hitting a bug on a later
19
+
line — used to answer the single-record `/api/v1/data` routes with the code's own
20
+
business status and the QuickJS debug sentence as the client-facing message, for
21
+
example `409 DELETE_RESTRICTED · "hook 'guard' threw: TypeError: x is not a
22
+
function"`. It now answers `500 UNCLASSIFIED_FAULT` with the sanitised message
23
+
and no crash text, which is what the same crash carrying no declared code has
24
+
always answered. The full wrapper still reaches the server log through the
25
+
existing `[REST] Unhandled error` / withheld-fault path, so nothing an operator
26
+
diagnoses with is lost.
27
+
28
+
**What does NOT change.** An ordinary declared refusal — a hook that throws a
29
+
business error carrying a code and does not crash — is untouched: same status,
30
+
same code, same sentence, same structured fields. So is every non-sandbox
31
+
producer of those codes, and so is the `developerMessage` channel, which keeps
32
+
the rule it already had for a fault.
33
+
34
+
**Why.** A declared code is the author's statement about the failure mode they
35
+
handled; a crash is not that mode. Answering one with a business status shipped
36
+
an internal, stack-shaped sentence to an end user and told the client the wrong
37
+
thing about what happened, while the door one branch down already sanitised the
38
+
identical crash. Maintainer ruling, 2026-09-04, decision batch #27, on #15071.
39
+
40
+
**If you were relying on the old answer,** the affected shape is a hook that
41
+
declares one of the classification's ten code-gated refusals and then faults: it
42
+
now surfaces as a 5xx to clients and retry policies rather than as a 4xx. That is
43
+
the point of the change — the crash was never the refusal the code named.
0 commit comments