Repository navigation
fix(core): an import row for a sandboxed body the door answers as a fault reads as that fault - #22740
Conversation
…ault reads as that fault A sandboxed hook body that crashed (`TypeError: boom`) and a sandboxed refusal that declared a 5xx status are answered by `POST /data/:object` and `/createMany` as server faults with their text withheld. The import row relayed both: the crash as the QuickJS debug wrapper through `sanitizeRowError`, the declared 5xx as the body's prose. `toFailedResult` now takes the create door's own sentence for a sandbox-origin error that `mapDataError` answers with a 5xx. The verdict and the sentence are `mapDataError`'s; the row adds no second withholding rule. The row's code is unchanged, and every non-sandbox error keeps its row text (driver text is still cleaned by `sanitizeRowError`). Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 27 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 2e694e5c9715faf1c8be06460e560f734ee19b85 && git checkout 2e694e5c9715faf1c8be06460e560f734ee19b85
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20 824d16b01212721d504fc5910e3b80f56457db51 && git checkout -B drift-repro b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20 && git merge --no-ff 824d16b01212721d504fc5910e3b80f56457db51
node scripts/docs-audit/affected-docs.mjs --json b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20 |
Merge-queue kick-out: not this PR's ·
|
Fixes #22718
Clause-②: no
What changes
An import row for a sandboxed hook body that crashed, or for a sandboxed refusal that declared a 5xx status, now carries the same sentence
POST /data/:objectand/createManyanswer for that error (Internal server error), instead of relaying the fault's text. The row still reads as a fault:ok: false,action: 'failed', and itscodeis unchanged (IMPORT_ROW_FAILEDfor a crash, the declared code for a declared 5xx). A refusal with no status or a 4xx status keeps the hook's own sentence, as PR #22716 made it.One producer-side line in
toFailedResult(packages/core/src/utils/import-runner.ts):dooris themapDataError(err, objectName)answer the function already computed foradoptDoorVerdict. No second withholding rule is added:mapDataError's verdict. For a sandbox-origin error it answers a 5xx in exactly two shapes: theisSandboxCrashterminal (500 INTERNAL_ERROR) anddeclaredServerFaultAnswer(the declared status, its code, the prose withheld). The row copies the door'serrorstring, so a later change to what the door withholds reaches the row unchanged.isSandboxOrigin, already exported from@objectstack/typesforrest's door). It is not the door's 5xx alone. A driver error also answers 5xx at the door, and its row text is the import's own reading throughsanitizeRowError, which stays as it is. Ablation B below shows that dropping the gate changes that row.IMPORT_ROW_FAILEDis the code [finding] import: a sandboxed hook's refusal reaches an import row as the debug wrapper (hook NAME threw: Error: SENTENCE), the last REST face of #22588's family and the home of its REST write-route enumeration pin #22694's control already pinned as that reading, and the declared-5xx row's declared code is the code the doors keep too.Measured before the change (
origin/maina360cee92e)These readings come from the real REST handlers and the real
runImport, through therest-write-route-hook-refusal-sentence.ledger.test.tsharness, with theSandboxErrorshapequickjs-runner.tsproduces.POST /data/:object/createMany/importrow/import/jobsstored rowinnerMessage: 'TypeError: boom')500INTERNAL_ERRORInternal server errorIMPORT_ROW_FAILEDhook 'mz_guard' threw: TypeError: boomstatus: 503,code: 'SERVICE_UNAVAILABLE')503SERVICE_UNAVAILABLEInternal server errorSERVICE_UNAVAILABLEThe ledger service is down.After the change, both import rows carry the doors' sentence in both rows of the table. PM mechanism assumption 1 (the
sandboxBusinessMessage(err) ?? sanitizeRowError(e?.message)read) held. Assumptions 2 and 3 were re-measured in the table above.Pins
packages/core/src/utils/import-runner-sandbox-refusal-row.test.ts:createDatapath each equal{ ok: false, action: 'failed', error: DOOR_SENTENCE, code }. The door's own fault verdict is asserted first as anti-vacuity, and the native text and the prose are asserted absent.packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts§4 (rewritten): for the crash and the declared 5xx, the test drivesPOST /data/:objectand/createManyand asserts that both answer a 5xx, that they agree, and that they withhold the text. The sync/importrow and the/import/jobsstored row must then equal that answer. The refusal control is that file's §2, which drives both import doors and requires the hook's sentence.File surface: the rest file is
domain:cliand outside the claim's declared surface. Only the test file changed there. Its §4 pinned the very crash row text this card changes (row === sanitizeRowError(crash.message)), so leaving it unchanged would have turned it red. The existing harness drives both import doors with the real handlers, which is the door-level pin triage asked for. No dogfood or verify file is touched.Evidence
@objectstack/restresolves@objectstack/corethroughdist/(no alias). With core'sdist/still at BASE (the markerdoor.status >= 500counted 0 indist/index.jsand 1 insrc), the rest ledger file ran2 failed | 45 passed. Both §4 cases failed at the sync/importdoor:expected "hook 'mz_guard' threw: TypeError: boom" to be "Internal server error"andexpected 'The ledger service is down.' to be 'Internal server error'. After rebuilding core (ablation-dist-preflight @objectstack/core 'door.status >= 500': marker present indist/index.jsanddist/index.cjs, tree clean) it ran47 passed.sandboxFault ??, viascripts/ablation-replace.mjs, anchor 1 to 0, blob8e511983ac2atob05265a89971): the core file ran2 failed | 13 passed, both §5 cases. The restore was proven: blob equal to HEAD andgit diff HEADempty.isSandboxOrigin(err) &&gate): the core file ran1 failed | 14 passed. The driver-text control receivedInternal data errorwhere it expectedSQLITE_BUSY: database is locked. The restore was proven the same way. The first attempt at B was refused by the tool before anything ran: the replacement was a substring of the anchor, so its count could not rise. I re-ran it with a distinct replacement, and only the second run counts.824d16b012(main merged once before opening; it touchedlintand a spec migration string, and I rebuilt spec, lint and core after the merge):pnpm --filter @objectstack/core test: 89 files, 2308 passed.pnpm --filter @objectstack/core typecheck: exit 0. Its test layer passes throughcheck:test-typecheck, with the 4 pinned debt signatures held.pnpm --filter @objectstack/rest typecheck: exit 0, with 0 test-layer debt.repoproject (4 security pins, which do not touch the runner) and rest's full suite. The public face is unchanged (no export or type moved), so only the row value moved.Gates (at
824d16b012)node scripts/pm/dispatch-gates.mjs --commandsderived 65 commands from this branch's change set (merge base0f77ff520): the dispatch's 51, plus these 14:check-adr-0087-registration(base and self-test)check-empty-changeset(base and self-test)release-rehearsal-cloneandrelease-pending-publishself-testscheck:engine-double-contract,check:objectql-double-limit,check:objectui-changeset,check:pm-changeset-deadline-census,check:query-options-erasure,check:type-check-coverage,check:type-check-debt,check:where-matcherI also ran 3 roster gates whose rosters sit under one of these paths:
check-changeset-fixed,check:authz-resolverandcheck:filter-alias-parity.Of the 68, 67 exited 0.
pnpm check:dual-build-cjs-loadsexited 3 withPREREQUISITE NOT MET(it needs every package'sdist/), so it is NOT MEASURED. As a narrowed reading, I loaded core's rebuiltdist/index.cjsdirectly (runImportandsanitizeRowErrorare functions); core is the only package whosedist/this diff changes. The--ranreconciliation, with exit codes recorded, reads65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN. The repo-widepnpm lintis CI's and was not run here.Acceptance notes (noted, not filed here)
A crash whose body attached a
codeto the thrown native error keeps that code on the row, while the doors answerINTERNAL_ERRORfor it (commit cf6e0a1's ruling at the doors). The row's code rule is unchanged by this card. Carrier: none.Two neighbours of this card show the same row-versus-door gap. They are out of this card's scope and reported to the seat, not fixed here:
SandboxErrorwith noinnerMessage(the sandbox's own CPU-budget or capability fault), where the door answers500 Internal server errorand the row relays.message.TypeError), where the row relaysx.trim is not a function.Both were read through
mapDataErrorandsanitizeRowErroronly, never at a public door. Rows relay unclassified non-sandbox text on purpose today, so the second one is a design question, not a defect reading.Generated by Claude Code