bridge the host pi config, with the credential by env only

The image ships pi with no provider and no preferences, so a sandboxed run
meant configuring the model twice. ai-sandbox now reads the host
~/.pi/agent/{models,settings}.json and rewrites them instead of copying them:
every provider apiKey becomes "$AI_API_TOKEN" (pi resolves an env reference
itself, so no value is written to disk), '!command' header values are dropped
rather than executed, settings that point into $HOME are dropped because that
path is not mounted, and auth.json / models-store.json / sessions / extensions
are never read.

The rewritten pair is seeded into /home/sandbox/.pi/agent at container start,
owned by uid 1000 and mode 600, and is treated as managed: a host change is
re-seeded, an edit made inside the sandbox is kept and reported, a deletion
takes the host version again.

The token follows the existing environment rules -- one variable, passed by
name through the 0600 env file, announced when auto-passed. A bridged run
whose token variable is unset or empty is refused instead of starting a pi
that cannot reach its model, and the bridge never widens the allowlist: it
warns with the exact --allow to add.

Docs and examples now use a self-hosted gateway (https://aipi.reeseapps.com)
instead of vendor API endpoints, since that is the only thing this tool is
used against. 391 assertions pass (make lint, ./tests/run.sh --e2e).

LLM-Generated: true
This commit is contained in:
2026-10-27 17:29:17 -04:00
parent 0242ab5b5e
commit 8ee535f6ae
14 changed files with 740 additions and 49 deletions
+59 -19
View File
@@ -7,7 +7,14 @@ Concrete invocations. `pi` below is whatever you aliased to `ai-sandbox`.
```bash
cd ~/Projects/myapp
ai-sandbox doctor # is this machine ready?
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY
pi --allow aipi.reeseapps.com --env AI_API_TOKEN
```
With your pi config bridged, the token is found on its own, so the allowlist is the
only thing you have to say:
```bash
pi --allow aipi.reeseapps.com
```
Same thing, but remembered by the repo (so teammates review the policy, not just
@@ -15,25 +22,58 @@ your shell history):
```bash
mkdir -p .ai-sandbox
printf 'api.anthropic.com\n' > .ai-sandbox/allow
pi --env ANTHROPIC_API_KEY
printf 'aipi.reeseapps.com\n' > .ai-sandbox/allow
pi
```
## Pi's own configuration
The bridge rewrites your host `~/.pi/agent/{models,settings}.json` instead of copying
it: providers, models, thinking budgets and preferences come along, the credential
becomes `"$AI_API_TOKEN"`, and anything that only works on the host (a `!command`
header, a path into `~`) is dropped with a warning. `auth.json` is never bridged.
```bash
# the everyday case: same provider as the host, token from the environment
pi --allow aipi.reeseapps.com
# a model your gateway serves, chosen per run
pi --allow aipi.reeseapps.com --model juggernaut
# keep the model's endpoint pinned by IP as well, on a LAN inference host
pi --allow aipi.reeseapps.com --allow 10.20.0.7:11434
# a different variable name for the credential
pi --allow aipi.reeseapps.com --pi-token-env GW_TOKEN
# do not hand the token over automatically: the run then fails closed unless you
# pass it yourself, which is what you want in shared scripts
pi --allow aipi.reeseapps.com --env AI_API_TOKEN --no-pi-token-env
# no bridge: pi starts with its own defaults inside the volume
pi --no-pi-config --allow aipi.reeseapps.com
```
Edits made inside the sandbox are kept; changes to the host config are picked up on
the next run; deleting `~/.pi/agent/models.json` inside the sandbox re-seeds it.
## Per harness
```bash
# pi (baked into the image)
# pi (baked into the image, config bridged)
pi --allow aipi.reeseapps.com
# pi with a model chosen on the command line
pi --harness 'pi --model turbo' --allow aipi.reeseapps.com
# a gateway that speaks a different protocol still needs only its host
pi --allow gateway.internal:8443 --env AI_API_TOKEN
# OpenAI Codex CLI / Claude Code: they read their own config files, so mount or
# generate what they need and allow their endpoints; the pi bridge does not apply
pi --allow chatgpt.com --env CODEX_API_KEY
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY
# pi with a proxy/harness you pass explicitly
pi --harness 'pi --model sonnet' --allow api.anthropic.com --env ANTHROPIC_API_KEY
# OpenAI Codex CLI (install it into the image with --install or use a custom base)
pi --allow chatgpt.com --allow api.openai.com --env OPENAI_API_KEY
# Claude Code
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY --env CLAUDE_CODE_OAUTH_TOKEN
# just a shell in the sandbox
pi --shell
@@ -64,7 +104,7 @@ pi --allow proxy.golang.org --allow sum.golang.org
pi --allow registry-1.docker.io --allow auth.docker.io --allow production.cloudflare.docker.com
# a project that also needs its dev database (non-HTTP: pinned IP + port)
pi --allow api.anthropic.com --allow 10.20.0.5:5432 --env ANTHROPIC_API_KEY
pi --allow aipi.reeseapps.com --allow 10.20.0.5:5432
```
## Git
@@ -77,7 +117,7 @@ pi --allow github.com --env GH_TOKEN -- git push origin main
pi --allow github.com --mount ~/.ssh:ro -- git push origin main
# read-only view of a sibling repo for context
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY --mount ../docs:/docs:ro
pi --allow aipi.reeseapps.com --mount ../docs:/docs:ro
```
## Tightening a run
@@ -96,12 +136,12 @@ pi --strict-args --net allow --allow-file .ai-sandbox/allow --env-file .ci/env -
## Long-lived gate shared by several sandboxes
```bash
ai-sandbox gate up --allow api.anthropic.com --allow registry.npmjs.org
ai-sandbox gate up --allow aipi.reeseapps.com --allow registry.npmjs.org
# name=aisb-gate
# port=18081
# proxy_env=export HTTPS_PROXY=http://100.64.0.1:18081
ai-sandbox --proxy 100.64.0.1:18081 --allow api.anthropic.com --shell
ai-sandbox --proxy 100.64.0.1:18081 --allow aipi.reeseapps.com --shell
ai-sandbox gate logs # what did it allow/deny?
ai-sandbox gate down
```
@@ -132,13 +172,13 @@ harness = pi
cpus = 8
memory = 8g
mount = ../specs:/specs:ro
env = ANTHROPIC_API_KEY
env = AI_API_TOKEN
```
`.ai-sandbox/allow`:
```
api.anthropic.com
aipi.reeseapps.com
registry.npmjs.org
```
+10
View File
@@ -70,6 +70,16 @@ Be honest about these, and design your work accordingly.
10. **Rootless podman still means shared kernel and shared UID namespace.** Files
the container writes are owned by *you*. There is no uid boundary between
"agent's file" and "your file" beyond ownership metadata and your own discipline.
11. **The pi bridge reads a file that may hold a live key.** It rewrites `apiKey` to
an environment reference and never copies `auth.json`, but the *endpoint* in your
`models.json` becomes an endpoint the sandbox can be pointed at, and the token it
uses is your real one. So the bridged config is a convenience, not a narrower
credential: prefer a per-sandbox token from your gateway if it can mint one, and
note that a bridged run whose token variable is unset or empty is refused rather
than started. Sandboxed edits to `~/.pi/agent` persist in the home volume and are
kept over later host changes (see the README), which is convenience with a
consequence: a hostile run could rewrite the sandboxed pi config for later runs of
that volume. `--ephemeral-home` removes that.
## Residual risk rating