feat: Per-tool approval mode (pre-approved / approve-once / always) with loop pause-resume
## Problem / motivation Agents currently invoke tools without a built-in approval gate. For destructive or sensitive tools (writing content, sending email, calling external APIs, running shell commands, etc.) this is risky — the agent can act before a human has a chance to review. Tools like [Claude Code](https://docs.claude.com/en/docs/claude-code) and [OpenAI Codex CLI](https://github.com/openai/codex) solve this by gating each tool call (in their case, each shell command) behind a per-tool approval policy. The user can mark a tool as always-allowed, approve-once-per-session, or always-prompt. We want the same model for AI Agents. ## Proposed resolution ### Per-tool approval mode On each tool's configuration, add an "Approval mode" setting with three options: 1. **Pre-approved** — the agent calls the tool freely, no human gate. Suitable for read-only / safe tools. 2. **Approve once per session** — first call asks the caller; if approved, all subsequent calls to that tool in the same session run without re-prompting. 3. **Always needs approval** — every call to this tool prompts the caller, regardless of prior approvals. Default for new/unconfigured tools should be **Always needs approval** (fail safe). ### Loop integration The agent loop needs a pause/resume contract: 1. When the model returns a tool call whose tool requires approval, the loop **does not execute the tool**. Instead it returns a "pending approval" state to the caller, including: - Which tool is being called. - The arguments the model wants to pass. - A handle/ID the caller uses to approve or reject. 2. The caller must call a new **approval method** (e.g. `approveToolCall($handle)` / `rejectToolCall($handle, $reason)`) before the loop can continue. 3. On approval, the loop executes the tool and proceeds to the next iteration as normal. 4. On rejection, the loop feeds a rejection message back to the model (so the model can choose a different action) rather than executing. 5. For the **"approve once per session"** mode, an approval is recorded against the (session, tool) pair so subsequent calls in that session skip the gate. ### Caller responsibility The caller (UI form, chatbot, REST endpoint, custom code, etc.) is responsible for surfacing the approval request to a human and calling the approval/rejection method. The agent module just exposes the state and the methods — it doesn't dictate UX. ## Research / open questions - **Session scope.** How is a "session" defined? Most likely the existing agent run / conversation ID, but confirm there's a stable handle that works for both the chatbot UI and the form-based callers. - **Argument-level approval.** Should the approval also let the caller *edit* the arguments before approval (similar to Claude Code's "edit and run" option), or is approve/reject sufficient for v1? Recommend approve/reject only for v1 to keep the surface small. - **Persistence.** Should "approve once per session" approvals survive across page loads / requests? For web UIs the session may span many HTTP requests — the approval store needs to be persistent (DB or keyvalue), not in-memory. - **Sub-agent / nested agent calls.** If an agent calls a sub-agent, does the sub-agent inherit the parent's approvals, or does it get its own approval scope? Recommend: inherit by default, configurable per agent. - **Streaming / async.** How does pause/resume interact with streaming responses? The loop pause needs to cleanly suspend without leaving an open stream. - **Audit log.** Every approval decision (and its outcome) should be logged for traceability — likely a new entity or extension of an existing log. - **Bulk override.** Should there be a global "trust everything in this session" escape hatch (similar to Claude Code's `--dangerously-skip-permissions`)? Probably yes for development / testing, gated behind a permission. ## Remaining tasks - [ ] Decide final scope of approval modes — see research above (argument editing? bulk override?). - [ ] Add per-tool config: "Approval mode" with three options (pre-approved / approve-once-per-session / always). - [ ] Define the new `approveToolCall` / `rejectToolCall` (names TBD) methods on the agent loop API. - [ ] Implement loop pause/resume returning a pending-approval state. - [ ] Persist session-scoped approvals across requests. - [ ] Audit log entry for each approval decision. - [ ] Update existing callers (chatbot, form integrations, etc.) so they don't break on the new "pending approval" return state — they should at minimum surface a sane error if they haven't been updated, and ideally render an approval prompt. - [ ] Tests covering: pre-approved tool runs, always-approve tool blocks the loop, approve-once works for second call, rejection feeds back to model, approval survives across requests, sub-agent inheritance. - [ ] Document the approval contract for tool authors and caller integrators. ## User interface changes - New "Approval mode" select on each tool's configuration form within an agent (default: Always needs approval). - Callers (chatbot, form-driven flows) need a new UI affordance to render pending approval requests and accept/reject them. Out of scope for the core module change — but the loop contract must enable it. ## API changes - New per-tool config field (e.g. `approval_mode`). - New return state from the agent loop indicating "paused, awaiting approval" with the pending tool call payload. - New public methods on the agent runner: `approveToolCall($handle)`, `rejectToolCall($handle, $reason = NULL)` (names TBD). - New event(s) so other modules can observe / influence approval decisions. ## Prior art - **Claude Code** — per-tool / per-pattern allowlist with `/permissions` UI and `--dangerously-skip-permissions` escape hatch. - **OpenAI Codex CLI** — `--approval-mode {suggest,auto-edit,full-auto}` flags controlling when human approval is required.
issue