Documented limits
Everything below is a limitation of the current release.
Everything below is a limitation of the current release. We publish it because a control you believe in and do not have is worse than one you know you lack.
An invitation or new-password link is a secret in a URL, and under tls_mode = "proxy"
your fronting server logs it. The authorization server hands an administrator a one-time
link of the form https://<issuer>/claim?t=<token> to pass on to a colleague
(OAuth admin panel §3 and §4). Inside this binary the token is handled as a secret: the
claim page sets Referrer-Policy: no-referrer so it does not leave in a Referer header,
the trouble log deliberately records the route and never the URL, the token is stored only
as a digest, and it is spent on first use. None of that reaches the log of a server in
front of us. A reverse proxy terminating TLS records the full request path in its access
log by default — combined, common and JSON log formats all include it — so the live token
lands in a file with a different retention policy from this server's, and possibly on a
different machine. That is outside this binary's control and it is not a defect we can fix
here; it is a deployment fact you should know before you choose proxy mode.
What to do about it, in the order we would do it: strip or redact the query string in the
fronting server's access-log format for the /claim path; failing that, treat that access
log as holding credentials and give it the retention and the permissions of one. The window
is small — an invitation lasts three days, a new-password link one hour, and either dies on
first use — but a link read out of a log before its owner opens it is a complete way in to
that account. Under tls_mode = "direct" there is no fronting server and this limit does
not apply.
Network egress is unrestricted for a run that does not ask to be judged, and that is every run in this fleet today. The confinement boundary governs writes. It does not restrict what an agent reads (beyond the named credential-file denial in section 8), and for a run that declares no egress policy it does not restrict where it connects: on macOS the profile denies writes but permits reads and network; on Linux the read-only root bind makes the whole filesystem readable and no network namespace is created; on the remote Landlock boundary, read and execute are granted everywhere by construction. Such an agent can read credentials present on that machine and reach the network freely. Treat egress control as your responsibility at your own network boundary, and keep nothing on a runner beyond that runner's own scope, until every dispatch in your fleet opts in. This is the industry's position as well: major agent vendors document the same limitation about their own products, and at least one states plainly that a session with networking fully disabled can still reach its own API, and so can still move data out. Filesystem confinement does not prevent exfiltration.
The answer to egress is not a container boundary. An earlier version of this section named
one; that direction was superseded by a ratified design, and what that design describes now
ships. The answer is an egress proxy the run is confined to: a run that declares a policy
reaches the network only through a per-run door, which admits a connection only if that policy
admits that host and that port, and refuses everything else — a total judgement whose
default admits nothing. Four properties are load-bearing and all four are in the build. The
proxy is a tunnel, not an HTTP proxy: it terminates no TLS and parses no HTTP, which is what
deletes the request-smuggling class by construction rather than by care. A run's identity is a
UNIX socket created for that run alone, so there is no credential to steal or forge and no
port to scan. A policy binds to a run by digest, never by name, and a seat's policy can only
narrow its machine's, never widen it — ⚠ that last clause describes egress.Effective, which
ships and which nothing calls (below). And the enforcement ladder ends in a refusal: network
namespace, then cgroup v2, then a separate UID, then the run does not start. Filtering by
process id does not exist. ⚠ The saphan-egress daemon this paragraph used to route the run
through is not what shipped: that daemon is NOT BUILT; the door is served in the run's own lifecycle
locally, and by the far host's own copy of this binary over ssh
(Egress §8, §9A, §9B).
What of that is shipped, precisely — and "shipped" is not the word. Three states, and
they must not be collapsed into two: NOT BUILT · BUILT, NOT WIRED · BUILT AND LIVE. What
each one means is defined in exactly one place — Egress §2's legend table — and
is deliberately not restated here, because a vocabulary written out in N documents drifts in
N−1 of them; the product refuses a second copy of any of the three
definitions in either document. §8 of that document carries the per-piece table the three words
belong to, and its rows are re-measured against the tree by that same test rather than asserted
in prose. As of 2026-09-02 six rows of that table are BUILT AND LIVE, and this sentence
replaces one that said none were — measured against §8's claim block in both directions, so
neither wording could be deleted to silence it. ⚠ The two most recent moves were CORRECTIONS,
not shipments: the console rule editor and the audit record surface were both ruled
"NOT BUILT" over code that was already in the build — for how long is not stated here, because
nobody has measured it — and §8's own guard was green over both because the absence predicates it evaluated
named symbols no author had chosen (Egress §8.1, blind spot 6). Read that as the
posture of this whole section: the direction of the arc's errors has now reversed, from
promising more than the code delivers to denying code that ships.
The retired wording for both was the single phrase "NOT BUILT", and it is quoted here rather
than asserted for a mechanical reason worth knowing: a check added the same day
(TestProseThatSpellsAStateTokenSpellsTheOneSection8Rules) refuses any sentence that names one
of these mechanisms and spells a state token that is not the one §8 rules — quotation marks are
how this pair of documents says "this is the sentence we retired" without asserting it again.
READ THAT NARROWLY. It means a production run can be governed — not that any has
been. ⛔ No run in this fleet has passed --egress-policy; the mechanism ships, it is
tested end to end, and it is opt-in and so far unused. It does not mean this fleet's runs
are confined by default: a run that does not pass the flag composes no network cut, mints no
socket and never has a door opened for it, exactly as before. Turning it on fleet-wide is an owner's act and has
not been taken, so the limits below still describe every silent run.
The check that would refute the paragraph above, so it is a claim and not a mood — and it is TWO reads, because one of them was blind where it mattered most:
Today both return zero rows. remark: read (a) alone — the only read this section carried until
2026-08-30 — cannot see a refused run, because the digest is written after the door opens and every
refusal ends the run before that; measured through every refusal path a policy-declaring run can
reach in the product — the control reports the count, which this
line deliberately does not restate — and that control is also what keeps read (b)'s term matching
them, and what keeps the copy of the query ABOVE identical to the one in
Egress §8 that owns it.
remark: a refusal raised at the CLI (an absent or unreadable policy document,
egress-unavailable) writes NO row at all, so neither read can see it — that one is on the
operator's terminal only. ⚠ Nothing turns red when the "no such row" claim itself goes stale —
Egress §8.1's assigned: predicate measures the ASSIGNMENT in the source, and its
blind spot 6 says in its own words that it cannot see whether a run was given one — so this is a
fact to re-check by query, not one the suite carries.
BUILT AND LIVE — reachable by a run that asks:
- the policy document, the per-run socket, the assignment and the network-layer ladder. A run
passing
--egress-policyis judged by the one canonical document (<state-dir>/egress/policy.json, the file the console's egress page edits): the engine mints a per-run UNIX socket, opens a real SOCKS5 door on it under that policy, composes the confinement's network cut, hands the agentALL_PROXY, records the policy's compiled digest on the run's row, and unlinks the socket when the run ends. Every failure on that path REFUSES the run at $0 — an absent or malformed document, a host with no measured egress rung, a lane that cannot carry a door (ssh, vendor-delegated confinement, a container spawn) — because a run told it would be governed and started anyway is the one outcome the control exists to prevent.
BUILT, NOT WIRED — the part that needs an auditor's eyes, because it ships:
- the
CONNECTfront-end and the intersection below are what remains here. ⚠ The judging core and its SOCKS5 front-end have MOVED OUT of this list: a library with a closed vocabulary of seventeen dial-time refusal classes, it is now reached in production through the product, which the product wires into the engine's run dependencies. Traffic from a run that declares a policy goes through it; - the
CONNECTfront-end — the door that reads aCONNECTrequest and judges it is in the build with no caller at all outside its own package's tests. Its transport is deliberately absent: measurement showed a mainstream client can reach a proxy over a per-run UNIX socket through SOCKS5 but not through an HTTP proxy, so that is an open question rather than a coding task; - ⚠ the policy document and the per-run socket have BOTH moved to the list above (2026-08-30)
and are named here only so a reader of an older edition can see where they went. The document —
a schema-versioned JSON file, its parser, its load-time refusals and the SHA-256 digest that is
the policy's identity, 650 lines of non-test code — is now read by a run rather than only by a
test; the lifecycle mints the socket, opens the door, hands the agent
ALL_PROXYand unlinks on exit, all three confinement arms carry their half, and that socket now meets an opener wired into the engine's dependencies, whereDeps.EgressDoorhad never been given a value in non-test code. ⛔ What is still absent for the document row is the STORE and the CONSOLE RECORD, not the format — a run names one document explicitly and reads it; nothing yet authors, versions or audits that document from the console; - the machine-and-seat intersection and content-identity. This paragraph said "nothing binds a policy to a run by digest, nothing computes a machine-and-seat intersection, and no type in the library carries a policy digest" — of those three, none is true. The intersection, the narrowing of every ceiling, the refusal on an empty intersection and the verification of a policy against a claimed digest are 697 lines of non-test code, and the loaded policy type carries both the source digest and the compiled-policy digest. It is unreached because nothing calls it — zero callers outside its own package, test or not. That last clause is a HAND grep re-run 2026-09-02, not a suite claim; Egress §8.1 records why no predicate carries it;
- the audit record surface, which arrived on this list on 2026-09-02 from the one below it.
the product carries
Fact— run id, host, port, verdict, reason class, a timestamp, policy digest — with the rule that a URL path can appear in none of them;factstore.gocarries the store and its reader over anegress_facttable migration 53 creates on both backends. Nothing constructs that store, so every door this engine opens judges without recording, and a governed run leaves no per-decision trail to audit. ⚠ That is a limit of this release and not of the design: it is one call.
NOT BUILT (as Egress §2's legend defines it): the saphan-egress daemon,
and the transport that would carry a mainstream client to the HTTP-style CONNECT door.
That is the whole list, and it lost two entries on 2026-09-02 without a line of code moving.
The console editor that authors policies is routed, session-guarded and rendering
(GET/POST /console/egress), and the audit record surface has its row type, its store and
its table — what it lacks is the one call that hands a door a store to write through
(Egress §8 rules the first BUILT AND LIVE and the second BUILT, NOT
WIRED).
⚠ The assignment came off that list on 2026-08-30 — it is now BUILT AND LIVE (Egress §8), and closing it is what moved four rows into the third state. It was once described here as the one thing keeping every row above unreached. What ships is the narrow, explicit form: a run names the one canonical document by passing a flag. A resolver that composes a machine document and a seat document into a per-run policy remains to be written, and it is what the intersection above waits on.
The network-layer enforcement — the part that makes the proxy mandatory rather than merely
available — became REACHABLE BY A RUN for the first time on 2026-08-30, and no run has reached
it. This paragraph once ruled it in the first state and called it "blocked on a separate-UID
track", and both halves are now wrong:
the product walks the ratified ladder — a network namespace
(bwrap --unshare-net, composed), then a control group and then a separate operating-system
user (both REFUSE the run by name in this build: the rung may be real on the host while this
engine has no lever for it, and a rung this engine cannot use is never spent as though it had
passed), then the run does not start. The one thing reachable through the wall is that run's
own door. ⚠ It is now reached on exactly one condition — the ladder is asked only for a spawn
whose SpawnSpec.EgressPolicy is non-empty, and that field is assigned only for a run that passed
--egress-policy —
so the practical consequence for a reader deciding whether to trust this control today is
unchanged, and it is stated in the paragraph below rather than softened here. This row is about
the LOCAL lane; the remote one has its own row in Egress §8 and its own
paragraph immediately below.
THE REMOTE (ssh) LANE — corrected 2026-08-30, and given its own paragraph on purpose. This
sentence read "a far leg carrying an egress policy is refused outright, because the door is a unix
socket on the dispatcher and a path is not a transport". It was made false by the rounds of this
branch that built the far door, and it survived them attached to the paragraph above, where the
state token that paragraph already carries excused it from the scan that is supposed to catch
exactly this — which is why it now stands alone. What is true: the far host
serves the far door out of its own copy of this engine's binary (saphan egress-door),
judged by the same the product core a local run is judged by, under a policy document that
travels to it as bytes with this engine's digest beside it. Egress §8 rules the far
door BUILT AND LIVE. Nothing in the product's own checks could see it**: the prose scan that guards these two documents
fires on phrases asserting ABSENCE, and a sentence that spells the WRONG STATE TOKEN asserts no
absence at all. That gap is now closed by a check added in the same round that found this
(TestProseThatSpellsAStateTokenSpellsTheOneSection8Rules). And a policy-carrying far spawn is
not answered with a single word: it is governed on
a far Darwin host whose measured rows read seatbelt and seatbelt-network-deny and whose backend
this engine jails on that platform, and refused by name on a far Linux (landlock) host, on an
unmeasured or unprobed confinement row, and on a backend this engine jails on some platforms and not
the one that host's row resolves to. ⇒ Which of the two a given spawn gets is read off a measured
machine row, never off the lane — Egress §9B.3 is the two-axis table.
So the guarantee that a seat can only ever narrow its machine's policy, and that an empty intersection refuses the run, is implemented and unreached rather than ratified design and nothing more — and for a reader deciding whether to trust this control today the practical consequence is identical, because code no run executes protects nothing. The difference is entirely about what an auditor should read. Egress is the operator's document for this control: how a policy is written, what no policy can open, the refusal vocabulary with a next act for each class, what lands in the audit record, and what each missing piece is waiting on.
⇒ Until a dispatch asks, everything in the first paragraph of this section remains true of that run, and the sentence to act on is the one above it: treat egress control as your responsibility at your own network boundary. A control an agent's dispatcher may decline to ask for is available, not mandatory, and we describe it as exactly that — available, ready, and off unless the run says otherwise. This section will say enforces when a negative control shows a run bypassing the proxy being refused at the network layer without having asked for the wall — and not before.
The engine's own outbound fetch is guarded, and one signed record can widen it. Verifying a bearer token from an external issuer requires fetching that issuer's public signing keys, and that fetch is the one outbound connection the request path makes. It is bounded: https only, no redirect followed at all, proxy environment variables deliberately ignored (a proxy would dial on our behalf and the address guard would never see the target), a 1 MiB body cap, five-second timeouts at every stage, and at most one fetch per endpoint per thirty seconds. The address it dials is inspected after DNS, at connect, on the IP actually resolved — so a hostname that re-binds to an internal address is judged on the address, not on the name.
By default that dial refuses every loopback, link-local, private, multicast and unspecified
address. One exception exists, and only one: a loopback or private address may be dialled if the
URI being fetched is, byte for byte, the jwks_uri of a root-signed issuer record for the issuer
whose token is being verified. This is what makes an on-premises identity provider on an internal
network usable (an on-premises provider reached over your internal network). The record is re-read and its Ed25519
signature re-verified against your root public key inside the dial decision itself, so an
unsigned, tampered or revoked record closes the address rather than opening it; the match is on the
recorded URI, so a record for one issuer never opens another issuer's endpoint, another host, or
another path. There is no flag, no configuration key and no environment variable that opens it
instead — the signed act that decides whose tokens you honour is the same act that opens the
address, and there is deliberately no second, weaker one beside it.
What no record can open, because these are refused before the record is ever consulted:
link-local addresses (169.254.0.0/16, fe80::/10 — the range holding the cloud
instance-metadata endpoint), multicast, and the unspecified address. The limit to state
plainly: the recorded host is resolved by the system resolver at dial time and the answer is not
pinned to a previously observed address. If the host named in a signed record resolves to an
internal address, this dials it — that is the feature, and the record is the statement of intent
that authorizes it. A deployment whose recorded host has attacker-controlled DNS should treat that
record as attacker-influenced.
Remote confinement is real, and narrower than local. Runs dispatched over SSH to a Linux runner are confined by Landlock (section 8) — but four residuals are specific to that lane and stated here so nobody reads more than was built. Metadata operations (permissions, owner, timestamps, extended attributes) are outside Landlock at every level. The record's ENFORCED wording is composed from the machine's signed capability row and the policy at composition time — the kernel's own confirmation line lands in the captured output, but no verb currently asserts it. The far copy of the engine that applies the boundary is resolved by name on the runner and is not digest-pinned, unlike the agent binary it jails. And the seat-token pre-flight that refreshes authentication before a confined spawn (section 8) does not yet run for SSH transports — a remote seat with a dead session fails in the run, not before it. A remote machine that is not Linux, was never probed, or measures below the enforcement floor refuses engine-confined work rather than running it unconfined.
Remote orders may cite only content the spawn can receive. Two sibling pre-spawn
refusals enforce this before any document is materialized or agent process starts. R30
rejects an instruction to read an absolute path beneath the control plane's own workspace:
that path is real only on the sending host. R37 treats every whitespace-delimited token containing
the literal .provision/ marker as a citation unless the token's structure proves a URL scheme before
the marker, an x.provision directory, or an assignment whose right-hand side starts at the marker (the
shape of a provision assignment). In particular, / before .provision/
is a path separator: absolute, relative, home-relative, variable-rooted, and placeholder-rooted
spellings are citations. A confidently normalized .provision/<filename.ext> destination must exist
in the same spawn's provisioning set;
otherwise R37 names the line, destination, and matching
--provision <source>=.provision/<filename.ext> remedy. If a citation token cannot be normalized
confidently, R37 refuses on the raw token verbatim and asks the author to write the path plainly or
ship it, without inventing a destination. The complete lexical-miss classes are: URL tokens with a
scheme before the marker (for example https://example.com/.provision/Plan); x.provision directories
(vendor.provision/config.md, including the ..provision/Plan form); assignment operands whose
right-hand side begins with the marker (/tmp/p.md=.provision/Plan, as used by --provision); bare
or nested directories (.provision/, .provision/canon/); concrete-name
placeholders and extensionless fragments (.provision/<document>, .provision/PLAN, .provision/PLAN_);
destinations split across whitespace tokens (including git -C /repo/wt/.provision show); and markers
that are not the exact case-sensitive byte sequence .provision/ because they use .PROVISION, a division
or fullwidth slash, an HTML-encoded slash, a backslash, or an inserted zero-width character. They
miss because R37 is lexical and only a fully spelled document beneath the literal namespace can
make a provisioning claim. Suffix-contaminated fully spelled names such as .provision/Plan…,
.provision/Plan</code>, and .provision/PLAN*.md are not misses: they refuse ambiguously on the raw
token. These checks apply to SSH
LLM-backend spawns carrying a kickoff brief. Local transport is intentionally outside this
pair: closing the analogous local .provision/ omission would require a separately scoped
local-spawn policy and compatibility measurement, not a filesystem guess added to R37.
A compromised control plane is currently unopposed. Every enforcement decision is made by the control plane, and nothing on the runner independently inspects a dispatch and can refuse it. The instruction chain is verified — on the control plane. Two consequences: instruction signature enforcement is a three-value setting defaulting to off, and must be switched on deliberately; and a runner-side admission check that could refuse a dispatch even from a compromised plane is designed but not shipped. In the default mode an unsigned instruction is admitted and the fact is recorded in the ledger, so the volume of unsigned traffic is known before the setting is tightened.
One agent backend cannot be confined by us at all. One supported backend applies its own operating-system sandbox, and nesting ours inside it fails outright at the kernel. Rather than disable its sandbox — which our own rules forbid — the engine composes that sandbox as tightly as the tool's configuration allows and pins the settings per run on the command line (section 8). Runs on that backend rely on the vendor's boundary, not ours — and on a kernel without Landlock that boundary's behaviour is undocumented by its vendor, which is one more reason confinement capability is measured per machine rather than assumed.
Confinement can be switched off. An installation-level setting hands confinement to the agent backend. When set, the run record states in as many words that the engine makes no claim the agent is confined. It is honest, and it is one line of configuration.
One accepted residual risk inside confinement. A confined run can write to the shared version-control object store, because neither sandbox policy language can express "append only". A careless run could corrupt a shared object. We accepted this over the alternative — a full repository copy per run — because the control guards against carelessness rather than attack, and nothing but the version-control tool writes there. The decision is recorded with its date, the rejected alternative, and an explicit trigger: if an untrusted executor is ever admitted, it must be revisited.
Authority can expire but cannot be revoked. There is no revocation list, no certificate revocation checking for delegations, and no root rotation. A delegation stops applying when its window closes or when the holding actor is retired. Root rotation is refused by design. Keep delegation windows short.
No signature threshold. Signing authority is single-key throughout. There is no quorum or multi-party requirement for any act, including ratifying law.
One integrity check is a receipt rather than a check. On one of the two content-transfer paths, the receiving machine computes a digest and it is recorded, but not compared against a locally computed one. On that path it tells you what landed, not that what landed is what was sent. The agent-binary path does compare and re-verifies identity after installation.
Content integrity has no freshness. A matching SHA-256 proves the bytes match a value, not that they are the current bytes. A genuine older binary with a genuine matching digest passes. Rollback and indefinite-freeze conditions are not currently detected. Build metadata that would reveal a binary built from a modified source tree is not currently read.
Machines cannot be quarantined. There is no per-machine disable or drain state. The nearest effect is a capability record going stale, which removes that machine from eligibility for the affected work.
Durable work claims are not shipped. Mutual exclusion today is per machine slot, up to
that slot's declared max_sessions. A durable
per-stream assignment record — preventing two dispatches against the same piece of work from
different sessions — is designed and in progress.
Not in the threat model: an operator already root on a runner; an actively hostile rather than careless executor; physical access; and prompt injection itself. On the last: nothing here prevents an agent from being persuaded. The controls bound what a persuaded agent can reach, and guarantee that what it did is recorded.