share the host ~/.agents read-only, so skills come along
A sandboxed harness found none of your skills, prompt templates or standing AGENTS.md, because those live in ~/.agents and only the project was mounted. It is now bind-mounted at /home/sandbox/.agents by default -- bound, never copied -- with --no-agents to opt out and --agents-dir for a curated set, reported by --print-policy and by doctor. Read-only is the load-bearing part and has no escape hatch: a skill is an instruction the model follows later, on the host, so a sandbox that could write ~/.agents had a persistence path out of itself, planted as a helpful-looking SKILL.md. It holds even under --insecure-keep-root, the mount lands after /home/sandbox itself so it cannot be shadowed by the home volume or tmpfs, and an explicit --mount on the same destination wins because podman refuses duplicates. A missing or unreadable directory is a silent skip like the pi bridge's; an explicit --agents-dir that resolves to nothing is a hard error. The exposure is stated in docs/threat-model.md: the tree, its .git history and anything pasted into a skill are readable by whatever runs in there. tests/unit/09_agents.sh covers resolution, precedence and the bind-never-copy canary; tests/e2e/12_sandbox.sh mounts it in a real container, checks the host tree survives a run byte for byte and that even root inside the sandbox cannot write it. LLM-Generated: true
This commit is contained in:
+29
-2
@@ -57,6 +57,33 @@ 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.
|
||||
|
||||
## Your skills and standing instructions
|
||||
|
||||
`~/.agents` is shared read-only at `/home/sandbox/.agents` by default, so the sandboxed
|
||||
harness finds the same skills, prompt templates and `AGENTS.md` you use on the host.
|
||||
A project's own `.agents/` needs nothing: it arrives with the project mount, and harnesses
|
||||
that read both see the project's alongside yours.
|
||||
|
||||
```bash
|
||||
# the everyday case: your skills are simply there
|
||||
pi --allow aipi.reeseapps.com
|
||||
|
||||
# a run where the harness should not have them: reviewing untrusted code, or a
|
||||
# skills set that mentions internal hosts you would rather it not repeat back
|
||||
pi --net none --no-agents -- bash -lc 'make lint'
|
||||
|
||||
# a curated set for sandboxes -- e.g. host skills stay yours, this dir is what you share
|
||||
pi --agents-dir ~/.config/ai-sandbox/agents --allow aipi.reeseapps.com
|
||||
|
||||
# two sets, per trust level, chosen by config file in a repo
|
||||
# .ai-sandbox/config: agents_dir = ./.agents-shared
|
||||
```
|
||||
|
||||
It is read-only and there is no flag to change that: a skill is an instruction you
|
||||
follow on the host later, so letting a sandbox write one would let it persist past the
|
||||
run. If a workflow really needs to *author* skills, do it in the project directory and
|
||||
move the result into `~/.agents` yourself, where you can read it first.
|
||||
|
||||
## Per harness
|
||||
|
||||
```bash
|
||||
@@ -126,8 +153,8 @@ pi --allow aipi.reeseapps.com --mount ../docs:/docs:ro
|
||||
# nothing persistent, nothing readable, nothing writable outside the repo
|
||||
pi --net none --ephemeral-home --read-only -- make build
|
||||
|
||||
# an entirely blank sandbox for untrusted code
|
||||
pi --net none --no-cwd --ephemeral-home --read-only -- bash
|
||||
# an entirely blank sandbox for untrusted code: no project, no home, no skills
|
||||
pi --net none --no-cwd --ephemeral-home --read-only --no-agents -- bash
|
||||
|
||||
# CI-ish: refuse to start if the policy or environment is not exactly right
|
||||
pi --strict-args --net allow --allow-file .ai-sandbox/allow --env-file .ci/env -- npm ci
|
||||
|
||||
+14
-2
@@ -19,7 +19,8 @@ registry you don't control, or someone with access to your host user account.
|
||||
|
||||
| Attack | Stopped by | Evidence |
|
||||
|---|---|---|
|
||||
| Editing / deleting anything outside the project directory | only `$PWD` is mounted; the rest of the host is not in the mount namespace | `tests/e2e/12_sandbox.sh` |
|
||||
| Editing / deleting anything outside the project directory | `$PWD` is the only writable mount; the rest of the host is not in the mount namespace | `tests/e2e/12_sandbox.sh` |
|
||||
| Rewriting your skills, prompts or global `AGENTS.md` | `~/.agents` is mounted, but read-only, and there is no flag to change that | `tests/e2e/12_sandbox.sh` |
|
||||
| Reading `~/.ssh`, `~/.aws`, `~/.config/gcloud`, browser profiles | same as above; those paths do not exist in the container | `tests/e2e/12_sandbox.sh` |
|
||||
| Exfiltrating your environment (API keys for *other* services) | nothing is passed through unless named with `--env` | `tests/unit/04_env.sh`, `tests/e2e/12_sandbox.sh` |
|
||||
| Passing secrets through the process list to other users | values travel in a `0600` file, never in argv | `tests/e2e/12_sandbox.sh` |
|
||||
@@ -70,7 +71,18 @@ 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
|
||||
11. **Your `~/.agents` is readable inside the sandbox.** This is a deliberate
|
||||
exception to "only your project directory is mounted", because a harness without
|
||||
your skills is a harness you re-brief every run: `~/.agents` arrives read-only at
|
||||
`/home/sandbox/.agents` (`--no-agents` for a run without it, `--agents-dir` for a
|
||||
curated set). Read-only means the agent cannot write a `SKILL.md` that you will
|
||||
follow tomorrow on the host — that is the part that matters, and it holds even under
|
||||
`--insecure-keep-root`. It does *not* mean the contents are secret: the whole tree,
|
||||
including its `.git` history and any token someone pasted into a skill, is readable
|
||||
by whatever you run in there. Treat a shared skill directory as published to the
|
||||
agent, lint it for secrets (`skill-lint`), and keep credentials in the environment
|
||||
you pass explicitly rather than in a file that is mounted everywhere.
|
||||
12. **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
|
||||
|
||||
Reference in New Issue
Block a user