Skip to content

feat(mcp)!: delete_record refuses a call without confirm: true - #30861

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-o7rpxo
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-o7rpxo

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#4 · os-elon-musk · session_016WV4czsSxuhcKFSWS5tLdW

Fixes #30813

Clause-②: yes (narrowing) — the MCP delete_record tool refuses every call that does not carry confirm: 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 lists ACTION_CONFIRMATION_REQUIRED under @objectstack/mcp as 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

维护者速读

  • 改了什么:MCP 的 delete_record 工具现在每次调用都必须带 confirm: true 才删除;不带(或带 false、字符串 "true")直接拒绝,返回与 run_action 完全相同的 428 ACTION_CONFIRMATION_REQUIRED(details 同形,actionName 写 delete_record),在到达数据桥之前就拒,什么都不读不删。工具 schema 公布 confirm,描述里让 agent 先问人再重试。HTTP 与 stdio 两种传输都拦。
  • 为什么改:原来 agent 一次调用就能永久硬删一条记录,唯一的提示 destructiveHint 客户端可以无视;记录正文里植入的指令能诱导 agent 删掉用户有权删的数据而无人过问。按卡片裁决选项 B(维护者两次「同意」)。
  • 风险与回滚:BREAKING,所有经 MCP 删除的客户端在补上 confirm 之前都会被拒;已能处理 run_action 这条拒绝的客户端零改动。REST DELETE /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 不受门约束。
  • 需要你做的:对这条 BREAKING 改动给出本 PR 的一句同意(契约复审由席位做)。

What changes

@objectstack/mcp — delete_record (src/mcp-http-tools.ts, registerObjectTools)

  • The tool's input declares the confirmation member, keyed off AI_ACTION_CONFIRMATION_MEMBER as run_action declares 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 carrying confirm was refused as an unknown key by the strict input schema.
  • The handler asks recordDeleteConfirmationRefusal(objectName, recordId, args) after the system-object guard and BEFORE the bridge. A call whose member is not the boolean true answers codedErrorResult({ code: 'ACTION_CONFIRMATION_REQUIRED', message, status: 428, details: { actionName: 'delete_record', objectName, confirmationMember: 'confirm' } }), the renderer run_action's refusal goes through (errorResultFromThrown ends in it), so the envelope keys and their order are the same. No bridge member is called on a refused call.
  • The code constant is typed ErrorCode (@objectstack/spec/api), so a misspelling does not compile; details is typed ActionConfirmationRequiredDetails.
  • The description: "Delete a record by id. This is destructive and permanent. A call is REFUSED unless it also sends confirm: true — ask the human first, then retry; nothing is deleted on a refused call."
  • Both transports gate: the HTTP door's per-request server and the long-lived server the stdio transport serves both register through wireBridgeTools → registerObjectTools.
  • src/skill-md.ts (the MCP-served SKILL.md): the delete_record line teaches confirm and 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 (the delete_record input); AIActionConfirmation gains "The second gate: the MCP delete_record tool" (unconditional, before the bridge, same refusal, actionName is the tool name, and the four bounds); ActionConfirmationRequiredDetails.actionName says what it holds on that door. No type changes.
  • api/error-code-ledger.zod.ts: the @objectstack/runtime row's comment names the second emitter, and @objectstack/mcp lists ACTION_CONFIRMATION_REQUIRED (check:error-code-provenance requires 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 saying delete_record needs confirm: true on every call, what the refusal is, and that REST DELETE and update_record take none.

Changeset — .changeset/30813-mcp-delete-record-confirmation.md, patch for @objectstack/mcp and @objectstack/spec, BREAKING banner, both declaration lines, the ADR-0087 disposition, Before / Now / Who moves / Remedy, and the four bounds (REST outside, update_record not gated, no per-object exemption, a future bulk-delete tool takes the same gate).

