Repository navigation
Commit d7b26df
fix(core): an import row for a sandboxed body the door answers as a fault reads as that fault (#22740)
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`):
```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
#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](https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3b5475a commit d7b26df
4 files changed
Lines changed: 95 additions & 33 deletions
File tree
- .changeset
- packages
- core/src/utils
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
32 | 35 | | |
33 | 36 | | |
34 | 37 | | |
35 | | - | |
| 38 | + | |
36 | 39 | | |
37 | 40 | | |
38 | 41 | | |
| |||
176 | 179 | | |
177 | 180 | | |
178 | 181 | | |
179 | | - | |
180 | | - | |
181 | | - | |
182 | | - | |
183 | | - | |
184 | | - | |
185 | | - | |
186 | | - | |
187 | | - | |
188 | | - | |
189 | | - | |
190 | 182 | | |
191 | 183 | | |
192 | 184 | | |
| |||
206 | 198 | | |
207 | 199 | | |
208 | 200 | | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
209 | 224 | | |
210 | 225 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
459 | 459 | | |
460 | 460 | | |
461 | 461 | | |
462 | | - | |
463 | | - | |
464 | | - | |
465 | | - | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
466 | 479 | | |
467 | 480 | | |
468 | 481 | | |
469 | 482 | | |
| 483 | + | |
470 | 484 | | |
471 | | - | |
| 485 | + | |
472 | 486 | | |
473 | 487 | | |
474 | 488 | | |
475 | 489 | | |
476 | 490 | | |
477 | | - | |
| 491 | + | |
| 492 | + | |
478 | 493 | | |
479 | 494 | | |
480 | 495 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
46 | 45 | | |
47 | 46 | | |
48 | 47 | | |
| |||
391 | 390 | | |
392 | 391 | | |
393 | 392 | | |
394 | | - | |
395 | | - | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
396 | 407 | | |
397 | | - | |
398 | | - | |
399 | | - | |
400 | | - | |
401 | | - | |
402 | | - | |
403 | | - | |
404 | | - | |
405 | | - | |
406 | | - | |
407 | | - | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
408 | 431 | | |
409 | 432 | | |
0 commit comments