Skip to main content
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.

coding_tools__revert_turn is governed by it

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:
  1. enforce_safety() determines the call requires approval and a pending request is created with a stable request_id.
  2. The Brain sends an approval_request frame to the active chat session and the same request becomes visible at GET /api/approvals.
  3. The user responds, either by acknowledging in chat or by hitting POST /api/approvals/{request_id}/approve or …/reject.
  4. If approved, the tool executes and the result flows back normally.
  5. If denied, the agent receives a “tool denied” signal and re-plans.

Escalation Rules