Approvals & TOTP Second Factor
Human-in-the-loop approvals let you keep an agent on a short leash: when it asks to run a sensitive tool, the request is held until you accept or reject it. The TOTP second factor adds a time-based one-time password check to high-risk actions so that a stolen API token alone is not enough to cause damage.
Included Topics
- Approval Policy
- Enabling TOTP
- Dashboard Flow
- API Reference
- Session-Scoped Batch Resolution
- Recovery Codes
- Hardening Checklist
Approval Policy
Config: [approval] section in ~/.librefang/config.toml.
Source: librefang-types/src/approval.rs (ApprovalPolicy).
[approval]
# Tools that always require approval. `true` = default set, `false` = disable all gating.
require_approval = ["shell_exec", "file_write", "file_delete", "apply_patch"]
timeout_secs = 60 # 10..=300
auto_approve_autonomous = false # auto-approve when agent runs unattended
auto_approve = false # clears the require list at boot
trusted_senders = [] # user IDs that skip the gate for low-risk tools (high-risk tools still gated)
# Second factor
second_factor = "none" # "none" | "totp" | "login" | "both"
totp_issuer = "LibreFang" # shown in authenticator apps
totp_grace_period_secs = 30 # skip TOTP for follow-ups within this window
totp_tools = [] # empty = all gated tools require TOTP
Second-factor scope
| Value | TOTP required for approvals | TOTP required for dashboard login |
|---|---|---|
none | — | — |
totp | yes | — |
login | — | yes |
both | yes | yes |
totp_tools accepts glob patterns (shell_*) — useful to gate only destructive tools with a
second factor while leaving read-only tools on the simple approval gate.
Timeout fallback
When an approval times out, the action falls through to timeout_fallback:
reject(default) — agent receives a rejection and halts.allow— action proceeds. Only safe in fully-trusted autonomous setups.retry— re-requests approval once more.
How trusted_senders composes with [[users]] RBAC
trusted_senders and [[users]] RBAC are two separate trust surfaces, and being listed in one does not exempt a sender from the other.
trusted_senders governs only the approval layer — it waives the approval prompt for a trusted sender's low-risk tools (high-risk tools stay gated, per the Hardening Checklist).
[[users]] RBAC governs the per-user tool gate, which is evaluated first and independently.
Because RBAC runs first, a "needs approval" verdict from it wins before the trusted_senders bypass is ever consulted.
So when [[users]] is configured and a sender is not a registered user on the api channel — the channel REST POST /api/agents/{id}/message requests arrive on — that sender falls to the built-in guest gate (guest_gate).
The guest gate allows only a small read-only set (file reads, search, web_fetch, …) and returns "needs approval" for everything else — including memory_store / memory_recall, which are otherwise low-risk.
The surprising consequence: an ID listed in trusted_senders that is not also a registered [[users]] on the api channel still has its memory_* calls gated, because the guest gate forced approval before trusted_senders could waive it.
Fix — register the operator as a user bound to the api channel, with a tool policy covering the tools it drives:
[[users]]
name = "ops-bot"
role = "user"
# Bind the REST sender_id (the `sender_id` field on the /message request)
# to this user on the "api" channel.
[users.channel_bindings]
api = "ops-bot-sender-id"
# Grant the low-risk tools this operator uses; the RBAC gate now returns
# Allow for these instead of routing them through the guest gate.
[users.tool_policy]
allowed_tools = ["memory_*"]
With this in place the RBAC gate returns Allow for memory_*, so approval is no longer forced and the trusted_senders bypass applies on top exactly as designed.
If no [[users]] are configured at all, RBAC is inactive — the per-user gate returns Allow for every sender — and trusted_senders works standalone with no extra setup.
Enabling TOTP
The TOTP flow is entirely UI-driven in the dashboard, but the underlying endpoints work from any client.
- Open Settings → Security → Second Factor.
- Click Enable TOTP. The server calls
POST /api/approvals/totp/setupwhich generates a random base32 secret, stores it encrypted in the vault, and returns anotpauth://URI plus a PNG QR code. - Scan the QR in Google Authenticator, 1Password, Authy, or any RFC 6238 app.
- Important: copy the 10 recovery codes that appear once. They are shown only at enrollment and are the only way back in if you lose the device.
- Enter a live 6-digit code to confirm enrollment
(
POST /api/approvals/totp/confirm). The secret becomes active only after a successful confirmation. - Flip
second_factorinconfig.tomlfromnonetototp,login, orbothand restart the daemon — or just set it through Settings.
Until step 5 succeeds, the pending secret is inactive and can be discarded by calling setup again.
Dashboard Flow
The Approvals page (/approvals) shows three tabs:
- Pending — live list of waiting requests with the agent, tool name, risk level, and full payload. Click an entry to see the structured arguments the agent intends to pass.
- Audit — paginated audit log with decision, decider, and whether a
TOTP code was used (
second_factor_used = 1). - TOTP — enrollment/revocation UI with the QR code, recovery codes, and a "regenerate codes" action.
When TOTP is enforced, the approval card shows a 6-digit input. The
Batch Approve button is automatically disabled
(approvals.batch_disabled_totp) to prevent mass approval with a single
stolen code.
Modify & Retry. Rejected tool calls can be edited and re-run from the approvals card. Useful when an agent asks for almost the right thing but needs a small parameter change (e.g. a different file path).
API Reference
All endpoints live under /api/approvals.
| Endpoint | Method | Purpose |
|---|---|---|
/api/approvals | GET | List pending requests (add ?audit=1 for the log) |
/api/approvals/{id}/approve | POST | Approve (body { "totp_code": "123456" } when enforced) |
/api/approvals/{id}/reject | POST | Reject with optional feedback |
/api/approvals/totp/setup | POST | Generate secret, QR code, and recovery codes |
/api/approvals/totp/confirm | POST | Confirm enrollment with a 6-digit code |
/api/approvals/totp/status | GET | { enrolled, confirmed, enforced, remaining_recovery_codes } |
/api/approvals/totp | DELETE | Revoke enrollment (requires a valid TOTP or recovery code) |
Approving with TOTP enforced:
curl -X POST "http://127.0.0.1:4545/api/approvals/${ID}/approve" \
-H "Content-Type: application/json" \
-d '{"totp_code": "123456"}'
A successful verification starts a totp_grace_period_secs window during which
subsequent approvals on the same session skip the TOTP check. This balances
safety with usability during rapid iteration.
Session-Scoped Batch Resolution
Source: librefang-api/src/routes/approvals.rs
When multiple agents run concurrently, each session may accumulate several
pending approval requests at once. The global /api/approvals endpoint
operates across all pending requests regardless of which session created them.
The session-scoped endpoints let you narrow the scope to a single session,
making it practical to review and resolve an entire agent's pending queue
without accidentally approving requests from unrelated sessions.
New Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/api/approvals/session/:session_id | GET | List all pending approval requests belonging to session_id |
/api/approvals/session/:session_id/approve_all | POST | Approve every pending request in the session (body: { "totp_code": "…" } when TOTP is enforced) |
/api/approvals/session/:session_id/reject_all | POST | Reject every pending request in the session with an optional { "reason": "…" } |
Example — approve everything queued for a session:
SESSION_ID="01J3WXYZ..."
curl -X POST \
"http://127.0.0.1:4545/api/approvals/session/${SESSION_ID}/approve_all" \
-H "Content-Type: application/json" \
-d '{"totp_code": "123456"}'
Example — list pending requests for a session:
curl "http://127.0.0.1:4545/api/approvals/session/${SESSION_ID}"
Difference from /api/approvals/all
The existing GET /api/approvals endpoint (with no additional path segments)
returns all pending requests across every active session and agent. If you
batch-approve using that list, you act on requests that may belong to entirely
different agents running different tasks.
The session-scoped endpoints operate only on requests whose
session_id field matches the path parameter, giving you fine-grained control
when multiple concurrent agents are all waiting for human input simultaneously.
session_id Field on Approval Requests
session_id is an optional field on ApprovalRequest. Approval requests
created by code paths that predate the session-scoped feature do not carry a
session_id and will never appear in the session-filtered list. Those older
requests remain fully accessible through the existing per-UUID endpoints:
# Still works for any approval regardless of session_id
curl -X POST "http://127.0.0.1:4545/api/approvals/${APPROVAL_UUID}/approve" \
-H "Content-Type: application/json" \
-d '{"totp_code": "123456"}'
Recovery Codes
Ten single-use codes are generated at enrollment and stored in the vault as
totp_recovery_codes. Each code:
- is 10 alphanumeric characters, random;
- is consumed on use (removed from the vault);
- can be used in place of a TOTP code anywhere the endpoint accepts
totp_code— including theDELETE /api/approvals/totprevocation flow.
The GET /api/approvals/totp/status response exposes remaining_recovery_codes
so the dashboard can warn you before they run out. If they do, revoke and
re-enroll — this regenerates a fresh set.
Hardening Checklist
- Set
second_factor = "both"on anything exposed to the network. - Keep
require_approvalnarrow but honest — list every tool that writes to disk, executes shell, or hits a mutating API. - Configure
trusted_sendersonly for first-party automation, never for shared webhook IDs. A trusted sender skips the prompt only for low-risk tools. High-risk tools —shell_exec,file_write,file_delete,apply_patch, and the control-plane toolsagent_spawn/agent_kill/config_set/kernel_reload— always require explicit approval and remain subject to channel deny rules, even from a trusted sender. The list is a convenience exemption, not a blanket waiver on code execution or destructive mutations. - Use
totp_toolsto require the second factor onshell_exec,file_delete, andapply_patcheven when the broader approval list is wider — a compromised first factor then still cannot run destructive tools. - Prefer
timeout_fallback = "reject"over"allow"in production. - Store recovery codes in a password manager, not in the same place as the API token. If both leak, TOTP adds nothing.
- Rotate the vault key (
LIBREFANG_VAULT_KEY, 32 raw bytes) on suspected compromise — this invalidates the stored TOTP secret and forces re-enrollment.
See also: Security Operations for loop-guard and session-repair guardrails, and the Communication API reference for the full approval request/response schema.