Repository navigation
Commit 6ee483c
Fixes #23718
Clause-②: no
## What changed
A sandboxed body (a `type: 'script'` action body, or a sandboxed hook
body) that throws a value which is not an `Error`, the plain `{ code,
message }` object metadata app bodies throw, is now answered with that
value's own `message` as the client sentence. Its code is kept, as
before. One place changed: the wrapper's `REJECT_HANDLER` in
`packages/runtime/src/sandbox/quickjs-runner.ts`, the one writer that
flattens a rejection into `__error`. No door changed.
`@objectstack/types` is not changed, and PR #23717's identity relay is
left as it landed.
- The handler flattened every rejection as `NAME: MESSAGE`, else
`String(e)`. A plain object has no class name and no `toString` of its
own, so the doors served `undefined: MESSAGE`, or `[object Object]` when
it had no `message`.
- A non-`Error` object is now flattened under the default name `Error`,
the one prefix `userFacingMessage` strips, so its `message` reaches
`innerMessage` byte for byte. A `name` on a plain object is a field, not
a frame, and gets no prefix.
- With no usable `message` (a non-blank string) and no `toString` of its
own, the sentence is `INTERNAL_ERROR_MESSAGE` (`Internal server error`),
read from `@objectstack/types`, never a literal. It is what the doors
already answer for a fault with no business message, measured below. A
value with a `toString` of its own keeps it.
- Unchanged: a thrown `Error` (or subclass), a thrown primitive, and a
value whose reads throw. The new branch is inside a `try`; any throw
there falls back to the old expression, so a throwing getter behaves
exactly as it does today.
- `code`, `status`, `fields` and `userMessage` still ride `__errorInfo`,
untouched.
- One line outside the handler: `INTERNAL_ERROR_MESSAGE` joins the
file's existing `@objectstack/types` import, so the handler can
interpolate it.
## Before / after, measured
Rig: PR #23717's. better-sqlite3 `:memory:` under the real `ObjectQL`,
action bodies through `actionBodyRunnerFactory`, hook bodies through
`hookBodyRunnerFactory`, both over a real `QuickJSScriptRunner`, and
`HttpDispatcher.dispatch()` over a real Fetch `Request`. The `/data`
classification is read at function level (`mapDataError`), as PR #23717
read it. "Before" was read at base `9e0945cf45` (and again under the
ablation below), "after" at `7bd01e068d`.
| thrown value | door | before | after |
|---|---|---|---|
| `{ code: 'EMAIL_TAKEN', message: 'That address is already on file' }`
| action | `400 VALIDATION_ERROR`, `declaredCode: EMAIL_TAKEN`,
`undefined: That address is already on file` | `400 VALIDATION_ERROR`,
`declaredCode: EMAIL_TAKEN`, `That address is already on file` |
| `{ code: 'DUPLICATE_RECORD' }` | action | `400 DUPLICATE_RECORD`,
`[object Object]` | `400 DUPLICATE_RECORD`, `Internal server error` |
| `{ code: 'DUPLICATE_RECORD', status: 409, message }` | action | `409
DUPLICATE_RECORD`, `undefined: …` | `409 DUPLICATE_RECORD`, the message
|
| `{}` | action | `400 VALIDATION_ERROR`, `[object Object]` | `400
VALIDATION_ERROR`, `Internal server error` |
| `[]` | action | `500 INTERNAL_ERROR` after the wall-clock ceiling (no
rejection was recorded) | `400 VALIDATION_ERROR`, `Internal server
error`, at once |
| `{ name: 'TypeError', message: 'forged' }` | action | `500
INTERNAL_ERROR` (read as a crash) | `400 VALIDATION_ERROR`, `forged` |
| hook body, `{ code: 'EMAIL_TAKEN', message }`, behind a host handler's
write | action | `400`, `declaredCode: EMAIL_TAKEN`, `undefined: …` |
`400`, `declaredCode: EMAIL_TAKEN`, the message |
| hook body, `{ code: 'DUPLICATE_RECORD' }`, behind a host handler's
write | action | `400 DUPLICATE_RECORD`, `[object Object]` | `400
DUPLICATE_RECORD`, `Internal server error` |
| hook body, `{ code: 'EMAIL_TAKEN', message }`, behind a script body's
write (two VM hops) | action | `400`, `undefined: …` | `400`, the
message |
| hook body, `{ code: 'EMAIL_TAKEN', message }` | `/data` classification
| `400 VALIDATION_ERROR`, `declaredCode: EMAIL_TAKEN`, `undefined: …` |
the same, with the message |
| hook body, `{ code: 'DUPLICATE_RECORD' }` | `/data` classification |
`400 DUPLICATE_RECORD`, `[object Object]` | `400 DUPLICATE_RECORD`,
`Internal server error` |
| CONTROL `new Error('x')` | action, hook at action, `/data` | `400`,
`x` | unchanged |
| CONTROL `Error` carrying `EMAIL_TAKEN` | action | `400
VALIDATION_ERROR`, `declaredCode: EMAIL_TAKEN`, its message | unchanged
|
| CONTROL `Error` with no message, carrying `DUPLICATE_RECORD` | action
| `400 DUPLICATE_RECORD`, `Error` | unchanged |
| CONTROL `'Name is required'` / `null` | action | `400`, `Name is
required` / `null` | unchanged |
| CONTROL `{ toString() { return 'custom words'; } }` | action | `400`,
`custom words` | unchanged |
| CONTROL capability denial (a sandbox fault) | action | `500
INTERNAL_ERROR`, `Internal server error` | unchanged |
| CONTROL `{ code, get message() { throw … } }` | action | `500
INTERNAL_ERROR` after the wall-clock ceiling | unchanged (falls back as
today) |
The no-message sentence: the action door answers a sandbox fault (a
capability denial) and a crash with `500 INTERNAL_ERROR`, `Internal
server error`, and the `/data` classification's unclassified-fault
terminal answers the same sentence. That is the one sentence the doors
already give for a fault with no business message.
## Readings the dispatch asked for
- **(a) The hook door crosses the same writer: holds.** A sandboxed hook
body's plain-object throw broke the same way, behind a host handler's
write, behind a script body's write (two VM hops, via the nested-refusal
relay), and in the `/data` classification. All three are measured and
pinned.
- **(b) No wrapper or `SandboxError:` prefix reaches the client:
holds.** The pump loop builds `innerMessage` as
`userFacingMessage(withoutSandboxErrorPrefix(__error))`. The new
flattening is `Error: MESSAGE`, which those two strips turn back into
exactly `MESSAGE`. The pins assert the client sentence by equality, so a
wrapper or prefix would turn them red.
- **The `400` question: not this writer.** The status comes from
`resolveThrownHttpError` (`@objectstack/types`): `.status`, then
`.statusCode`, then the door's fallback, never a status derived from the
code. A thrown `Error` carrying `DUPLICATE_RECORD` and no `status` also
answers `400 DUPLICATE_RECORD`, and a plain object that declares
`status: 409` answers `409`. So it is not specific to plain objects, and
nothing in the reject handler decides it. The resolver's precedence
table and ADR-0112's 2026-08-27 amendment ("`SandboxError` answers
**400**") both declare this behaviour. Not folded in.
## Tests
- `packages/runtime/src/sandbox/plain-object-throw-sentence.test.ts`
(new, 10, beside the change). Plain object with a message (action and
hook origin); no message; `[]`; a message beginning `Error: `; a named
plain object. CONTROLS: `Error` (the log reads `action 'submit' threw:
Error: x`, exactly as before), `TypeError`, a string, a custom
`toString`.
-
`packages/runtime/src/sandbox-plain-object-throw-client-sentence.test.ts`
(new, 10): the door rows above. Action door (2 + CONTROL), hook at the
action door (behind a handler: 2; two VM hops: 1; CONTROL), `/data`
classification (2 + CONTROL).
- Ablation, through `node scripts/ablation-replace.mjs` (wrap mode) with
an outer `trap … EXIT INT TERM` restore on the absolute path. The anchor
`globalThis.__error = flat !== undefined ? flat : (…)` was replaced by
the old expression and a marker: anchor 1 to 0, blob `bfc8615afe02` to
`d757d0101457`. Result `Tests 13 failed | 7 passed (20)`. Exactly the 13
plain-object rows went red (6 unit, 7 door: `expected 'undefined: That
address is already on…'`, `expected '[object Object]'`, `expected
'TypeError: forged'`, and the `[]` row timing out). All 7 CONTROLS
stayed green. Restore: blob == HEAD `bfc8615afe02`, and `git diff HEAD`
is empty. The pins import the runner from `src` by relative path, so no
build sits in the resolution path.
At `a4145db7b2`:
- `pnpm --filter @objectstack/runtime exec vitest run --project local
--maxWorkers=2` (the package's `test` script): `Test Files 395 passed
(395)`, `Tests 5918 passed | 19 skipped (5937)`.
- `pnpm --filter @objectstack/runtime typecheck`: exit 0.
`check:test-typecheck: OK`: 27 files / 190 errors / 68 pinned signatures
held. Both new test files are in `tsconfig.test.json`'s program
(`--listFilesOnly`: 2).
- The two new files by path: `Test Files 2 passed (2)`, `Tests 20 passed
(20)`.
## Gates
At `a4145db7b2`, `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 61 commands from 4 changed paths
(merge base `9e0945cf4`). All 61 ran, with exit codes captured before
any pipe. 60 answered exit 0 on the first pass.
`check:dual-build-cjs-loads` answered `PREREQUISITE NOT MET` (exit 3, no
workspace `dist`). After `pnpm build` (75 tasks), it answered exit 0
(110 entry points across 68 packages). `--ran`: `61 derived, 61 run, 0
NOT-MEASURED, 0 UNRUN`, exit 0.
Lint: a narrowed run, in the scoped form `eslint.config.mjs`'s own
header prescribes.
- `node --stack-size=4000 node_modules/eslint/bin/eslint.js
--no-inline-config packages/runtime` (the `pnpm lint` binary and flags):
exit 0.
- `--format json` over the four changed files: 0 errors, 0 warnings. The
changeset is ignored by the config, as under `pnpm lint`.
- Positive control: the same command reds on a restricted root
`@objectstack/spec` import (`Data`) piped on stdin at a
`packages/runtime` path: exit 1, `no-restricted-imports`.
CI's families are this PR's check-runs.
## Acceptance notes
- In-process: a caller that catches the `SandboxError` for such a throw
reads the value's `message` as `innerMessage`. Its log-only `message`
now reads `KIND 'NAME' threw: Error: MESSAGE`. The changeset states it.
- A plain object's `message` that begins with a native error name
(`TypeError: …`) is now read as a crash (`500`), the same way `new
Error('TypeError: …')` already is. Before, the `undefined: ` prefix hid
it.
- Not addressed here, and reported to the dispatching seat: a rejection
whose flattening throws, or flattens to an empty string, is never
recorded. `throw ''`, `throw Object.create(null)` and a throwing
`message` getter all leave the pump to spin to the wall-clock ceiling,
then answer `500` with a "while awaiting host calls" log line. Measured
before and after; this PR keeps that path exactly as today, as the
dispatch ruled.
- `origin/main` moved by one commit after this branch was cut
(`a515abdd8c`, PR #23719, a `packages/rest` refactor with no file in
common with this diff). It is not merged here; the PR's CI runs on the
merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f40d877 commit 6ee483c
4 files changed
Lines changed: 418 additions & 2 deletions
File tree
- .changeset
- packages/runtime/src
- sandbox
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
0 commit comments