Skip to content

feat(agents,approvals): tool-call approval interception on every surface (Milestone 1) - #581

Merged
arantespp merged 4 commits into
mainfrom
claude/approval-roadmap-next-1c4msp
Jul 18, 2026
Merged

feat(agents,approvals): tool-call approval interception on every surface (Milestone 1)#581
arantespp merged 4 commits into
mainfrom
claude/approval-roadmap-next-1c4msp

Conversation

@arantespp

Copy link
Copy Markdown
Member

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_policy validation) shipped earlier; this PR implements the runtime enforcement — tasks 1.1–1.5.

An agent↔tool binding's approval_policy is 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

Task Change
1.1 Policy evaluation 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 the recovery path. Zero-overhead when no binding declares a policy.
1.2 Return-pending require_approval freezes the call into an item and returns { status: "pending_approval", approval_id, expires_at } as the tool result — the turn completes normally (no requires_action pause). deny returns a structured refusal. The 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 A tool-call resume handler (agentToolApprovalContinuation.ts, registered at startup) executes the frozen/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. Reject/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.

Data model / API

  • ApprovalItem gains session_id (continuation routing) and proposed_action.action (resolution-time execution of soat/mcp actions).
  • OpenAPI (approvals.yaml) updated → SDK/CLI regenerate cleanly.
  • Docs updated: approvals.md (data model + producer semantics), agents.md (justification-field injection scope note).

Design notes

  • The gate lives in the resolver's per-binding wrap point (mirrors the existing preset_parameters schema-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.
  • Justification-field schema injection applies to tools whose parameter schema is defined locally (http/pipeline/inline); mcp/soat tools are still gated and any supplied reasoning is still frozen, but their remote/per-action schemas are not augmented (documented in agents.md).
  • The continuation is fire-and-forget from the resolve request so approve/reject returns promptly, matching the manage-by-exception model.

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_call filtering.
  • Smoke test — approval-gated tool binding flow (tool_bindings + approval_policy → force call → list-approvals --origin tool_callapprove-approval).
  • pnpm typecheck + eslint --fix clean; no as any / as unknown.

Note: the one unrelated files.test.ts failure seen locally is a SOAT_BASE_URL environment leak in the dev container (unset in CI), not from this change.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 3 commits July 18, 2026 00:11
…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
…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
@github-actions

Copy link
Copy Markdown

Deploy Outputs

Package Stack Output Key Output Value
@soat/website SoatWebsite-claude-approval-roadmap-next-1c4msp BucketWebsiteURL http://soatwebsite-claude-approval-roadmap-n-staticbucket-bfr2srwvqrta.s3-website-us-east-1.amazonaws.com

…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
@arantespp
arantespp merged commit da69b2e into main Jul 18, 2026
6 checks passed
@arantespp
arantespp deleted the claude/approval-roadmap-next-1c4msp branch July 18, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants