Repository navigation
fix(core): an import row for the sandbox's own fault reads the door's fault sentence (#22741) - #22867
Conversation
… fault sentence The QuickJS runner raises SandboxErrors of its own (CPU budget, wall-clock ceiling, capability denial, install and marshal failures) with no innerMessage. The row keyed its door-sentence rule on a sandbox origin, so it relayed the runner's diagnostic text where the create door answers Internal server error. isSandboxOwnFault (types) recognises them by the producer's declared class name and empty innerMessage, and the row applies the same rule to them. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…ort row to the door's sentence Parses packages/runtime/src/sandbox/quickjs-runner.ts with the TypeScript compiler: every SandboxError construction with no innerMessage and every throwSandboxFault call is a site, rendered from its own template. Each asserts the door's withheld verdict first, then that the row reads that sentence. Controls: driver text, the same text on a non-sandbox error, a code hook's native TypeError. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…ndbox-fault-import-row
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 30 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 04bd8cb781bee5ae9035389a5c814c10b2e46144 && git checkout 04bd8cb781bee5ae9035389a5c814c10b2e46144
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8532783f651a4a48b68e7d89579856e45501885e 23a4a8a83f12634e900b9de2a2d8f1c51ddd88d6 && git checkout -B drift-repro 8532783f651a4a48b68e7d89579856e45501885e && git merge --no-ff 23a4a8a83f12634e900b9de2a2d8f1c51ddd88d6
node scripts/docs-audit/affected-docs.mjs --json 8532783f651a4a48b68e7d89579856e45501885e |
…for types @objectstack/types gains the isSandboxOwnFault export, a public-surface widening, so the changeset carries the Clause-② yes (widening) line and types moves to minor. core stays patch. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #22867 for card #22741, read as the net diff from merge-base ① Derived judgments
② Semver level
③ Boundary flagsTwo Round 1
Round 1
Round 2
Round 2
Round 2 Implemented-by: VERDICT: PASS Adopted by |
Fixes #22741
Clause-②: yes (widening)
What
An import row for a fault the QuickJS sandbox raises itself now reads the create door's fault sentence instead of the runner's diagnostic text. These faults are the CPU budget, the wall-clock ceiling, a denied capability, an unavailable
ctx.api, a failed install or marshal, and a nested body's crash relayed across a VM hop.POST /api/v1/data/:objectand/createManyanswer them500 INTERNAL_ERRORInternal server error. The sync/importrow and the/import/jobsstored row relayed.messageinstead, for examplehook 'mz_lock_insert' exceeded CPU budget of 50ms (after 0 pump iterations).Landing point, and why no runner marker
isSandboxOwnFault(error)inpackages/types/src/data-error-classification.ts, besideisSandboxOrigin. It answerstruewhenname === 'SandboxError'andisSandboxOriginis false (no non-emptyinnerMessage). Both halves are read off the producer.quickjs-runner.tssetsthis.name = 'SandboxError', and itsinnerMessagecontract says the field is undefined for "the sandbox's own internal errors (capability denials, timeouts, marshalling failures)". The producer already marks its own faults, so the runner needs no new marker andpackages/runtimeis untouched.instanceof:@objectstack/runtimedepends on@objectstack/core(packages/runtime/package.json), so core cannot import the class.isEngineDuplicateRecordEnvelopein the same file recognises a class by its declared contract for the same reason, andobjectql'shook-withheld-readonly-fault.tsalready reads'SandboxError'by name.toFailedResult(packages/core/src/utils/import-runner.ts) now computessandboxed = isSandboxOrigin(err) || isSandboxOwnFault(err), and takes the door's sentence whensandboxed && door.status >= 500. That is the rule PR fix(core): an import row for a sandboxed body the door answers as a fault reads as that fault #22740 applies to a crash. ⛔ There is no second withholding rule:mapDataErrorstill decides which errors are faults and what their sentence is. The row'scodestaysIMPORT_ROW_FAILED.sanitizeRowErrortext.Measured before the fix
This ran through the real
QuickJSScriptRunnerateed637c09f, feeding the realmapDataErrorandrunImport(a scratch test, not committed). Import objectmz_locked:name/innerMessagefor(;;){}, 50ms)SandboxError/ undefinedInternal server errorhook 'mz_lock_insert' exceeded CPU budget of 50ms (after 0 pump iterations)SandboxError/ undefinedInternal server errorhook 'mz_lock_insert' exceeded wall-clock ceiling of 300ms while awaiting host calls (after 40 pump iterations)api.readdenied on another objectSandboxError/ undefinedInternal server errorcapability 'api.read' not granted to hook 'mz_lock_insert' (called ctx.api.object('mz_other').find)logdeniedSandboxError/ undefinedInternal server errorcapability 'log' not granted to hook 'mz_lock_insert'ctx.apimethodSandboxError/ undefinedInternal server errorctx.api.object('mz_other').find not implementedapi.readdenied on the import's own objectSandboxError/ undefinedOBJECT_NOT_FOUNDObject 'mz_locked' is not registeredThe last row is a separate door defect (see Acceptance notes). The row does not copy a 4xx sentence, so it keeps the capability text there.
The enumeration pin
packages/core/src/utils/import-runner-sandbox-fault-row.test.tssits in core'srepovitest project and is listed invitest.repo-tests.json, because it readspackages/runtime/src/sandbox/quickjs-runner.ts. Core's declared radiuspackages/**/*.tsalready covers that read. The test parses the runner with the TypeScript compiler, so the twothrow new SandboxError(examples inside doc comments are not sites.new SandboxError(…)with noinnerMessage(:235,:240,:660,:891,:917,:1025,:1104,:1199,:1553) and everythrowSandboxFault(…)call (:634,:730,:756,:803,:1173,:1180). Each message is rendered from its own template literal with a fixed sample per interpolation. Each case first asserts the door's verdict (500,{ error: 'Internal server error', code: 'INTERNAL_ERROR' }, the text withheld) as anti-vacuity. It then assertsisSandboxOwnFaultand that the row equals{ row: 1, ok: false, action: 'failed', error: DOOR_SENTENCE, code: 'IMPORT_ROW_FAILED' }. The batchedinsertManyDatapath is driven once for the budget fault.messageinthrowSandboxFault, andwithoutSandboxErrorPrefix(String(errStr))in the pump loop) must be exactly the declared relays. A site the pin cannot render, such as a new interpolation with no sample, is red and the failure names it. Each named kind keeps its measured floor: CPU budget 1, wall-clock 1, capability denial 5,ctx.apiunavailable 1, install 4, marshal 1, nested transaction 1, missing method 1. The classnameis read from the producer's constructor, so a rename there reaches every case.innerMessage(:280,:349,:403,:448) are rendered after a nested body crashed (TypeError: boom). That is the.messagethe outer pump rebuilds as a fault with noinnerMessage. The door withholds it, and the row takes the door's sentence.ErrorkeepssanitizeRowError's reading (the predicate reads class identity, never the message). A code-defined hook's nativeTypeErrorkeeps its text. Driver text keepssanitizeRowError's reading although its door answers 5xx. A body's refusal or crash, and non-error values, answerfalse.import-runner-sandbox-refusal-row.test.ts§4, the driver-text control PR fix(core): an import row for a sandboxed body the door answers as a fault reads as that fault #22740's ablation B pinned, is not edited.Ablations
All were run from committed HEAD
2180c575bfthroughnode scripts/ablation-replace.mjs. In each, the anchor hit once and moved from 1 to 0, the blob changed, and the restore was proven with the blob equal to HEAD andgit diff HEADempty. The command waspnpm --filter @objectstack/core exec vitest run --project repo --maxWorkers=2 src/utils/import-runner-sandbox-fault-row.test.ts.toFailedResult:isSandboxOrigin(err) || isSandboxOwnFault(err)becomesisSandboxOrigin(err)isSandboxOwnFaultbody becomesreturn false && …this.name = 'SandboxError'becomes'SandboxFault'inquickjs-runner.ts:660becomes a template with${txState.depth}quickjs-runner.ts:660: no sample for the interpolation txState.depthThe test imports
./import-runnerrelatively, and core's vitest aliases@objectstack/typesto its source, so nodist/sits on these paths. The runner is read from disk.Tests
All on HEAD
bb09729a0a(the branch merged withorigin/main55382dc02a). The dependency closure was built first withpnpm --filter '@objectstack/core...' build(exit 0).pnpm --filter @objectstack/core exec vitest run --project local --project repo --maxWorkers=2: 100 files / 2451 tests passed (all 100 test files in the package, both projects).pnpm --filter @objectstack/types exec vitest run --project local --project repo --maxWorkers=2: 28 files / 765 tests passed.pnpm --filter @objectstack/types typecheck: exit 0.pnpm --filter @objectstack/core typecheck: exit 0. The test layer stays at its 4 pinned signatures, and the new test adds none.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 66 families from this diff, and all 66 exit 0. Two of them,check:dual-build-cjs-loadsandcheck:lean-entry-closure, first answeredPREREQUISITE NOT MET(exit 3, not measured). Both were re-run green on the same HEAD aftercheck:type-check-debt's full build.--ranreconciliation: 66 derived, 66 run, 0 NOT-MEASURED (a derived zero, since every line carries its exit code). Re-run on23a4a8a83f(the changeset commit): the same 66 families, all exit 0, none exit 3;--ran66 derived, 66 run, 0 NOT-MEASURED.pnpm exec eslint --no-inline-config --format jsonon the three changed.tsfiles gives 3 files, 0 errors and 0 warnings (the count is read from the JSON). The population was read from ESLint itself:isPathIgnoredis false andcalculateConfigForFileyields rules for all three. This repo never enables type-aware linting (parserOptions.projectis null for all three, andeslint.config.mjssays so), so the diff cannot move the verdict on any untouched file. The repo-widepnpm lintis left to CI.in_progresswhen this PR opened, and not waited on.Acceptance notes
mapDataError's unknown-object heuristic,lower.includes("'OBJECT'") && lower.includes('not'), answers404 OBJECT_NOT_FOUNDObject 'OBJECT' is not registeredfor a sandbox fault whose text names the requested object. A capability denial on that object, such ascapability 'api.read' not granted to hook '…' (called ctx.api.object('OBJECT').find), does. It was measured through the real runner as shown above. The row does not copy that 4xx, so it keeps the capability text there. If the door is fixed to 500, the row follows with no change here. This is reported to the seat for filing.innerMessagesites (:235,:240,:660,:891,:917,:1025,:1104,:1199). The enumeration also finds the marshal failure (:1553), the sixthrowSandboxFaultcapability andctx.apisites, and the two relays (:432,:1524). The PM's 17new SandboxErrorlines are 15 constructions plus 2 inside doc comments.isSandboxOriginis not module-private. It is in the file's trailingexport { … }block, so@objectstack/typesalready exports it, and core imported it before this PR.@objectstack/types'.entry gains one export,isSandboxOwnFault(error: unknown): boolean. It was measured on the builtdist/index.d.ts(the declaration and the export list) and ondist/index.d.mts. No other name changes, and nothing re-exports@objectstack/typeswholesale (git grep), so it cannot collide. The seat settled the line asClause-②: yes (widening). The changeset declares it too and takesminorfor@objectstack/types;@objectstack/corestayspatch.packages/core/vitest.repo-tests.jsongains the new file, which the corerepoproject requires for a test that reads outside the package.Generated by Claude Code