Status: Stable — The three autonomy modes and the ApprovalManager are production-ready.
Autonomy Levels
FERAL’s autonomy system controls how much freedom the agent has to act without human approval. Three modes — strict, hybrid, and loose — determine which tools auto-execute and which require explicit confirmation.
Strict Mode
Every tool call requires user approval. The agent proposes an action and waits.
Best for: first-time setup, untrusted environments, auditing all agent behavior.
Strict additionally requires an explicit operator grant for any host shell
command. A folder covered only by the sandbox policy’s default roots is not
enough; see The two gates below.
Hybrid Mode
Safe actions (passive + active permission tiers) execute automatically. Risky actions (privileged + dangerous) require approval.
This is the default mode. Most users stay here — the agent handles research and information retrieval instantly, but pauses before actions with real-world consequences.
What Auto-Executes in Hybrid
Loose Mode
Everything except dangerous-tier tools auto-executes. The agent runs on full autopilot for most tasks.
dangerous tools still require approval even in loose mode. See Security Model for the hard-coded deny list.
The two gates
The autonomy mode is consulted at two separate points. Until recently the
persisted setting only reached one of them.
Gate 1: the approval gate (ToolRunner.enforce_safety). This is the one the
tables above describe. It decides whether a tool call executes, raises a pending
approval, or is denied outright:
- strict: anything not resolved as read-only needs approval.
- hybrid: only calls resolved at the
CONFIRM safety level need approval.
- loose: nothing needs approval. A
CONFIRM-level call is logged and
auto-executed. Hard-denied tools are still denied; loose does not reach them.
Gate 2: the shell execution-mode gate
(security/exec_mode.resolve_execution_mode). This decides whether a shell
command runs on the host, runs in the container, or is refused, as a function of
(command, resolved path, autonomy mode, grant state). Under strict, a host
shell requires an explicit operator grant for the working directory: a path
covered only by the sandbox policy’s own roots is refused, naming the policy
source that did cover it. hybrid and loose accept a policy-covered path.
Why it needed fixing
Both gates resolve the mode from the FERAL_AUTONOMY environment variable and
nothing else. The setup wizard wrote security.autonomy_mode to
settings.json, and api/state.py applied the persisted value to the
ToolRunner at boot, but nothing exported it. So the approval gate honoured
your pick and the shell gate stayed on hybrid. Picking “strict” silently ran
strict approvals against a hybrid shell policy.
ConfigLoader.export_as_env() now emits FERAL_AUTONOMY from
security.autonomy_mode, normalising anything outside strict / hybrid / loose
to hybrid, and BrainState.__init__ applies the export to os.environ before
anything reads it. Both gates now see the same value.
POST /api/autonomy updates the running ToolRunner and persists the choice to
settings.json, but it does not rewrite os.environ["FERAL_AUTONOMY"]. So
a live toggle changes the approval gate immediately and the shell gate only on
the next restart. Restart the brain if you are switching to or from strict and
care about the shell policy.
Undoing the file writes a turn made is declared safety_tier: "confirm" in the
coding_tools manifest, so it lands on gate 1 like any other confirm-tier tool:
strict and hybrid ask you, loose runs it. That is deliberately the
operator’s call rather than the tool’s. See
Coding Harness for what a
revert covers, and in particular for what it does not
(coding_tools__bash changes are never checkpointed).
Configuration
Environment Variable
The env var takes precedence over the config file, useful for per-session
overrides:
Precedence works by round trip rather than by special-casing: an
explicitly-set FERAL_AUTONOMY is merged into security.autonomy_mode by
_apply_env_overrides, and export_as_env then re-emits it verbatim.
Config File
Live toggle
Plan mode is a separate axis
Do not reach for autonomy modes to stop an agent changing things during a
research turn. Plan mode is per-session and ephemeral, and
refuses every mutating call at dispatch regardless of tier. The two are kept in
separate state and never read each other.
ApprovalManager
The ApprovalManager handles approval requests and maintains a list of standing approvals — pre-authorized tool+argument patterns that skip the approval prompt.
Standing approvals are stored in ~/.feral/exec_approvals.db and persist across restarts. They are managed today through the Python ApprovalManager API (used by skills, channels, and the orchestrator); a public CLI/REST surface for granting and listing them is tracked as a follow-up.
Approval Inbox API
Every blocked tool call also surfaces in a non-chat approval inbox so a web UI, mobile client, or automation can resolve it without typing into a chat session. See REST API → Approvals (Execution Inbox) for the full reference.
Both write endpoints accept an optional { "session_id": "…" } body. When supplied it must match the session that owns the pending request, or the call returns 409 session_mismatch. Resolving an unknown request returns 404.
Approval Flow
When a tool call needs approval, the following sequence occurs:
enforce_safety() determines the call requires approval and a pending request is created with a stable request_id.
- The Brain sends an
approval_request frame to the active chat session and the same request becomes visible at GET /api/approvals.
- The user responds, either by acknowledging in chat or by hitting
POST /api/approvals/{request_id}/approve or …/reject.
- If approved, the tool executes and the result flows back normally.
- If denied, the agent receives a “tool denied” signal and re-plans.
Escalation Rules