Skip to content

feat: add require_approval action with async approval workflow#3

Merged
L1AD merged 6 commits intomainfrom
feat/require-approval
Mar 31, 2026
Merged

feat: add require_approval action with async approval workflow#3
L1AD merged 6 commits intomainfrom
feat/require-approval

Conversation

@L1AD
Copy link
Copy Markdown
Contributor

@L1AD L1AD commented Mar 30, 2026

Summary

Adds require_approval as a third action type for Intercept policy rules. When a tool call matches a require_approval rule, the proxy returns a structured error. A human approves via CLI or local HTTP API, then the agent retries and the call goes through exactly once.

Design: async model (no hold-open), fingerprint-based dedup, one-time consumption with rollback on upstream failure.

What's included

  • Config: action: require_approval, approval_timeout, top-level approvals: block with webhook config
  • Engine: DecisionRequireApproval type with RuleHash for fingerprinting
  • Approval store: internal/approvals/ -- Store interface, SQLite backend, canonical JSON fingerprinting, dedup, one-time atomic consumption, rollback
  • Proxy integration: async approval flow in handleApproval, JSON-RPC -32003 error response with human_instructions field, bounded webhook goroutines
  • CLI: intercept approvals list/show/approve/deny/expire
  • Admin API: 4 HTTP endpoints (pending, pending/:id, approve/:id, deny/:id), opt-in via --enable-admin-api, loopback-only
  • Webhook: HMAC-SHA256 signed POST on approval_requested, fire-and-forget
  • Events: 6 approval event types in JSONL audit trail
  • Docs: POLICY.md and USAGE.md updated with full approval reference

What's NOT included

  • No hosted approval queue or web UI
  • No team auth, SSO, roles
  • No Slack/Teams integration (webhook is the primitive)
  • No multi-step approval chains
  • No hold-open synchronous approval (async only)

Review findings addressed

The final commit fixes correctness issues found during parallel code review:

  • ConsumeApproved atomicity: transaction with SELECT+UPDATE by ID prevents multi-row consumption on duplicate fingerprints
  • MCP result.isError rollback: upstream tool failures reported via result.isError (not just JSON-RPC errors) now trigger approval and state rollback
  • Expired approval guard: MarkApproved/MarkDenied check expires_at to prevent "approved but unusable" state
  • Deterministic fingerprinting: arrays of objects in tool arguments are now recursively sorted
  • Timeout resolution: scoped to tool name first, then wildcard, preventing nondeterministic results on duplicate rule names
  • Pending list filtering: admin API and CLI exclude naturally expired records

Test plan

  • go test ./... passes (13 packages)
  • go test -race clean on approvals, proxy, engine
  • make build succeeds
  • New tests: duplicate fingerprint consumption, approve non-pending, approve expired, array-of-objects fingerprinting, MCP isError rollback

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
intercept Error Error Mar 30, 2026 11:51am

Request Review

L1AD added 4 commits March 30, 2026 12:49
- Cache computePolicyRevision (no per-call JSON marshal)
- Unify scanRecord/scanRecordRows via scannable interface
- Deduplicate handleApprove/handleDeny into handleDecision
- Cache HashArgs (compute once, pass through)
- Bound webhook goroutines with semaphore (cap 10)
- Use interceptSubdir helper in CLI
- Include approval CLI command in error message
- Add approval rules to POLICY.md and USAGE.md
- MarkApproved/MarkDenied now include WHERE status='pending', preventing
  re-arming of consumed/denied/expired records from CLI or admin API
- FindByFingerprint accepts dedupeWindow param, adds created_at bound
  so dedupe_window config is actually respected
- Fix POLICY.md docs: webhook config uses notify.webhook.url not webhook_url
- FindByFingerprint: dedupe_window restricts pending record reuse only;
  approved records remain valid until their full expires_at lifetime
- USAGE.md: fix approval ID prefix (appr_ -> apr_)
- ConsumeApproved: use transaction with SELECT+UPDATE by ID to prevent
  multi-row consumption on duplicate fingerprints
- ConsumeApproved error in handler: return deny instead of falling
  through to create duplicate pending records
- isErrorResponse: detect MCP result.isError in addition to JSON-RPC
  errors, triggering rollback on upstream tool failures
- MarkApproved/MarkDenied: check expires_at to prevent approving
  naturally expired records ("approved but unusable" state)
- List: filter expired pending records so admin API and CLI show only
  actionable approvals
- resolveApprovalTimeout: scope lookup to tool name first, then
  wildcard, preventing nondeterministic timeout on duplicate rule names
- Canonical JSON: recursively sort maps inside arrays for deterministic
  fingerprinting of array-of-objects arguments
- scanFromRow: return errors on malformed timestamps instead of silent
  zero-value times
- Extract defaultApprovalTimeout constant, remove dead sortedMap
  wrapper, use strings.Join for SQL conditions
- Add tests: duplicate fingerprint consumption, approve non-pending,
  approve expired, array-of-objects fingerprinting, MCP isError
- Update POLICY.md validation errors table with 12 approval errors,
  add require_approval to evaluation order
- Update README.md with approval feature and example
- remove em dashes from prose and feature list
- active voice throughout
- parallel construction in feature list (bold label + period + description)
- tighten verbose sentences
- fix approval example header to match active voice pattern
@L1AD L1AD merged commit 63065b5 into main Mar 31, 2026
1 check passed
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.

1 participant