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.Hybrid Mode
Safe actions (passive + active permission tiers) execute automatically. Risky actions (privileged + dangerous) require approval.
What Auto-Executes in Hybrid
Loose Mode
Everything exceptdangerous-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
CONFIRMsafety 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.
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 theFERAL_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 what a turn did 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).
Earned autonomy
Under hybrid only, a tool that has run cleanly five times in a row stops raising the confirm-tier prompt. A single failure resets the streak to zero, and reverting a turn resets every tool: the tool reported success and you disagreed, which is a stronger signal than a failed execution.Trust never exceeds undo
Latitude is granted only for actions FERAL can take back, which is exactly whatskills/checkpoints.py covers:
Nothing else is ever promoted, however long its record. Sending email has no
unsend; retraction on Slack, Telegram, WhatsApp and iMessage is time-bounded,
provider-specific and does not un-notify anybody; a purchase moves money;
coding_tools__bash can change anything and is not checkpointed at all. That
is the answer to “why does it still ask about sending email?” - because it
cannot unsend it.
Three boundaries hold regardless of track record: strict is never
overridden (choosing it is a request to be consulted, and a record is not
consent), a DENY verdict is never promoted, and no tool gets credit for runs
the ledger did not itself observe.
GET /api/approvals/trust reports where every eligible tool stands.
FERAL_TRUST_PROMOTE_AFTER changes the threshold; FERAL_TRUST_DISABLED=1
turns the whole thing off.
Configuration
Environment Variable
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
TheApprovalManager handles approval requests and maintains a list of standing approvals — pre-authorized tool+argument patterns that skip the approval prompt.
~/.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.{ "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 stablerequest_id.- The Brain sends an
approval_requestframe to the active chat session and the same request becomes visible atGET /api/approvals. - The user responds, either by acknowledging in chat or by hitting
POST /api/approvals/{request_id}/approveor…/reject. - If approved, the tool executes and the result flows back normally.
- If denied, the agent receives a “tool denied” signal and re-plans.
