Sophon 2.0 is here
Sophon Docs
Core Concepts

Approval Gates & Risk Levels

How Sophon classifies tool risk, when approvals fire, and how users approve, reject, or edit before the action runs.

Sophon treats "the agent wants to do something irreversible" as a first-class concept. Every tool has a risk level, and the host carries an approval threshold — Medium by default. A call whose effective risk lands at or above the threshold pauses execution and asks a human before running, and a Critical action always asks whatever the threshold says. Approvals can go to the Dashboard, to mobile as a push notification, to a voice surface, or back to the channel the message came from (Telegram, WhatsApp, etc.).

This is what keeps Sophon safe by default.

Risk levels

There are five levels, declared by each tool in its manifest:

LevelMeaningExample tools
NoneRead-only, deterministic, cannot affect anythingdatetime.now, memory.search
LowRead with side-effects, or minor writesbrowser.navigate, document.extract
MediumWrites data, sends messages you could undomemory.write, calendar.create_event
HighExternal, costly, or hard to reversegmail.send, workflow.create, connection.configure
CriticalDestructive or privilegedsystem.execute, memory.forget_all, remote.control (shell action)

The rule is evaluated per call, in this order:

  1. A per-tool override replaces the tool's static risk. An entry in Sophon:ApprovalPolicy:PerToolOverrides (case-insensitive) is a full replacement — it can raise a level or lower it.
  2. An assessed per-call risk can only raise it. Tools whose risk depends on their arguments — remote.control, for one — classify each call, and the effective risk is the higher of the two. An override can never push a call below what that call actually assessed to.
  3. Critical always prompts. If the tool's static risk or the assessed call risk is Critical, the request is raised at Critical regardless of any override or threshold. On such a tool an override changes nothing at all.
  4. Otherwise, prompt when the effective risk is at or above the threshold. Sophon:ApprovalPolicy:DefaultRiskThreshold defaults to Medium; an unparsable value falls back to Medium.

The threshold and the per-tool overrides are edited at Settings → Security → Approvals and apply to the next tool call — no Gateway restart.

Whole-plan gating is separate — it's governed by PlanApprovalMode, not by individual step risk (see plan approvals).

A remote.control run carries a batch of desktop actions and raises one approval, at the highest risk in the batch. See one approval for the whole run.

When approvals fire

The gate sits immediately in front of tool execution in the orchestration pipeline. As of 2.0 there is one prompt, and it arrives before the tool runs — earlier versions asked twice, and the second prompt turned up after the tool had already executed.

  1. For each tool call, the gate resolves the effective risk (override, assessed risk, Critical floor, threshold).
  2. If the call needs approval, it raises one request.
  3. The gate waits for the result (default timeout: 5 minutes).
  4. Approved tools execute. Rejected, cancelled and timed-out calls never run, and come back to the LLM as an error result it can react to.

One exception, and it is a fail-safe rather than a fail-open: a shell command on a paired desktop carries its own Critical approval, with the exact command string as an editable preview. At the default Medium threshold the generic gate prompts first, so you see two prompts for one shell command — the plain one, then the one showing the command. Collapsing them is a follow-up; a per-tool override is the lever if it matters today.

Plan-level approvals work differently: gating is governed by PlanApprovalMode (Sophon:AgentExecution:PlanApproval, live-reloaded). Interactive sessions gate the entire plan by default (Always) — you approve once, before execution starts, and the plan runs to completion. Unattended runs (heartbeat, cron, subagent, webhook) auto-proceed past the plan gate; individual steps still gate on their own when a call reaches the threshold, and always at Critical.

What an approval looks like

Over the API, an approval request carries:

FieldTypeMeaning
idstringUnique request identifier
toolNamestringThe tool awaiting approval
descriptionstringHuman-readable summary of what the tool will do
parametersobjectThe exact arguments the LLM chose
riskLevelstringThe effective risk the request was raised at, after overrides and per-call assessment
previewstring (optional)Preview of the effect (e.g., draft email body)
allowEditbooleanCan the user modify parameters before approving?
replyChannelConfigIdstring (optional)Where to ask (Dashboard or channel)
timeoutdurationHow long before the request auto-times out

