Repository navigation
fix(rest): hand-built write routes withhold the sandbox's own fault as the data door does (#22885) - #22896
Conversation
…s the data door does sandboxFaultAnswer now answers isSandboxOwnFault (a SandboxError the runner raised itself: a denied capability, a missing or unavailable ctx.api method, a budget, an install or marshal failure) with the classification door's own answer, the withheld 500 INTERNAL_ERROR it already gives a crash. One predicate, one terminal. The record-share arms' sentence goes through the same answer: sharingFaultMessage asks sandboxFaultAnswer, and the family keeps its own 500 SHARE_*_FAILED code, the write-route ledger's recorded exception. The ledger's section 6 drives every write row with one own-fault shape per kind the runner raises, the crash and declared-5xx shapes kept as the control, and holds the full error to the server log. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2e643aa3056f85431e283686f395d928b236b03f && git checkout 2e643aa3056f85431e283686f395d928b236b03f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 45e206e4987494dcb3f762ae527caf28f8ca3491 24f3dc6f46edfb3c2c9ab66f9343eb11454ae355 && git checkout -B drift-repro 45e206e4987494dcb3f762ae527caf28f8ca3491 && git merge --no-ff 24f3dc6f46edfb3c2c9ab66f9343eb11454ae355
node scripts/docs-audit/affected-docs.mjs --json 45e206e4987494dcb3f762ae527caf28f8ca3491
|
Fixes #22885
Clause-②: no
What this does
The QuickJS runner raises some
SandboxErrors itself, with noinnerMessage: a denied capability, a missing or unavailablectx.apimethod, a CPU budget or wall-clock ceiling, an install or marshal failure, a nested body's fault relayed one VM hop up.isSandboxOwnFault(@objectstack/types) recognises them off the producer, and the/datadoor answers them500 INTERNAL_ERROR/Internal server error. Twenty-three hand-built REST write rows relayed the runner's diagnostic text instead.packages/rest/src/error-response.ts:sandboxFaultAnsweranswersisSandboxOwnFaultwithresolveErrorResponse, the same classification door and the same withheld500it gives a crash. One predicate, one terminal, no own-fault branch anywhere else.packages/rest/src/rest-server.ts, record-share arms only:sharingFaultMessageaskssandboxFaultAnswerfor its sentence instead of keeping its own sandbox read. The family keeps its own500 SHARE_*_FAILEDstatus and code, as it does for a crash (see "The record-share rows" below).packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts§6: nine own-fault shapes, one per kind the runner raises, over every row the ledger already drives (ANSWERS_THE_SENTENCE, held toREST_ROUTE_LEDGERby §1, not hand-listed). The crash and declared-5xx shapes stay as the CONTROL, unchanged. Each own-fault case asserts the ADR-0112status+code, the doors' withheld sentence, no trace of the runner's text in the body, and that the wholeSandboxErrorreachesconsole.error(the file now declaresOS_REST_LOG: 'info'inside §6, ascheck:rest-log-spy-declaredrequires)..changeset/22885-sandbox-own-fault-rest-write-routes.md:@objectstack/restpatch,Clause-②: no.Measured at RED (the card's "Not measured"), BASE
2e11d9006cThe new §6 cases, run before the fix: 207 failed / 370 passed, which is exactly 23 rows x 9 shapes. Every named downstream was called, and no upstream arm (a prefix map, a 403/404 arm,
classifiedRefusalAnswer) intercepted any shape. Every row reached its own terminal with the runner's text:POST /analytics/dataset/query500 ANALYTICS_QUERY_FAILEDPOST /security/explain500 EXPLAIN_FAILEDPOST/DELETE /data/:object/:id/shares500 SHARE_GRANT_FAILED/SHARE_REVOKE_FAILED500 RULE_*_FAILEDconfirm/dismiss500 SUGGESTION_*_FAILEDPOST /approvals/requests/:id/*writes500 APPROVAL_*_FAILEDPOST /packages/publish500 INTERNAL_ERROR(text relayed)POST /datasources/:name/external/*routes400 EXTERNAL_DATASOURCE_ERROR/EXTERNAL_IMPORT_ERRORThe other 20 driven rows already answered every shape
500 INTERNAL_ERROR(the import rows per row inside200/201), and all 86 control readings already withheld. Before any edit,sendThrownError(the classification door) answered all eight runner kinds500 INTERNAL_ERROR/Internal server error, whilesandboxFaultAnswerandclassifiedRefusalAnswerboth answeredundefined. So the change is one line at the function every family already asks.After: 21 rows answer
500 INTERNAL_ERROR/Internal server error. The two record-share write rows answer500 SHARE_*_FAILED/Internal server error.GET /data/:object/:id/shares, which the write ledger does not drive, was measured by a throwaway probe (deleted):500 SHARES_LIST_FAILED/Internal server error; the same words on a plainErrorkeep the relayed text there.The record-share rows: one reading of the triage text
Triage says the record-share rows "go through the same answer" and "Do not patch their own terminal separately", and also that "The crash rows stay as the CONTROL". The dispatch's reading was that these arms ask
sandboxFaultAnsweras the other families do. Measured, that literal shape moves the CONTROL: on the two share write rows a crash would go from500 SHARE_*_FAILEDto500 INTERNAL_ERROR, and a sandboxed declared 5xx from500 SHARE_*_FAILEDto503 SERVICE_UNAVAILABLE, four existing assertions, and it would delete the ledger'sOWN_FAULT_TERMINALexception that #22764 recorded as "a decision this card does not take".So the share arms' sentence is
sandboxFaultAnswer's (one predicate, no own-fault check of their own), and their status and code stay their own, exactly as for a crash.OWN_FAULT_TERMINALkeeps its two rows; its reason now names the shared read. If the family's status and code should move too, that is a four-line change plus deletingOWN_FAULT_TERMINAL, and it moves the crash and declared-5xx answers on these routes with it.Files outside the claim's surface, declared
packages/rest/src/package-routes.tsandpackages/rest/src/external-datasource-routes.ts, comment-only. Both docblocks said sentences this change makes false ("sandboxFaultAnsweris the doors' answer for exactly those two", "Every other throw keeps this family's own400"). No code there changes; both callers already asksandboxFaultAnswerfirst.Reverse verification (one-time; all from committed
24f3dc6f46, viascripts/ablation-replace.mjs, each restore proven by blob == HEAD and an emptygit diff HEAD)The tested subjects resolve by relative
srcimport, not through a dependency'sexports, so nodistrebuild leg applies.sandboxFaultAnswer: anchor 1 to 0, blob1c91226dfd37toaa8e4dc71f19. Ledger 207 failed / 370 passed, the RED population exactly (the share rows go red too, since their sentence reads the same answer).sandboxFaultAnswerread: blob8e11bff9ffa9toe31a3df9f2c5. 18 failed, only the two record-share write rows (9 shapes each).logError: blob8e11bff9ffa9to96834aaee48f. 81 failed (9 approval rows x 9 shapes), each on "the runner's error reaches the server log". The log assertion can fail.Tests, at
24f3dc6f46pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-write-route-hook-refusal-sentence.ledger.test.ts: 577 passed (189 before this change).pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: 273 files, 5721 passed, 326 skipped. This includessharing-envelope.test.ts.pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2: 5 files, 209 passed, 1 skipped. This includes the threeerror-response-*suites.pnpm --filter @objectstack/rest typecheck: exit 0,check:test-typecheck: OK.--listFilesputs the ledger in thetsconfig.test.jsonprogram (1 hit).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths, on this tree. Reconciled with--ran: 65 derived, 64 run at exit 0, 1 NOT-MEASURED, 0 unrun.pnpm check:dual-build-cjs-loads, reason: exit 3,PREREQUISITE NOT MET. 42 packages this diff does not touch have nodist/in this container. Narrower reading:@objectstack/restis built andrequire('./dist/index.cjs')loads, with 34 exports. CI runs the full gate.pnpm lint:eslint --no-inline-config --format jsonover the five touched.tsfiles. ① Population, read from eslint's own config:--print-configresolves a config for each of the five (6/6/6/6/5 rules), and the changeset.mdmatches none. ② The JSON result holds 5 files, 0 errors, 0 warnings. ③ Invariance:eslint.config.mjssets noparserOptions.projectand noprojectService(its own comment: it "never enables type-aware linting"), and this diff touches no config or data file a rule reads, so it cannot move any untouched file's verdict.Acceptance notes
sandboxFaultAnswercallers inrest-server.ts(analytics, explain, sharing rules, suggested bindings, approvals) still describe the answer as "(a crash, or a refusal declaring a 5xx)". That is now incomplete but not false, and it was left alone to keeprest-server.tsto the record-share arms. Carrier: none.resolveErrorResponseasksisSandboxCrashat its top but notisSandboxOwnFault. An own fault reachesclassifyDataError's own-fault terminal only because it carries nostatusorcode. That holds for every raise site inquickjs-runner.tstoday (no own-fault construction passesSandboxErrorInfo), so it is dormant. Carrier: none.Generated by Claude Code