You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
mcp: the data tools answer an undeclared sandboxed hook abort with no code and the sandbox wrapper text, where /data answers 400 HOOK_ABORTED #31218
Filing-gate class b (door parity, measured). Found by the #31145 card thread while tracing the scenario runner's "(tool error without a code)" on leave_balances submit_over_balance; reported in its os-dev report (comment 6711051465 on #31145), out of that card's scope (a scenario-runner card fixes no platform package).
Reproduction (measured on MCPServerRuntime, no booted stack)
HTTP /mcpupdate_record with a data bridge that throws the QuickJS sandbox's error shape for an undeclared hook abort answered: {content:[{type:"text",text:"hook 'demoleave_leave_request_guard' threw: Error: Not enough annual leave remaining"}],isError:true}
There is no ErrorCode, and the text is the sandbox's debug wrapper (hook name included).
mapDataError on the same error (packages/types/src/thrown-http-error.ts, seam UNDECLARED_HOOK_REFUSAL_CODE) answers 400 HOOK_ABORTED with the inner sentence only. So /api/v1/data and the MCP data tools disagree on the same refusal.
Cause read in code: packages/mcp/src/mcp-http-tools.tserrorResultWithCode → thrownRefusalEnvelope reads only err.code, so the undeclared-abort classification that the REST door applies never runs at the MCP door.
Same family (read in code, not measured)
A code hook's bare throw: MCP uncoded vs REST 500 INTERNAL_ERROR.
The runtime callData throw {statusCode:503,message}.
Ask
Make the MCP data tools classify a thrown error the way the data door does (one shared seam, not a second copy), so an undeclared hook abort answers HOOK_ABORTED with the inner sentence and no sandbox wrapper. Cover the family members above or state why each differs. Pin with a test at the MCP door. Clause-② and changeset readings belong to whoever claims it.
NOT MEASURED
A booted stack.
Which throw the leave_balances run actually hit (no recording).
Prior art, all closed: #27508 / #27597 (HOOK_ABORTED at the data door), #27672 (doors resolving through resolveThrownHttpError), #29978 (import door parity), #23509 (MCP rendered a coded refusal as message only).
Door parity. An undeclared sandboxed hook abort reaches MCP's data tools as isError text with no ErrorCode, and that text is the sandbox's debug wrapper, including the hook name. /data answers the same abort with 400 HOOK_ABORTED. Measured on MCPServerRuntime.
p2: an agent cannot tell a business refusal from a fault, and it sees internal hook names. Lane domain:server (packages/mcp, mapDataError). finding removed, pm:queue.
Claim: PM loop round 3
Session: session_017ihL2dLQEXWGsE2SZ3Qn5F
Account: marchtian
Branch: claude/project-thread-vvleyi
Worktree: card thread container (project thread form)
Domain: domain:server
Seat: domain:server#4
File surface: packages/mcp/src/mcp-http-tools.ts (errorResultWithCode / thrownRefusalEnvelope), packages/types/src/thrown-http-error.ts only additively if the shared seam must be exported, their tests, .changeset/ (stop on breach; explain in the report)
Container & model: M, mode:thread (a project card thread started by the coordinator), model: default, effort: high
Clause-②: yes
Metadata-protocol: none
Responsibility: objectstack (packages/mcp data tools) | /api/v1/data classifies an undeclared sandboxed hook abort as 400 HOOK_ABORTED through mapDataError / resolveThrownHttpError; the MCP data tools read only err.code and answer the sandbox's wrapper text uncoded, hook name included | reached by every MCP agent writing through a package with a sandboxed hook
Thread-read: 6711643853
Premise: holds — measured by the filer on MCPServerRuntime; re-measured by the dev at branch start
Serial constraints cleared: #31022 / PR #31141 (merged) touched packages/mcp elsewhere; no open PR touches packages/mcp/src/mcp-http-tools.ts or packages/types/src/thrown-http-error.ts in this lane (#31227 runtime DELETE arm, #31156packages/cli)
{
"issue": 31218,
"status": "done",
"branch": "claude/project-thread-vvleyi",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31245",
"head": "a0f8cfb56f18b996a72a5edb3fb396710823f849",
"session": "session_017ihL2eNMbdYCpXSovm5QP5",
"premise_still_valid": true,
"summary": "Re-measured at branch start on MCPServerRuntime: with mcp-http-tools.ts at the base blob, update_record (and the other five record data tools) answered the QuickJS undeclared abort as the bare wrapper text `hook 'demoleave_leave_request_guard' threw: Error: ...`, no code. The six record data tools (query_records, aggregate_records, get_record, create_record, update_record, delete_record) now render a code-less throw through mapDataError (@objectstack/types), the /data door's own classifier, via one reader dataDoorAnswerOf (rowRefusalOf of delete_records now reads it too): HOOK_ABORTED + inner sentence for an undeclared abort, 500 INTERNAL_ERROR with the message withheld and logged once via logServerFault for a bare code-hook throw or a crash, the declared status with no code invented for {statusCode:503|400, message} (5xx prose withheld). A coded throw renders exactly as before. No packages/types or packages/spec change.",
"tests": "pnpm --filter @objectstack/mcp test on a0f8cfb5: Test Files 60 passed (60), Tests 789 passed (789), lock VERDICT command-exit 0. New #31218 describe in mcp-coded-refusal.test.ts (16 tests: each tool's abort, lost-instance wrapper, declared 422, 4xx not logged, bare throw withheld + one [5xx] line, crash, 503, declared 400, coded CONTROL x6 tools, non-data tools CONTROL); every case asserts the literal answer AND equality with mapDataError on the same throw. Base re-measure (src at base blob 2b752f12): 13 failed | 3 passed of the block. Ablation via ablation-replace.mjs (return codedErrorResult(door) -> return errorResult(messageOf(err))): mutation landed anchor 1->0, Tests 19 failed | 43 passed (62); restored blob == HEAD, git diff HEAD empty. Source-imported, no dist involved.",
"gates": "60 derived commands on a0f8cfb5, all exit 0 (incl. check:dual-build-cjs-loads, check:lean-entry-closure, check-plugin-teardown-shape --self-test); dispatch-gates --ran: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --filter @objectstack/mcp typecheck exit 0 (check:test-typecheck held). eslint --no-inline-config on the 3 touched .ts files exit 0 (mcp has no lint script). CI check-runs: in_progress at report time.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: yes as the order stated)",
"line_budget": "351 changed lines (+318/-33), 4 files, under 3000",
"deviations": [
"delete_records' outer catch kept on errorResultWithCode (not the door): it also renders this file's own code-less sentence `delete_records could not read the records the query on 'OBJ' answered`, which mapDataError's unknown-object limb would misread as 404 OBJECT_NOT_FOUND (quoted object + `not`).",
"mcp-server-runtime.http.test.ts: two existing pins updated (uncoded `RLS: not permitted` now answers the door's 500; the aggregate FLS fixture became the coded PermissionDeniedError the security plugin really throws)."
],
"files_changed": [
".changeset/31218-mcp-data-tools-door-classification.md",
"packages/mcp/src/mcp-http-tools.ts",
"packages/mcp/src/mcp-coded-refusal.test.ts",
"packages/mcp/src/mcp-server-runtime.http.test.ts"
],
"open_questions": [
{
"question": "A CODED sandboxed refusal (hook body throws with code+status) still reaches the MCP data tools as the wrapper text with the hook name (measured on MCPServerRuntime: {code:'RECORD_LOCKED', message:\"hook 'guard' threw: Error: Locked.\", status:409}); /data answers the inner sentence. Fix here or card it?",
"options": [
"A: follow-up card: thrownRefusalEnvelope reads sandboxBusinessMessage(err) ?? messageOf(err); touches the record resource and run_action too",
"B: extend this PR (the order's CONTROL said the coded path stays as today)"
],
"recommendation": "A, because it moves the coded path of three surfaces, which this order fenced off as CONTROL."
}
],
"out_of_scope_findings": [
"class: b · reach: measured on MCPServerRuntime /mcp tools/call update_record (harness bridge throwing the QuickJS coded shape) · a coded sandboxed refusal answers the debug wrapper + hook name where /data answers the inner sentence · dedupe: mcp coded sandbox wrapper, thrownRefusalEnvelope innerMessage, hook name MCP",
"noted, not filed: metadata tools (list_objects, describe_object, validate_expression), list_actions, the record resource, run_action/resume_run and delete_records' outer catch still render a code-less throw as its bare message (read in code) · carrier: none",
"noted, not filed: delete_records' fault answer quotes the faulting row's thrown message; a driver's own string code (SQLITE_BUSY) rides MCP's coded path with its message where /data answers 500 INTERNAL_ERROR (read in code) · carrier: none"
],
"mcp_calls": "0",
"api_writes": "2 — fleet relay pr_create (#31245, dispatch fw-20261107T193919Z-18383d); this os-dev-report comment via post-stamped",
"security_findings": "none"
}
Landed · PR #31245 merged by the merge queue as 1aa4dd10ce1588eeb329fa38653232dce97a580d.
Reading
Value
PR state
merged, merged_at from the pulls API
Card state
closed completed by Fixes #31218
origin/main content
dataDoorErrorResult answers the six record data tools' catches in packages/mcp/src/mcp-http-tools.ts
Follow-up: #31246 (coded sandboxed refusal wrapper text on MCP, and the other MCP faces' divergences from REST). pm:dispatched removed with this record.
Activity
objectstack-fleet commentedon Nov 7, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 ·
domain:server· 2026-11-07T18:56Z⛔ Not a claim.
isErrortext with noErrorCode, and that text is the sandbox's debug wrapper, including the hook name./dataanswers the same abort with400 HOOK_ABORTED. Measured onMCPServerRuntime.domain:server(packages/mcp,mapDataError).findingremoved,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#4 · marchtian · session_017ihL2dLQEXWGsE2SZ3Qn5F
Claim: PM loop round 3
Session:
session_017ihL2dLQEXWGsE2SZ3Qn5FAccount:
marchtianBranch:
claude/project-thread-vvleyiWorktree: card thread container (project thread form)
Domain:
domain:serverSeat:
domain:server#4File surface:
packages/mcp/src/mcp-http-tools.ts(errorResultWithCode/thrownRefusalEnvelope),packages/types/src/thrown-http-error.tsonly additively if the shared seam must be exported, their tests,.changeset/(stop on breach; explain in the report)Container & model:
M,mode:thread(a project card thread started by the coordinator),model: default,effort: highClause-②: yes
Metadata-protocol: none
Responsibility: objectstack (
packages/mcpdata tools) |/api/v1/dataclassifies an undeclared sandboxed hook abort as400 HOOK_ABORTEDthroughmapDataError/resolveThrownHttpError; the MCP data tools read onlyerr.codeand answer the sandbox's wrapper text uncoded, hook name included | reached by every MCP agent writing through a package with a sandboxed hookThread-read: 6711643853
Premise: holds — measured by the filer on
MCPServerRuntime; re-measured by the dev at branch startSerial constraints cleared: #31022 / PR #31141 (merged) touched
packages/mcpelsewhere; no open PR touchespackages/mcp/src/mcp-http-tools.tsorpackages/types/src/thrown-http-error.tsin this lane (#31227 runtime DELETE arm, #31156packages/cli)objectstack-fleet commentedon Nov 7, 2026
domain:server#4 · marchtian · session_017ihL2eNMbdYCpXSovm5QP5
os-dev-report
{ "issue": 31218, "status": "done", "branch": "claude/project-thread-vvleyi", "pr": "https://github.com/objectstack-ai/objectstack/pull/31245", "head": "a0f8cfb56f18b996a72a5edb3fb396710823f849", "session": "session_017ihL2eNMbdYCpXSovm5QP5", "premise_still_valid": true, "summary": "Re-measured at branch start on MCPServerRuntime: with mcp-http-tools.ts at the base blob, update_record (and the other five record data tools) answered the QuickJS undeclared abort as the bare wrapper text `hook 'demoleave_leave_request_guard' threw: Error: ...`, no code. The six record data tools (query_records, aggregate_records, get_record, create_record, update_record, delete_record) now render a code-less throw through mapDataError (@objectstack/types), the /data door's own classifier, via one reader dataDoorAnswerOf (rowRefusalOf of delete_records now reads it too): HOOK_ABORTED + inner sentence for an undeclared abort, 500 INTERNAL_ERROR with the message withheld and logged once via logServerFault for a bare code-hook throw or a crash, the declared status with no code invented for {statusCode:503|400, message} (5xx prose withheld). A coded throw renders exactly as before. No packages/types or packages/spec change.", "tests": "pnpm --filter @objectstack/mcp test on a0f8cfb5: Test Files 60 passed (60), Tests 789 passed (789), lock VERDICT command-exit 0. New #31218 describe in mcp-coded-refusal.test.ts (16 tests: each tool's abort, lost-instance wrapper, declared 422, 4xx not logged, bare throw withheld + one [5xx] line, crash, 503, declared 400, coded CONTROL x6 tools, non-data tools CONTROL); every case asserts the literal answer AND equality with mapDataError on the same throw. Base re-measure (src at base blob 2b752f12): 13 failed | 3 passed of the block. Ablation via ablation-replace.mjs (return codedErrorResult(door) -> return errorResult(messageOf(err))): mutation landed anchor 1->0, Tests 19 failed | 43 passed (62); restored blob == HEAD, git diff HEAD empty. Source-imported, no dist involved.", "gates": "60 derived commands on a0f8cfb5, all exit 0 (incl. check:dual-build-cjs-loads, check:lean-entry-closure, check-plugin-teardown-shape --self-test); dispatch-gates --ran: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --filter @objectstack/mcp typecheck exit 0 (check:test-typecheck held). eslint --no-inline-config on the 3 touched .ts files exit 0 (mcp has no lint script). CI check-runs: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: yes as the order stated)", "line_budget": "351 changed lines (+318/-33), 4 files, under 3000", "deviations": [ "delete_records' outer catch kept on errorResultWithCode (not the door): it also renders this file's own code-less sentence `delete_records could not read the records the query on 'OBJ' answered`, which mapDataError's unknown-object limb would misread as 404 OBJECT_NOT_FOUND (quoted object + `not`).", "mcp-server-runtime.http.test.ts: two existing pins updated (uncoded `RLS: not permitted` now answers the door's 500; the aggregate FLS fixture became the coded PermissionDeniedError the security plugin really throws)." ], "files_changed": [ ".changeset/31218-mcp-data-tools-door-classification.md", "packages/mcp/src/mcp-http-tools.ts", "packages/mcp/src/mcp-coded-refusal.test.ts", "packages/mcp/src/mcp-server-runtime.http.test.ts" ], "open_questions": [ { "question": "A CODED sandboxed refusal (hook body throws with code+status) still reaches the MCP data tools as the wrapper text with the hook name (measured on MCPServerRuntime: {code:'RECORD_LOCKED', message:\"hook 'guard' threw: Error: Locked.\", status:409}); /data answers the inner sentence. Fix here or card it?", "options": [ "A: follow-up card: thrownRefusalEnvelope reads sandboxBusinessMessage(err) ?? messageOf(err); touches the record resource and run_action too", "B: extend this PR (the order's CONTROL said the coded path stays as today)" ], "recommendation": "A, because it moves the coded path of three surfaces, which this order fenced off as CONTROL." } ], "out_of_scope_findings": [ "class: b · reach: measured on MCPServerRuntime /mcp tools/call update_record (harness bridge throwing the QuickJS coded shape) · a coded sandboxed refusal answers the debug wrapper + hook name where /data answers the inner sentence · dedupe: mcp coded sandbox wrapper, thrownRefusalEnvelope innerMessage, hook name MCP", "noted, not filed: metadata tools (list_objects, describe_object, validate_expression), list_actions, the record resource, run_action/resume_run and delete_records' outer catch still render a code-less throw as its bare message (read in code) · carrier: none", "noted, not filed: delete_records' fault answer quotes the faulting row's thrown message; a driver's own string code (SQLITE_BUSY) rides MCP's coded path with its message where /data answers 500 INTERNAL_ERROR (read in code) · carrier: none" ], "mcp_calls": "0", "api_writes": "2 — fleet relay pr_create (#31245, dispatch fw-20261107T193919Z-18383d); this os-dev-report comment via post-stamped", "security_findings": "none" }objectstack-fleet commentedon Nov 7, 2026
domain:server#4 · marchtian · session_017ihL2dLQEXWGsE2SZ3Qn5F
Landed · PR #31245 merged by the merge queue as
1aa4dd10ce1588eeb329fa38653232dce97a580d.merged_atfrom the pulls APIcompletedbyFixes #31218origin/maincontentdataDoorErrorResultanswers the six record data tools' catches inpackages/mcp/src/mcp-http-tools.tsFollow-up: #31246 (coded sandboxed refusal wrapper text on MCP, and the other MCP faces' divergences from REST).
pm:dispatchedremoved with this record.