Tests

  • New packages/mcp/src/mcp-delete-record-confirmation.test.ts (9), through the HTTP /mcp transport 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 exact details, the engine's delete is never called and the row is intact; false, "true" and 1 are refused and nothing is deleted; confirm: true deletes once; the refusal's code, status, envelope keys and details keys equal run_action's served through the same door; update_record writes 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.
  • New packages/qa/dogfood/test/mcp-delete-record-confirmation.dogfood.test.ts (4), on a booted showcase with MCPServerPlugin, one admin caller: unconfirmed delete_record refused and the record still reads 200 over REST; the refusal held against the REAL runtime gate's refusal (run_action on showcase_portfolio_snapshot, flagged ai.requiresConfirmation: true in a derived configuration) through the same transport: same code, status, envelope keys, confirmation member, and every details key the action refusal carries; confirm: true deletes (REST then reads 404); REST DELETE /data/:object/:id with no confirmation deletes (control).
  • Stale fixtures, same PR (calls that pin a gate or behaviour behind the confirmation gate now send 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 dogfood mcp-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/mcp 57 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 head 36030eabba.

Acceptance notes

  • Where the gate sits, and the order it answers in. In the tool, after the system-object guard and before the bridge, so it holds whichever bridge serves the server (the runtime's, the stdio one, a host's own). 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 excludes delete answers 428 first and the 405 only 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.actionName is delete_record. The details shape has a required actionName and 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.
  • No shared builder with the runtime. @objectstack/runtime and @objectstack/mcp do not depend on each other, so actionConfirmationRefusal is out of reach. One shape is held by the contract types (AI_ACTION_CONFIRMATION_MEMBER, ActionConfirmationRequiredDetails, ErrorCode), the shared renderer codedErrorResult, and the dogfood file serving both refusals to one caller.
  • Changeset level patch, where the card and the dispatch said minor. Metadata-protocol: none grades every introduced entry patch (AGENTS.md Post-Task Checklist step 3; check-changeset-no-major.mjs refuses minor beside none). An MCP tool's input is not something a metadata author writes, so none is the kind the claim declared. The BREAKING banner and the ADR-0087 disposition stay.
  • No literal FROM → TO block, where the dispatch asked for one. Measured: with a FROM → TO label the ADR-0087 gate reads the body as a ledger prescription and refuses not-required (no-migration-prescription) beside it. Registering a ledger entry instead would put a client tool-call rewrite into the ledger that objectstack migrate meta, spec-changes.json and 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).
  • Files beyond the claim's listed surface, reported to the seat: packages/mcp/src/skill-md.ts and packages/mcp/README.md (they teach the tool, and this PR made their delete_record lines false), the two stale dogfood fixtures and the new dogfood file, and scripts/engine-double-contract.pinned.json (gate-forced: check:engine-double-contract asks for the new test file's pinned doubles to be recorded; generated by --write, three rows added, none lost).
  • Not edited, outside the claim's surface: skills/objectstack-ai/SKILL.md L202 (Tier H; the Skills-docs: follow-up) and the Console's mirror of the MCP skill, ui/apps/console/src/pages/developer/objectstack-skill.ts L98, L109, L117, which still teaches delete_record({ objectName, recordId }); an agent following either gets the 428 with its retry details.
  • On main the three it.each refusal cases were already green, for another reason: the strict input schema refused confirm as an unknown key. The other five pins were red on main (5 failed, 4 passed with the update_record control); they are what this change turns green.
  • Deferred and not built, per the card: the data:delete scope split, a record-bound confirmation token, MCP elicitation.

Verification

Head 36030eabba (branch merged with origin/main at 8f601501f2). Every command below ran in the worktree under NODE_OPTIONS=--max-old-space-size=4096; tests and builds ran under scripts/pm/os-verify-lock.sh.

Red on main, then green: the new pins

Against the main source (the gate reverted), pnpm --filter @objectstack/mcp exec vitest run src/mcp-delete-record-confirmation.test.ts:

     × advertises the confirmation member on the tool schema, so a model can discover the retry
     × refuses a call without the member: code, status and the retry details; the record is intact
     × deletes on the retry with confirm: true, once
     × answers with run_action's refusal shape: the same code, status, envelope keys and details keys
     × the long-lived server the stdio transport serves gates the same way
 Test Files  1 failed (1)
      Tests  5 failed | 4 passed (9)

