Skip to content

fix(types): the data door answers the sandbox's own fault as a fault, not as an unknown object (#22868) - #22882

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22868-sandbox-fault-not-unknown-object
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22868-sandbox-fault-not-unknown-object

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22868
Clause-②: no

What was wrong

The QuickJS runner raises some SandboxErrors itself, with no innerMessage: a budget, a denied capability, an unavailable ctx.api, a missing ctx.api method, a failed install or marshal. Two of those texts name the object the hook body addressed:

  • the ctx.api.object(...) capability gate (quickjs-runner.ts:1173, throwSandboxFault): capability 'api.read' not granted to hook 'H' (called ctx.api.object('OBJ').find);
  • the missing-method arm (quickjs-runner.ts:1199): ctx.api.object('OBJ').find not implemented.

classifyDataError's unknown-object limb answers 404 OBJECT_NOT_FOUND ("Object 'OBJ' is not registered") for a message holding the quoted requested object and not. So when the body addressed the request's own object, mapDataError, and every door that reaches it (POST /api/v1/data/:object, resolveErrorResponse's fall-through, the import rows), answered a sandbox fault as "this object does not exist".

The fix: one statement in packages/types/src/data-error-classification.ts

classifyDataError asks isSandboxOwnFault (PR #22867) at its top, beside the isSandboxCrash terminal, and answers UNCLASSIFIED_FAULT(): 500, code: INTERNAL_ERROR, error: Internal server error. No second rule: the predicate reads the producer's identity (name === 'SandboxError' and no innerMessage), never the text. The isSandboxOwnFault docblock's "mapDataError asks nothing here" paragraph now says that it does.

Where it is asked, measured

mapDataError(SandboxError(text), 'mz_locked') on the source through tsx, with the fix line ablated (scripts/ablation-replace.mjs, restored, blob == HEAD) and with the fix:

runner text fix ablated with the fix
capability gate, requested object 404 OBJECT_NOT_FOUND 500 INTERNAL_ERROR
missing method, requested object 404 OBJECT_NOT_FOUND 500 INTERNAL_ERROR
capability gate, another object 500 INTERNAL_ERROR 500 INTERNAL_ERROR
capability gate, body-chosen object name no such table 500 DATABASE_ERROR 500 INTERNAL_ERROR
capability gate, body-chosen object name unknown object 404 OBJECT_NOT_FOUND 500 INTERNAL_ERROR
capability gate, body-chosen object name has no column named x 400 INVALID_FIELD 500 INTERNAL_ERROR
ctx.title gate, body-chosen field unique constraint 500 DATABASE_ERROR 500 INTERNAL_ERROR
capability 'log' not granted to hook 'g', request object named log 404 OBJECT_NOT_FOUND 500 INTERNAL_ERROR
CPU budget 500 INTERNAL_ERROR 500 INTERNAL_ERROR
nested crash relayed as a fault (hook 'g' threw: TypeError: boom) 500 INTERNAL_ERROR 500 INTERNAL_ERROR

The runner interpolates the body's own arguments (ctx.api.object(name), ctx.title(field)) into its texts, so the missing-relation, unknown object, missing-column and leak limbs can each misread one too. An exclusion beside the substring limb alone would leave them. Asked at the top, the answer is the terminal the other runner faults already reached, so only misread answers move. Own faults carry no code or status, so the code-gated arms between the top and the message limbs never matched them.

The pin: the producer enumeration, not the REST write-route ledger

The triage named the ledger #22764 rides (packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts) as the family's enumeration. Measured, these two texts cannot live there inside this card:

  • I added the two texts to its §6 SANDBOX_FAULTS on the fixed tree: 46 failed, 229 passed. The 21 rows that answer through the classification passed. 23 hand-built rows relay the runner's text verbatim: analytics dataset query, security explain, record shares (2), sharing rules (3), suggested bindings (2), approvals (9), package publish, external datasources (4). For example: Received: "capability 'api.read' not granted to hook 'mz_guard' (called ctx.api.object('mz_locked').find)". Their sandboxFaultAnswer (packages/rest/src/error-response.ts) answers only for a sandbox ORIGIN (innerMessage set), so an own fault falls to each family's own terminal. That is a separate defect in packages/rest, outside this claim's file surface. It is reported for filing, not fixed here, and the probe was reverted.
  • This member does fit in packages/core/src/utils/import-runner-sandbox-fault-row.test.ts (PR fix(core): an import row for the sandbox's own fault reads the door's fault sentence (#22741) #22867). That file parses quickjs-runner.ts with the TypeScript compiler and renders every own-fault site. It holds each site to mapDataError's 500 INTERNAL_ERROR body and holds the import row to that sentence. Its sample object was deliberately NOT the import's object, and its header called this door defect out of scope. This PR makes the sample the requested object, so the two sites that name it are pinned at 500 from the producer. It also adds:
    • a census floor: at least one capability-denial site and one missing-method site name the requested object (no wording pinned);
    • CONTROL: a real SQLite or Postgres unknown-object text on the requested object still answers 404 OBJECT_NOT_FOUND;
    • CONTROL: the same object-naming texts on a plain Error (not the sandbox) keep the heuristic's 404, which proves the door asks the producer, not the words.

The @objectstack/core suite aliases @objectstack/types to its source (packages/core/vitest.config.ts), so this pin reads the fixed classification directly, with no dist/ in between.

Evidence, at HEAD 31b25500d1

  • Reverse verification, with the fix committed first (d457a70fbd): node scripts/ablation-replace.mjs --file packages/types/src/data-error-classification.ts --anchor " if (isSandboxOwnFault(error)) return UNCLASSIFIED_FAULT();" --replacement " /* ABLATED-22868 */" -- pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/utils/import-runner-sandbox-fault-row.test.ts. The mutation landed (anchor 1 to 0). Result: 2 failed, 26 passed, exactly quickjs-runner.ts:1173 throwSandboxFault(…) and quickjs-runner.ts:1199 new SandboxError(…), both expected 404 to be 500. Every control and every other site stayed green. After the restore, blob == HEAD and git diff HEAD is empty. With the fix: 28 of 28.
  • pnpm --filter @objectstack/types test: 27 files, 754 passed. pnpm --filter @objectstack/types typecheck: exit 0.
  • pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/utils/import-runner-sandbox-fault-row.test.ts src/utils/import-runner-sandbox-refusal-row.test.ts: 2 files, 43 passed. pnpm --filter @objectstack/core typecheck: exit 0. The test file is in tsconfig.test.json's program (--listFiles count 1) and has no errors.
  • @objectstack/rest, against the rebuilt @objectstack/types dist: 21 files that exercise the sandbox and unknown-object answers, 938 passed. They include rest-unknown-object-heuristic, rest-hook-script-fault-envelope, the unchanged write-route ledger and rest.test.ts.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands (no paths) derived 65. All 65 ran: 64 exit 0, and 1 NOT MEASURED. pnpm check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET: it needs a whole-workspace build, and 44 packages had no dist/. Declared narrow substitute: @objectstack/types' CJS and ESM dist both load and answer the fixed 500. --ran reconciliation: 65 accounted, 0 UNRUN.
  • Lint, as a proven narrowing: eslint --no-inline-config --format json over the two touched TS files reports 2 files, 0 errors, 0 warnings. There is no "file ignored" warning, so both files are in the config's packages/**/*.{ts,…} population. The config never enables type-aware linting (no parserOptions.project), so this diff cannot move a verdict in an untouched file. The .md changeset is outside eslint's population.

Public surface

The one code line is a statement inside the non-exported classifyDataError. No declaration moved, so @objectstack/types' type surface is unchanged: the only non-comment changed line in packages/types/src is the new if. The runtime answer of mapDataError moves for an error named SandboxError with no innerMessage. Changeset: @objectstack/types patch.

Acceptance notes

  • The REST hand-built write routes that relay the runner's own-fault text (above) are reported to the dispatching seat for filing, with the probe. They are not addressed here.

Generated by Claude Code

…t it is, before the unknown-object heuristic

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
…and holds its controls

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/types, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via api.read (literal, a string literal in a comment in classifyDataError))
  • content/docs/automation/hooks.mdx (via api.read (literal, a string literal in a comment in classifyDataError))
  • content/docs/automation/jobs.mdx (via api.read (literal, a string literal in a comment in classifyDataError))
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 — 5 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 75b6d6a0483645a62d753557e82701ea53d99dfd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b56bbffe0dfb7428b286c012b781f8e0a9dff3db — the merge of head 31b25500d1d89d8548c5535296db713485247399 into base 75b6d6a0483645a62d753557e82701ea53d99dfd, 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 b56bbffe0dfb7428b286c012b781f8e0a9dff3db && git checkout b56bbffe0dfb7428b286c012b781f8e0a9dff3db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 75b6d6a0483645a62d753557e82701ea53d99dfd 31b25500d1d89d8548c5535296db713485247399 && git checkout -B drift-repro 75b6d6a0483645a62d753557e82701ea53d99dfd && git merge --no-ff 31b25500d1d89d8548c5535296db713485247399

node scripts/docs-audit/affected-docs.mjs --json 75b6d6a0483645a62d753557e82701ea53d99dfd

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 75b6d6a0483645a62d753557e82701ea53d99dfd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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/s tests tooling

Projects

None yet

2 participants