Turns a project directory into that project's policy, so a plain `ai-sandbox` in the directory is the correct, reviewable invocation. It writes the two files load_config() actually reads -- .ai-sandbox/allow and .ai-sandbox/config -- and nothing else: no sandbox.yaml, no local override, no invented schema, because a file nobody reads is a policy nobody has. scripts/survey.sh gathers the evidence read-only and prints no values: variable NAMES, remote and registry HOSTS, service PORTS, with URL credentials stripped before anything is printed -- the same rule --dry-run, --print-policy and the session log hold themselves to. The agent decides from that evidence, then verifies with --dry-run and --print-policy rather than assuming an allowlist entry means a working toolchain. references/settings-map.md maps every accepted key and flag to its default, plus per-ecosystem allowlists and the survey-to-policy cheat sheet. references/port-and-loopback.md is the honest answer on ports: outbound to a service on the machine via the mapped loopback address 100.64.0.1 (needs an allow entry and a client that does not hand the request to the egress proxy), and why a port the sandbox listens on is not reachable from the host in --net allow -- with probes, because that half is a reading of gen_rules(), not a measurement. Policy files only. Never builds, never starts a container, and never writes a real credential: it writes .ai-sandbox/env.example and tells the user what to put in the gitignored 0600 file. LLM-Generated: true
6.2 KiB
Ports and loopback
Two separate problems wear the same name. Get the direction right before touching a config.
| what it is | possible in --net allow? |
|
|---|---|---|
| outbound | the sandboxed process reaches a service running on your machine (dev postgres, redis, a mock API, a local model server) | yes — one allow entry, sometimes plus a proxy bypass |
| inbound | your browser on the host reaches a server the sandbox started | no — the generated ruleset drops it; three workarounds below |
Container-to-container is a third case, already measured on the reference host: two containers on a shared user-defined network cannot reach each other, so do not design a sidecar proxy, a service-mesh sandbox or a "sandbox plus its own database container" topology. The database runs on the host (compose) and is reached as in §1.
1. Outbound: a service on your machine
With --net allow the container is started as
--network pasta:--map-host-loopback,<gate_addr> (gate_addr, default 100.64.0.1). Inside the
container, 100.64.0.1:PORT lands on the host's 127.0.0.1:PORT. 127.0.0.1 inside the
container is the container itself and almost never what you meant.
Two layers have to agree, and this is where every "it still does not work" comes from:
- The ruleset. Only the gate's port is routable through that address by default. Add:
(IP entries become direct netfilter rules — the right shape for a non-HTTP protocol.)
allow = 100.64.0.1:5432 # postgres published by docker compose on the host - The proxy variables. When a gate is in use, the container gets
HTTP(S)_PROXY/ALL_PROXYandNO_PROXY=localhost,127.0.0.1,::1. A client that honours them sends the request to the gate, which allows the destination if it is in the allowlist and the port is one of 80, 443, 8443.
So:
| the service | what the config needs |
|---|---|
| postgres, redis, amqp, ssh, a raw socket — anything that ignores proxy variables | allow = 100.64.0.1:<port> |
| HTTP on 80, 443 or 8443 | allow = 100.64.0.1:<port> (the gate relays it) |
| HTTP on any other port (3000, 8000, 5173, 9200…) | allow = 100.64.0.1:<port> and a proxy bypass for that address, and measure which of the two NO_PROXY values wins |
The bypass, and the honest way to confirm it:
# .ai-sandbox/config -- the dev API on the host is not reachable through the egress gate
env = NO_PROXY=100.64.0.1,localhost,127.0.0.1,::1
env = no_proxy=100.64.0.1,localhost,127.0.0.1,::1
ai-sandbox sets NO_PROXY with --env before the env file, so the interaction between that and
an env-file value is podman's flag handling, not ai-sandbox's. Do not reason about it: run
ai-sandbox --dry-run, look at the order of the flags, and probe for real:
ai-sandbox --allow 100.64.0.1:3000 --shell
curl -sS -m 3 -o /dev/null -w '%{http_code}\n' http://100.64.0.1:3000/health
env | grep -i -E 'proxy|no_proxy'
Then point the app at the mapped address without putting a secret in a committed file — a URL with no password in it is not a secret:
env = DATABASE_URL=postgresql://dev@100.64.0.1:5432/app_dev
If 100.64.0.1 collides with a real network the project must reach, change gate_addr (and
remember the gate itself is reached at that address too).
2. Inbound: "I want to see the app the agent is building"
There is no publish flag (--publish/-p are not in parse_args()). You can push one through
the escape hatch — extra_args = -p 8000:8000 or AISB_EXTRA_ARGS="-p 8000:8000" — and then read
the ruleset the entrypoint installs: the input chain is policy drop with room only for lo,
established,related and a few icmpv6 types, so a fresh inbound connection is dropped inside the
container. Confirm on the machine instead of trusting that reading:
AISB_EXTRA_ARGS="-p 8000:8000" ai-sandbox --shell
# inside: python3 -m http.server 8000 &
# host: curl -sS -m 3 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/
Expected on a stock host in --net allow: no answer. If yours answers, that is a fact worth adding
to docs/networking.md — measure and report it, do not build the workflow on it.
What to tell the user instead, in order of preference:
- Run the server on the host. The project mount is their disk: the agent edits, the host's
npm run dev/runserverpicks it up, the browser hits a normal host port. Nothing is published, nothing is opened, egress stays enforced. This is the answer for 90% of "preview the app" requests. - One named preview run, when the server itself must be the sandboxed process (the agent needs
to drive it, seed it, watch its logs):
Say out loud what this is:
AISB_EXTRA_ARGS="-p 8000:8000" ai-sandbox --net full -- npm run dev--net fullmeans that run has no egress policy. It is a preview of a trusted project, not a project setting — which is why it never belongs in.ai-sandbox/config. - Split the loop: the agent builds and unit-tests in the sandbox, writes a screenshotable
static export or a
make previewtarget, and the human runs that target. This keeps the review step where it belongs.
If the user genuinely needs publish-with-enforcement, the honest answer is that it is not
implemented: a --publish PORT would need an input accept rule per port in gen_rules(), the
port list carried in the session policy, and a threat-model line about who may connect. Offer to
write it as a change to bin/ai-sandbox — as its own phase, with a tests/e2e probe — rather than
hacking extra_args into a committed config and calling it secure.
3. Notes that keep coming up
- The gate binds the host's loopback only, on a free port at or above
gate_port(18081);ai-sandbox gate statusandgate logsshow a long-lived one. NO_PROXYcoveringlocalhostis correct and must stay — tooling that refuses to proxy the container's own loopback (dev databases, auth servers, vite HMR) breaks otherwise.--no-gateopens DNS inside the container; the mapped loopback address still needs itsallowline.--net nonehas no interface at all: neither direction works, which makes it the right mode formake teston a toolchain that is already installed.