§ 07.5 — Approvals

Nothing destructive runs without a click.

Two distinct approval flows. The destructive-tool gate intercepts every write tool call mid-stream and routes it through a user click. The MCP-connection admin gate stops user-defined external tools from being seen by the agent until a tenant admin signs off.

Destructive-tool approval

backend/src/AgenticIT.Agent/Approval/ApprovalGate.cs · IApprovalManager.cs

Tools listed in ToolRegistry.DestructiveTools route through the gate before execution. The flow:

  1. The LLM emits a tool_use block for a destructive tool.
  2. The agent loop pauses and calls ApprovalGate.RequestAsync(toolCallId, toolName, toolInput).
  3. The gate registers a pending approval keyed by toolCallId and returns a Task that completes when resolved.
  4. The channel emits {type:"approval_required", id, tool, input}.
  5. The frontend's ApprovalCard renders inside the chat with two buttons.
  6. User clicks ✓ → frontend sends {type:"approval", id, decision:"approve"}.
  7. The handler routes to IApprovalManager.Resolve(id, true); the gate's task completes; ToolExecutor runs the tool.
  8. Tool result, input, and approval status are written to AuditLogEntity.
sequenceDiagram autonumber participant L as LLM participant Loop as SingleAgentLoop participant G as ApprovalGate participant Ch as ChatChannel participant FE as Frontend participant U as User L->>Loop: tool_use {name: "delete_resource"} Loop->>Loop: ToolRegistry.IsDestructive(name) ⇒ true Loop->>G: RequestAsync(toolCallId, name, input) Note over G: pending approval registered G->>Ch: { type: "approval_required", id, tool, input } Ch->>FE: streamed event FE->>U: render ApprovalCard U->>FE: clicks ✓ FE->>Ch: { type: "approval", id, decision: "approve" } Ch->>G: Resolve(id, true) G-->>Loop: task completes Loop->>Loop: ToolExecutor.ExecuteAsync(...) Loop->>Ch: { type: "tool_result", ... }

Figure §07.5 — destructive-tool approval flow

Properties
  • The agent loop's cancellation token can interrupt a pending approval.
  • Approvals are in-memory only — a server restart loses them. Acceptable: the user can just resubmit the prompt.
  • One-shot. Approving or rejecting resolves the future immediately.

What's "destructive"

Every tool in ToolRegistry.WriteTools that mutates cloud state, e.g.:

Read tools (list_*, get_*) execute without prompting. The line is at "this writes to a cloud API in a way the user can detect after the fact".

Tenant access overrides

TenantToolAccessEntity.access_level can be:

allowed
Default. Approval still applies for destructive tools.
user_approve
Even read tools require approval — useful for sensitive subscriptions.
denied
Tool removed from the catalogue entirely. Agent doesn't know it exists.
hidden
Synonym for denied at the catalogue layer. Used in admin UI to distinguish soft-disabled from hard-removed.

Headless approval mode

Scheduled jobs and proactive scans have no live user. The HeadlessApprovalManager auto-approves all destructive ops with a 1-hour timeout (which exists only in case the manager does get called for some reason — it shouldn't block a job indefinitely).

This is a deliberate trade-off: scheduled jobs that perform writes need to actually perform them. Tenant admins who don't want headless writes should set destructive tools to denied.

User MCP connection approval

entity UserMcpConnectionEntity — columns approval_status, approval_requested_at, approval_decided_at, approval_decided_by_user_id, approval_reject_reason

A separate gate, governed by TenantEntity.require_user_mcp_approval:

  1. User configures a custom MCP connection in Settings.
  2. If the tenant flag is on, the connection's approval_status starts as pending; tools are not exposed to the agent.
  3. Tenant admin sees the pending connection in MCPApprovalsTab and approves / rejects.
  4. On approval: approval_status='approved', tools become discoverable, approval_decided_at + approval_decided_by_user_id recorded.
  5. On rejection: approval_status='rejected', optional approval_reject_reason shown to the user.
  6. Admin can later revoke an approved connection (approval_status='revoked') — tools immediately disappear from the agent's catalogue.

PKCE protection

For OAuth-based MCP connections (auth_mode = oauth2_cc or interactive OAuth), the PKCE code_verifier is stored server-side, encrypted, in pending_auth_code_verifier_enc. This defeats the case where an authorisation code leaks via referer or proxy logs — the attacker can't redeem it without the verifier.

Audit trail

entity AuditLogEntity

Append-only log of every tool execution:

This is the trail for "who deleted that VM and when". Querying it from the admin console is exposed; deeper investigation via direct DB access.