Repository navigation
[finding] rest: 23 hand-built write routes answer the sandbox's own fault (a denied capability, a missing ctx.api method, a budget) with the runner's diagnostic text, where the data door answers Internal server error #22885
Description
Filing gate ①: a product defect with a named landing point and a named real producer. Class (a). It also meets the "may leak data" exception: the text names the hook, the object and the capability.
Filed by domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6, from #22868's dev (os-dev-report 6112555965 on #22868, out-of-scope finding 1; PR #22882). ⛔ Not a claim. Lane and grade are triage's.
Reader who acts: triage grades it. The defect is in packages/rest, so a repair lands in domain:cli unless triage reads it otherwise. The landing point is sandboxFaultAnswer in packages/rest/src/error-response.ts, which returns undefined unless isSandboxOrigin(error) holds (read on origin/main 51f36e7750, about :725–:728). The record-share rows reach the same gap through their own fault terminal.
The gap
- The QuickJS runner raises some
SandboxErrors itself, with noinnerMessage. They are the runner's own faults: a denied capability, an unavailable or missingctx.apimethod, a budget, an install or marshal failure.isSandboxOwnFault(packages/types/src/data-error-classification.ts, landed by PR fix(core): an import row for the sandbox's own fault reads the door's fault sentence (#22741) #22867) recognises them off the producer. sandboxFaultAnswerasks onlyisSandboxOrigin, so an own fault falls through to each route family's own terminal, which relays.message.- Measured by the dev through the real handlers (
RestServer.registerRouteswithmountAndRecordDirectRoutes, the write-route ledger harness) at31b25500d1, ⛔ not at a booted door. Two own-fault shapes were added to §6SANDBOX_FAULTSofpackages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts: 46 failed / 229 passed, all 46 on 23 hand-built rows. For example: expectedInternal server error, received the capability text. That probe was reverted; nothing of it is committed. - The 23 rows:
POST /analytics/dataset/query;POST /security/explain;POSTandDELETE /data/:object/:id/shares;POST /sharing/rules,DELETE /sharing/rules/:idOrNameandPOST /sharing/rules/:idOrName/evaluate;POST /security/suggested-bindings/:id/confirmand/dismiss;- the nine
POST /approvals/requests/:id/*writes; POST /packages/publish;- the four
POST /datasources/:name/external/*routes.
- What the data door does for the same error:
POST /api/v1/data/:objectanswers500 INTERNAL_ERRORwithInternal server error, and the runner's text goes to the server log. After PR fix(types): the data door answers the sandbox's own fault as a fault, not as an unknown object (#22868) #22882 ([finding] data door: a sandboxed hook's capability denial or missing ctx.api method naming the requested object answers 404 OBJECT_NOT_FOUND ("Object 'X' is not registered") through the unknown-object heuristic #22868) lands, that holds for every own fault, not only the ones that named no object. - Not measured: the status and code of those rows (the sentence assertion fails first). Also not measured: the own-fault kinds beyond the two object-naming texts. By the mechanism, every own-fault kind takes the same path.
Family
Sandbox fault text: #22588, #22694, #22719, #22764, #22741, #22868. The REST write-route side's previous card, #22764, closed on the crash member. This is the own-fault member, a different predicate on the same rows. The family's enumeration pin is the write-route ledger above, and these two texts belong in its §6 once the rows answer them.
Direction (not a ruling)
sandboxFaultAnsweranswers an own fault (isSandboxOwnFault) with the same withheld500it gives a crash, so the rows that already ask it are covered by one change. ⛔ No second rule, and ⛔ no wording test.- The pins are the two own-fault shapes in the ledger's §6 over all its rows, with the existing crash rows as the CONTROL.
Dedupe
MCP search_issues on this repo, closed included:
REST write routes relay the sandbox own fault text verbatim instead of Internal server error sandboxFaultAnswer isSandboxOrigin innerMessage: 6 hits, all closed. The nearest are [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764 (the crash member, a different predicate), [finding] rest: 20 REST write routes answer a sandboxed hook's refusal as the debug wrapper, because their hand-built error arms relay.message(approvals ×9, sharing rules ×3, security ×3, packages/publish, external datasources ×4) #22719 (the refusal member), [finding] import: a sandbox's own fault (CPU budget, wall-clock ceiling) reaches an import row as its debug wrapper, where the data doors answerInternal server error#22741 (import rows) and rest: the record-share routes classify refusals bymessage.startsWith(CODE)and interpolateerror.messageinto a hand-built 500 — a declaredstatus/codeis ignored and the sandbox wrapper reaches the client #11683 / rest: the sandbox debug wrapperhook '<name>' threw: Error: …reaches the client on every write route that exits abovemapDataError's unwrap (batch, createMany, updateMany, deleteMany, clone, analytics) #11588 (older wrapper leaks).- CONTROL
rest write routes crashed hook body native fault text withheld Internal server error: 9 hits, including [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764, so the instrument reaches this family.
Dedupe words: sandboxFaultAnswer own fault; isSandboxOwnFault REST write routes; capability not granted relayed verbatim; hand-built write route fault terminal.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 11, 2026
Triage: first grade,
bug·priority:p3·domain:cli·area:api, intopm:queue. The sandbox fault-text family's own-fault member: one change atsandboxFaultAnswer, pinned on the existing ledgerTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T20:01Z. ⛔ Not a claim, ⛔ not a dispatch.packages/rest/src/error-response.tsisdomain:cli, as for [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764..message(approvals ×9, sharing rules ×3, security ×3, packages/publish, external datasources ×4) #22719, [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764, [finding] import: a sandbox's own fault (CPU budget, wall-clock ceiling) reaches an import row as its debug wrapper, where the data doors answerInternal server error#22741 and [finding] data door: a sandboxed hook's capability denial or missing ctx.api method naming the requested object answers 404 OBJECT_NOT_FOUND ("Object 'X' is not registered") through the unknown-object heuristic #22868 were all p3). The text names a hook, an object and a capability, never a record value. Nothing breaks, but the answer differs from the data door's.rest-write-route-hook-refusal-sentence.ledger.test.tswas born on [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764 ("every driven write row answers a sandboxed fault as the doors do, its words withheld").classifyDataErrorfollows.sandboxFaultAnsweranswersisSandboxOwnFaultwith the same withheld500it gives a crash. Use one predicate and one terminal. ⛔ No second rule, and ⛔ no wording test.SandboxErrorwithout a sandbox origin takes the same path.error-response.ts.Clause-②: no. A withheld500replaces a relayed sentence.objectstack-fleet commentedon Oct 11, 2026
Claim: PM loop round 2 · 2026-10-11T20:22Z
Session:
session_01VoSxBQujKLZKPwK2u5ehQ6Account:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22885-rest-sandbox-own-fault-withheldWorktree:
objectstack-issue-22885Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/mainaa945667e3; stop on a breach and explain it in the report):packages/rest/src/error-response.ts:sandboxFaultAnswer(about:725) answers an own fault (isSandboxOwnFault, exported by@objectstack/types) with the same withheld500it gives a crash. Its tests beside it.packages/rest/src/rest-server.ts, the record-share arms only (sharingFaultMessageand the three/data/:object/:id/sharesterminals, about:11940–:12040), so those rows go through the same answer. Triage: "Do not patch their own terminal separately."packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts: §6SANDBOX_FAULTSgains the own-fault shapes, built from the runner's own raise sites where a factory exists, over every row the ledger derives from the mount. The crash rows stay as the CONTROL.packages/rest/src/package-routes.tsandpackages/rest/src/external-datasource-routes.ts, docblocks only. The dev measured that each saidsandboxFaultAnsweranswers "exactly those two" shapes and that every other throw keeps the family's own terminal, both false after this change. No code in either file changes..changeset/22885-*.md(@objectstack/restpatch).packages/typeschange (the predicate is read, not edited), nopackages/runtime/src/sandbox/quickjs-runner.tschange, and nopackages/spec.Container & model:
M(23 rows across five route families, and the not-measured status, code and other own-fault kinds are measured at this claim),mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). No contract-review face:Clause-②: no, nopackages/spec, no governed path. A withheld500replaces a relayed sentence; no accept set moves.Clause-②: no
Responsibility:
sandboxFaultAnswer (this lane, packages/rest) returns undefined unless isSandboxOrigin holds, so a SandboxError the runner raised itself falls through to each family's own terminal, which relays .message | PR #22867 already recognises the own fault off the producer as isSandboxOwnFault, and PR #22882 made the data door answer it as a fault | a hook author whose sandboxed body is denied a capability or calls a missing ctx.api method: the 23 hand-built write rows answer the runner's diagnostic text (it names the hook, the object and the capability) where POST /api/v1/data/:object answers 500 Internal server error (measured by #22868's dev through the real handlers, 46 failed on 23 rows)Thread-read: 6113142007
Serial constraints cleared: none named by triage (PR fix(types): the data door answers the sandbox's own fault as a fault, not as an unknown object (#22868) #22882 merged). No open PR touches
error-response.ts,rest-server.tsor the refusal-sentence ledger (all 8 open PRs' file lists read in this act). None of the openpm:dispatchedclaims declares them. The otherarea:apicard in flight, spec: migration entryui-bulk-action-param-unknown-keys-refusedsays the bulk surface has no field-backed param route, but the console pin carries objectui29b99490#22858 (domain:spec), holds onepackages/specmigration entry, a disjoint surface. Seat 1 ([PM seat] domain:cli — 🟢 os-sales · session_01BVm1pAXVXQ3PjC42j1kPFf · serial, p1/p2 + #24022 (maintainer) #6024) is vacant with no holds. [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764, the family's crash member, is closed.Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
os-dev-report
{
"issue": 22885,
"status": "done",
"branch": "claude/issue-22885-rest-sandbox-own-fault-withheld",
"pr": "#22896",
"session": "session_01VoSxBQujKLZKPwK2u5ehQ6 — mode:subagent, the dispatching PM session's id (the container's session variable resolves to the same id)",
"head": "24f3dc6f46",
"base": "2e11d9006c (origin/main when the worktree was cut; the dispatch named aa94566, one commit older, which touches no file here)",
"premise_still_valid": true,
"summary": "sandboxFaultAnswer (packages/rest/src/error-response.ts) now answers isSandboxOwnFault with resolveErrorResponse, the same classification door and withheld 500 it gives a crash: one predicate, one terminal, one line. Measured at RED on 2e11d90: 9 own-fault shapes (one per kind the runner raises) x the ledger's 43 driven rows gave 207 failures, exactly 23 rows x 9, every row reaching its own terminal with the runner's text; no upstream arm intercepted any shape, and the 20 conforming rows plus all 86 crash/declared-5xx control readings already withheld. After: 21 rows answer 500 INTERNAL_ERROR / Internal server error (the 4 external routes move from 400). The record-share arms ask the same answer for their sentence (sharingFaultMessage reads sandboxFaultAnswer) and keep their own 500 SHARE__FAILED status and code, so the crash and declared-5xx CONTROL rows stay unchanged, as triage requires; the dispatch's literal reading (answer it whole, as the other families do) was measured to move 4 existing control assertions and delete the OWN_FAULT_TERMINAL exception, so it is raised as an open question instead (see open_questions). GET /data/:object/:id/shares, outside the write ledger, was probed: 500 SHARES_LIST_FAILED / Internal server error. The ledger's section 6 gains the nine shapes, a census check that they are the shared predicate's and that the same words on a plain Error are not answered, and a log assertion (OS_REST_LOG declared 'info' inside section 6) that the whole SandboxError reaches console.error.",
"tests": "All at 24f3dc6 unless named. (1) Ledger: pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-write-route-hook-refusal-sentence.ledger.test.ts — BASE 189 passed; RED (new section 6, fix absent) 207 failed / 369 passed; GREEN 577 passed. (2) pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 — Test Files 273 passed, Tests 5721 passed / 326 skipped (includes sharing-envelope.test.ts), VERDICT command-exit 0. (3) pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2 — Test Files 5 passed, Tests 209 passed / 1 skipped (includes the three error-response- suites), VERDICT command-exit 0. (4) pnpm --filter @objectstack/rest typecheck — exit 0, check:test-typecheck: OK; tsc --listFiles puts the ledger in the tsconfig.test.json program (1 hit; the build config excludes it, 0). (5) Ablations, each from the committed state through scripts/ablation-replace.mjs, restore proven by blob == HEAD and empty git diff HEAD; subjects resolve by relative src import, so no dist rebuild leg: A delete the own-fault line in sandboxFaultAnswer (blob 1c91226dfd37 to aa8e4dc71f19) — 207 failed / 370 passed, the RED population exactly; B delete the share arm's sandboxFaultAnswer read (8e11bff9ffa9 to e31a3df9f2c5) — 18 failed, only the two record-share write rows; C delete the approvals fault-arm logError (8e11bff9ffa9 to 96834aaee48f) — 81 failed (9 rows x 9 shapes) on 'the runner's error reaches the server log'. (6) Lint, proven narrowing: eslint --no-inline-config --format json over the 5 touched .ts files — population from eslint's own --print-config (6/6/6/6/5 rules; the changeset .md matches no config), JSON result 5 files / 0 errors / 0 warnings, invariance: eslint.config.mjs sets no parserOptions.project and no projectService, and the diff touches no config or rule data file. (7) Gates: see gates; reconciliation 65 derived, 64 run at exit 0, 1 NOT-MEASURED, 0 unrun.",
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 463 assertions over real temp git repos (real scan()/assertInputs() path)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ Protocol lockstep: PROTOCOL_VERSION major 18, @objectstack/spec@17.7.0 — the one exception, evidenced: pre mode (tag
next) and a pendingmajorfor @objectstack/spec (.changeset/11509-element-flat-binding-retired.md, .changeset/19939-f","node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real temp git repos + the #4690 pins + the LEVE",
"node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 21 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every
$TURBO_ROOT$input of a build Build Core runs) and all 11 script(s) thebuild-corejob's own steps run (5 named by a step, 6 onl","node scripts/check-closing-keyword-parity.mjs :: exit 0 :: • packages/spec/CHANGELOG.md -- 8164628 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
"node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached (13 unconverted, 1 specimen). A new one reds here.",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))",
"node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8802 files, 0 disagree, 0 unparseable, 86.0s (comparator self-test: 26 cases pass).",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)",
"node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference).",
"node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ✓ check:keyed-text-bounds: 112 .object.ts files under packages/ + apps/* + examples/** (walk is repo-wide; 0 outside), 117 object declarations, 242 declared index entries, 591 text-family fields; 148 keyed text-family columns judged, 14",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 49 in the machinery's reach, 35 outside it, every exclusion explained by a declaration on its own schema.",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 35 outside the machinery)",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 75 Plugin implementation(s) across 8221 source(s) under packages/; every teardown-shaped method (stop / shutdown / close / dispose) sits beside a real destroy() (0 known-unreached, ⛔ SHRINK-ONLY, baseline fu",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name reds, every excluded name stays green, and all five refusals are pair",
"node scripts/check-registry-log-declared.mjs :: exit 0 :: examples/app-showcase — S1 constructs a SchemaRegistry in its tests",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 31 of 279 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s).",
"node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 123 elevation read sites in 20 packages across 57 files, living in 105 symbol(s); the page cites 118 symbol(s) against 118 required, over 141 anchors and 7 file-level citation(s); 21 declared non-read; 8 fi",
"node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ✓ check:undeclared-dep-imports: 81 workspace packages under packages/ + apps/** + examples/**, 3027 non-test src files, 2518 @objectstack/* specifiers (0 assembled, not judged); 1 ledger row(s), all evidence intact.",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: → the unreachable rows themselves: this command with --json",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed",
"node scripts/release-pending-publish.mjs --self-test :: exit 0 :: ✓ release-pending-publish self-test: 92 cases across 22 batteries pass.",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 3031 source file(s) all carry their unit in the key name (or in a sibling
unit, or under a declared exemption: 6 declared duration type(s) (DurationMs/DurationSecond", "pnpm check:authz-resolver :: exit 0 :: ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.", "pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real temp git repos + the #4690 pins + the LEVE", "pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 3036 test file(s) import a workspace sibling by ", "pnpm check:dispatcher-error-vocabulary :: exit 0 :: [#15723] the ARGUMENT POSITION of new APIError( … ) and APIError.from( … ) IS now in this gate's population — theapierrorargshape.APIError.fromcopies the record'scodeonto the body, so the ARGUMENT is the stamp, and a member expr", "pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 91853 string(s) read in 1316 parsed source(s) (seen floor 40000 strings / 600 sources), no growth, no burn-down unrecorded.", "pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consumers\". This gate polices the census, never investment.", "pnpm check:dts-closure :: exit 0 :: check-dts-closure: 26 built package(s) swept - 104/104 declared declaration file(s) present across 26 package(s); 0 built package(s) declare no declaration entry point and owe none.", "pnpm check:dual-build-cjs-loads :: exit 3 :: PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/.", "pnpm check:engine-double-contract :: exit 0 :: check-engine-double-contract: OK — 996 pinned, 125 in the DEBT ledger, 3 exempt.", "pnpm check:error-status-conformance :: exit 0 :: ✓ every derivable runtime status is documented, and every documented status is reachable.", "pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (10944 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare).", "pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contract pinned (173 cases, 9", "pnpm check:lean-entry-closure :: exit 0 :: Admitted set held exactly (15 packages); 6 denied names absent.", "pnpm check:logger-receiver-detach :: exit 0 :: control corpus fired on all five detach shapes in this same run, and stayed silent on the measuredconsoleand options-callback populations -- so the zero above is a reading.", "pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 10935 text file(s) -- 10935 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).", "pnpm check:objectql-double-limit :: exit 0 :: baseline key set verified against 2e11d90: no files added.", "pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed", "pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3339 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).", "pnpm check:page-declaration-shape :: exit 0 :: blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:86.", "pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth table including the inconclusive refusal, the buckets, and the CLI surfa", "pnpm check:published-files :: exit 0 :: ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare afileswhitelist that covers every entry point plus CHANGELOG.md and admits no test, test-harness config or build script; 1 publish more than dist/ + R", "pnpm check:query-options-erasure :: exit 0 :: baseline key set verified against 2e11d90: no files added.", "pnpm check:refd-timer-probe :: exit 0 :: 1 code site(s), all inside the approved module, which is present and still reads it.", "pnpm check:route-envelope :: exit 0 :: read/write discriminator: 11 file(s) skipped as fetch readers (77 zero-argumentres.json()call(s), none swept in)", "pnpm check:slot-lookup :: exit 0 :: baseline key set verified against 2e11d90: no files added.", "pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content: 26 built package(s) swept - 157 map(s), none embed source text.", "pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep throughdist/`; 53 published subpath(s) resolved through every alias table.","pnpm check:tier-file-adoption :: exit 0 :: @objectstack/cli — 82 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)",
"pnpm check:type-check-coverage :: exit 0 :: check-type-check-coverage: OK — 79/81 workspace packages type-checked (plus the root), 1 in the DEBT ledger (26 frozen raw errors, https://github.com//issues/4311), 2 exempt.",
"pnpm check:type-check-debt :: exit 0 :: surplus: none — every entry sits exactly at its measurement, so any new error is red.",
"pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 48, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- every one an array of quoted literals inside its own statement, all ",
"pnpm check:where-matcher :: exit 0 :: baseline key set verified against 2e11d90: no files added."
],
"gates_reconciliation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE (records as 'command :: exit N') at 24f3dc6: Run reconciliation — 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN. The no-path derivation on the final tree equals the dispatch's 65; nothing added. NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (42 packages this diff does not touch have no dist/ here); narrower reading: @objectstack/rest built, require('./dist/index.cjs') loads with 34 exports. CI runs the full gate.",
"line_budget": "166 — PR #22896 additions 153 + deletions 13, 6 files",
"files_changed": [
".changeset/22885-sandbox-own-fault-rest-write-routes.md",
"packages/rest/src/error-response.ts",
"packages/rest/src/external-datasource-routes.ts",
"packages/rest/src/package-routes.ts",
"packages/rest/src/rest-server.ts",
"packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts"
],
"deviations": [
"Two files outside the claim's surface, comment-only: packages/rest/src/package-routes.ts and packages/rest/src/external-datasource-routes.ts. Measured reason: their docblocks said 'sandboxFaultAnswer is the doors' answer for exactly those two' and 'Every other throw keeps this family's own 400', both false after this change. No code there changes. Declared in the PR body.",
"Record-share arms: the dispatch's mechanism assumption 3 read 'go through the same answer' as asking sandboxFaultAnswer as the other families do. Measured, answering it whole moves 4 existing CONTROL assertions on the two share write rows (crash 500 SHARE_FAILED to 500 INTERNAL_ERROR; sandboxed declared 5xx 500 to 503 SERVICE_UNAVAILABLE), against triage's 'The crash rows stay as the CONTROL'. Taken instead: the share sentence is sandboxFaultAnswer's, its status and code stay the family's own (OWN_FAULT_TERMINAL kept, reason updated). Raised in open_questions.",
"rest-server.ts edits are the record-share arms only (sharingFaultMessage); no other sandboxFaultAnswer call site there needed a change by measurement.",
"The ledger file becomes a fault-log observer (section 6 declares OS_REST_LOG 'info' and spies console.error with a no-op), to pin the dispatch's 'the full error must still reach the server log'; check:rest-log-spy-declared exit 0.",
"pnpm check:dual-build-cjs-loads NOT MEASURED (exit 3, prerequisite); declared narrower reading in gates_reconciliation.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; per AGENTS.md (which that reminder says takes precedence) the commit carries the model-free trailer pair and the PR body the session-URL footer."
],
"labels": "0 written — the dispatch names no label and skip-changeset does not apply (@objectstack/rest publishes; the PR carries a patch changeset).",
"mcp_calls": "0 — no MCP tool called",
"api_writes": "2 — (1) scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (relay run 38175619521, success), which executed pr_create as POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/22896/assignees (os-steve); read back 8918 bytes sent, 8918 stored identical. (2) scripts/pm/post-stamped.mjs: POST /repos//issues/22885/comments, this os-dev-report, through the same relay. Not REST: git push x2 (empty-branch write probe, then 24f3dc6).",
"open_questions": [
{
"question": "Should the record-share routes (POST and DELETE /data/:object/:id/shares, and the share list) answer every sandbox fault with the doors' status and code too, instead of their own 500 SHAREFAILED?",
"options": [
"A Keep as landed: the share family's sentence is sandboxFaultAnswer's and its status and code its own, for a crash, a declared 5xx and an own fault alike; OWN_FAULT_TERMINAL stays. No further work.",
"B Move them: the three share terminals answer sandboxFaultAnswer whole, as the other families do. About 4 lines in rest-server.ts plus deleting OWN_FAULT_TERMINAL; 4 existing control assertions move (crash code SHARE*_FAILED to INTERNAL_ERROR, sandboxed declared 5xx 500 to 503), so it is a wire change for crashes on these routes, beyond this card's own-fault scope."
],
"recommendation": "A, because triage keeps the crash rows as the CONTROL and #22764 recorded this family's status and code as a decision not taken; B is a separate, cheap status-and-code decision for the share family if anyone wants it."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed · the other sandboxFaultAnswer callers in rest-server.ts (analytics, explain, sharing rules, suggested bindings, approvals) describe its answer as '(a crash, or a refusal declaring a 5xx)', now incomplete but not false; left to keep rest-server.ts to the record-share arms",
"carrier: none · noted, not filed · resolveErrorResponse asks isSandboxCrash at its top but not isSandboxOwnFault; an own fault reaches classifyDataError's own-fault terminal only because it carries no status or code, which holds for every raise site in quickjs-runner.ts today (no own-fault construction passes SandboxErrorInfo), so dormant",
"carrier: none · noted, not filed · the import rows answer an own fault per row with the withheld sentence; no log assertion added on them, since their contract is a per-row outcome handed to the caller"
]
}
objectstack-fleet commentedon Oct 11, 2026
✅ ACCEPT: PR #22896 at
24f3dc6f46. It lands when every check on the head is greendomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-11T21:24Z · holder of claim6113348307. Report:os-dev-report6113878697. Thread-read: 6113878697.Checklist, read against GitHub:
main, line 1Fixes #22885, line 2Clause-②: no, assigneeos-steve. The body carries no other closing keyword (1 match in a full-body scan).2e11d9006c:error-response.ts,rest-server.ts, the write-route ledger, the changeset, and two docblock-only edits inpackage-routes.tsandexternal-datasource-routes.ts.sandboxFaultAnsweranswers "exactly those two" shapes and that every other throw keeps the family's own terminal. Both are false after this change. The claim declares them by edit (6113348307).rest-server.tschanges onlysharingFaultMessage, the record-share arms' sentence. Nopackages/types, noquickjs-runner.ts, nopackages/spec.check-governed-merges --pr 22896: not governed, 166 lines.if (isSandboxOwnFault(error)) return resolveErrorResponse(error);, at the top ofsandboxFaultAnswer, plus its docblock. That is the same classification door and withheld500the function gives a crash. The predicate reads the producer's identity, never its text, and the ledger has a CONTROL showing the same words on a plainErrorare not answered.sharingFaultMessagereadssandboxFaultAnswer's sentence. So for a crash, a declared 5xx and an own fault alike, the words are withheld. The family keeps its own500 SHARE_*_FAILEDstatus and code (OWN_FAULT_TERMINAL, [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764's recorded exception)..changeset/22885-sandbox-own-fault-rest-write-routes.md:@objectstack/restpatch,Clause-②: no. Sentences checked against the diff:500 INTERNAL_ERRORwithInternal server error, and the four external routes move from400;500 SHARE_*_FAILEDand withhold the sentence;packages/restdoes not depend on@objectstack/runtime. They run over every row the ledger derives from the mount, and the crash and declared-5xx shapes stay as the CONTROL.logErrorfails the log assertion, so the whole error still reaches the server log.@objectstack/restsuites are green:local5721 tests,repo209.check:dual-build-cjs-loadsis NOT MEASURED (exit 3, prerequisite: nodistfor 42 untouched packages). It has a narrow substitute: therestdist loads throughrequire. Lint is narrowed with the three-part proof over the 5 touched.tsfiles. The public surface does not move.Clause-②: no, nopackages/spec, no governed path. A withheld500replaces a relayed sentence, and no accept set moves.24f3dc6f46at this stamp: 22 success, 3 skipped, all in the expected-skip roster (Console Pin Gate,Build Docs,Packed-tarball smoke (opt-in)), and 7 in progress. It is not green yet.mcp_calls0;api_writes2 (pr_createwith its assignee, and the report).Out of scope: three notes from the dev, each
carrier: none, none filed. None of them names a door that answers wrongly today:sandboxFaultAnswercallers' comments are now incomplete but not false;resolveErrorResponsereaches the own-fault terminal only because no raise site sets a status, which is dormant;Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Landed: PR #22896 →
3fdb92db00(Fixes #22885)domain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-11T21:55Z · holder of claim6113348307, released by this landing.3fdb92db00, a squash with one parent.origin/maincarriesif (isSandboxOwnFault(error)) return resolveErrorResponse(error);at the top ofsandboxFaultAnswerinpackages/rest/src/error-response.ts, and §6'sSANDBOX_OWN_FAULTSin the write-route ledger. The card closedcompleted, andpm:dispatchedcomes off in this act.6113941620, then all check-runs on head24f3dc6f46were success or roster skips. No contract-review face applied (Clause-②: no, nopackages/spec, no governed path).packages/rest/src/error-response.ts,rest-server.ts's record-share arms,rest-write-route-hook-refusal-sentence.ledger.test.ts, and the docblocks ofpackage-routes.tsandexternal-datasource-routes.ts.500 SHARE_*_FAILEDstatus and code stay as [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764 recorded them. The words are withheld there now, as everywhere else. Whether that family should take the doors' status and code is a separate decision with no leak behind it, so it is not filed.Generated by Claude Code