blob: 4054c8d3e954f939c912c9a3f421be90c7df92cd [file] [view]
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Pi harness](#pi-harness)
- [Harness contract](#harness-contract)
- [Install](#install)
- [Security model](#security-model)
- [What asks and what denies](#what-asks-and-what-denies)
- [Sandbox boundaries](#sandbox-boundaries)
- [Limits](#limits)
- [Spec-loop runner](#spec-loop-runner)
- [Upgrade Magpie](#upgrade-magpie)
- [Verify](#verify)
- [Update](#update)
- [Doctor](#doctor)
- [setup-isolated lifecycle](#setup-isolated-lifecycle)
- [Upstream references](#upstream-references)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Pi harness
**Capability:** capability:platform
**Harness:** Pi
[Pi](https://pi.dev/) (CLI `pi`, npm `@earendil-works/pi-coding-agent`) is a minimal terminal coding agent.
Out of the box it runs every tool call without asking and has no sandbox, so Magpie ships the secure setup as a Pi extension: an action guard, permission rules, and an OS sandbox for bash.
The setup is **per user, never per repository**: one `pi install` adds the skills and the extension to the user's `~/.pi/agent/settings.json`, and the extension reads no project configuration, so a repository cannot weaken it.
The adapter is **experimental**: it was validated with Pi 1.4.0 on macOS; Linux and Windows have not been verified.
The integration follows the harness contract in [add-a-harness](add-a-harness.md) and [RFC-AI-0004](../rfcs/RFC-AI-0004.md).
## Harness contract
| Magpie requirement | Pi implementation |
|---|---|
| Skill discovery | Pi reads `.agents/skills/` from the working directory up to the repository root, the canonical `universal` row in `skills/setup/agents.md`. The Pi package adds the framework's `skills/`. Each skill is also a `/skill:<name>` command. |
| Repository instructions | Pi loads `AGENTS.md` (and `CLAUDE.md`) natively, before project trust. |
| Tool bridges | Skills invoke the existing `tools/*` CLI adapters through the `bash` tool, subject to the extension's rules and sandbox. |
| Deterministic guard | The extension sends every bash command to `agent_guard/__init__.py --pi` before it runs; a deny blocks the call. See [agent-guard](../../tools/agent-guard/README.md#pi). |
| Permission rules | The extension derives them from the Claude Code baseline ([`expected.json`](../../tools/sandbox-lint/expected.json)): denied commands are blocked, asked ones need confirmation, and native reads and writes of credential paths, `.env` files, agent settings and Pi's own configuration are refused. |
| Filesystem and network | Bash runs in [`@anthropic-ai/sandbox-runtime`](https://github.com/anthropics/sandbox-runtime), the engine of Claude Code's sandbox, with the baseline's `denyRead` / `allowRead` / `allowWrite` and domain allowlist. |
| Credential isolation | The sandbox strips the baseline's credential variables (`GH_TOKEN`, cloud keys, …) from bash; `agent-iso pi` launches Pi through the generic clean-environment wrapper. |
| Spec-loop | The `pi` profile runs `pi -p` headless; see [Spec-loop runner](#spec-loop-runner). |
| MCP servers | Pi has no built-in MCP; Magpie's skills use the `tools/*` CLI adapters instead. |
The extension is [`tools/agent-isolation/pi/magpie.ts`](../../tools/agent-isolation/pi/magpie.ts); its decisions live in [`policy.ts`](../../tools/agent-isolation/pi/policy.ts), which reads the same baseline the Claude Code profile is linted against, so the two profiles cannot drift apart.
The repository's root [`package.json`](../../package.json) makes the repository a Pi package that carries both the skills and the extension.
## Install
Use Pi **1.4.0 or later** and Python 3.11+ (for the guard).
On Linux the sandbox also needs `bubblewrap`, `socat` and `ripgrep`; use the framework's [sandbox primitive versions](../../tools/agent-isolation/pinned-versions.toml).
```bash
pi install git:github.com/apache/magpie
```
That one command is the whole install, for every repository: Pi clones the framework, installs the pinned sandbox dependency, and records the package in `~/.pi/agent/settings.json`.
Pi runs `npm install --omit=dev --legacy-peer-deps` in its clone, which honours the committed [`package-lock.json`](../../package-lock.json): every package in the sandbox runtime's tree is pinned to an exact version and integrity hash, and none has an install script.
To refresh the lockfile, resolve with the same flags and the repository's 7-day cooldown, without installing or running anything:
```bash
npm install --package-lock-only --ignore-scripts --omit=dev --legacy-peer-deps \
--before="$(python3 -c 'import datetime; print(datetime.date.today() - datetime.timedelta(days=7))')"
```
Dependabot proposes the same updates weekly, with the same cooldown.
Never install it per project with `pi install -l`; the secure setup belongs to the user.
For guided setup, ask Pi: `/skill:isolated-setup-install`.
Then start every session through the clean-environment wrapper:
```bash
source "<framework>/tools/agent-isolation/agent-iso.sh"
agent-iso pi
```
Pi keeps provider credentials in `~/.pi/agent/auth.json` after `/login`; pass an API-key variable explicitly with `AGENT_ISO_ALLOW=<NAME> agent-iso pi` when you use one instead.
## Security model
### What asks and what denies
| Requested action | Result |
|---|---|
| A bash command that matches no rule | Runs in the OS sandbox without a prompt. |
| A read-only `gh` command from the baseline's allow list, as plain words | Runs outside the sandbox, with `gh`'s own credentials. |
| `git push`, mutating `gh` commands, `gh api` with a method or field flag | Asks; refused in a non-interactive run. |
| Any other `gh` command outside the sandbox | Asks. |
| Credential and export commands, such as `curl`, `gh auth token`, cloud CLIs | Denied. |
| A command agent-guard denies (a `Co-Authored-By` trailer, `--no-verify`, …) | Denied, with the guard's reason. |
| Native `read` / `grep` / `find` / `ls` of credential paths or `.env` files, or rooted above them | Denied, symlinks resolved. |
| Native `write` / `edit` of agent settings, `~/.pi/agent/` or `.pi/` | Denied. |
| Native `write` / `edit` outside the working folder, symlinks resolved | Asks; refused in a non-interactive run. |
Deny outranks ask, which outranks allow, as in Claude Code.
A command leaves the sandbox only when it is plain words: quotes, escapes, expansions, redirections and operators keep it inside.
### Sandbox boundaries
A probe with Pi 1.4.0 on macOS observed:
| Probe | Result |
|---|---|
| `cat ~/.ssh/known_hosts` | Blocked. |
| `test -r ~/.gitconfig` | Readable (in the baseline's `allowRead`). |
| `touch ~/probe` | Blocked. |
| A request to `example.com` | Blocked by the domain allowlist. |
| A request to `api.github.com` | Allowed. |
| A write in the workspace | Allowed. |
| `echo $GH_TOKEN` | Empty. |
If the sandbox cannot start, the extension refuses bash rather than running it unsandboxed.
`pi --magpie-no-sandbox` runs one session without it; the guard and rules still apply.
### Limits
- Native file tools and extension tools run in the Pi process, outside the sandbox; the rules above gate the built-in ones, not tools other extensions add.
- A rule for a file name anywhere (`**/.env`) cannot stop a directory search from reading such a file; Pi's `grep` and `find` follow `.gitignore`, which usually lists them.
Neither follows symlinks, so a search cannot walk through a link into a denied directory, and file tools are handed the checked path with links already resolved.
- The extension's command rules are a friction layer; the guard and the sandbox are the enforcement.
- `@anthropic-ai/sandbox-runtime` 0.0.80 pulls `node-forge` and `zod` versions with open advisories in its TLS-interception and validation paths, which this extension does not drive with untrusted input.
## Spec-loop runner
The `pi` profile in [`tools/spec-loop/lib.sh`](../../tools/spec-loop/lib.sh) runs `pi -p`, forwarding `--model` and `--thinking` when supplied.
A non-interactive run has no UI, so every command that would ask is refused: a build iteration needing a push or a mutating `gh` call cannot complete unattended.
See the [spec-loop guide](../../tools/spec-loop/README.md).
## Upgrade Magpie
Ask *"upgrade magpie"* or run `/skill:upgrade` after `pi update --extensions`.
Pi has no session hook and no plugin listing the pre-flight can read, so nothing prompts you: run it after every update.
Every harness: [Upgrading Magpie](../setup/marketplace.md#upgrading-magpie).
## Verify
Ask `/skill:isolated-setup-verify`, or check by hand:
```bash
pi --version
pi list
```
`pi list` must show `git:github.com/apache/magpie`, and the startup header of an interactive session must list the `magpie.ts` extension.
Then make these requests to the model rather than typing the commands yourself:
| Request | Expected result |
|---|---|
| Run exactly `git status --short`. | Runs. |
| Run exactly `cat ~/.ssh/known_hosts`. | Fails inside the sandbox. |
| Run exactly `gh auth token --help`. | Denied by the rules. |
| Run exactly `git commit --dry-run --no-verify -m hook-probe`. | `agent-guard[no-verify]` denial. |
| Run exactly `git push --dry-run`. | Asks before running. |
| Read `~/.ssh/config` with the read tool. | Denied. |
Inspect the actual tool results; a verbal refusal by the model does not establish enforcement.
## Update
`pi update --extensions` pulls the framework package to its pinned ref and reinstalls its dependencies.
Ask `/skill:isolated-setup-update` for a read-only drift report first.
## Doctor
| Symptom | Check and next step |
|---|---|
| Bash refused: "the OS sandbox could not start" | On Linux install `bubblewrap`, `socat` and `ripgrep`; check that `pi update --extensions` installed the package's dependencies. |
| Guard never denies | Check `python3 --version` (3.11+) and that `pi list` shows the framework package. |
| A command fails only inside the sandbox | Network and most of the home directory are unavailable there. Report what it needs; do not widen the baseline without the operator's decision. |
| A command keeps asking in a headless run | Asked commands need an interactive session. |
## setup-isolated lifecycle
The four `setup-isolated-setup-*` skills route Pi sessions to [Install](#install), [Verify](#verify), [Update](#update), and [Doctor](#doctor).
Uninstall is `pi remove git:github.com/apache/magpie`, which removes the skills, the guard and the sandbox together.
## Upstream references
- [Pi packages](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md)
- [Skills](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/skills.md)
- [Extensions](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md)
- [Security](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/security.md)
- [Containerization](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/containerization.md)
- [CLI](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/cli.md)