Skip to content

Approval Flow

When a policy rule has action: human_approval, the tool call enters an approval queue. The agent waits. A human (or supervisor) resolves it. The tool call proceeds or is rejected.

How it works

Agent calls filesystem.write_file
  ├── Policy: human_approval
  ├── Check 1: Temporal grant active?
  │   yes → bypass approval, proceed
  ├── Check 2: flux7-memory auto-approve? (3+ past approvals)
  │   yes → proceed, traced as supervisor:mem7
  └── Submit to approval queue
      ├── MCP mode: routed per approval.channel (TTY prompt or queue)
      ├── HTTP mode: always the queue — blocks until resolved
      └── Human/supervisor resolves
          ├── approved → tool call executed, result returned
          ├── denied → 403 returned
          └── timeout → 408 returned (default 5 min)

Routing: approval.channel

Version

Since v0.15.0. Older binaries silently ignore the key.

In MCP mode, approval.channel decides where a human_approval request goes:

approval:
  timeout_seconds: 300
  channel: queue        # queue | tty | tty-fallback
Channel Behavior
queue Always enqueue. For daemons and supervisor setups — a service has no terminal to prompt on.
tty Require the interactive /dev/tty prompt. If no TTY is available, the call is denied (fail-closed), never silently queued.
tty-fallback Try the TTY prompt, fall back to the queue. Default — matches the historical behavior.

Without an explicit channel, routing depends on how the process was launched: a daemon started from a terminal keeps a usable /dev/tty and will prompt in a window nobody watches. Set channel: queue for any unattended deployment (systemd, container, supervisor loop).

The HTTP proxy path (POST /tool/{name}) always uses the queue regardless of this setting.

Resolving approvals

In Claude Code (MCP mode)

Claude Code shows a permission prompt inline. The developer says yes or no. This is the default for solo dev use.

Via MCP virtual tools

The approval.* virtual tools are operator-only: only a declared supervisor agent (supervisor.supervisor_agents glob) may list or resolve approvals over MCP. A regular agent cannot — otherwise it could approve its own pending human_approval request and defeat the gate. With no supervisor configured, resolve approvals via the CLI or HTTP API below.

approval.pending                → list pending approvals (supervisor agents only)
approval.resolve {id, decision} → approve or deny (supervisor agents only)

Via CLI

# List pending
mesh pending

# Approve (prefix match)
mesh approve a1b2c3d4

# Deny
mesh deny a1b2c3d4

# Watch (live updates)
mesh watch

Via HTTP API

# List all approvals
curl http://localhost:9090/approvals

# Get details (includes recent traces and active grants)
curl http://localhost:9090/approvals/a1b2c3d4e5f6g7h8

# Approve
curl -X POST http://localhost:9090/approvals/a1b2c3d4/approve \
  -H "Content-Type: application/json" \
  -d '{"resolved_by":"user:marc","reasoning":"routine operation","confidence":0.95}'

# Deny
curl -X POST http://localhost:9090/approvals/a1b2c3d4/deny \
  -d '{"resolved_by":"user:marc","reasoning":"unexpected target"}'

Prefix matching: a1b2c3d4 matches the full ID if the prefix is unique.

Temporal grants

Repeated approvals for the same tool pattern get tedious. Grants are like sudo — a temporary bypass:

grant.create {tools: "filesystem.write_*", duration: "30m"}

For the next 30 minutes, all filesystem.write_* calls bypass the approval queue. Traced as grant:<id>.

MCP tools

grant.create  {tools: "filesystem.*", duration: "1h"}
grant.list
grant.revoke  {id: "abc123"}

HTTP API

# Create
curl -X POST http://localhost:9090/grants \
  -d '{"agent":"claude","tools":"filesystem.*","duration":"30m"}'

# List
curl http://localhost:9090/grants

# Revoke
curl -X DELETE http://localhost:9090/grants/abc123

Grants only bypass human_approval

Tools marked deny remain blocked. A grant cannot override a deny rule — that requires a policy edit.

Timeouts

Unanswered approvals time out after 5 minutes (configurable):

approval:
  timeout_seconds: 300    # default

Timed-out approvals are recorded in traces and written to flux7-memory (if configured) with status timeout.

Webhooks

Get notified when a new approval is pending:

approval:
  notify_url: https://hooks.slack.com/services/...

flux7-mesh POSTs to this URL with the pending approval details. Useful for Slack/Teams alerts.

Callback URL

HTTP agents can provide a callback URL to receive the resolution:

curl -X POST http://localhost:9090/tool/gmail.send_email \
  -H "Authorization: Bearer agent:my-bot" \
  -H "X-Callback-URL: http://my-bot:8080/approval-callback" \
  -d '{"params":{"to":"user@example.com","subject":"Hello"}}'

When the approval resolves, flux7-mesh POSTs the result to X-Callback-URL.

Supervisor mode

When supervisor.enabled: true, the approval tools (approval.resolve, approval.pending) are hidden from agents. Only an external supervisor can resolve approvals:

supervisor:
  enabled: true
  expose_content: false       # redact params for the supervisor
  supervisor_agents:          # whitelist (glob) for cloud supervisors
    - "supervisor-*"

In this mode, MCP tool calls block until the supervisor resolves. The agent waits transparently.

See Supervisor Protocol for the external resolver API.

Traces

Every approval is traced:

{
  "tool": "filesystem.write_file",
  "policy": "human_approval",
  "approval_id": "a1b2c3d4e5f6g7h8",
  "approval_status": "approved",
  "approved_by": "user:marc",
  "supervisor_reasoning": "routine write operation",
  "supervisor_confidence": 0.95,
  "latency_ms": 4200
}

Query approval history:

curl "http://localhost:9090/traces?tool=filesystem.write_file" | python3 -m json.tool

Decision persistence

When flux7-memory is configured, every approval resolution (approve, deny, timeout) is stored as a queryable fact. This enables:

  • Auto-approve — routine patterns resolve without human intervention
  • Audit trail — "who approved what, when, why" is queryable
  • Cross-session memory — decisions survive process restarts

Next steps