# pi-sandbox-docker

On-demand, disposable Docker sandbox for [Pi](https://pi.dev). The Pi Coding
Agent gets a `sandbox_run` tool that executes code inside a fresh, hardened,
offline Debian container instead of on the host.

Every `sandbox_run` call creates a brand-new container and destroys it when the
run ends — including on failure, timeout, and cancellation. No containers,
volumes, or package caches persist between runs.

## Why

- **Isolation** — model-generated code runs away from your host filesystem,
  shells, credentials, and network.
- **Fresh state** — nothing leaks between runs; no pip/npm/apt caches survive.
- **Read-only project** — your repo is mounted read-only at `/workspace`.
- **Offline by default** — networking is off unless a human approves a single
  run.

## Prerequisites

- Pi (see <https://pi.dev/docs/latest>)
- Docker CLI with a reachable daemon (`docker ps` works)
- The sandbox image (build with `/sandbox-build`)

## Installation

```bash
pi install npm:pi-sandbox-docker
```

For local development:

```bash
# from this repo
pi -e ./pi-sandbox-docker
```

## Build the sandbox image

The image is not built during install. Build it once:

```text
/sandbox-build
```

This builds `pi-sandbox:debian-slim` from the bundled `image/Dockerfile`
(bash, coreutils, git, curl, Python 3 + pip, Node.js + npm, build-essential,
jq, ripgrep, and friends).

Check readiness:

```text
/sandbox-status
```

This reports whether the Docker CLI exists, the daemon is reachable, and the
image is present, with remediation steps if something is missing.

## The `sandbox_run` tool

The model calls it to execute code in the sandbox. Two modes (exactly one):

**Shell mode** — pipelines, redirects, `&&`, etc.:

```js
sandbox_run({ command: "pytest -q && python3 /workspace/check.py" })
```

**Argv mode** — no shell interpretation:

```js
sandbox_run({ executable: "python3", args: ["/workspace/check.py"] })
```

### Parameters

| Parameter | Type | Description |
| ----------- | ------ | ------------- |
| `command` | string | Shell command (mutually exclusive with `executable`) |
| `executable` | string | Executable to run without a shell |
| `args` | string[] | Arguments for the executable (argv mode) |
| `setup` | string[] | Privileged shell commands run as root before the main command (e.g. package install) |
| `network` | boolean | Enable network for THIS run only — requires human approval. Default `false`. |
| `timeout_seconds` | number | Timeout in seconds (default 120, max 3600) |
| `artifacts` | string[] | Relative paths under `/output` to export to the host |

### Filesystem model

```
HOST PROJECT ──(read-only)──▶ /workspace
                       /output  (writable, ephemeral export boundary)
                       /tmp     (writable, ephemeral)
```

- `/workspace` is your project, mounted **read-only**. The container can never
  modify it.
- `/output` is where code writes files the user/model explicitly wants out.
- Export them with `artifacts: ["report.json", "coverage/index.html"]`. Only
  files under `/output` are exported; absolute paths, `..`, `/workspace`, and
  `/tmp` are rejected.

### Network policy

- **Off by default.** If `network` is omitted or `false`, the run gets
  `--network none` and no permission is asked.
- If `network: true`, Pi prompts the human **for that individual run** with
  Deny / Allow-once. There is no "always allow" and no session-wide grant.
- **No UI means no network.** If `ctx.hasUI` is false and network is requested,
  the run fails closed and no container is created.

### Security model

- Fresh container per run, removed afterwards (including failure/timeout/cancel).
- Drops dangerous Linux capabilities (`SYS_ADMIN`, `SYS_PTRACE`, `NET_ADMIN`,
  `NET_RAW`, `MKNOD`, and others) while keeping the ones package installation
  needs; `--security-opt no-new-privileges`; PID/memory/CPU limits.
- No `--privileged`. No Docker socket exposed. No host env, keys, SSH agents,
  or home directories leaked into the container.
- Normal code runs as the unprivileged `sandbox` user. Root is used only for
  the `setup` phase (privileged package installation).
- The project mount uses `:ro,z` so it works on hosts with
  `selinux-enabled=true` (Docker marks the volume shared).
- **This is not an absolute security boundary.** Docker shares the host kernel;
  a determined attacker who achieves container escape could reach the host.
  For strong isolation use a micro-VM (see Pi's Gondolin extension) or a
  dedicated sandbox runtime.

## Development & testing

```bash
npm install            # installs typebox + pi types for typecheck
npm run check          # no build step (jiti runs TS directly)
npm test               # unit tests (no Docker required)
```

Integration tests require Docker and the built image; run them when Docker is
available.

## Publishing

The package uses `pi.extensions` and lists `@earendil-works/pi-coding-agent`
and `typebox` as peer dependencies (Pi provides them at runtime).

> **Name:** the npm names `pi-sandbox` and `pi-docker-sandbox` are already
> taken. This package is published as `pi-sandbox-docker`.

### Manual publish

```bash
npm adduser            # once, if not already authenticated
npm pack               # inspect the tarball
npm publish --access public
```

Then install it in Pi:

```bash
pi install npm:<your-package-name>
```

### CI publish (recommended, no manual token)

A `.github/workflows/publish.yml` is included. It publishes via OIDC trusted
publishing when you push a `vX.Y.Z` tag. To enable it:

1. Push this repo to GitHub.
2. Create an `npm-publish` environment on the repo.
3. Add the npm OIDC publish-access trust relationship for that environment
   (npm docs: "Publishing with provenance and publish access / OIDC").
4. Bump + tag with `npm version patch && git push --tags`.

Fixtures: the `repository.url` placeholder in `package.json` must be set to
the real repo before publishing.

## License

MIT. See `LICENSE`.
