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