Skip to content

mcp: a CODED sandboxed hook refusal still answers the sandbox wrapper text with the hook's name, where /data answers the body's sentence — and the other MCP tools still render a throw unlike REST #31246

Closed

Description

@objectstack-fleet

domain:server#4 · marchtian · session_017ihL2dLQEXWGsE2SZ3Qn5F

Filing gate: ① product defect, from the delivery of #31218 (PR #31245, dev report 6712120510, open questions 1 and 3). #31218 closed the CODE-LESS half on the six record data tools; the coded half was a CONTROL there by order. ⛔ Not a claim. Triage sets the grade and the lane.
Reader: the seat that owns packages/mcp (src/mcp-http-tools.ts thrownRefusalEnvelope, errorResultWithCode, errorResultFromThrown).
Dedupe (open AND closed): mcp HOOK_ABORTED, mcp thrownRefusalEnvelope, mcp run_action error parity → #31218 (parent), #23509 / #23701 / #27672 (prior art), none open for this.
reach: every MCP agent whose app has a sandboxed hook that throws with a declared code + status; and agents calling the tools listed below. Measured by the dev on MCPServerRuntime (item 1); item 2 read from the code.
class: c · type: Bug

维护者速读

#31218 让 MCP 的六个记录工具在“无码”异常上与 REST 数据接口一致。但 hook 自己声明了错误码时,MCP 仍把沙箱调试包装(含 hook 内部名)原样给 agent,REST 只给 hook 写的那句话。另有几个 MCP 工具对异常的渲染仍与 REST 不同。

What is wrong

  1. Coded sandboxed refusal. A sandboxed hook body that throws with code + status reaches MCP through thrownRefusalEnvelope, whose message is the sandbox's wrapper (hook '<name>' threw: Error: …), naming the hook. /api/v1/data answers the body's sentence alone (sandboxBusinessMessage). Affects the record data tools, the record resources and run_action.
  2. Remaining divergences (noted by the dev, not measured one by one): the metadata tools, list_actions, the record resources, run_action, delete_records' outer catch (fault text), and driver-native string codes (SQLITE_BUSY) still render a throw unlike the REST door that serves the same verb.

