Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 /version table has private_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" with strict, 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.