On this head: 9 passed (9).

Package suite, typecheck, build
pnpm --filter @objectstack/mcp test                  Test Files 57 passed (57) · Tests 689 passed (689) · VERDICT command-exit 0
pnpm --filter @objectstack/mcp run typecheck         exit 0
pnpm --filter @objectstack/mcp run check:test-typecheck   exit 0 (6 files / 53 errors / 8 pinned signatures held in the debt ledger, unchanged)
pnpm --filter @objectstack/dogfood run typecheck     exit 0
pnpm --filter @objectstack/spec exec tsc --noEmit    exit 0
pnpm exec turbo run build --filter=!@objectstack/docs --concurrency=2   Tasks 118 successful, 118 total · VERDICT command-exit 0
pnpm --filter @objectstack/spec check:generated      All 14 generated artifacts are up to date
Dogfood: the full battery, once, on the final head

pnpm --filter @objectstack/dogfood test (no shard, no path):

 Test Files  424 passed | 3 skipped (427)
      Tests  3537 passed | 41 skipped (3578)
os-verify-lock: VERDICT command-exit 0 · held the lock 1643s (27m23s) · waited 0s

The new and the two touched files, verbose: mcp-delete-record-confirmation.dogfood.test.ts 4/4, mcp-stdio-package-object-discovery.test.ts and mcp-stdio-runtime-object-exposure.test.ts green; 3 files / 94 tests passed.

Local gates: 112 derived, 112 run, all exit 0

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 104 commands at 7723700f04; all 104 ran there and 103 exited 0. check:engine-double-contract exited 1 asking for the new test file's pinned doubles to be recorded; node scripts/check-engine-double-contract.mjs --write recorded 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 and check:engine-double-contract ran at 36030eabba, all exit 0.

✓ dispatch-gates --ran: 112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED (a DERIVED zero — all 112 recorded an exit code and none of them is 3).
✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.
    .changeset/30813-mcp-delete-record-confirmation.md  [BREAKING+bang+clause-②-narrowing]  not-required (no-migration-prescription)

Not measured locally, CI's to answer: the four value-bearing families (check-issue-citations --census, the two dogfood shard attestations, check-test-completeness on CI's log), the path-scheduled CI jobs and the type-check lanes beyond the three package typechecks above.


Generated by Claude Code

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>
…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>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/mcp, @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/mcp/README.md, packages/mcp/src/skill-md.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via ACTION_CONFIRMATION_REQUIRED (symbol, a top-level const object), ACTION_CONFIRMATION_REQUIRED (literal, a string literal in ACTION_CONFIRMATION_REQUIRED; a string literal in ERROR_CODE_LEDGER), delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/index.mdx (via delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/automation/flows.mdx (via delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/deployment/cli.mdx (via delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/frontend/layout.mdx (via delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object), delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/releases/v17/17-1.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-2.mdx (via delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/releases/v17/17-4.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-5.mdx (via ACTION_CONFIRMATION_REQUIRED (symbol, a top-level const object), ACTION_CONFIRMATION_REQUIRED (literal, a string literal in ACTION_CONFIRMATION_REQUIRED; a string literal in ERROR_CODE_LEDGER), delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER))
  • content/docs/releases/v17/17-7.mdx (via delete_record (literal, a string literal in DELETE_RECORD_TOOL; a string literal in a comment in ERROR_CODE_LEDGER), /api/v1/data/... (route, a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/mcp/README.md, packages/mcp/src/skill-md.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a2f523aa36155385da59b2ef3cbdf8ee336747d5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 83a6a4070c3b3767b7a4cd48c9d5438612285556 — the merge of head 273858232db5774419d8d4a267d5e0dc208ec2c2 into base a2f523aa36155385da59b2ef3cbdf8ee336747d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a2f523aa36155385da59b2ef3cbdf8ee336747d5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

Contract review for card #30813 · narrowing face (Clause-②: yes (narrowing)) plus two packages/spec/src/** hits · read-only reviewer at the contract-review tier · 2026-11-06T07:21Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 36030eabba1430e89bb9172865317568e8b8d1fd
Local-runs: none
Scope: .changeset/30813-mcp-delete-record-confirmation.md, packages/mcp/src/mcp-http-tools.ts, packages/mcp/src/skill-md.ts, packages/mcp/src/mcp-delete-record-confirmation.test.ts, packages/mcp/src/mcp-coded-refusal.test.ts, packages/mcp/src/mcp-http-tools.unknown-argument-keys.test.ts, packages/mcp/src/mcp-server-runtime.http.test.ts, packages/mcp/src/mcp-unique-violation-wire-code.test.ts, packages/mcp/src/mcp-update-delete-record-stale-summary.test.ts, packages/qa/dogfood/test/mcp-delete-record-confirmation.dogfood.test.ts, packages/qa/dogfood/test/mcp-stdio-package-object-discovery.test.ts, packages/qa/dogfood/test/mcp-stdio-runtime-object-exposure.test.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/contracts/ai-service.ts, scripts/engine-double-contract.pinned.json + packages/runtime/src/action-execution.ts (actionConfirmationRefusal, its call site), packages/mcp/src/stdio-data-bridge.ts (remove), packages/mcp/src/mcp-server-runtime.ts (bridgeDataTools, the per-request HTTP server), packages/runtime/src/domains/mcp.ts (the runtime's own bridge), packages/spec/scripts/check-error-code-provenance.ts, scripts/check-engine-double-contract.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, packages/runtime/CHANGELOG.md 17.5.0 (the consumed precedent .changeset/action-confirmation-gate-enforced.md), .claude/skills/pm-dispatch/references/lanes/spec.md, scripts/pm/check-governed-merges.mjs, skills/objectstack-ai/SKILL.md L202, ui/apps/console/src/pages/developer/objectstack-skill.ts L98-L117. Prose faces read, not judged: content/docs/ai/connect-mcp.mdx, packages/mcp/README.md.

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 (GOVERNED_SURFACES register: none of the 17 paths match), so this record is owed by the narrowing declaration and the two packages/spec/src/** hits, not by a tier.

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 judgments

Accept-set and public-surface changes, each judged on the tree:

  1. delete_record refuses without confirm: true, on both transports, nothing deleted. Right. The gate is recordDeleteConfirmationRefusal(objectName, recordId, args) in registerObjectTools (mcp-http-tools.ts), called after the system-object guard and before bridge.remove, so a refused call never reaches resolvePrincipal, enforceApiExposure, findById or engine.delete in stdio-data-bridge.ts remove (L1146-L1166 on main), nor the runtime's own bridge remove in packages/runtime/src/domains/mcp.ts L822. Both transports register through one composition: the per-request HTTP server calls wireBridgeTools(server, opts.bridge, ...) (mcp-server-runtime.ts L1951) and the long-lived server the stdio transport attaches to calls it from bridgeDataTools (L1398); wireBridgeTools calls registerObjectTools (L828). No other site registers a record-delete tool (the only registerTool outside mcp-http-tools.ts is the AI-service ToolRegistry bridge at mcp-server-runtime.ts L1120, a different surface). Pinned: the HTTP door against the real stdio bridge on an engine double that holds the row (engine.delete never called, row intact), and the long-lived server over an in-memory client; dogfood pins it on a booted showcase with the record read back 200 over REST.

  2. Refusal shape reuses run_action's. Right. Same code (ACTION_CONFIRMATION_REQUIRED, typed ErrorCode so a misspelling fails to compile), same status 428, same details shape ActionConfirmationRequiredDetails (actionName, objectName, confirmationMember echoed off AI_ACTION_CONFIRMATION_MEMBER), rendered by the same codedErrorResult that errorResultFromThrown ends in for run_action, so the envelope keys and their order match. Pinned in-package against a double of the runtime gate's throw and in dogfood against the real runtime gate (run_action on showcase_portfolio_snapshot flagged ai.requiresConfirmation: true): same code, status, envelope keys and every details key the action refusal carries. details.actionName is the tool's own name; the contract docblock now says so. No shared builder with @objectstack/runtime: the two packages do not depend on each other, so the contract types and the shared renderer are what hold one shape. Accepted.

  3. confirm is a declared member of the tool schema. Right. [AI_ACTION_CONFIRMATION_MEMBER]: z.boolean().optional() inside strictToolInput, keyed off the contract's constant exactly as run_action and resume_run declare it; tools/list advertises it as an optional boolean and required does not carry it (pinned). On main a call carrying confirm was refused as an unknown key by the .strict() wrap; the member and the gate land in one change, which is the run_action precedent's own rule (a gate whose member is stripped is unsatisfiable). Optional on purpose: a required member would answer a bare input-validation error with no code and no retry details before the handler could answer the gate.

  4. No per-object exemption. Right. recordDeleteConfirmationRefusal reads request[confirm] === true and nothing else; no object, flag or principal is consulted. The gate follows the guard so a sys_ object still answers the system-object refusal first; that is the existing posture, not an exemption.

  5. REST DELETE untouched. Right. No file under packages/rest or packages/runtime is in the diff; the dogfood control pins REST DELETE /data/:object/:id deleting with no confirmation.

  6. update_record unchanged. Right. The update_record registration in the diff context is byte-identical to main; the in-package control pins a write without the member.

  7. Gate order. The gate answers before the bridge's exposure, credential, permission and not-found refusals, where run_action's gate is the last pre-dispatch check. So an unconfirmed delete of an object whose API excludes delete, or that the caller may not delete, answers 428 first and the 405/403 only after the human confirms. The dev's acceptance note states this and the reason: gating behind those checks means gating inside every bridge (the stdio one, the runtime's, a host's own). The new contract docblock says "before its data bridge is reached", so the contract is honest about where it sits. Accepted; not a finding.

  8. Stale fixtures. The eight calls in five mcp test files and two dogfood files that pin behaviour BEHIND the gate (coded-refusal rendering of a thrown remove, the unique-violation wire code, the stale-summary earned answer, the unknown-key sweep's VALID_ARGS, exposure refusals on gated objects) now send confirm: true, so each still reaches the gate it pins. Right; without it those pins would go dark or flip.

  9. scripts/engine-double-contract.pinned.json. Three rows for the new test file (delete, findOne, update, each pinned: 1): the test's engine double reaches assertEngineDeleteDispatch, assertEngineUpdateDispatch and assertEngineFindOnePredicate, which is what the gate records. Gate-forced, generated by --write, nothing lost. Right.

② Semver level

  • patch, not minor. Right on the tree, and the card's minor is superseded by the standing rule, not disobeyed. AGENTS.md Post-Task Checklist step 3: "The level follows the kind alone: a kind takes at least minor on a package the diff moves, none takes patch; a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition." check-changeset-no-major.mjs enforces it (its docblock: a none beside any minor is refused whatever the package). Metadata-protocol: none is the right kind: what moves is the accept set of an MCP tool call, and a metadata author writes nothing differently; no spec key, value, type, meaning or default changes. The card's "ship a minor changeset, precedent .changeset/action-confirmation-gate-enforced.md" cites a changeset that shipped under 17.5.0 Minor Changes (packages/runtime/CHANGELOG.md) before the kind-to-level ruling the gate now carries; the precedent is right about content, not about level. Check Changeset is green on the head. The maintainer's per-PR word on the BREAKING change is still owed under the claim and is not this record's to give.
  • BREAKING banner. Present: **BREAKING** accept-set narrowing at the MCP delete_record tool ... in the changeset body.
  • ! title. Present on both carriers: PR title and changeset summary feat(mcp)!: ....
  • Clause-②: yes (narrowing) and Metadata-protocol: none. Both line-initial in the PR body and in the changeset body, identical. The arm is right: a call that succeeded is now refused; that is an accept-set narrowing of a published tool surface.
  • ADR-0087 marker. One HTML-comment marker, adr-0087: not-required (no-migration-prescription), with its reason. Right category: no schema key is added, removed, renamed or re-shaped and nothing at rest records a tool call, so objectstack migrate meta, spec-changes.json and the upgrade guide have nothing to rewrite; registered would put a client call rewrite into a metadata-migration ledger, and runtime-interface-only needs a named exported TS declaration and also inherits the prescription refusal. The gate reads a literal FROM → TO label or arrow rewrite as a migration prescription and refuses this category beside it, so the dev's Before / Now / Who moves / The remedy prose carries the migration (old call, new call, who changes, the one-line fix) without the label. That delivers what the card asked for; the label was a gate artefact. Accepted. The record of the local run names the carriers the gate saw: BREAKING, bang, clause-②-narrowing.

③ Boundary flags

  • Files beyond the claim's listed surface. packages/mcp/src/skill-md.ts and packages/mcp/README.md: both teach delete_record and this PR made their lines false; a PR that narrows a tool and leaves the server-served SKILL.md teaching the old call would be the declared-but-not-taught defect. In surface. The new dogfood file and the two stale dogfood fixtures: tests of the changed behaviour and pins behind it. In surface. scripts/engine-double-contract.pinned.json: gate-forced, judged in ① item 9. In surface. The seat's third claim amendment (6679397812) already records all of them.
  • Ledger provenance row. ACTION_CONFIRMATION_REQUIRED under @objectstack/mcp, plus a comment above the existing @objectstack/runtime entry naming the second emitter. check:error-code-provenance demands that every stamp site of a registered code in a package's non-test source be listed under that package's own owner key or carry a PROVENANCE_WAIVERS entry; recordDeleteConfirmationRefusal stamps the code in @objectstack/mcp, so the row is forced. The ledger's own rule is "listed once per emitting package; provenance, not identity" and the union dedupes, so ErrorCode is unchanged. The spec lane's pre-approved class, read in lanes/spec.md: ① the code's existence was ruled on its owning card (MCP run_action never enforces ai.requiresConfirmation — it is surfaced in list_actions only, while @objectstack/spec guidance tells authors it stops execution #15942 / spec: define the confirmation contract behind action.ai.requiresConfirmation — an unconfirmed run_action on a gated action is refused, and the refusal carries the way to confirm #16293), ② the catch-all is unchanged and the Clause-② path is hit with the contract review before the queue (this record), ③ pure append: no schema shape, no existing entry moved, no ledger mechanism touched, only the appender's own package section. All three hit; the runtime-row comment is the "widen the existing member's comment" path the lane names as the no-new-branch answer. In class; the spec seat's signature is waived by the lane's standing commitment.
  • Out-of-scope findings. skills/objectstack-ai/SKILL.md L202 (Tier H) and ui/apps/console/src/pages/developer/objectstack-skill.ts L98, L109, L117 both still teach delete_record without confirm; an agent following either gets the 428 with the retry in details, so the defect is a stale teaching, not a silent failure. Right not to edit them here (skills/** is Tier H and ui/** is outside the claim). Escalated to the seat: the PR body's Skills-docs: follow-up owed — skills/objectstack-ai/SKILL.md names one file; the owed card must name the Console mirror too, and the seat rewrites owed — FILES to #N before ACCEPT (pm-dispatch SKILL.md). Should-fix at the seat, not in this tree. The Console mirror test (objectstack-skill.test.ts) pins tool presence, not equality with skill-md.ts, so this PR does not red the UI.

④ Design judgment

The narrowing against the published spec on main. The contract (AIActionConfirmation, ai-service.ts) says confirm: true is an attestation on an AI-facing request, that only the boolean true attests, that a refusal is 428 ACTION_CONFIRMATION_REQUIRED with ActionConfirmationRequiredDetails, and that nothing dispatches on a refusal. The runtime gate's docblock (action-execution.ts) says the enforced set is bounded by ai.exposed, REST is outside, and "widening the set is its own decision, not a thing to infer". Card #30813 is that decision, made by the maintainer twice (「同意」, option B), for one tool. The PR makes the spec's own refusal real at a second door and takes no declared capability away: every delete a caller could make on main is still makeable with one member added, and the member, code, status and details are the ones a client already handles for run_action. What changes at the consumer is one refusal it is already built for. That is the right shape for a safety backstop the card itself calls a backstop ("the flag asserts an approval; it does not obtain one"), and the bounds the card decided are stated where they bind: the changeset, the contract docblock and the code comment.

Spec hit 1, contracts/ai-service.ts. Docblock only: the member's placement note names the third request shape, AIActionConfirmation gains "The second gate" naming the door, the unconditional predicate, the bridge-before placement, the shape and the four bounds, and actionName says what it holds on that door. confirm?: boolean and both interfaces are unchanged; no z schema is touched; ErrorCode is unchanged. Holds against its first consumer (recordDeleteConfirmationRefusal) and the existing one (actionConfirmationRefusal).

Spec hit 2, api/error-code-ledger.zod.ts. Pure append of the established "Second EMITTER ... provenance, not identity" class the file already uses for @objectstack/rest, @objectstack/plugin-approvals and others; the right class, judged in ③.

Trying to break it:

  • confirm: "true" (a string), and 1: refused, nothing deleted, as pinned. But the refusal is the closed input schema's type error from the SDK's own validation (a tool error with no code and no details), not the 428 envelope; the handler is never reached. This is exactly run_action's behaviour on the same input (same z.boolean().optional() declaration) and the contract's "a truthy string is a transport artefact" holds. The changeset's Now bullet lists "true" beside absent and false as answering the run_action refusal, and the new contract paragraph says every call not carrying the boolean true gets "the refusal above". Both overclaim the shape for a non-boolean value. Should-fix: one sentence in each saying a non-boolean confirm is refused by the tool's closed input schema with no code, as on run_action, and absent or false answers the 428 envelope. Not a FAIL: the accept set is as decided, nothing is deleted in any case, and the shape is right for the two cases an agent will meet.
  • confirm: false: reaches the handler, === true fails, the 428 envelope with details is answered, bridge untouched. Pinned (error and row intact). Right.
  • confirm present on a stdio call: the long-lived server's delete_record is the same registration, so the strict schema admits the declared member and the gate reads it; pinned over an in-memory client on the server bridgeDataTools wires. Note for the reader: the pin drives runtime.server over InMemoryTransport, not StdioServerTransport; the tool registration and handler are the same object, and the transport frames no arguments differently, so the pin holds what it names.
  • An agent principal (an OAuth-connected agent at its delegator's depth, an API key, a session): the gate is principal-blind and runs before resolvePrincipal() is called in the bridge, so no principal class bypasses it and none is gated differently; the exposure, credential and permission gates behind it are unchanged.
  • A bulk-delete tool: none exists in packages/mcp (no bulk tool, no multi-delete member on McpDataBridge; registerAuthoringTools is metadata, not records). The bound "any future bulk-delete tool takes the same gate" is stated in the changeset, the contract docblock and the gate's comment, and enforced by nothing mechanical; the card asked for it to be stated, and the contract docblock is where a future author reads it. Accepted.

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 Skills-docs follow-up card naming both stale teaching files. Landing still waits on the 22 in-progress check-runs reaching green and the maintainer's per-PR word on the BREAKING change.

Implemented-by: claude/project-thread-o7rpxo
Reviewed-by: session_016WV4czvZykUqhYxYXSSDt4

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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

维护者速读

  1. 改了什么:MCP 的 delete_record 现在必须带 confirm: true 才会删除。不带、或为 false 时,返回和 run_action 一样的 428 ACTION_CONFIRMATION_REQUIRED,details 里写明怎么重试,并且什么都不读、不删。HTTP 和 stdio 两种 MCP 传输都生效。REST 的 DELETE 和 update_record 不变。
  2. 为什么改:MCP delete_record refuses a call without confirm: true (ACTION_CONFIRMATION_REQUIRED), the run_action confirmation gate widened to deletes #30813,你两次「同意」的方案 B。
  3. 风险:中。这是破坏性改动:不带 confirm 的 AI 删除调用会被拒绝,改法是加一个参数。没有按对象豁免。以后的批量删除工具也走同一道门。
  4. 先看:packages/mcp/src/mcp-http-tools.ts 的 recordDeleteConfirmationRefusal;packages/spec/src/contracts/ai-service.ts 的「second gate」说明。
  5. 两分钟验证:pnpm --filter @objectstack/mcp exec vitest run src/mcp-delete-record-confirmation.test.ts。
  6. 需要你做的:在席位线程的决策卡上点头(破坏性)。点头后由席位入队落地。不属 Tier H,647 行 ≤ 3000。

席位意见:ACCEPT(#30813)。

席位核对

Head-sha: 273858232db5774419d8d4a267d5e0dc208ec2c2 · read 2026-11-06T08:06Z

Item Reading
Shape Draft → main, Fixes #30813, title feat(mcp)!:. 18 files, +627/−20. Branch claude/project-thread-o7rpxo, whole claim re-posted with the measured surface and the spec pin file.
Seat diff read The gate sits in registerObjectTools after the system-object guard and before bridge.remove, so both transports pass through it via wireBridgeTools. confirm is a declared optional boolean keyed off AI_ACTION_CONFIRMATION_MEMBER. The refusal is rendered by the shared codedErrorResult. Since the review, the delta on 27385823 touches the spec confirmation pin (it now names exactly one second emitter, @objectstack/mcp) plus one prose sentence each in the changeset and the ai-service.ts docblock: a non-boolean confirm is refused by the closed input schema, as on run_action. Test and prose only, read by the seat.
Evidence os-dev-report 6679368164. Dogfood 424 files / 3537 tests on 36030eab; dispatch-gates --ran 112/112 exit 0 on both heads. The spec package suite has 816 files / 25829 tests on 27385823. Contract review PASS 6679537708 on 36030eab, no must-fix. Its prose should-fix is folded in, and the Test Core (2/8) red on 36030eab was the stale pin this delta fixes.
Gates (check-runs on this head) 65 completed, 0 failed (ci-failure.mjs --sha: GREEN). The PR-body guards are re-running after the seat's body patch.
Accepted deviations patch, not minor (Metadata-protocol: none, per check-changeset-no-major). Before/Now/Who moves/Remedy prose instead of a literal FROM → TO, because the ADR-0087 gate reads that label as a prescription. The ledger provenance row under @objectstack/mcp is the spec lane's pre-approved append class.
Out of scope #30863 (skills line, Tier H) and #30865 (Console skill mirror), both pm:blocked behind this PR; the body's Skills-docs: line names them.
Semver @objectstack/mcp, @objectstack/spec: patch. Clause-②: yes (narrowing), Metadata-protocol: none, BREAKING banner, ADR-0087 not-required (no-migration-prescription).
Landing Not governed (check-governed-merges --pr 30861: NOT governed), 647 lines ≤ 3000, BREAKING. ⛔ Not queued until the maintainer gives the per-PR word. Then the seat lands it with pr_ready + automerge_enable on this head, or re-reviews first if the head moves.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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 27385823 (card #30813: MCP delete_record requires confirm: true). CI is green on that head and the seat ACCEPT stands, so the seat lands it now with pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 15:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42239989142 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/platform-objects:test:  FAIL  src/apps/translations/app-nav-translation-parity.test.ts > every nav id an app presents is translated in every locale > account — zh-Hant
      ↳ 失败原因: @objectstack/platform-objects:test: AssertionError: untranslated nav ids in apps.account.navigation: expected [ 'grp_account_ai_assistant' ] to deeply equal []
    @objectstack/spec:test:      ✓ inverts the card's measured control: the three bogus keys now FAIL parse instead of adding zero warnings  369ms
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 23 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit c77dd17 Nov 6, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-o7rpxo branch November 6, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP delete_record refuses a call without confirm: true (ACTION_CONFIRMATION_REQUIRED), the run_action confirmation gate widened to deletes

2 participants