Summary
When [[users]] is configured (RBAC enabled) but a tool call has no sender_id / channel (autonomous loops, scheduled triggers without a synthesized SenderContext, internal dispatch sites that haven't been wired to system_call=true), AuthManager::resolve_user short-circuits to None and the call falls into guest_gate(). For any tool not on the read-only safe list, that returns UserToolGate::NeedsApproval — and the hand-agent auto-approval carve-out in submit_tool_approval is explicitly skipped when force_human=true. Net effect: every autonomous tool call (e.g. shell_exec, memory_recall, schedule_list) is queued for human approval and times out / floods the queue.
This is largely intentional for inbound calls from unknown channels, but it makes RBAC + autonomous hands effectively unusable: the moment you register a single [[users]] entry, all your background agents stop functioning.
Reproduce
~/.librefang/config.toml:
[[users]]
name = "Owner"
role = "owner"
[users.channel_bindings]
telegram = "12345"
- Activate any hand with an autonomous agent loop (e.g. browser hand:
[agents.main.autonomous] heartbeat_interval_secs = 30).
- Watch
/api/approvals fill with memory_recall, shell_exec, schedule_list requests, each timing out after timeout_secs.
Logs show WARN Approval system error tool_name="memory_recall" error=Too many pending approval requests for this agent once the per-agent backlog cap is hit, killing the loop.
Why the obvious fixes don't work
[users.tool_policy] allowed_tools = ["*"] — only consulted if resolve_user returns Some(uid). Autonomous calls have no sender/channel, so resolution fails before Layer A runs.
[users.channel_bindings] — same problem: there's nothing to bind to. The autonomous loop carries no inbound channel.
trusted_senders — same: there's no sender_id to trust.
- Hand auto-approval carve-out in
kernel/mod.rs submit_tool_approval — skipped because force_human=true when the user gate produced NeedsApproval. The comment correctly notes this is the intended Phase-2 behaviour for chat-driven hand agents, but it conflates "hand invoked by a Viewer in chat" (must gate) with "hand running its own autonomous loop" (no user involved).
system_call=true — only set today by the cron dispatcher (kernel/mod.rs ~10876). Autonomous heartbeat dispatch and other internal call sites don't set it, so they fall through to user RBAC.
Suggested directions (open to feedback)
- Treat autonomous-loop tool calls as
system_call=true when there is no inbound SenderContext. The autonomous loop runs under the agent's own identity, not on behalf of any user; it should bypass per-user RBAC the same way cron does. Risk: a malicious manifest could mark itself autonomous; mitigated because the manifest is operator-controlled.
- Allow agents to carry an "owner" identity at spawn, so calls without an inbound
SenderContext resolve to that identity for RBAC purposes. The owner of the autonomous agent is configured at install time; tool calls inherit their policy.
- Distinguish hand auto-approve from user-context auto-approve: keep the
force_human skip for inbound user messages, but allow the carve-out to fire when the call has no sender_id/channel at all (i.e. it isn't a user-driven request).
Pointers:
crates/librefang-kernel/src/auth.rs:549-555 — resolve_user short-circuit
crates/librefang-kernel/src/auth.rs:976-996 — guest_gate safe list
crates/librefang-kernel/src/kernel/mod.rs:16169-16191 — hand auto-approve carve-out skipped under force_human
crates/librefang-kernel/src/kernel/mod.rs:11930-11960 — cron's system_call=true synthesized SenderContext
Workaround
Remove the [[users]] block (disable RBAC entirely). Global require_approval then governs alone.
Environment
- librefang
2026.4.24-beta5 (docker-packaged)
- Hand triggering most failures:
browser (autonomous loop on [agents.main])
Summary
When
[[users]]is configured (RBAC enabled) but a tool call has nosender_id/channel(autonomous loops, scheduled triggers without a synthesized SenderContext, internal dispatch sites that haven't been wired tosystem_call=true),AuthManager::resolve_usershort-circuits toNoneand the call falls intoguest_gate(). For any tool not on the read-only safe list, that returnsUserToolGate::NeedsApproval— and the hand-agent auto-approval carve-out insubmit_tool_approvalis explicitly skipped whenforce_human=true. Net effect: every autonomous tool call (e.g.shell_exec,memory_recall,schedule_list) is queued for human approval and times out / floods the queue.This is largely intentional for inbound calls from unknown channels, but it makes RBAC + autonomous hands effectively unusable: the moment you register a single
[[users]]entry, all your background agents stop functioning.Reproduce
~/.librefang/config.toml:[agents.main.autonomous] heartbeat_interval_secs = 30)./api/approvalsfill withmemory_recall,shell_exec,schedule_listrequests, each timing out aftertimeout_secs.Logs show
WARN Approval system error tool_name="memory_recall" error=Too many pending approval requests for this agentonce the per-agent backlog cap is hit, killing the loop.Why the obvious fixes don't work
[users.tool_policy] allowed_tools = ["*"]— only consulted ifresolve_userreturnsSome(uid). Autonomous calls have no sender/channel, so resolution fails before Layer A runs.[users.channel_bindings]— same problem: there's nothing to bind to. The autonomous loop carries no inbound channel.trusted_senders— same: there's nosender_idto trust.kernel/mod.rssubmit_tool_approval— skipped becauseforce_human=truewhen the user gate producedNeedsApproval. The comment correctly notes this is the intended Phase-2 behaviour for chat-driven hand agents, but it conflates "hand invoked by a Viewer in chat" (must gate) with "hand running its own autonomous loop" (no user involved).system_call=true— only set today by the cron dispatcher (kernel/mod.rs~10876). Autonomous heartbeat dispatch and other internal call sites don't set it, so they fall through to user RBAC.Suggested directions (open to feedback)
system_call=truewhen there is no inboundSenderContext. The autonomous loop runs under the agent's own identity, not on behalf of any user; it should bypass per-user RBAC the same way cron does. Risk: a malicious manifest could mark itself autonomous; mitigated because the manifest is operator-controlled.SenderContextresolve to that identity for RBAC purposes. The owner of the autonomous agent is configured at install time; tool calls inherit their policy.force_humanskip for inbound user messages, but allow the carve-out to fire when the call has nosender_id/channelat all (i.e. it isn't a user-driven request).Pointers:
crates/librefang-kernel/src/auth.rs:549-555—resolve_usershort-circuitcrates/librefang-kernel/src/auth.rs:976-996—guest_gatesafe listcrates/librefang-kernel/src/kernel/mod.rs:16169-16191— hand auto-approve carve-out skipped underforce_humancrates/librefang-kernel/src/kernel/mod.rs:11930-11960— cron'ssystem_call=truesynthesized SenderContextWorkaround
Remove the
[[users]]block (disable RBAC entirely). Globalrequire_approvalthen governs alone.Environment
2026.4.24-beta5(docker-packaged)browser(autonomous loop on[agents.main])