Policies
Which policy judges a run, what each backend is allowed by default, and how to write your own.
Every judged run is judged by exactly one policy document. Which one is decided at dispatch:
| the run | its policy | on the run record |
|---|---|---|
| says nothing about its network | its backend's baseline, plus the workload additions that name its permission class or grant | baseline:<name>+workload:<name>… |
is dispatched with --egress-policy | your document, on its own — no baseline, no additions | named:<path> |
is dispatched with --egress-open "<reason>" | none: the run's network is open | open:<reason>, <actor> |
Baselines
A baseline is the smallest set of hosts a backend needs to reach its own model. Port 443 unless noted.
| backend | default hosts |
|---|---|
claude-code | api.anthropic.com |
codex | api.openai.com, chatgpt.com |
droid | app.factory.ai |
qwen-code | token-plan.maas.qwencloudapi.com, token-plan.ap-southeast-1.maas.aliyuncs.com |
agent-loop, openai-compat | the model endpoint the run itself declares — its host, on the URL's port (443 for https, 80 for http, or the explicit port). An endpoint on a private address needs egress.allow_unsafe_networks |
proc | not applicable — no model, no network |
⚠ A backend with no shipped baseline (today: cursor) is refused as egress-baseline-unknown unless
the run is given your own document or an explicit --egress-open.
⚠ A model endpoint on a loopback address can never be named in a policy. A run whose backend points
at one is refused by name; the only way to run it is --egress-open.
Workload additions
An agent also runs a toolchain, and a toolchain fetches from its own registry. A silent run therefore
gets its baseline plus the shipped addition of each workload that names the run's permission
class or worktree grant. The mapping lives in the shipped workload documents, not in the class: for
example, the go addition names the shipped classes executor-go and executor-container. All are
port 443.
| workload | hosts |
|---|---|
go | proxy.golang.org, sum.golang.org |
npm | registry.npmjs.org |
android-gradle-maven | repo1.maven.org |
- A class or grant that declares no workload gets the bare baseline.
- A workload is honoured only for a shipped grant. A custom grant that merely reuses a shipped
grant's file name gets nothing, and the run record carries the note
EGRESS-WORKLOAD-GRANT-NOT-SHIPPED. - ⚠ Docker image pulls are not covered: the pull is made by the Docker daemon, not by the run, and
the door never sees it. A registry not listed above — PyPI, for example — is refused today; use your
own document or
--egress-openfor that run.
Your own document
--egress-policy judges the run by the one policy document stored in the engine's state directory,
<state dir>/egress/policy.json. If the file is missing, the run is refused before it starts.
⚠ Your document replaces the baseline; it is not added to it. Include the model hosts your backend needs, or the agent will not reach its model.
You can edit the document in the console, on the Egress page, or write the file directly. The console validates the document before saving and shows its digest.
The format
| field | meaning |
|---|---|
schema_version | must be 1 |
name | a label for people; it does not change the policy's identity |
rules[].id | unique within the document |
rules[].hosts | host names, exact or with one leading wildcard label (*.example.com, which does not match example.com itself). Addresses are not accepted here |
rules[].literals | IP addresses, or ranges written a.b.c.d/n or first-last, that the run may dial directly. A connection to an address that is not listed is refused |
rules[].ports | the ports the rule covers, 1–65535 |
rules[].verdict | allow, or deny — a deny is accepted only inside a strictly wider allow, to carve an exception out of it |
limits | seven connection ceilings, all required, all positive — see What no policy can open |
A document is refused when it loads if it has — among others — a schema_version other than 1, an
unknown key, no name, no allow rule, a missing or
duplicate rule id, a missing verdict, a rule without hosts, literals or ports, a port outside
1–65535, an address under hosts, a bare * wildcard, a range that touches the hard floor, two rules
for the same hosts with conflicting verdicts, a rule already covered by a wider one on the same ports,
a deny no wider allow covers, or a missing or non-positive ceiling. The refusal names the rule or
field.
Identity
A document is identified by the digest of its compiled form. Reordering rules or keys, changing whitespace or host-name case, or renaming it does not change the digest; changing a host, a port or a ceiling does. The digest of the document that judged a run is written on the run's record.
Private networks
The private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) can be opened only
when both hold:
- the installation allows it:
saphan config set egress.allow_unsafe_networks true; - the document names a range that lies wholly inside one of them, under
literals.
Write a single private address as a range, for example 10.1.2.3/32; a bare private address is
refused even with the setting on.
This applies to host names too: an internal host name that resolves to a private address is reached only when its address also falls inside a listed range.
Even then, loopback, link-local and metadata addresses stay closed. The console editor never accepts private ranges; set them in the file.
Opening one run
This runs one agent with an open network. The reason may not be blank, and it cannot be combined with
--egress-policy. The reason and the dispatching actor are written on the run record as
open:<reason>, <actor>, where they can be counted afterwards.
⚠ Use it for a run that genuinely needs a destination no policy names, or a lane that cannot carry the wall. Do not use it to get past a refusal you have not read: the refusal usually names a cheaper fix.
Not available yet
- Per-seat or per-machine policies. There is one document per installation; a run either uses it, uses its baseline, or is opened.
- Additional workload classes, such as PyPI.