Repository navigation
feat(mcp)!: delete_record refuses a call without confirm: true - #30861
Conversation
The MCP delete_record tool takes run_action's confirmation gate: a call that does not carry the confirmation member as the boolean true is refused with ACTION_CONFIRMATION_REQUIRED (428) and the run_action refusal's details shape, before any bridge is reached, so nothing is deleted. The member is declared on the tool schema (optional, so the handler answers the coded refusal) and the description tells the agent to ask the human first and then retry. Both transports gate: they register their tools through wireBridgeTools. Tests that drove delete_record to reach the bridge now send confirm: true. Claude-Session: https://claude.ai/code/session_016WV4czsSxuhcKFSWS5tLdW Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czsSxuhcKFSWS5tLdW Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czsSxuhcKFSWS5tLdW Co-authored-by: Claude <noreply@anthropic.com>
…confirm: true Claude-Session: https://claude.ai/code/session_016WV4czsSxuhcKFSWS5tLdW Co-authored-by: Claude <noreply@anthropic.com>
…s in the pinned ledger check:engine-double-contract asks for the new file's pinned delete/findOne/update doubles to be recorded (generated by --write; no seam lost). Claim-Session: session_016WV4czsSxuhcKFSWS5tLdW Claude-Session: https://claude.ai/code/session_016WV4czsSxuhcKFSWS5tLdW Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 83a6a4070c3b3767b7a4cd48c9d5438612285556 && git checkout 83a6a4070c3b3767b7a4cd48c9d5438612285556
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a2f523aa36155385da59b2ef3cbdf8ee336747d5 273858232db5774419d8d4a267d5e0dc208ec2c2 && git checkout -B drift-repro a2f523aa36155385da59b2ef3cbdf8ee336747d5 && git merge --no-ff 273858232db5774419d8d4a267d5e0dc208ec2c2
node scripts/docs-audit/affected-docs.mjs --json a2f523aa36155385da59b2ef3cbdf8ee336747d5
|
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Contract review for card #30813 · narrowing face ( Contract reviewServed-tier: Inputs: card #30813 body and all five comments (triage 6677209837, claims 6677602473 / 6677666991 / 6679397812, os-dev-report 6679368164); PR #30861 body, its 17-file list and the net diff at the head; the head's check-runs as read on 2026-11-06. No governed path is in the file list ( Check-runs on the head, as read: 42 runs, 13 success, 7 skipped, 22 in_progress, 0 failure. Green: Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Check PR Size, Check Documentation Links, Flag docs affected, Plan Test Core shards, Dogfood Regression Gate (planner), Auto Label, filter. Still running: Build Core, Build Docs, Build UI Deps, Test Core 1/8 to 8/8, the four Type Check lanes, the six Lint lanes (the ADR-0087 and provenance gates ride there), Live E2E UI. Their conclusions are the gate verdicts; this record judges the tree and does not pre-empt them. ① Derived judgmentsAccept-set and public-surface changes, each judged on the tree:
② Semver level
③ Boundary flags
④ Design judgmentThe narrowing against the published spec on main. The contract ( Spec hit 1, Spec hit 2, Trying to break it:
No must-fix found. Should-fix: the non-boolean refusal-shape sentence (changeset Now bullet, ai-service.ts second-gate paragraph), and at the seat, the Implemented-by: VERDICT: PASS |
…oolean confirm is refused by the input schema The action-confirmation contract pin asserted ACTION_CONFIRMATION_REQUIRED is listed under no package but @objectstack/runtime; the MCP delete_record gate is its second emitter (the provenance row this PR adds), so the pin now names exactly that one, and a further emitter still moves it (Test Core 2/8 on 36030ea). Contract review should-fix: the changeset's Now bullet and the contract's "second gate" paragraph said the string "true" (and 1) answer the 428 ACTION_CONFIRMATION_REQUIRED envelope. They do not: the tool's closed input schema refuses a non-boolean member first, with no code and no details, exactly as on run_action. Only an absent or false member reaches the gate. Claude-Session: https://claude.ai/code/session_016WV4czsSxuhcKFSWS5tLdW Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 维护者速读
席位意见:ACCEPT(#30813)。 席位核对Head-sha:
|
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Maintainer's per-PR word (BREAKING) · 2026-11-06T15:01Z Elon chose 「同意落地」 on the seat's decision card in the server seat 4 thread, 2026-11-06T14:59Z, for this PR at head |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42239989142 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Brings in #30861 (delete_record confirm) and the rest of main before the draft PR opens. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyqd3A7fbCWrqM3KCz
Requested by Elon · project thread
domain:server#4 · os-elon-musk · session_016WV4czsSxuhcKFSWS5tLdW
Fixes #30813
Clause-②: yes (narrowing) — the MCP
delete_recordtool refuses every call that does not carryconfirm: true, where it deleted the record. The confirmation contract in@objectstack/spec/contracts(AIActionConfirmation,ActionConfirmationRequiredDetails) gains its second door in its docblock, and the error-code ledger listsACTION_CONFIRMATION_REQUIREDunder@objectstack/mcpas a second emitter; no shape changes.Metadata-protocol: none
Skills-docs: #30863 (skills/objectstack-ai/SKILL.md L202) and #30865 (the Console mirror ui/apps/console/src/pages/developer/objectstack-skill.ts), both parked behind this PR
Claim: 6677666991 · Branch:
claude/project-thread-o7rpxo· Ruling: card body, maintainer 「同意」 twice (option B) · Triage: 6677209837维护者速读
delete_record工具现在每次调用都必须带confirm: true才删除;不带(或带false、字符串"true")直接拒绝,返回与run_action完全相同的 428ACTION_CONFIRMATION_REQUIRED(details同形,actionName写delete_record),在到达数据桥之前就拒,什么都不读不删。工具 schema 公布confirm,描述里让 agent 先问人再重试。HTTP 与 stdio 两种传输都拦。destructiveHint客户端可以无视;记录正文里植入的指令能诱导 agent 删掉用户有权删的数据而无人过问。按卡片裁决选项 B(维护者两次「同意」)。confirm之前都会被拒;已能处理run_action这条拒绝的客户端零改动。RESTDELETE /api/v1/data/...与update_record不受影响。confirm只是调用方的声明,门让「忘了问」变响,不证明真有人类。回滚即撤 PR。packages/mcp/src/mcp-http-tools.ts(recordDeleteConfirmationRefusal与delete_record的注册处),packages/mcp/src/mcp-delete-record-confirmation.test.ts,.changeset/30813-mcp-delete-record-confirmation.md。pnpm --filter @objectstack/mcp exec vitest run src/mcp-delete-record-confirmation.test.ts,预期 9 条全绿:未确认被拒且记录仍在、confirm: true删除、拒绝形状与run_action相同、stdio 长连接服务器同样拦、update_record不受门约束。What changes
@objectstack/mcp—delete_record(src/mcp-http-tools.ts,registerObjectTools)AI_ACTION_CONFIRMATION_MEMBERasrun_actiondeclares it:confirm, an optional boolean. Optional on purpose: a required member would refuse an unconfirmed call as a bare input-validation error with no code and no retry details. Before this change a call carryingconfirmwas refused as an unknown key by the strict input schema.recordDeleteConfirmationRefusal(objectName, recordId, args)after the system-object guard and BEFORE the bridge. A call whose member is not the booleantrueanswerscodedErrorResult({ code: 'ACTION_CONFIRMATION_REQUIRED', message, status: 428, details: { actionName: 'delete_record', objectName, confirmationMember: 'confirm' } }), the rendererrun_action's refusal goes through (errorResultFromThrownends in it), so the envelope keys and their order are the same. No bridge member is called on a refused call.ErrorCode(@objectstack/spec/api), so a misspelling does not compile;detailsis typedActionConfirmationRequiredDetails.wireBridgeTools→registerObjectTools.src/skill-md.ts(the MCP-servedSKILL.md): thedelete_recordline teachesconfirmand the refusal.README.md: one tool-list comment.@objectstack/spec— comments, plus one provenance row:contracts/ai-service.ts: the member's docblock names the third request shape (thedelete_recordinput);AIActionConfirmationgains "The second gate: the MCPdelete_recordtool" (unconditional, before the bridge, same refusal,actionNameis the tool name, and the four bounds);ActionConfirmationRequiredDetails.actionNamesays what it holds on that door. No type changes.api/error-code-ledger.zod.ts: the@objectstack/runtimerow's comment names the second emitter, and@objectstack/mcplistsACTION_CONFIRMATION_REQUIRED(check:error-code-provenancerequires a row under the stamping package). The code was already registered, so the vocabulary is unchanged.Docs —
content/docs/ai/connect-mcp.mdx, "The security model": one bullet sayingdelete_recordneedsconfirm: trueon every call, what the refusal is, and that RESTDELETEandupdate_recordtake none.Changeset —
.changeset/30813-mcp-delete-record-confirmation.md,patchfor@objectstack/mcpand@objectstack/spec, BREAKING banner, both declaration lines, the ADR-0087 disposition, Before / Now / Who moves / Remedy, and the four bounds (REST outside,update_recordnot gated, no per-object exemption, a future bulk-delete tool takes the same gate).Tests
packages/mcp/src/mcp-delete-record-confirmation.test.ts(9), through the HTTP/mcptransport against the real stdio data bridge on an engine double that holds the row: the member is advertised as an optional boolean and the description says to ask the human; an unconfirmed call is refused with code, status 428 and the exactdetails, the engine'sdeleteis never called and the row is intact;false,"true"and1are refused and nothing is deleted;confirm: truedeletes once; the refusal's code, status, envelope keys anddetailskeys equalrun_action's served through the same door;update_recordwrites without the member (control); the long-lived server (the stdio transport's) refuses and then deletes on the retry, over an in-memory MCP client.packages/qa/dogfood/test/mcp-delete-record-confirmation.dogfood.test.ts(4), on a booted showcase withMCPServerPlugin, one admin caller: unconfirmeddelete_recordrefused and the record still reads200over REST; the refusal held against the REAL runtime gate's refusal (run_actiononshowcase_portfolio_snapshot, flaggedai.requiresConfirmation: truein a derived configuration) through the same transport: same code, status, envelope keys, confirmation member, and everydetailskey the action refusal carries;confirm: truedeletes (REST then reads404); RESTDELETE /data/:object/:idwith no confirmation deletes (control).confirm: true):mcp-coded-refusal.test.ts,mcp-http-tools.unknown-argument-keys.test.ts(the sweep's VALID_ARGS),mcp-server-runtime.http.test.ts,mcp-unique-violation-wire-code.test.ts,mcp-update-delete-record-stale-summary.test.ts(8 tests, 5 files), and dogfoodmcp-stdio-package-object-discovery.test.ts,mcp-stdio-runtime-object-exposure.test.ts.Results: the new MCP file 9/9 green (5 red on
main),@objectstack/mcp57 files / 689 tests green, the touched dogfood files 3 files / 94 tests green, the full dogfood battery 424 passed and 3 skipped of 427 files (3537 passed and 41 skipped of 3578 tests) on head36030eabba.Acceptance notes
run_action's gate is the LAST pre-dispatch check; this one comes before the bridge's exposure, credential, permission and not-found refusals. So an unconfirmed delete of an object whose API excludesdeleteanswers428first and the405only after the human confirms. Accepted because the requirement is unconditional and advertised in the schema and description, so a well-behaved agent sends the member on its first call; moving it behind those checks would mean gating inside each bridge.details.actionNameisdelete_record. The details shape has a requiredactionNameand no other slot for the door; the tool's own name is what the refused call addressed, and the contract docblock now says so. No key was added.@objectstack/runtimeand@objectstack/mcpdo not depend on each other, soactionConfirmationRefusalis out of reach. One shape is held by the contract types (AI_ACTION_CONFIRMATION_MEMBER,ActionConfirmationRequiredDetails,ErrorCode), the shared renderercodedErrorResult, and the dogfood file serving both refusals to one caller.patch, where the card and the dispatch saidminor.Metadata-protocol: nonegrades every introduced entrypatch(AGENTS.md Post-Task Checklist step 3;check-changeset-no-major.mjsrefusesminorbesidenone). An MCP tool's input is not something a metadata author writes, sononeis the kind the claim declared. The BREAKING banner and the ADR-0087 disposition stay.FROM → TOlabel the ADR-0087 gate reads the body as a ledger prescription and refusesnot-required (no-migration-prescription)beside it. Registering a ledger entry instead would put a client tool-call rewrite into the ledger thatobjectstack migrate meta,spec-changes.jsonand the upgrade guide project, which carry metadata migrations only. The migration ships as Before / Now / Who moves / The remedy prose, the shape stock narrowing changesets use (29236-meta-list-names-filter.md).packages/mcp/src/skill-md.tsandpackages/mcp/README.md(they teach the tool, and this PR made theirdelete_recordlines false), the two stale dogfood fixtures and the new dogfood file, andscripts/engine-double-contract.pinned.json(gate-forced:check:engine-double-contractasks for the new test file's pinned doubles to be recorded; generated by--write, three rows added, none lost).skills/objectstack-ai/SKILL.mdL202 (Tier H; theSkills-docs:follow-up) and the Console's mirror of the MCP skill,ui/apps/console/src/pages/developer/objectstack-skill.tsL98, L109, L117, which still teachesdelete_record({ objectName, recordId }); an agent following either gets the428with its retry details.mainthe threeit.eachrefusal cases were already green, for another reason: the strict input schema refusedconfirmas an unknown key. The other five pins were red onmain(5 failed, 4 passed with theupdate_recordcontrol); they are what this change turns green.data:deletescope split, a record-bound confirmation token, MCP elicitation.Verification
Head
36030eabba(branch merged withorigin/mainat8f601501f2). Every command below ran in the worktree underNODE_OPTIONS=--max-old-space-size=4096; tests and builds ran underscripts/pm/os-verify-lock.sh.Red on main, then green: the new pins
Against the
mainsource (the gate reverted),pnpm --filter @objectstack/mcp exec vitest run src/mcp-delete-record-confirmation.test.ts:On this head: 9 passed (9).
Package suite, typecheck, build
Dogfood: the full battery, once, on the final head
pnpm --filter @objectstack/dogfood test(no shard, no path):The new and the two touched files, verbose:
mcp-delete-record-confirmation.dogfood.test.ts4/4,mcp-stdio-package-object-discovery.test.tsandmcp-stdio-runtime-object-exposure.test.tsgreen; 3 files / 94 tests passed.Local gates: 112 derived, 112 run, all exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 104 commands at7723700f04; all 104 ran there and 103 exited 0.check:engine-double-contractexited 1 asking for the new test file's pinned doubles to be recorded;node scripts/check-engine-double-contract.mjs --writerecorded them (36030eabba). That path added 8 families (check:agent-test-spelling,check:bash32-floor,check:cli-command-ids,check:entry-guard,check:parse-guard,check:pnpm-filter-targets,check:pnpm-flag-spellings,check:scripts-symbol-anchors); those 8 andcheck:engine-double-contractran at36030eabba, all exit 0.Not measured locally, CI's to answer: the four value-bearing families (
check-issue-citations --census, the two dogfood shard attestations,check-test-completenesson CI's log), the path-scheduled CI jobs and the type-check lanes beyond the three package typechecks above.Generated by Claude Code