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 bypass the gate
# 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.
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. - 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.