feat(agents,approvals): tool-call approval interception on every surface (Milestone 1) - #581
Merged
Merged
Conversation
…ace (Milestone 1)
Milestone 1 of the manage-by-exception roadmap — extends the approval queue
from orchestrated (DAG) runs to every execution surface (chat sessions,
direct generations, MCP) via an `approval_policy` enforced in the platform
tool-dispatch path. Task 1.0 (canonical tool_bindings + approval_policy
validation) shipped earlier; this implements tasks 1.1–1.5.
- 1.1 Policy evaluation in the dispatch path. A new pure module
(`agentToolApproval.ts`) evaluates a binding's allow / require_approval /
deny policy (JSON Logic over `{ action, arguments }`, first match wins) and
gates each resolved tool's `execute`. Threaded into the resolver via a
`ResolverApprovalContext` built at the generation entry point (and recovery
path); zero-overhead when no binding declares a policy.
- 1.2 Return-pending. `require_approval` freezes the call into an approval
item and returns `{ status: "pending_approval", approval_id, expires_at }`
as the tool result — the turn completes normally, no `requires_action`
pause. The optional `approval_reasoning` / `approval_evidence` /
`approval_predicted_impact` justification fields are injected into the
model-visible schema (http/pipeline/inline), stripped from executed args,
and frozen onto the item.
- 1.3 Continuation on resolution. A tool-call resume handler
(`agentToolApprovalContinuation.ts`, registered at startup) executes the
frozen (or edited) action on approval, populates `DecisionOutput.result`,
and fires a continuation generation linked via `initiator_generation_id` —
routed into the originating session thread when there was one, else a
standalone linked generation. Rejection/expiry carry the decision and
execute nothing.
- 1.4 Dedup. A `(project, agent, tool, action, args)` digest returns the
existing pending item on re-proposal; the partial unique index backs the
concurrent-emit race.
- 1.5 `origin: tool_call` wired end-to-end; the lifecycle never branches on it.
Model: `ApprovalItem` gains `session_id` and `proposed_action.action`;
OpenAPI + docs (`approvals.md`, `agents.md`) updated; smoke-test flow added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWGkJE9gG4tdJkaPxmtEFT
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWGkJE9gG4tdJkaPxmtEFT
…ation Restores global function coverage to 100%: - exercise emitApproval's create-time unique-violation backstop via a sanctioned force-failure spy (tests.md exception #2) - make runToolCallContinuation self-contained (never rejects) so the resume handler needs no separate .catch arrow; cover the swallow path Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWGkJE9gG4tdJkaPxmtEFT
Deploy Outputs
|
…registration - evaluateApprovalEffect: use plain JS truthiness for rule matching, so a `when` yielding 0/''/null/NaN is falsy (matches the JSON Logic convention used by orchestrationPollNode), instead of firing on 0. - Freeze the effective (preset-merged) arguments the policy evaluated onto the item (and dedup key), so the approver sees the full call and the platform re-executes exactly what was decided. - gateResolvedTools: spread the resolved tool in the schema-injection branch so properties beyond description/inputSchema/execute survive gating; fix the JSDoc to match localInjectableSchema (http/pipeline + inline http). - Register the tool-call resume handler from the approval expiry sweeper too, not only the REST router, so a scheduler-only process still fires continuations for items it expires. - Clarify the session-continuation provenance in JSDoc (thread linkage today; initiator_generation_id FK-threading through the session stack is a follow-up). - Document the inline-policy ordering invariant shared with deriveLegacyToolFields. - Add an end-to-end test that resolves an item through the real notifyResume path and asserts the registered handler fires the continuation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWGkJE9gG4tdJkaPxmtEFT
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Milestone 1 — Approvals on every surface of the manage-by-exception roadmap (prd-approvals.md Phase 2). Task 1.0 (canonical
tool_bindings+approval_policyvalidation) shipped earlier; this PR implements the runtime enforcement — tasks 1.1–1.5.An agent↔tool binding's
approval_policyis now enforced by the platform in the tool-dispatch path, extending the approval queue from orchestrated DAG runs to every execution surface (chat sessions, direct generations, MCP) — the surfaces a DAG-resident gate never protects.What changed
agentToolApproval.tsevaluates a binding'sallow/require_approval/denypolicy (JSON Logic over{ action, arguments }, first-match-wins) and gates each resolved tool'sexecute. Threaded into the resolver via aResolverApprovalContextbuilt at the generation entry point and the recovery path. Zero-overhead when no binding declares a policy.require_approvalfreezes the call into an item and returns{ status: "pending_approval", approval_id, expires_at }as the tool result — the turn completes normally (norequires_actionpause).denyreturns a structured refusal. Theapproval_reasoning/approval_evidence/approval_predicted_impactjustification fields are injected into the model-visible schema (http/pipeline/inline), stripped from executed args, and frozen onto the item.agentToolApprovalContinuation.ts, registered at startup) executes the frozen/edited action on approval, populatesDecisionOutput.result, and fires a continuation generation linked viainitiator_generation_id— routed into the originating session thread when there was one, else a standalone linked generation. Reject/expiry carry the decision and execute nothing.(project, agent, tool, action, args)digest returns the existing pending item on re-proposal; the partial unique index backs the concurrent-emit race.origin: tool_callData model / API
ApprovalItemgainssession_id(continuation routing) andproposed_action.action(resolution-time execution of soat/mcp actions).approvals.yaml) updated → SDK/CLI regenerate cleanly.approvals.md(data model + producer semantics),agents.md(justification-field injection scope note).Design notes
preset_parametersschema-surgery). The approval logic is split into a pure/gate module (safe for the resolver to import) and a startup-registered continuation module (imports the heavy generation/session libs) to keep the resolver import-cycle-free.agents.md).Testing
agentToolApproval.test.ts— pure policy/dedup/schema logic (keep-list rule 1).agentToolApprovalGate.test.ts— real-DB dispatch-path integration (allow/deny/require_approval/rule-precedence/dedup) and continuation (approved/rejected/expired/inline/session-routed/failed-execution) via local fake HTTP + OpenAI-compatible servers.approvals.test.ts— tool-call provenance exposure +origin=tool_callfiltering.tool_bindings+approval_policy→ force call →list-approvals --origin tool_call→approve-approval).pnpm typecheck+eslint --fixclean; noas any/as unknown.🤖 Generated with Claude Code
Generated by Claude Code