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:
@@ -2,7 +2,7 @@
|
||||
|
||||
Execution plan for `ai-sandbox`, with the state each phase leaves behind.
|
||||
Every phase ends with a gate that must pass before the next one starts.
|
||||
Status as of this writing: **phases 1-8 complete**, follow-ups listed at the end.
|
||||
Status as of this writing: **phases 1-9 complete**, follow-ups listed at the end.
|
||||
|
||||
| # | Phase | Gate | Status |
|
||||
|---|-------|------|--------|
|
||||
@@ -15,6 +15,7 @@ Status as of this writing: **phases 1-8 complete**, follow-ups listed at the end
|
||||
| 6 | Limits, lifecycle, observability | exit codes, cleanup, caps, session log, `--keep` | ✅ done (`tests/e2e/12_sandbox.sh`) |
|
||||
| 7 | Harness defaults, docs, portability sweep | alias UX, README/threat model, shellcheck clean, no personal paths | ✅ done |
|
||||
| 8 | Supply chain: pinned base, TTL refresh, audit trail | digest in `FROM` and in the content hash; `update`/`--check`; labels + in-image package manifest | ✅ done (`tests/unit/07_base.sh`, `tests/e2e/10_image.sh`) |
|
||||
| 9 | Pi config bridge: same provider inside, credential by env | host `models.json`/`settings.json` rewritten not copied; canary secret nowhere in state; seeding, refresh and edit-presence asserted in a real container | ✅ done (`tests/unit/08_pi_config.sh`, `tests/e2e/12_sandbox.sh`) |
|
||||
|
||||
## Phase detail
|
||||
|
||||
@@ -91,6 +92,24 @@ One bug worth remembering: `POL[base_digest]="$(base_pin …)"` looked right and
|
||||
silently produced `unpinned` labels, because the subshell's assignment was discarded.
|
||||
Helpers return text; callers assign.
|
||||
|
||||
### 9 — Pi configuration bridge
|
||||
The image ships `pi` with no provider and no preferences, so the first run asked you to
|
||||
configure the model twice. It now reads `~/.pi/agent/{models,settings}.json` and
|
||||
rewrites them: every provider `apiKey` becomes `"$AI_API_TOKEN"` (pi resolves an env
|
||||
reference itself, so no value is ever written to disk), `!command` header values are
|
||||
dropped rather than executed, settings pointing into `$HOME` are dropped because that
|
||||
path is not mounted, and `auth.json`/`models-store.json`/sessions/extensions are never
|
||||
read. The pair lands in `/home/sandbox/.pi/agent` at container start, owned by uid 1000,
|
||||
mode 600, and is a managed file: a host change is re-seeded, a sandbox edit is kept and
|
||||
reported, a deletion takes the host version again.
|
||||
|
||||
Two decisions worth keeping. The token follows the existing env rules — one variable,
|
||||
passed by name through the `0600` file, announced when auto-passed — and a bridged run
|
||||
where that variable is unset *or empty* is refused instead of starting a pi that cannot
|
||||
reach its model, because "sandbox is broken" is a worse bug report than an explicit
|
||||
refusal. And the bridge never widens the allowlist; it warns with the exact `--allow`
|
||||
to add, since an agent silently unable to answer looks like a network failure.
|
||||
|
||||
## Follow-ups (not yet done)
|
||||
|
||||
* **`--net unix` mode**: `--network=none` plus a unix-socket proxy and an in-container
|
||||
@@ -112,3 +131,11 @@ Helpers return text; callers assign.
|
||||
* `update --auto` as a timer unit, if TTL-on-next-run turns out to be too lazy for
|
||||
security response — deliberately not done yet, since a background rebuild that
|
||||
surprises a running session is its own hazard.
|
||||
* **Scoped sandbox tokens**: if the gateway can mint a short-lived, per-sandbox key,
|
||||
`--pi-token-env` should ask for one rather than forwarding the long-lived host
|
||||
credential. The bridge deliberately does not guess a minting endpoint.
|
||||
* **Bridging extensions/skills on request**: `--pi-mount-ext` or similar for the
|
||||
extension paths named in `enabledExtensionPaths`, which are host paths and therefore
|
||||
currently dropped; needs the same explicit-grant shape as `--mount`.
|
||||
* **`models.json` review surface**: `inspect` could print the exact diff between host
|
||||
config and bridged config, so a change is reviewable before a run rather than after.
|
||||
|
||||
Reference in New Issue
Block a user