Skip to content

Resolving approvals

When a rule's action is require_approval, the matched request is paused — the agent's call hangs, and the request appears on the Approvals page waiting for a human to decide.

What you see

Screenshot: Approvals page with pending itemsdocs/public/screenshots/approvals.png

Each pending approval is a panel showing:

  • The agent and destination (mono — copy-paste friendly).
  • The rule that paused it (policies.yaml · approve_external_writes).
  • Why we paused — the rule's reason field, in plain English.
  • The arguments (collapsed by default; click "show args" to expand). Sensitive headers like Authorization are scrubbed before the audit row even reaches the cloud — but the body is intact for review.
  • Two buttons: Allow (A) and Deny (D).

Resolving

Click Allow → the agent's hung call resumes, with verdict rewritten to allow. Click Deny → the call returns the configured failure to the agent, verdict stays deny.

The keyboard navigation matches the page header hint:

  • J / K — move between items.
  • A — allow the focused item.
  • D — deny the focused item.

Timeout

If nobody resolves within 60 seconds (the collector-side default; tunable per rule), the request fails closed — the agent gets a deny verdict with reason "approval timed out". The audit row records it as verdict=deny, reason=approval_timeout so you can grep for unattended queues later.

What's a good require_approval rule

The pattern is "expensive, irreversible, or untrusted":

  • External writes — anything that creates / mutates / deletes on a third-party (POST/PUT/DELETE to api.notion.so, hooks.slack.com).
  • Cost-bounded — an agent that's about to exceed a daily token budget.
  • First-time provider — never-before-seen destination from this agent.
  • High-blast-radius toolsfs.write to /etc/*, shell.exec with a non-trivial command.

Examples in policies.yaml:

yaml
- name: approve_external_writes
  match:
    action: http_request
    arg_regex: { method: "POST|PUT|DELETE" }
  action: require_approval
  reason: "external write requires human review"

- name: approve_new_provider
  match:
    action: llm_prompt
    destination: "*"          # any model
    # `first_seen_for_agent` is an upcoming predicate; until then,
    # use a deny rule with `unknown_host` semantics and reverse the
    # decision once you've vetted the provider.
  action: require_approval

Don't park approvals long

The Approvals queue is a work surface, not a queue of permanent decisions. Two patterns to keep it small:

  1. Approve → add a rule. If you've allowed the same kind of request three times in a row, write a rule that whitelists it. The Approvals page has a "+ Add rule" button on each panel that pre-fills the Editor with the matched request as a template.
  2. Deny → escalate. If you find yourself denying a class of request repeatedly, write a more specific deny so the agent gets the failure faster instead of hanging for 60 seconds.

What gets recorded

Every resolve writes to the audit log:

json
{
  "verdict": "allow",
  "rule": "approve_external_writes",
  "reason": "human:pradeep@acme.com",
  "resolution_latency_ms": 4_812,
  ...
}

reason includes the user who clicked; resolution_latency_ms is the wall-clock delay between request and decision. Useful for setting timeouts and SLO reporting.

→ See Live Traffic for the full audit row, and the Audit Log for the immutable trail.

Documentation for kilasec — the AI Agent Firewall.