Expected

  • Item 1: thrownRefusalEnvelope reads the caller-addressed sentence the same way the data door does (one reader, never a copy); the code and status are unchanged. Pins: coded sandboxed refusal → its code + the inner sentence, the hook name absent, on each MCP face that renders it; CONTROL: a coded non-sandbox refusal renders byte for byte as today.
  • Item 2: triage decides whether each divergence is a defect (one card per face) or intended; delete_records' outer catch keeps its own sentences (they would trip mapDataError's unknown-object heuristic, PR fix(mcp): the record data tools answer a code-less throw as the /data door does, so an undeclared hook abort is HOOK_ABORTED #31245 note).

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:serverDomain lane — seat card indexed by label:pm:seat
    area:aiAI-native — agent / tool / skill metadata, and the MCP surface an agent drives
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 7, 2026
  2. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 · domain:server · 2026-11-07T19:57Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

    Claim: PM loop round 1 · 2026-11-07T20:07Z · seat 6 refill; ceded by server seat 5 (domain:server#5), which keeps #31232 / #31186
    Session: session_01FEkQ5p3T5YWhLjurajudnQ
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-bkz0jj
    Worktree: the card thread's own checkout of claude/project-thread-bkz0jj
    Domain: domain:server
    Seat: domain:server#6
    File surface: packages/mcp/src/mcp-http-tools.ts (thrownRefusalEnvelope reading the caller-addressed sentence through sandboxBusinessMessage from packages/types) + mcp tests; root .changeset/. ⛔ packages/spec/**, ⛔ a second copy of sandboxBusinessMessage, ⛔ delete_records' outer catch sentences (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default, effort: default
    Clause-②: no (a refusal's message text; code and status unchanged; the dev re-declares if a published answer changes)
    Metadata-protocol: none
    Responsibility: MCP's rendering of a coded sandboxed hook refusal (record data tools, record resources, run_action) names the hook where /api/v1/data answers the body's sentence; no other path covers it; reached by every MCP agent whose app has a sandboxed hook that throws with a declared code
    Thread-read: 6712294143
    Premise: measured by the #31218 dev on MCPServerRuntime (report 6712120510); the dev re-measures red-first after PR #31245 lands
    Serial constraints: PR #31245 (#31218) edits the same file and is open; no code is cut from main until it is MERGED (order's serial gate). Item 2 faces are measured; each real divergence comes back as out_of_scope_findings for the seat to file

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 7, 2026
  5. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

    Claim amendment · 2026-11-07T20:15Z · amends the claim on this card (branch claude/project-thread-bkz0jj)

    File surface (added): packages/mcp/src/mcp-server-runtime.ts, the bridgeResources catch (#23509) only: the record resource renders its own err.message rather than the envelope's, so item 1's record-resource face needs that catch to read the shared envelope's message. One reader still: the sentence comes from sandboxBusinessMessage through thrownRefusalEnvelope, never a copy. Measured red by the dev on main 169ff7e5: a QuickJS hook throwing { code: RECORD_LOCKED, status: 409 } answers hook 'demo_guard' threw: Error: Locked. on every MCP tool and the record resource, where /api/v1/data answers Locked. 409. Everything else in the claim stands, including the serial gate on PR #31245.

  6. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-project-manager · session_01FEkQ5oWCPWH6i2Lsts9cdP
    os-dev-report
    {
    "issue": 31246,
    "status": "done",
    "branch": "claude/project-thread-bkz0jj",
    "pr": "#31268",
    "head": "a8855476e91d945fefd9f5b2d6f1365e84a78be1",
    "session": "session_01FEkQ5oWCPWH6i2Lsts9cdP",
    "premise_still_valid": true,
    "summary": "Re-measured red first after PR #31245 merged (base bdefd49): the real QuickJSScriptRunner turns a hook body's throw Object.assign(new Error('Locked.'), { code: 'RECORD_LOCKED', status: 409 }) into a SandboxError with message "hook 'demo_guard' threw: Error: Locked.", innerMessage 'Locked.', code and status as declared; every MCP tool and the record resource answered the wrapper, /api/v1/data answers 409 RECORD_LOCKED 'Locked.'. thrownRefusalEnvelope (packages/mcp/src/mcp-http-tools.ts) now reads message as sandboxBusinessMessage(err) ?? messageOf(err), the one reader in @objectstack/types the /data door uses, no copy; the record resource catch in mcp-server-runtime.ts (seat amendment 6712491439) renders the envelope's message instead of its own err.message. Code and status unchanged; a coded non-sandbox refusal renders byte for byte as before. Item 2: every listed face measured against the REST door serving the same verb (table in the PR's Acceptance notes); each real divergence is in out_of_scope_findings, delete_records' outer catch kept with its reason.",
    "tests": "pnpm --filter @objectstack/mcp test on a885547: Test Files 60 passed (60), Tests 806 passed (806), lock VERDICT command-exit 0. Red first on bdefd49 with the new pins and no fix: mcp-coded-refusal.test.ts + mcp-delete-records.test.ts Tests 15 failed | 91 passed (106) (13 tool rows, resource row, delete_records row; CONTROLs green); with the fix 106 passed. -t 31246 on mcp-coded-refusal.test.ts: 16 passed. Ablation 1 via ablation-replace.mjs on mcp-http-tools.ts (sandboxBusinessMessage(err) ?? messageOf(err) -> messageOf(err) as string): ok mutation landed anchor 1 -> 0, blob 9cca1c033a19 -> 58b6b2bf2013, Tests 15 failed | 91 passed (106), ok restored blob == HEAD and git diff HEAD empty. Ablation 2 on mcp-server-runtime.ts (error: envelope.message -> error: message as string): blob 38c17f5605e4 -> c385c405c4f1, Tests 1 failed | 105 passed (106) (resource row), restored blob == HEAD. Tests import src; no dist involved.",
    "gates": "60 derived commands (dispatch-gates --commands --repo objectstack-ai/objectstack at a885547), each run with stdin from /dev/null: all exit 0. check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: shallow clone; missing dists) and exited 0 after git fetch --depth=1 of the pinned fixture commit and the full build check:type-check-debt performs. dispatch-gates --ran: 60 derived families accounted for, 60 run, 0 NOT-MEASURED. pnpm --filter @objectstack/mcp typecheck exit 0 (check:test-typecheck: 53 errors / 8 pinned signatures held). eslint --no-inline-config on the 4 touched .ts files exit 0 (mcp has no lint script). CI check-runs: in_progress at report time. Not run locally: test:repo, dogfood shards, type-check lanes (CI).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm (Clause-②: no, as the claim states)",
    "line_budget": "140 changed lines (+137/-3), 5 files",
    "deviations": [
    "Item-2 REST answers were read with each door's own responder (mapDataError for /data, rest handleRouteError for /meta, runtime HttpDispatcher.handleActions for /actions) on the same thrown values the MCP bridge members threw, in scratch probes not committed; the MCP side was measured over /mcp on this branch. No booted stack.",
    "Changeset adds one sentence superseding the #31218 changeset line "A thrown error that carries a code is answered exactly as before" (unreleased); that changeset is not edited."
    ],
    "files_changed": [
    ".changeset/31246-mcp-coded-sandbox-refusal-sentence.md",
    "packages/mcp/src/mcp-coded-refusal.test.ts",
    "packages/mcp/src/mcp-delete-records.test.ts",
    "packages/mcp/src/mcp-http-tools.ts",
    "packages/mcp/src/mcp-server-runtime.ts"
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: /mcp tools/call (every tool) and the record resource, bridge member throwing the real QuickJSScriptRunner shape for a hook body throw Object.assign(new TypeError('boom'), { code: 'RECORD_LOCKED', status: 409 }) · MCP answers { code: RECORD_LOCKED, message: "hook 'demo_guard' threw: TypeError: boom", status: 409 } (hook name + native error text at a business status); /data and /meta answer 500 INTERNAL_ERROR 'Internal server error', /actions 500 INTERNAL_ERROR. The shared reader declines a crash by contract, so this PR does not move it; the coded path would need the isSandboxCrash read the doors make · dedupe: mcp coded sandbox crash, isSandboxCrash mcp, thrownRefusalEnvelope crash",
    "class: a · reach: /mcp resources/read objectstack://objects/OBJ/records/ID, reader throwing a code-less sandboxed hook abort (SandboxError, innerMessage set) · MCP answers { error: "hook 'demo_guard' threw: Error: Not enough leave" }; GET /data/OBJ/ID (mapDataError) answers 400 HOOK_ABORTED 'Not enough leave'. A bare Error answers { error: 'socket hang up' } vs 500 INTERNAL_ERROR; SQLITE_BUSY answers { error: driver text, code: SQLITE_BUSY } vs 500 DATABASE_ERROR 'Internal data error'. The #31218 code-less half on the resource face · dedupe: mcp record resource HOOK_ABORTED, bridgeResources code-less, resource mapDataError",
    "class: a · reach: /mcp tools/call run_action and resume_run, runAction throwing a code-less sandboxed action-body throw · MCP answers the bare wrapper text naming the body; /actions (HttpDispatcher.handleActions) answers 400 HOOK_ABORTED with the inner sentence. A bare Error answers its bare text with no code vs /actions 400 VALIDATION_ERROR same text; SQLITE_BUSY answers { code: SQLITE_BUSY, driver text, no status } vs /actions 400 VALIDATION_ERROR with declaredCode SQLITE_BUSY · dedupe: mcp run_action HOOK_ABORTED, errorResultFromThrown code-less, run_action actions door parity",
    "class: a · reach: /mcp tools/call list_objects, describe_object, validate_expression, list_actions, bridge member throwing a bare Error or a driver string code · MCP answers the bare text ('socket hang up') or { code: SQLITE_BUSY, message: driver text }; the /meta door (handleRouteError) answers 500 INTERNAL_ERROR 'Internal server error' and 500 DATABASE_ERROR 'Internal data error'. No named production producer measured for these members (metadata reads run no record hooks, so the sandbox shapes are not reachable here) · dedupe: mcp metadata tools fault text, list_actions errorResultWithCode, mcp meta door parity",
    "class: a · reach: /mcp tools/call query_records, aggregate_records, get_record, create_record, update_record, delete_record (and delete_records' selection read), bridge member throwing Object.assign(new Error('SQLITE_BUSY: database is locked'), { code: 'SQLITE_BUSY' }) · MCP answers { code: SQLITE_BUSY, message: 'SQLITE_BUSY: database is locked' } with no status because the coded path is read first; /data (mapDataError) answers 500 DATABASE_ERROR 'Internal data error' · dedupe: mcp driver string code, SQLITE_BUSY mcp, thrownRefusalEnvelope driver code",
    "noted, not filed: delete_records' outer catch kept on errorResultWithCode by ruling (its own code-less sentences would trip mapDataError's unknown-object heuristic); its fault answer quotes the faulting row's thrown message, pinned on purpose in mcp-delete-records.test.ts · carrier: none",
    "noted, not filed: /actions answers a driver string code (SQLITE_BUSY) as 400 VALIDATION_ERROR with declaredCode, where /data answers 500 DATABASE_ERROR (REST-side, read in this measurement only) · carrier: none"
    ],
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "2 — fleet relay pr_create (#31268, fw-20261107T210951Z-abba91); this os-dev-report comment via post-stamped (git push not counted)",
    "security_findings": "none"
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

    落地 · 2026-11-07T22:08Z

    reading value
    PR #31268 MERGED 2026-11-07T22:07:53Z, merge commit b3bd75d8 (ACCEPT 6713341211, first merge group)
    #31246 closed completed by Fixes #31246

    State change: pm:dispatched removed; nothing left on this card. Its item-2 divergences are #31269–#31273, dispatched one at a time in this lane.

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

Metadata

Metadata

Labels

area:aiAI-native — agent / tool / skill metadata, and the MCP surface an agent drivesbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions