ADR 0009 — The API may allow private addresses when its operator says so
A record of the decision as it was taken; the numbers in it are as of its date. Today’s numbers are in chapter 25.
Status: accepted, 2026-09-26. Prompted by the Elixir embedder of ADR 0007,
moving its scripts from the zygo CLI to runtime pools over the API.
Context
That product’s scripts read their own project’s tables while they run. A script asks, the application answers, and the script carries on. Under the CLI the question and the answer were files in a directory both sides could see. A runtime pool has no such directory: the script arrives with the request and nothing of the host is mounted into it. The answer that fits a pool is an HTTP call from the script to the application.
Chapter 14 already says how a sandbox reaches a service on its own host. The
service listens on an address the host really has, an allow rule names
that address and port, and --allow-private-net is typed by a person,
because every such address is in a private range. A spec file served by
hand can do all three. The API could do the first two and never the third:
PUT /fn/{name} and POST /runtimes sent the supervisor
allow_private_net: false whatever happened. The comment beside it gave the
reason: a caller that could widen the boundary over HTTP would make the flag
on the server meaningless.
Two findings came with it. An address written without a prefix,
192.168.1.70:8765, was read as a host name and passed the private-range
check, and the firewall then refused every connection to it. And a pool,
strict by default, had no socket at all, so a pool with an allowlist
reached nothing, allowed hosts included.
Decision
zygo api --allow-private-net sets allow_private_net for what deploy
callers serve, on PUT /fn/{name}, POST /runtimes and POST /run. The
reason in the old comment still holds and is kept: a request body cannot set
it, and deny_unknown_fields still refuses a body that tries. What changes
is that the server’s own command line is also “where the sandbox is
declared”. The person starting the API types the flag, as they type
--allow-deploy.
It widens less than it sounds. It is inert without --allow-deploy, and a
deploy caller can already serve any image with any mount as the API’s user,
which chapter 17 calls a shell. A rule still names one address and one port,
and the rest of every private range stays shut. GET /version reports it as
private_net, so an embedder can say “reads are off on this server” instead
of failing its first query.
A private address without a prefix is refused like a private CIDR. And
a network under strict is refused when the sandbox is declared. Both
used to be accepted and then failed on every request with nothing to say
why.
Consequences
- Chapters 14, 17 and 19 describe the flag, and chapter 17’s
/versiontable hasprivate_net. - A pool whose scripts call out names
seccomp = "default". It already had to in practice; now it is told so when it is served. - A spec that paired
network = "egress"withstrict, or allowed a bare private address without the flag, is refused where it used to be accepted. Neither ever worked.
What would reopen this
- A channel for a request to ask its caller something mid-run, through the
supervisor and the stream, with no network at all. That would suit a
network = "none"pool better than any allowlist. It is a protocol change across three agents, three SDKs and the API, and one embedder asking is not yet enough to design it well. - A deployment where deploy rights are handed to parties the operator does not trust. There this flag would need to be per token rather than per listener.