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:
| Level | Meaning | Example tools |
|---|---|---|
None | Read-only, deterministic, cannot affect anything | datetime.now, memory.search |
Low | Read with side-effects, or minor writes | browser.navigate, document.extract |
Medium | Writes data, sends messages you could undo | memory.write, calendar.create_event |
High | External, costly, or hard to reverse | gmail.send, workflow.create, connection.configure |
Critical | Destructive or privileged | system.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:
- For each tool call, the middleware looks up the tool's risk level.
- If ≥ High, it sends an
ApprovalRequestviaIApprovalGate. - The middleware waits for the result (default timeout: 5 minutes).
- 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
TimedOutand 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
- Orchestration Pipeline — Approval middleware
- Planning — plan-level approval
src/Sophon.Core/Approval/in the Sophon repo for the authoritative types