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, list_agents / list_skills, …) and returns "needs approval" for everything else — including memory_store / memory_recall, which are otherwise low-risk, and including web_search / web_fetch / tool_load / tool_search, which are gated as network access and capability discovery (#6908).
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.
The binding is now enforced, not merely consulted (#7744).
An authenticated caller below Admin may only assert a sender_id that resolves back to themselves through their own channel_bindings; anything else is silently replaced with their authenticated identity on the api channel, and the request still succeeds.
So the ops-bot recipe above keeps working, while a User-role bearer can no longer put another user's platform id in the request body and inherit that user's role, tool policy and peer memory.
Admin and above are exempt, because an operator impersonating a user for support, or a gateway relaying real platform users, is a legitimate use of the field.
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.