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:
- The LLM emits a
tool_useblock for a destructive tool. - The agent loop pauses and calls
ApprovalGate.RequestAsync(toolCallId, toolName, toolInput). - The gate registers a pending approval keyed by
toolCallIdand returns aTaskthat completes when resolved. - The channel emits
{type:"approval_required", id, tool, input}. - The frontend's
ApprovalCardrenders inside the chat with two buttons. - User clicks ✓ → frontend sends
{type:"approval", id, decision:"approve"}. - The handler routes to
IApprovalManager.Resolve(id, true); the gate's task completes;ToolExecutorruns the tool. - Tool result, input, and approval status are written to
AuditLogEntity.
Figure §07.5 — destructive-tool approval flow
- 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.:
delete_resourceresize_vmremediate_nsg_ruleenable_storage_httpsapply_tagassign_policy
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:
- User configures a custom MCP connection in Settings.
- If the tenant flag is on, the connection's
approval_statusstarts aspending; tools are not exposed to the agent. - Tenant admin sees the pending connection in
MCPApprovalsTaband approves / rejects. - On approval:
approval_status='approved', tools become discoverable,approval_decided_at+approval_decided_by_user_idrecorded. - On rejection:
approval_status='rejected', optionalapproval_reject_reasonshown to the user. - 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:
ts,session_id,user_idtool_nametool_input(jsonb) — what was senttool_result(jsonb) — what came backapproval_status—approved/rejected/auto_approved/not_required
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.