Skip to content

fix(core): an import row for a sandboxed body the door answers as a fault reads as that fault - #22740

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22718-import-crash-row
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22718-import-crash-row

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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/:object and /createMany answer 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 its code is unchanged (IMPORT_ROW_FAILED for 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):

const sandboxFault = isSandboxOrigin(err) && door.status >= 500 ? String(door.body.error) : undefined;
const message = sandboxFault ?? sandboxBusinessMessage(err) ?? sanitizeRowError(e?.message);

door is the mapDataError(err, objectName) answer the function already computed for adoptDoorVerdict. No second withholding rule is added:

  • Which errors are faults, and what their sentence is, are both mapDataError's verdict. For a sandbox-origin error it answers a 5xx in exactly two shapes: the isSandboxCrash terminal (500 INTERNAL_ERROR) and declaredServerFaultAnswer (the declared status, its code, the prose withheld). The row copies the door's error string, so a later change to what the door withholds reaches the row unchanged.
  • The gate is the sandbox origin (isSandboxOrigin, already exported from @objectstack/types for rest'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 through sanitizeRowError, which stays as it is. Ablation B below shows that dropping the gate changes that row.
  • The row's code is not touched. The card asks for "a failed row with a fault code". The crash row's IMPORT_ROW_FAILED is 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/main a360cee92e)

These readings come from the real REST handlers and the real runImport, through the rest-write-route-hook-refusal-sentence.ledger.test.ts harness, with the SandboxError shape quickjs-runner.ts produces.

error POST /data/:object /createMany sync /import row /import/jobs stored row
crash (innerMessage: 'TypeError: boom') 500 INTERNAL_ERROR Internal server error same IMPORT_ROW_FAILED hook 'mz_guard' threw: TypeError: boom same
declared 5xx (status: 503, code: 'SERVICE_UNAVAILABLE') 503 SERVICE_UNAVAILABLE Internal server error same SERVICE_UNAVAILABLE The ledger service is down. same

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:
    • §5 (new): a crash row and a declared-5xx row on the inline createData path 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.
    • §4: the crash control that pinned the old row text is removed; §5 now covers the crash. The driver-text control gains one line asserting the door answers that error with a 5xx too, so the control witnesses the sandbox gate.
  • packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts §4 (rewritten): for the crash and the declared 5xx, the test drives POST /data/:object and /createMany and asserts that both answer a 5xx, that they agree, and that they withhold the text. The sync /import row and the /import/jobs stored 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:cli and 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

  • Red, then green, at the door level. @objectstack/rest resolves @objectstack/core through dist/ (no alias). With core's dist/ still at BASE (the marker door.status >= 500 counted 0 in dist/index.js and 1 in src), the rest ledger file ran 2 failed | 45 passed. Both §4 cases failed at the sync /import door: expected "hook 'mz_guard' threw: TypeError: boom" to be "Internal server error" and expected 'The ledger service is down.' to be 'Internal server error'. After rebuilding core (ablation-dist-preflight @objectstack/core 'door.status >= 500': marker present in dist/index.js and dist/index.cjs, tree clean) it ran 47 passed.
  • Ablation A (remove sandboxFault ??, via scripts/ablation-replace.mjs, anchor 1 to 0, blob 8e511983ac2a to b05265a89971): the core file ran 2 failed | 13 passed, both §5 cases. The restore was proven: blob equal to HEAD and git diff HEAD empty.
  • Ablation B (drop the isSandboxOrigin(err) && gate): the core file ran 1 failed | 14 passed. The driver-text control received Internal data error where it expected SQLITE_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.
  • Tests at the merge commit 824d16b012 (main merged once before opening; it touched lint and 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 through check:test-typecheck, with the 4 pinned debt signatures held.
    • pnpm --filter @objectstack/rest typecheck: exit 0, with 0 test-layer debt.
    • The rest ledger file: 47 passed.
    • Declared to CI: core's repo project (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 --commands derived 65 commands from this branch's change set (merge base 0f77ff520): the dispatch's 51, plus these 14:

  • check-adr-0087-registration (base and self-test)
  • check-empty-changeset (base and self-test)
  • the release-rehearsal-clone and release-pending-publish self-tests
  • check: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-matcher

I also ran 3 roster gates whose rosters sit under one of these paths: check-changeset-fixed, check:authz-resolver and check:filter-alias-parity.

Of the 68, 67 exited 0. pnpm check:dual-build-cjs-loads exited 3 with PREREQUISITE NOT MET (it needs every package's dist/), so it is NOT MEASURED. As a narrowed reading, I loaded core's rebuilt dist/index.cjs directly (runImport and sanitizeRowError are functions); core is the only package whose dist/ this diff changes. The --ran reconciliation, with exit codes recorded, reads 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN. The repo-wide pnpm lint is CI's and was not run here.

Acceptance notes (noted, not filed here)

  • A crash whose body attached a code to the thrown native error keeps that code on the row, while the doors answer INTERNAL_ERROR for 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:

    • A SandboxError with no innerMessage (the sandbox's own CPU-budget or capability fault), where the door answers 500 Internal server error and the row relays .message.
    • A non-sandbox native error (for example a code-defined hook's TypeError), where the row relays x.trim is not a function.

    Both were read through mapDataError and sanitizeRowError only, 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

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e694e5c9715faf1c8be06460e560f734ee19b85 — the merge of head 824d16b01212721d504fc5910e3b80f56457db51 into base b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 23:05
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 23:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 10, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Merge-queue kick-out: not this PR's · domain:engine seat 1 (#6367) · session_01JfJfBUC3cQ6hhgm9MQK76T · 2026-10-10T23:32Z

Dequeued CI_FAILURE at 2026-10-10T23:32:13Z. Not re-queued.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants