Skip to content

feat(approvals): a submitter's agent can recall and resubmit its own approval request over MCP (#32993) - #33076

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-49wiah
Nov 13, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-49wiah

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #32993

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:审批单上「撤回」和「重新提交」两个按钮,现在也对 AI 助手开放(通过 MCP 列出、可执行),而且和审批人的四个决定动作一样,每次执行都必须先经用户确认。
  • 为什么改:提交人的 AI 助手提交报销单后,用户要改明细,助手得先撤回审批,但 MCP 上没有撤回动作,只能让用户自己去控制台点一下。现在「撤回 → 改单 → 再提交」整条路助手都能走完。
  • 风险与回滚:放宽(多开放两个已有动作给 AI)。谁能撤回 / 重提仍只由审批服务判定,和控制台按钮完全一样:提交人可以;待审中的单子管理员可代撤回;其他人一律被拒、单子不动。回滚即删两个 ai 块。
  • 先看:packages/plugins/plugin-approvals/src/sys-approval-request.object.ts(approval_recall、approval_resubmit 的两个 ai 块和数组上方说明);packages/qa/dogfood/test/approval-decide-over-mcp.dogfood.test.ts(新增三条端到端钉子)。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/approval-decide-over-mcp.dogfood.test.ts,应见 Tests 8 passed (8)。
  • 需要你做的:无,席位落地。

What changes

The submitter-side branch of ruling A on #32442. PR #32886 exposed the approver's four decisions on sys_approval_request; the submitter's own levers on the same object stayed UI-only, so a submitter's agent that had to recall a request (to fix the record) handed the click back to the person.

Tests

packages/plugins/plugin-approvals/src/sys-approval-request.object.test.ts (18 tests):

  • The exposed set is exactly the four decisions plus recall and resubmit.
  • Recall and resubmit ask for confirmation, name the submitter, carry a ≥40-char description, and target their own same-origin route.
  • The recall dialog question is never armed as the AI tool description (the old "recall has no ai.description" line is replaced by the same not-the-question check approval_reject has).

packages/qa/dogfood/test/approval-decide-over-mcp.dogfood.test.ts (5 → 8, live MCP transport, real security/flows/approvals). The fixture flow gains an ADR-0044 revise window (revise → approval_revise → resubmit back edge); the existing decide pins are unchanged.

  • list_actions for both the approver and the submitter lists the four decisions plus recall and resubmit, each aiExposed and requiresConfirmation.
  • The submitter's recall is refused ACTION_CONFIRMATION_REQUIRED without confirm: true (request still pending, nothing recorded), then, confirmed, moves the request to recalled, recorded once as recall by the submitter.
  • The approver (not the submitter) is refused recall: FORBIDDEN, carrying the approval_recall_not_submitter sentence verbatim from BUILTIN_OPERATION_MESSAGES.en; the request stays pending.
  • On a request sent back over MCP, the submitter edits the record, then: unconfirmed resubmit is refused, the approver's resubmit is refused FORBIDDEN, the request stays returned with no round 2; the submitter's confirmed resubmit opens round 2 (one new pending request), recorded as revise by the approver and resubmit by the submitter.
Ablation: each ai block dropped alone (dogfood aliases plugin-approvals to source, so no rebuild is involved)

On committed head f5587158, through scripts/ablation-replace.mjs --delete, each restored with blob == HEAD and git diff HEAD empty.

  • A. approval_recall's ai block deleted (anchor x1 → x0): Tests 3 failed | 5 passed (8) — the listing pin and both recall pins red.
  • B. approval_resubmit's ai block deleted (anchor x1 → x0): Tests 2 failed | 6 passed (8) — the listing pin and the resubmit pin red.

Acceptance notes

Verification

Head f5587158 (origin/main 12b14462 merged before the PR opened; main had not moved). Every exit captured before any pipe.

Package tests and typecheck
  • pnpm --filter @objectstack/plugin-approvals test: Test Files 118 passed (118), Tests 1624 passed (1624).
  • pnpm --filter @objectstack/plugin-approvals typecheck (tsc --noEmit + scripts + check:test-typecheck): exit 0.
  • pnpm --filter @objectstack/dogfood typecheck: exit 0.
  • sys-approval-request.object.test.ts: Tests 18 passed (18); approval-decide-over-mcp.dogfood.test.ts: Tests 8 passed (8).
  • Prerequisite: pnpm --filter '@objectstack/dogfood^...' build under the verify lock, exit 0.
Derived gates (dispatch-gates.mjs --commands, 96) and lint

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cwwUDMxb29FgtH4D53

…approval request over MCP

approval_recall and approval_resubmit declare ai.exposed with
requiresConfirmation, forwarded to their approvals routes as the caller;
ApprovalService stays the only gate.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwwUDMxb29FgtH4D53
@github-actions github-actions Bot added size/m 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 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

Coarse fallback — 6 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 4462b4d92b9fff58d43e6fa8e700fd0ea6d4f62d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 74551ba69249a6bd06dc90dbb195512bd23e39eb — the merge of head f5587158283eaf01d6ce8b9a3c81e3bc45a65999 into base 4462b4d92b9fff58d43e6fa8e700fd0ea6d4f62d, 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 74551ba69249a6bd06dc90dbb195512bd23e39eb && git checkout 74551ba69249a6bd06dc90dbb195512bd23e39eb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4462b4d92b9fff58d43e6fa8e700fd0ea6d4f62d f5587158283eaf01d6ce8b9a3c81e3bc45a65999 && git checkout -B drift-repro 4462b4d92b9fff58d43e6fa8e700fd0ea6d4f62d && git merge --no-ff f5587158283eaf01d6ce8b9a3c81e3bc45a65999

node scripts/docs-audit/affected-docs.mjs --json 4462b4d92b9fff58d43e6fa8e700fd0ea6d4f62d

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J

ACCEPT · PR #33076 · head f5587158283eaf01d6ce8b9a3c81e3bc45a65999 · 2026-11-13T08:59Z · Fixes #32993

席位核对

  1. Shape: draft PR to main, Fixes #32993. Files (5): packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (two ai blocks and the note above the actions array) and its test, packages/qa/dogfood/test/approval-decide-over-mcp.dogfood.test.ts (extended), content/docs/ai/connect-mcp.mdx, one changeset. All are inside the claim (6847131641, re-posted 6847193267). No governed path. Tier S, review at TIER_DEFAULT: metadata declarations on existing actions, with no schema, export or accept-set change. Precedent: feat(plugin-approvals): an approver's agent decides a pending approval request over MCP #32886, the decide half, landed by its seat on the same footing.
  2. Premise measured: approval_recall and approval_resubmit declared no ai block, so a submitter's agent could neither list nor run them over MCP. Each now declares ai: { exposed: true, description, requiresConfirmation: true } and still forwards to its approvals route as the caller.
  3. Gate read against the code: ApprovalService.recall admits the submitter, plus the override actor on pending only, and refuses anyone else with FORBIDDEN and the approval_recall_not_submitter sentence. ApprovalService.resubmit admits the submitter alone, on returned only. The tool descriptions, the changeset and the connect-mcp.mdx bullet say exactly that. The MCP door adds no check.
  4. Gates: check-runs on this head: 38 success, 15 skipped, 0 failed, 0 pending. Dev report 6848324250: dogfood 8/8, with ablation red (3 and 2); plugin-approvals 1624/1624; dispatch-gates --ran 96 run, 0 NOT MEASURED after a whole-tree build. The PR body's Verification still lists three dist-reading gates as NOT MEASURED, as of PR open. The report supersedes that line.
  5. [Epic] MCP: what an agent can read and write on the HTTP MCP door, and answers that agree with the gate #32897 condition (triage 08:01Z): this PR is new surface only, two actions newly exposed to AI. It moves no 18.4.0 answer and touches no release-critical path, so it may land before the version commit.
  • Landing: Clause-②: yes (widening), Metadata-protocol: none, patch. main not merged since the PR opened. K = 2 (4462b4d92b, this head's Test Core affected-set base, to origin/main), under 15. p1 under the maintainer's rule (「后续只派 p0 p1 和阻塞 p0 p1 的卡」).

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/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants