Skip to content

runtime(sandbox): a sandboxed body whose rejection cannot be flattened (throw '', a null-prototype object, a message getter that throws) is never recorded, so the action door waits for the 30 s wall-clock ceiling and answers 500 #23723

Closed

Description

@objectstack-fleet

Filing gate ①, class (a). reach: measured at a public door by #23718's dev (os-dev-report on #23718, out-of-scope finding 1): POST /api/v1/actions/:object/:action on a type: 'script' action whose body throws { code: 'EMAIL_TAKEN', get message() { throw new Error('boom') } } answered 500 INTERNAL_ERROR only after the 30 s default wall-clock ceiling.

Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU. ⛔ Not a claim. Grade and lane are triage's.

Why, read

Reader who acts

Triage grades it and routes it. The landing is the same writer (REJECT_HANDLER, plus the pump loop's "rejected but unrecorded" case), domain:cli.

  • The fix shape is the claimant's. One option: the handler records a fixed no-message sentence when every read fails, so a rejection always ends the run at once.
  • Pins:
    • each of the three throws answers without waiting for the ceiling;
    • CONTROL: a body that never settles still hits the ceiling.

Dedupe words: sandbox reject handler hang wall-clock ceiling 'awaiting host calls' empty string throw getter throws null-prototype.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · priority:p3 · domain:cli · pm:queue · land after PR #23722 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T21:53Z

    Read fresh. When a sandboxed body's rejection cannot be flattened, REJECT_HANDLER (packages/runtime/src/sandbox/quickjs-runner.ts) leaves __error unset: a message getter that throws, a null-prototype object, or throw ''. The pump loop never sees it.

    Grade p3: the shapes are uncommon (a throwing getter, a null-prototype object, an empty string). When one occurs, it holds a worker for the full ceiling and gives the author a 500 with no cause.

    Lane domain:cli (packages/runtime).

    Order: it lands after PR #23722 (#23718, open), on the same writer.

    Scope:

    finding leaves on grading.

  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4, batch 3 (the maintainer's 「并发恢复3」, recorded in seat post #6024 §1): this fills the slot #23623 freed when PR #23741 landed (2906cd8d2e). Label order: the oldest unclaimed bug at priority:p3 in domain:cli's open queue; no p0, pm:blocking, open target:, p1 or p2 card is open for dispatch (#23497 is pm:blocked; #23050 is seat 2's, whose scope is p1 + p2). #23723: a sandboxed body's rejection that cannot be flattened (throw '', a null-prototype object, a message getter that throws) is recorded, so the run ends at once with a named refusal instead of waiting for the wall-clock ceiling · 2026-10-16T00:00Z
    Session: session_01FLwgbTSmGMrqsXkBDsiwfG
    Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23723-sandbox-unreadable-rejection
    Worktree: objectstack-issue-23723
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main 2906cd8d2e; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23723,
    "status": "done",
    "branch": "claude/issue-23723-sandbox-unreadable-rejection",
    "pr": "#23774",
    "head": "f7ddaeb73884ca8220f45ba99650f13a729e3402",
    "session": "session_01FLwgbTSmGMrqsXkBDsiwfG (this run's harness-stamped id; subagent, so it is the parent's)",
    "premise_still_valid": true,
    "claim_check": "newest Claim: on #23723 is 6204824875 (2 comments read: triage 6203064606, claim 6204824875); it names branch claude/issue-23723-sandbox-unreadable-rejection and worktree objectstack-issue-23723, which this run used (created from origin/main 2906cd8, BASE pinned). No claim posted, no card-assignee write. Card body read in full (ends with its footer).",
    "summary": "Premise holds, measured at base 2906cd8: throw '', throw Object.create(null) and throw { code, get message() { throw } } each waited the full wall-clock ceiling (3000 ms probe ceiling at the runner; 30.1 s at POST /api/v1/actions/:object/:action, then 500 INTERNAL_ERROR naming no refusal). The fix is REJECT_HANDLER only (packages/runtime/src/sandbox/quickjs-runner.ts): its unchanged fallback read now sits in a try, and when no read yields a non-empty string the handler records 'Error: ' + the ruled sentence 'the script threw a value that could not be read' (module constant UNREADABLE_REJECTION_SENTENCE), so the run ends at once on the threw path; the pump loop is unchanged (H3 holds). After: 9 to 34 ms at the runner and 108 to 118 ms at the door, 400 VALIDATION_ERROR with the sentence (declaredCode EMAIL_TAKEN for the getter shape), the status the door already gives a body that threw; every readable value (39 rows, runner and door, incl. PR #23722 plain objects, real Errors, primitives, the PR #23717 engine relay, __errorInfo and __errorRefusal channels, a capability denial) is byte-identical before and after, and a never-settling body still hits the ceiling. Draft PR #23774 (assignee os-bill) with a unit pin, a door pin and a runtime patch changeset.",
    "tests": "Pins: packages/runtime/src/sandbox/unreadable-rejection-recorded.test.ts (new, 8) and packages/runtime/src/sandbox-unreadable-rejection-door.test.ts (new, 5), on a runner with a 1000 ms wall ceiling and CPU budget, so a named refusal is an answer within a second by construction and a regression is the budget error, never a clock assertion. At e02875a, the two new files + PR #23722 pins + PR #23717 pins + the temporary probe: 'Test Files 7 passed (7)', 'Tests 51 passed (51)'. Ablation via node scripts/ablation-replace.mjs (wrap mode) under an outer trap EXIT INT TERM restore on the absolute path, fix committed first (HEAD blob 9870e8bc5b0c): anchor = the new guarded try plus the last-resort assignment (4 lines), replaced by the base one-line assignment plus marker ABLATION-23723; anchor 1 -> 0, marker 0 -> 1, blob 9870e8bc5b0c -> 5569a01b0649 (in-leg grep counts 0/1); over the two new files + PR #23722 unit pin: 'Tests 9 failed | 14 passed (23)' = exactly the 9 shape rows red (6 unit received 'exceeded wall-clock ceiling of 1000ms ...', 3 door received 500 INTERNAL_ERROR), 4 controls + 10 PR #23722 rows green; restore: blob == HEAD 9870e8bc5b0c, git diff HEAD empty (tool and outer trap both). Pins import the runner from src by relative path: no dist in the resolution path. At f7ddaeb: pnpm --filter @objectstack/runtime test 'Test Files 397 passed (397)', 'Tests 5931 passed | 19 skipped (5950)'; test:repo 'Test Files 3 passed (3)', 'Tests 808 passed (808)'; typecheck exit 0, 'check:test-typecheck: OK ... 27 file(s) / 190 error(s) / 68 pinned signature(s) held'; both new test files in tsconfig.test.json program (--listFilesOnly count 2). Every build/test ran through os-verify-lock with OS_VERIFY_LOCK_SLOT=issue-23723.",
    "gates": "Local, at f7ddaeb: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 61 commands from 4 changed paths (merge base 140238a). All 61 ran, exit codes captured to files before any pipe: 60 exit 0 on the first pass; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (no workspace dist), then exit 0 after a full turbo build (75 tasks, 74 cached; runtime dist carries the sentence, grep 1 in index.js and index.cjs): '110 published require entry point(s) across 68 package(s) load'. --ran: '61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. Lint (lane blind spot, edited package): node --stack-size=4000 node_modules/eslint/bin/eslint.js --no-inline-config packages/runtime exit 0; --format json over the 3 changed .ts files 0 errors 0 warnings each; positive control: root @objectstack/spec import on stdin at a packages/runtime path exit 1 no-restricted-imports. CI (PR #23774 check-runs at f7ddaeb, read right after the create): success: No other open PR may claim the same issue; skipped: Packed-tarball smoke (opt-in); in_progress (21): The card this PR closes must claim this branch, No other open PR may claim the same single-writer path, Check Changeset, Lint · PM dispatch-gates, Type Check · workspace / source gates / consumer gates / debt ledger, Check PR Size, Lint · scoped · query-options erasure / slot lookup & corpus / tooling self-tests, Lint · docs & code guards / ESLint / CI, release & engine gates / repo & tooling gates, Part-of PR must not also close its card, filter, Flag docs affected by code changes, Check Documentation Links, Governed Surface Queue Guard. Build Core / Test Core not yet listed at that read. Not re-run locally; convergence is the PM's.",
    "line_budget": "n/a",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes, each one session POST /repos/objectstack-ai/objectstack/dispatches executed by objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23774/assignees (os-bill), read back 9510 bytes sent == stored; (2) this os-dev-report comment = POST /repos//issues/23723/comments. Plus 5 git pushes (empty branch probe, fix, pins, changeset, origin/main merge), not REST. label-write: none (forbidden by the dispatch).",
    "open_questions": [
    {
    "question": "The fixed sentence starts lowercase, byte-faithful to triage grade 6203064606 ("the script threw a value that could not be read"). Should the client sentence be sentence-cased instead?",
    "options": [
    "A: keep the ruled words byte for byte (as landed in PR #23774); the runner's own SandboxError texts are lowercase too",
    "B: capitalise the first letter (a one-character change in the constant, the two pins and the changeset)"
    ],
    "recommendation": "A, because the ruling quotes the sentence and Zone 1 is not re-opened; B is a later cosmetic call for the seat if wanted"
    }
    ],
    "out_of_scope_findings": [],
    "hypotheses": {
    "H1": "HOLDS (measured by outcome): the fallback read sat outside any try; the getter and null-prototype shapes left __error unset and throw '' left it '', so the run never ended on the threw path. Also measured: for the getter shape the handler threw before __errorInfo, so its code was lost too (now kept).",
    "H2": "HOLDS: before, each shape ended as SandboxError 'action \'submit\' exceeded wall-clock ceiling of 3000ms while awaiting host calls (after N pump iterations)' with no innerMessage, and at the door 500 INTERNAL_ERROR after 30.1 s.",
    "H3": "HOLDS: a final guarded fallback in the handler always sets a non-empty __error; the pump loop (:447 region) is untouched and the empty-string case needed no loop check."
    },
    "before_after": [
    "runner (3000 ms probe ceiling) getter { code: EMAIL_TAKEN, get message() { throw } }: 3014 ms budget SandboxError, no innerMessage, no code -> 23 ms 'action \'submit\' threw: Error: the script threw a value that could not be read', innerMessage the sentence, code EMAIL_TAKEN",
    "runner getter, hook origin: 3013 ms budget error -> 9 ms 'hook \'submit\' threw: Error: ...', the sentence, code EMAIL_TAKEN",
    "runner throw '': 3025 ms budget error -> 34 ms, the sentence",
    "runner throw Object.create(null): 3014 ms budget error -> 28 ms, the sentence",
    "door (default 30 s ceiling) getter: 30117 ms 500 INTERNAL_ERROR 'Internal server error' -> 108 ms 400 VALIDATION_ERROR declaredCode EMAIL_TAKEN 'the script threw a value that could not be read'",
    "door throw '': 30104 ms 500 INTERNAL_ERROR -> 118 ms 400 VALIDATION_ERROR the sentence",
    "door throw Object.create(null): 30116 ms 500 INTERNAL_ERROR -> 116 ms 400 VALIDATION_ERROR the sentence",
    "extra (same defect, not in the card) Error with empty name and message: runner 3111 ms budget -> 131 ms sentence; door 30142 ms 500 -> 109 ms 400 VALIDATION_ERROR sentence",
    "extra Error whose message getter throws: runner 3009 ms budget -> 9 ms sentence; door 30116 ms 500 -> 114 ms 400 VALIDATION_ERROR sentence",
    "CONTROL { code: EMAIL_TAKEN, message }: runner innerMessage 'That address is already on file' code EMAIL_TAKEN -> byte-identical; door 400 VALIDATION_ERROR declaredCode EMAIL_TAKEN its message -> byte-identical (PR #23722 answer kept)",
    "CONTROL never-settling body: runner 3014 ms budget error -> 3010 ms budget error; door 30129 ms 500 INTERNAL_ERROR -> 30101 ms 500 INTERNAL_ERROR (ceiling still ends it)",
    "Door status for these shapes measured, not changed: it is the status the door gives any body that threw a value with no registered code (throw {} answers 400 VALIDATION_ERROR before and after). Door timings in the probe were taken with the five unreadable shapes and the never-settling body dispatched concurrently in one process; shared box."
    ],
    "unchanged_answers": [
    "runner · plain_code_message · innerMessage "That address is already on file", code EMAIL_TAKEN -> byte-identical",
    "runner · plain_code_only · innerMessage "Internal server error", code DUPLICATE_RECORD -> byte-identical",
    "runner · plain_status_409 · innerMessage "That address is already on file", code DUPLICATE_RECORD, status 409 -> byte-identical",
    "runner · plain_empty · innerMessage "Internal server error" -> byte-identical",
    "runner · plain_array · innerMessage "Internal server error" -> byte-identical",
    "runner · plain_named · innerMessage "forged" -> byte-identical",
    "runner · plain_error_prefixed · innerMessage "Error: lead is incomplete" -> byte-identical",
    "runner · plain_tostring · innerMessage "custom words" -> byte-identical",
    "runner · error_x · innerMessage "x" -> byte-identical",
    "runner · type_error · innerMessage "TypeError: t" -> byte-identical",
    "runner · error_with_code · innerMessage "taken", code DUPLICATE_RECORD -> byte-identical",
    "runner · error_no_message_code · innerMessage "Error", code DUPLICATE_RECORD -> byte-identical",
    "runner · error_info_channel · innerMessage "bad input", code VALIDATION_FAILED, status 422, fields [{"field":"name","message":"required"}], userMessage "Please fix the name" -> byte-identical",
    "runner · string · innerMessage "Name is required" -> byte-identical",
    "runner · null · innerMessage "null" -> byte-identical",
    "runner · undefined · innerMessage "undefined" -> byte-identical",
    "runner · zero · innerMessage "0" -> byte-identical",
    "runner · false · innerMessage "false" -> byte-identical",
    "door · plain_code_message · 400 VALIDATION_ERROR "That address is already on file" declaredCode EMAIL_TAKEN -> byte-identical",
    "door · plain_code_only · 400 DUPLICATE_RECORD "Internal server error" -> byte-identical",
    "door · plain_status_409 · 409 DUPLICATE_RECORD "That address is already on file" -> byte-identical",
    "door · plain_empty · 400 VALIDATION_ERROR "Internal server error" -> byte-identical",
    "door · plain_array · 400 VALIDATION_ERROR "Internal server error" -> byte-identical",
    "door · plain_named · 400 VALIDATION_ERROR "forged" -> byte-identical",
    "door · plain_error_prefixed · 400 VALIDATION_ERROR "Error: lead is incomplete" -> byte-identical",
    "door · plain_tostring · 400 VALIDATION_ERROR "custom words" -> byte-identical",
    "door · error_x · 400 VALIDATION_ERROR "x" -> byte-identical",
    "door · type_error · 500 INTERNAL_ERROR "Internal server error" -> byte-identical",
    "door · error_with_code · 400 DUPLICATE_RECORD "taken" -> byte-identical",
    "door · error_no_message_code · 400 DUPLICATE_RECORD "Error" -> byte-identical",
    "door · error_info_channel · 422 VALIDATION_FAILED "bad input" -> byte-identical",
    "door · string · 400 VALIDATION_ERROR "Name is required" -> byte-identical",
    "door · null · 400 VALIDATION_ERROR "null" -> byte-identical",
    "door · undefined · 400 VALIDATION_ERROR "undefined" -> byte-identical",
    "door · zero · 400 VALIDATION_ERROR "0" -> byte-identical",
    "door · false · 400 VALIDATION_ERROR "false" -> byte-identical",
    "door · engine_refusal · 409 UNIQUE_VIOLATION "Duplicate record refused on 'udoc': a unique constraint already holds these values. No record was written." -> byte-identical",
    "door · nested_refusal · 400 VALIDATION_ERROR "x" -> byte-identical",
    "door · capability_denial · 500 INTERNAL_ERROR "Internal server error" -> byte-identical"
    ],
    "files_changed": [
    "packages/runtime/src/sandbox/quickjs-runner.ts (REJECT_HANDLER: the fallback read guarded and a last-resort fixed sentence; plus the module constant UNREADABLE_REJECTION_SENTENCE beside the other module constants)",
    "packages/runtime/src/sandbox/unreadable-rejection-recorded.test.ts (new)",
    "packages/runtime/src/sandbox-unreadable-rejection-door.test.ts (new)",
    ".changeset/23723-sandbox-unreadable-rejection.md (new; @objectstack/runtime patch, fix(runtime):, FROM/TO measured at the door, Clause-②: no)"
    ],
    "deviations": [
    "File surface: one line outside REJECT_HANDLER in quickjs-runner.ts, the module constant UNREADABLE_REJECTION_SENTENCE beside the other module-level constants (not exported); same file, inside the claimed surface.",
    "Two values beyond the card's three move, both inside triage's scope ('Any rejection is recorded, even when reading it throws or yields '''): an Error whose name and message are both empty (String() is '') and an Error whose message getter throws. Measured, pinned, stated in the changeset and PR body.",
    "The PR was opened after the local union, not at the first showable content, so the body (written once, no PATCH) carries the final readings; every step was committed and pushed before each minutes-long run.",
    "origin/main moved by one commit after the branch was cut (140238a, the console pin: .changeset/console-f96f771140f1.md, .objectui-sha, packages/sdui-parser/objectui-lockstep.json, scripts/sdui-manifest.record.json; no file in common); merged before opening the PR (AGENTS.md multi-agent section 10), and every reading in the body is at the merged head f7ddaeb.",
    "The first after-probe attempt queue-timed-out (os-verify-lock exit 99, 540 s behind #23714's locked chain); re-entered with the same slot and measured; that attempt measured nothing and is not reported as a run.",
    "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; the dispatch and AGENTS.md win, so commits carry the model-free Co-Authored-By: Claude + Claude-Session pair and the PR body ends with the session-URL footer."
    ],
    "pr_body_changes": [],
    "cleanup": "Worktree ../objectstack-issue-23723 removed with git worktree remove (no --force) after deleting its node_modules; tree was clean and local head == origin branch == f7ddaeb before removal. The temporary probe (src/__probe-23723.test.ts) was deleted before any commit and never pushed. No dev server or monitor started; every background run this session started (install, runtime suite, gate battery, full build) had exited before removal.",
    "labels": "none written: the dispatch forbids label-write and a changeset is present."
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23774 → b55be4213c (#23723). The card is closed completed, and pm:dispatched is stripped

    domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-16T01:53Z · holder of claim 6204824875, released by this landing.

    reading value
    queue ready and auto-merge armed 01:22Z, enqueued 01:24Z, merged 01:52Z
    origin/main b55be4213c: one parent (a queue squash), an ancestor of origin/main, 4 files +322 / −1, identical to the reviewed head f7ddaeb73; no queue branch left
    card closed completed by the PR's Fixes #23723; pm:dispatched removed by targeted delete
    • Review chain: ACCEPT 6205646622 on a true-green head. No contract review was owed.
    • What landed:
      • The QuickJS runner's reject handler records a rejection no read can flatten as the fixed sentence the script threw a value that could not be read. That covers throw '', a null-prototype object, a throwing message getter, and an Error with an empty or throwing message.
      • The run now ends at once on the "threw" path: about 0.1 s and 400 VALIDATION_ERROR at the action door, where it used to wait out the 30 s ceiling and answer 500. A code the value carries is kept.
      • Every readable value's answer is byte-identical, and a never-settling body still ends at the ceiling.
    • Holds released: packages/runtime/src/sandbox/quickjs-runner.ts.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions