Repository navigation
Commit 95024e6
fix(mcp): the record resource answers a code-less throw as /api/v1/data does, so a sandboxed hook abort no longer names the hook (#31300)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X3apZSKwFDd6zvjM4gJPKAy)_
Fixes #31270
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:MCP
的记录资源(`objectstack://objects/{name}/records/{id}`)遇到不带错误码的异常时,改为与六个记录数据工具相同的读法(`/api/v1/data`
的 `mapDataError`):沙箱 hook 业务中止回 `HOOK_ABORTED` + 作者那句话 + 400;裸 `Error`
和沙箱崩溃(带不带码都一样)回 `500 INTERNAL_ERROR` `Internal server error`,原文只记服务端日志。
- 为什么改:之前资源对无码异常原样回 `{ error: 抛出的消息 }`,沙箱中止时就是 `hook 'NAME' threw: …`,把
hook 内部名交给客户端;故障原文也直接外泄。
- 风险与回滚:资源 JSON 的成员仍是 #23509 的 `{ error, code, status }`,只是无码异常现在也会带
`code`(门给出时)和 `status`;读资源的客户端会在无码异常上也看到这两个成员。只改
`@objectstack/mcp`,patch。回滚即还原 `bridgeResources` 的 catch。
- 先看:`packages/mcp/src/mcp-server-runtime.ts` 的 `bridgeResources`
catch;`packages/mcp/src/mcp-coded-refusal.test.ts` 记录资源 describe 里的
#31270 块。
- 两分钟验证:`pnpm --filter @objectstack/mcp exec vitest run
src/mcp-coded-refusal.test.ts -t 31270`,预期 5 个用例全部通过。
- 需要你做的:无,席位落地。
## What changes
The record resource's catch (`MCPServerRuntime.bridgeResources`,
`packages/mcp/src/mcp-server-runtime.ts`) answered a throw with no
`code` as `{ error: THROWN_MESSAGE }`. It now reads the throw the way
the six record data tools have since #31218. That read is
`dataDoorAnswerOf` in `mcp-http-tools.ts`, which asks the `/data` door's
own `mapDataError`. Its one change is the `export` keyword (seat
amendment). It is not on `packages/mcp/src/index.ts`, so nothing
published moves, and there is no copy of the classification. A 5xx is
logged once through `logServerFault` (`[5xx] 500 resources/read
objectstack://objects/OBJ/records/ID — …`), the same line the data tools
print with their own coordinates.
The body keeps the #23509 members `{ error, code, status }`; only their
values change on a code-less throw, and `code` and `status` can now
appear there:
| Throw (code-less unless stated) | Before | Now | `GET
/api/v1/data/OBJ/ID` (`mapDataError`) |
|---|---|---|---|
| sandboxed hook abort (`SandboxError`, `innerMessage`) | `{ error:
"hook 'demo_guard' threw: Error: Not enough leave" }` | `{ error: 'Not
enough leave', code: 'HOOK_ABORTED', status: 400 }` | `400 HOOK_ABORTED`
`Not enough leave` |
| bare `Error('socket hang up')` | `{ error: 'socket hang up' }` | `{
error: 'Internal server error', code: 'INTERNAL_ERROR', status: 500 }`,
logged once | `500 INTERNAL_ERROR` `Internal server error` |
| sandboxed CRASH, no code | `{ error: "hook 'demo_guard' threw:
TypeError: boom" }` | `{ error: 'Internal server error', code:
'INTERNAL_ERROR', status: 500 }`, logged once | same |
| sandboxed CRASH with a declared code (the #31269 shape) | `{ error:
"hook 'demo_guard' threw: TypeError: boom" }` (since #31269, = the
code-less crash) | the same sanitised 500: this closes #31269's
deviation on this face | same |
| CONTROL: coded sandboxed refusal | `{ error: 'Locked.', code:
'RECORD_LOCKED', status: 409 }` | unchanged | `409 RECORD_LOCKED`
`Locked.` |
| CONTROL: coded non-sandbox refusal | its own sentence, code, status |
unchanged | n/a |
| `SQLITE_BUSY` (a coded driver string) | `{ error: 'SQLITE_BUSY:
database is locked', code: 'SQLITE_BUSY' }` | unchanged: it rides the
coded path | `500 DATABASE_ERROR` `Internal data error` |
The `SQLITE_BUSY` row was measured as the order asked. This code-less
fix does not settle it: it is a coded driver string, #31273's subject,
and is left for that card. Every tool's answer is unchanged (measured
before and after on the same probe of 5 throws × 13 tools).
## Tests
`packages/mcp/src/mcp-coded-refusal.test.ts`, the record-resource
describe. The old row "a code-less throw answers `{ error }`
byte-identically" is replaced by a `#31270` describe:
- undeclared sandboxed abort: `{ error: 'Not enough leave', code:
'HOOK_ABORTED', status: 400 }`, equal to `mapDataError`'s answer; no
hook name, no `threw`;
- bare `Error`: the exact sanitised 500 text, equal to the door's; one
`[5xx] 500 resources/read objectstack://objects/task/records/t1 — ` log
line carrying the thrown text;
- sandboxed crash without and with a declared code: the exact sanitised
500; no hook name, no `TypeError`;
- a thrown string and an empty `code`: the door's answer;
- CONTROL: a coded sandboxed refusal keeps `Locked.` / `RECORD_LOCKED` /
409, a coded non-sandbox refusal keeps its own sentence byte for byte,
and neither is logged.
<details><summary>Red first and ablation</summary>
Red first on `7c523aa1` (`main`, with PR #31290), new pins and no fix:
`Tests 4 failed | 93 passed (97)`. The abort, bare `Error`, crash and
string/empty-code rows failed; the CONTROL passed. With the fix: `Tests
97 passed (97)`.
Ablation (`node scripts/ablation-replace.mjs`, `mcp-server-runtime.ts`):
the code-less body `body = { error: door.message, ...(door.code !==
undefined ? { code: door.code } : {}), status: door.status };` was
replaced by one that answers the thrown message again. Output: `ok
mutation landed: anchor 1 -> 0, blob d53fa45 -> 412ab95bb3d0`, then
`Tests 4 failed | 93 passed (97)`, then `ok restored: blob == HEAD
(d53fa45) and git diff HEAD is empty`. The tests import the
package's source, so no `dist/` is involved.
</details>
## Acceptance notes
- `Clause-②: no`. The resource's members are the ones #23509 set, and
the values change on a code-less throw: `code` and `status` now appear
there. The changeset says so for resource readers. `dataDoorAnswerOf` is
exported from its module only; `index.ts` re-exports `mcp-http-tools.js`
by name and does not list it.
- Not touched: the faces of #31271 (`run_action` / `resume_run`), #31272
(metadata tools, `list_actions`) and #31273 (driver string codes).
## Verification
Base: `7c523aa1`; head: `e196ba77`.
- `pnpm --filter @objectstack/mcp test`: `Test Files 60 passed (60)`,
`Tests 826 passed (826)`, lock `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/mcp build` and `typecheck`: exit 0
(`check:test-typecheck`: 53 errors / 8 pinned signatures held).
- `pnpm exec eslint --no-inline-config` on the three touched `.ts`
files: exit 0 (`@objectstack/mcp` has no `lint` script).
<details><summary>Derived gates (node scripts/pm/dispatch-gates.mjs
--commands, 65 commands, all on e196ba7)</summary>
- `node scripts/check-adr-0087-registration.mjs --base origin/main` ::
exit 0
- `node scripts/check-adr-0087-registration.mjs --self-test` :: exit 0
- `node scripts/check-changeset-no-major.mjs --base origin/main` :: exit
0
- `node scripts/check-closing-keyword-parity.mjs` :: exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-adoption.mjs` :: exit 0
- `node scripts/check-comment-mask-adoption.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-corpus.mjs` :: exit 0
- `node scripts/check-dts-emitted.mjs --self-test` :: exit 0
- `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 0
- `node scripts/check-empty-changeset.mjs --self-test` :: exit 0
- `node scripts/check-issue-citations.mjs` :: exit 0
- `node scripts/check-plugin-teardown-shape.mjs` :: exit 0
- `node scripts/check-plugin-teardown-shape.mjs --self-test` :: exit 0
- `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` :: exit 0
- `node scripts/ci/plan-test-core-shards.mjs --self-test` :: exit 0
- `node scripts/ci/prune-turbo-cache.mjs --self-test` :: exit 0
- `node scripts/ci/snapshot-next.mjs --self-test` :: exit 0
- `node scripts/docs-audit/check-affected-docs.mjs` :: exit 0
- `node scripts/docs-audit/check-drift-comment.mjs` :: exit 0
- `node scripts/pm/release-rehearsal-clone.mjs --self-test` :: exit 0
- `node scripts/release-pending-publish.mjs --self-test` :: exit 0
- `pnpm --filter @objectstack/spec run check:duration-unit-keys` :: exit
0
- `pnpm --filter @objectstack/spec run check:error-code-provenance` ::
exit 0
- `pnpm check:adr-symbol-anchors` :: exit 0
- `pnpm check:changeset-gate-self-tests` :: exit 0
- `pnpm check:ci-filter-parity` :: exit 0
- `pnpm check:closed-ceremony-text` :: exit 0
- `pnpm check:cross-package-test-inputs` :: exit 0
- `pnpm check:dispatcher-error-vocabulary` :: exit 0
- `pnpm check:doc-authoring` :: exit 0
- `pnpm check:driver-memory-census` :: exit 0
- `pnpm check:dts-closure` :: exit 0
- `pnpm check:dual-build-cjs-loads` :: exit 0
- `pnpm check:engine-double-contract` :: exit 0
- `pnpm check:error-status-conformance` :: exit 0
- `pnpm check:gitlink-declared` :: exit 0
- `pnpm check:issue-citations` :: exit 0
- `pnpm check:keyed-text-bounds` :: exit 0
- `pnpm check:lean-entry-closure` :: exit 0
- `pnpm check:logger-receiver-detach` :: exit 0
- `pnpm check:nul-bytes` :: exit 0
- `pnpm check:object-def-param-keys` :: exit 0
- `pnpm check:objectql-double-limit` :: exit 0
- `pnpm check:optional-error-sink` :: exit 0
- `pnpm check:org-identifier` :: exit 0
- `pnpm check:page-declaration-shape` :: exit 0
- `pnpm check:platform-object-tenancy-census` :: exit 0
- `pnpm check:published-files` :: exit 0
- `pnpm check:query-options-erasure` :: exit 0
- `pnpm check:refd-timer-probe` :: exit 0
- `pnpm check:registry-log-declared` :: exit 0
- `pnpm check:rest-log-spy-declared` :: exit 0
- `pnpm check:settings-bind-window` :: exit 0
- `pnpm check:slot-lookup` :: exit 0
- `pnpm check:sourcemap-no-sources-content` :: exit 0
- `pnpm check:system-context-census` :: exit 0
- `pnpm check:test-source-alias` :: exit 0
- `pnpm check:tier-file-adoption` :: exit 0
- `pnpm check:type-check-coverage` :: exit 0
- `pnpm check:type-check-debt` :: exit 0
- `pnpm check:undeclared-dep-imports` :: exit 0
- `pnpm check:vouch-for-reference-callers` :: exit 0
- `pnpm check:watch-hint-literal` :: exit 0
- `pnpm check:where-matcher` :: exit 0
`node scripts/pm/dispatch-gates.mjs --ran`: `65 derived famil(ies)
accounted for — 65 run, 0 NOT-MEASURED`. `check:dual-build-cjs-loads`
and `check:lean-entry-closure` first exited 3 (PREREQUISITE NOT MET:
built entry points missing). Both exited 0 when re-run after
`check:type-check-debt` had built every package.
Not run locally (CI's): `test:repo`, the dogfood shards, the type-check
lanes.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the touched
files.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5oWCPWH6i2Lsts9cdP
Co-authored-by: Claude <noreply@anthropic.com>1 parent e669735 commit 95024e6
4 files changed
Lines changed: 132 additions & 21 deletions
File tree
- .changeset
- packages/mcp/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
564 | 564 | | |
565 | 565 | | |
566 | 566 | | |
567 | | - | |
568 | | - | |
569 | | - | |
570 | | - | |
571 | | - | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
572 | 585 | | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
573 | 645 | | |
574 | 646 | | |
575 | 647 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
535 | 535 | | |
536 | 536 | | |
537 | 537 | | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
538 | 541 | | |
539 | | - | |
| 542 | + | |
540 | 543 | | |
541 | 544 | | |
542 | 545 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
1555 | 1556 | | |
1556 | 1557 | | |
1557 | 1558 | | |
1558 | | - | |
1559 | 1559 | | |
1560 | 1560 | | |
1561 | 1561 | | |
1562 | 1562 | | |
1563 | 1563 | | |
1564 | 1564 | | |
1565 | | - | |
1566 | | - | |
1567 | | - | |
| 1565 | + | |
| 1566 | + | |
1568 | 1567 | | |
1569 | 1568 | | |
1570 | 1569 | | |
1571 | 1570 | | |
1572 | 1571 | | |
| 1572 | + | |
| 1573 | + | |
| 1574 | + | |
| 1575 | + | |
| 1576 | + | |
| 1577 | + | |
| 1578 | + | |
| 1579 | + | |
| 1580 | + | |
1573 | 1581 | | |
| 1582 | + | |
| 1583 | + | |
| 1584 | + | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
| 1593 | + | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
1574 | 1601 | | |
1575 | 1602 | | |
1576 | | - | |
| 1603 | + | |
1577 | 1604 | | |
1578 | | - | |
1579 | | - | |
1580 | | - | |
1581 | | - | |
1582 | | - | |
1583 | | - | |
1584 | | - | |
1585 | | - | |
1586 | | - | |
| 1605 | + | |
1587 | 1606 | | |
1588 | 1607 | | |
1589 | 1608 | | |
| |||
0 commit comments