Sophon 2.0 is here
Sophon Docs
Sophon Node

Gates, Limits & Audit

The four checks every node command passes, the per-command rate limits, the on-device consent prompt, and what the audit log records.

Every command that reaches a node crosses four gates in a fixed order. The first one that says no ends it, and each refusal is recorded distinctly, so "it didn't run" is always answerable with which gate stopped it.

The four gates

OrderGateChecksOutcome on refusal
1HandlerThe command type exists at allrejected.unknown
2Permission scopeThe node holds the scope this command needsrejected.permission
3Rate limiterThis command type is not being called too fastrejected.rate_limit
4ConsentThe person at the machine approved itrejected.consent

The scope check happens twice on purpose: once on the Gateway before dispatch, and again on the node itself. A node does not assume the server upstream is honest.

Rate limits

Each command type carries its own burst allowance and refill rate, applied on the node. They exist to bound a runaway loop, not to shape normal use: ordinary automation never touches them.

Command typeBurstRefill
screen.capture, screen.region, screen.queryElements105 per second
input.mouse.move6060 per second
input.mouse.click, input.mouse.scroll, input.keyboard.press2020 per second
input.keyboard.type, input.keyboard.hotkey1010 per second
app.launch, app.close41 per second
app.focus, clipboard.read, clipboard.write105 per second
notify.show51 per second
system.execute30.5 per second
everything else, including the activity.* and filesystem.* commands3015 per second

A refusal here is recorded, not silent. A node being hammered shows up in the audit log rather than merely going quiet.

This is not the same thing as the Gateway's HTTP rate limiting, which bounds API callers rather than desktop commands. See Rate Limiting for that.

Some commands raise a native dialog on the node's own screen before they run, a final local line of defence that holds even if everything upstream is compromised. The prompt names the command and, for a shell command, the exact command line about to execute.

It times out after 15 seconds and fails closed: no answer means refused.

It also fails closed when the prompt cannot be shown at all. A node with no interactive desktop session cannot obtain consent, so it cannot run consent-required commands. That is one of the reasons a node is meant to run as a user-level service, not a system one.

By default this covers system.execute and canvas.present. The list is configurable per device through consentRequiredCommands in the config file; adding a command type to it makes that command ask locally every time.

Approving a desktop run can also cover further actions at that risk level, on that machine, for the rest of the run, so a five-step batch does not produce five prompts.

It is bounded on every axis:

  • Interactive chat turns only. A workflow, a scheduled or cron run, a heartbeat turn, a webhook turn and an MCP call are never covered: each one asks again on every single call. Nobody is watching those runs, so nothing is carried through them.
  • Thirty minutes, then it lapses.
  • One conversation. It does not leak into another.
  • Held in memory, never written to disk. A Gateway restart clears it.
  • Never covers Critical. A shell command asks again, every time, no matter what was approved before.
  • Dropped when the session's work is cancelled, which is what Stop in the Dashboard chat does.

An automatic approval never establishes it. Only a person can, and the prompt only promises run-wide coverage where that coverage is real: a caller that will be asked again every time is not told otherwise.

Approvals fire once, before execution

An approval is raised before the tool runs, not partway through it, so a refusal means nothing happened rather than something half-happened. It is raised once per call, and it is delivered to the channel the conversation is actually in — Telegram, WhatsApp, and the rest — rather than only to a Dashboard nobody has open.

At the shipped default threshold, a node shell command raises two prompts: the general one for the tool call, then the Critical one carrying the exact command line about to run. It is redundant rather than unsafe, and it fails closed in both places. Collapsing the two is a roadmap item, not current behaviour.

The shell denylist

Beyond scope, approval, and consent, the node refuses a built-in list of catastrophic command patterns outright. It covers recursive deletes of a filesystem root, disk formatting, shutdown and reboot, piping a download straight into a shell, and a few others in the same family.

The denylist is a backstop, not the security model. It catches the obviously destructive; it is not a sandbox and cannot be relied on to make shell access safe. If you need shell access to be safe, do not grant it.

Note also that a shell command must arrive as one complete string. Separate arguments are refused, precisely so that approval, denylist, audit, and execution are all reviewing the same text. An argument list that gets recombined later is a place for the reviewed command and the executed command to drift apart.

Audit outcomes

Every dispatch produces exactly one outcome:

OutcomeMeaning
rejected.unknownNo handler for that command type
rejected.permissionThe node does not hold the required scope
rejected.rate_limitToo many of that command type, too fast
rejected.consentRefused at the machine, or the prompt timed out
completedRan and returned a result
failedRan and returned an error
cancelledStopped in flight, by a person or by a cancelled turn
errorThe node could not process the command at all

Each carries the node, the command type, the scope that authorised it, timing, and the initiating user. They stream into the Gateway's audit log and appear inline in the Dashboard chat and on the Devices page.

The node also reports per-command latency and per-gate refusal counts with its heartbeat, so a device refusing everything is visible without opening the log. If it is offline when something happens, audit events are buffered locally and delivered when it reconnects.

Where to go next