Five possible outcomes:

  • Approved — run with the original parameters
  • Edited — run with user-modified parameters (only if AllowEdit)
  • Rejected — skip, return an error to the LLM
  • Cancelled — the caller or the run ended the wait. A cancel is not a decision, so it can never resume anything or stand in for an approval
  • Timed out — treated as rejected

An outcome is recorded exactly once. If two devices answer the same card, the first answer is the one that counts and the second is told what was already recorded, and a request that timed out or was cancelled can never later become an approval.

How users approve

Dashboard

Open Dashboard → Approvals. Pending requests show the tool, risk level, parameters, and preview. Buttons: Approve, Edit (if allowed), Reject. A countdown shows remaining time before auto-timeout.

Mobile push

If the user has a registered mobile device with approvalRequests notifications enabled (and is not inside their push quiet hours), the request arrives as a push notification with iOS/Android actionable buttons: tap Approve or Reject without opening the app.

Push is bridged through Expo → APNs/FCM. See Approving from where you are and Push Notifications.

Channel routing

New in 2.0: if the conversation is running on a channel (Telegram, WhatsApp, Slack, …), the approval request reaches that same channel. Before, nothing filled in the reply channel, so a channel user could be blocked on a gate they never saw. The user sees:

Sophon wants to send an email to team@example.com: Subject: Q3 competitive analysis Body: Attaching the comparison we discussed…

Reply approve <id> or reject <id>.

The replyChannelConfigId field carries the destination. It is filled in from the inbound message's channel metadata, so prompts raised outside a conversation turn — an MCP tools/call, a workflow, a background job — have no channel to go to and are answered on the Dashboard or mobile instead. Web chat and voice sessions also resolve to no channel: they already receive the request over SignalR.

A channel reply is accepted only from the chat the request was delivered to. The delivery point is recorded before the prompt is sent, so later traffic on the same bot cannot move it, and a member of some other group wired to the same bot can no longer answer by guessing an id. Two limits worth stating plainly, both roadmap rather than mitigations: there is no per-sender binding, so anyone in the delivery chat can answer, and after a Gateway restart a still-pending approval loses that recorded delivery point and falls back to the channel's default chat.

By voice

An approval raised on a voice conversation is read out and can be answered out loud — on the Dashboard Voice page, on the Command Bridge, and in Sophon Mobile's Voice tab. Risk decides what counts as an answer: None, Low and Medium approve on a plain "yes", while High and Critical need the spoken phrase "yes, approve" — a bare "yes" is too easy to mishear or say by reflex. An unclear answer is asked again rather than guessed. See Approvals & Questions by Voice.

Voice started inside a chat (the chat voice ribbon) deliberately never speaks prompts — that chat already has its own approval card on screen.

Multi-device approval routing

Approval events are session-scoped: the detailed approval request — tool, parameters, preview, action buttons — goes to the devices actively in that session. Every other device still gets a lightweight badge, so wherever you are, you can see there's a pending approval and navigate to it. Pending counts are tracked per session, so switching between open conversations always shows the right approval state for each. See SignalR API for the event details.

Answer in one place and it closes everywhere. When a request is decided, times out, or is cancelled, a resolution event closes the open chat cards on Dashboard and Mobile and any voice prompt waiting on it, and the history row shows how it actually ended — Approved, Edited, Rejected, Cancelled or Timed out — instead of a blanket "Rejected".

Info requests (non-binary)

Sometimes an agent needs information, not a yes/no. An info request is a sibling of the approval request, and as of v1.15 it's a structured ask: one ask carries 1–4 questions, each with a short header chip and 2–4 options (label + description), with optional multi-select — and free text is always allowed alongside the options. Answers arrive from the Dashboard question card (or simply typing in the composer while the question is active), sequential CLI pickers, the mobile card, or numbered-option replies parsed on external channels. An unanswered ask expires with an explicit TimedOut result — the first option is never silently assumed.

Example: the agent needs to send an email but you have three configured accounts. Instead of guessing, it asks "Which email address should I send from?" with an option per account (plus free text). The user picks one; the agent continues.

