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. Any tool rated ≥ High pauses execution and asks a human for approval before running. Approvals can go to the Dashboard, to mobile as a push notification, 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, node.command (shell)

The rule: level ≥ High triggers an approval request. Medium triggers approval only when part of a plan (the whole plan is gated).

When approvals fire

Approval is middleware #10 in the orchestration pipeline. After the LLM returns a batch of tool calls, but before any of them execute:

  1. For each tool call, the middleware looks up the tool's risk level.
  2. If ≥ High, it sends an ApprovalRequest via IApprovalGate.
  3. The middleware waits for the result (default timeout: 5 minutes).
  4. Approved tools execute. Rejected or timed-out tools are replaced with an error message the LLM can react to.

Plan-level approvals work differently: if any step is ≥ Medium risk, the entire plan is gated by one approval before execution starts. You approve once, and the plan runs to completion.

What an approval looks like

public sealed record ApprovalRequest(
    string Id,
    string ToolName,
    string Description,          // Human-readable summary
    object Parameters,           // The exact args the LLM chose
    RiskLevel RiskLevel,
    string? Preview,             // Optional preview (e.g., draft email body)
    bool AllowEdit,              // Can the user modify params before approving?
    string? ReplyChannelConfigId,// Where to ask (Dashboard or channel)
    TimeSpan Timeout);

Four 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
  • TimedOut — treat as rejected

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 not in 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 Notifications and docs/NOTIFICATIONS.md in the Sophon repo.

Channel routing

If the original user message came from a channel (Telegram, WhatsApp, Slack, …), the approval request is routed back to that same channel. 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, reject, or edit.

The ReplyChannelConfigId field in ApprovalRequest is what enables this. The Approval middleware reads the message metadata and decides whether to prompt on Dashboard or on the channel.

Info requests (non-binary)

Sometimes an agent needs information, not a yes/no. InfoRequest is a sibling type:

public sealed record InfoRequest(
    string Id,
    string Question,
    IReadOnlyList<string>? Choices,  // Optional predefined answers
    TimeSpan Timeout);

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 choices [personal@…, work@…, side-project@…]. The user picks one; the agent continues.

Trusted tools and per-skill thresholds

You can override risk levels per agent or per skill:

  • Agent trust list — mark a specific tool as "never prompt" for a specific agent.
  • Skill trust toggle — disable approval for an entire skill (e.g., you've reviewed every tool in your custom skill and trust it).
  • Quiet hours — between 22:00 and 07:00 (configurable), auto-reject anything ≥ Critical and defer High to the next morning rather than pinging the user.

These overrides live in ~/.sophon/config/approvals.json:

{
  "quietHours": { "start": "22:00", "end": "07:00", "timezone": "Europe/Tirana" },
  "trustedSkills": ["my-internal-company-skill"],
  "perAgentOverrides": {
    "research": { "trustedTools": ["browser.navigate", "web.search"] }
  }
}

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