Pending asks are exposed at GET /api/info-requests/pending and answered via POST /api/info-requests/{id}/respond. See the approvals guide for the full interaction model.

Approvals raised inside a plan step

New in 2.0. A tool approval or a question raised inside a plan step, or by a sub-agent working on your behalf, is escalated to the conversation you are actually looking at. The card renders in your main chat on the Dashboard, in Sophon Mobile and in the CLI, and goes out to the channel the conversation came from; a plan-step request is tagged with the step it came from, and the plan card flips that step to a waiting state with a link into its own session. Before this, a sub-agent's approval request could expire without ever being shown to anyone.

What it does not do is survive a restart. These waits live in memory and are bounded by Sophon:GraphRuntime:ChildHitlTimeout (15 minutes by default), so a Gateway restart while one is open fails that step attempt outright and the retry ladder re-runs it. Durable, restart-proof gates exist, but they are used for re-approving a revised plan, not for tool approvals inside a running step.

Nobody answering is a decision too, and it is always the safe one: an unanswered approval is rejected, and an unanswered question tells the step to proceed on its best judgment. Never an auto-approve, never a silently picked first option.

Tuning the policy

Two knobs, both host-wide:

  • Threshold — the level at or above which a call needs approval. Default Medium.
  • Per-tool overrides — replace a single tool's declared risk, up or down.

Both live under Sophon:ApprovalPolicy and are edited at Settings → Security → Approvals. A save takes effect on the next tool call; the Gateway is not restarted and no snapshot is held from startup.

{
  "Sophon": {
    "ApprovalPolicy": {
      "DefaultRiskThreshold": "Medium",
      "PerToolOverrides": {
        "email.send": "High",
        "code.execute": "High",
        "remote.control": "Critical"
      }
    }
  }
}

An override on a Critical tool is inert — rule 3 above decides that call before the override is consulted.

Separately, a single conversation can be put into auto-approve with a risk ceiling of your choosing, so routine calls below that ceiling run without a card. Critical is never auto-approved, whatever the ceiling says, and every auto-approval is still written to the audit trail as such.

Unattended runs (heartbeat)

Heartbeat runs an agent on a schedule with nobody watching. The gate itself is the same as in an attended session — a call at or above the threshold (Medium by default) raises an approval request — but unattended there is nobody to answer, so the request pages you and, on timeout, the action is rejected (default-deny). With the agent's autoApprove setting enabled, the heartbeat session self-approves anything that asks for approval at Medium risk or below, so checklists that act can run without stalling. Plan approvals never stall unattended runs at all: under PlanApprovalMode, heartbeat — like cron, subagent, and webhook runs — auto-proceeds past the plan gate. That ceiling is hard: High and Critical actions — shell commands, destructive operations — always page you, regardless of any heartbeat setting, and reject on timeout.

What the ceiling does not hold back is ordinary messaging: message.send is Medium risk, so an auto-approving heartbeat can send messages — including email — on your behalf. If you want outbound email held for human approval, add an explicit rule to the agent's HEARTBEAT.md checklist.

Audit trail

Every approval decision is logged with timestamp, user, tool, parameters, outcome, and (if approved) the resulting tool output. The Dashboard → Admin → Audit page shows the full history, filterable by user / tool / date.

Rejections don't auto-retry. The agent sees the rejection as a tool error and decides what to do next — usually asking the user for a different approach.

Multi-user approvals

In multi-tenant deployments, any user in a tenant can approve any pending request. Approvals are not tied to the user who sent the message — a manager can approve their direct report's pending email, a security team can reject an exfiltration attempt before anyone else sees it.

This is a deliberate choice for team environments. If you need stricter isolation, scope agents to individual users and the approval will only reach that user.

Timeout behavior

  • Default timeout: 5 minutes.
  • On timeout, the request transitions to TimedOut and the tool is rejected.
  • Timeout is configurable per ApprovalRequest (workflows with long approval cycles can set 30 min or longer).
  • A timed-out approval does not auto-retry. The agent has to re-invoke the tool if it still wants to.

Where to go next