# pi-guard

> Distributed as the npm/git package **`pi-os-guard`** (the name `pi-guard` was
> already taken on npm). The extension, its `/guard` command, and its config
> keys all keep the `pi-guard` name internally.

OS-sandboxed **read-only / write-restricted** execution for pi. Routes the
write-capable built-in tools (`write`, `edit`, `bash`) and `!` commands through a
per-command OS sandbox — **bubblewrap (`bwrap`) on Linux, `sandbox-exec` on
macOS** — so the operating system, not a heuristic, enforces the write boundary
on the **real host filesystem**. No container, no image: the sandboxed shell
shares your installed binaries and environment.

Toggle the mode live during a session with `/guard`.

## Why the OS, not a heuristic

An in-process guard can only *block or rewrite* a tool call; it can't change what
a command is allowed to do, so "read-only for bash" degenerates into guessing
whether a command writes. That guess is unsound. Here, commands run under a
read-only view of the filesystem, so writes fail at the syscall for *any*
command — including ones no heuristic would catch.

This is a real **write** boundary, but **not** a hardened sandbox: it does not
confine reads (your whole filesystem is readable), and (by default) the process
keeps network access. Use it to make a session safely "look but don't touch" and
confine writes — not to run actively malicious code. For stronger isolation add a
VM/micro-VM.

## Modes

| Mode | Filesystem | Writes |
|------|-----------|--------|
| `off` | host tools (pi default) | normal |
| `readonly` | whole FS readable | everything fails at the syscall |
| `restricted` | whole FS readable | only cwd + `allow` dirs (and scratch) succeed; rest fails |

**Identity paths.** The sandbox shares the host filesystem at the *same* paths —
`/Users/you/project` is the same path inside and out — so absolute paths, `$PWD`,
and tool output line up with the host. There is no `/workspace` remapping.

**Home is readable, not writable.** Your `$HOME` is readable in every mode, so
**skills** (`~/.agents/skills`), `git` (`~/.gitconfig`), and `gh`
(`~/.config/gh`) work against your real config. The trade-off: guard confines
*writes*, not *reads* — the model can read anything you can (including
`~/.ssh`), so this is not a secrets boundary. The sandbox inherits your shell
env (tokens included), matching that posture.

**Reads run on the host.** `read`, `ls`, `find`, and `grep` are pi's normal
host tools — a read can't breach a *write* boundary, so there's nothing to
sandbox, and they stay fast.

**Session scratch.** Each session gets one writable scratch dir, pinned as
`TMPDIR` in every command, so temp files created by one command are visible to
the next. It's removed when the session ends (not persisted across reboots).
In `readonly` mode the scratch (and `TMPDIR`) is the *only* writable path.

**Fail-closed:** in `readonly`/`restricted` with no backend available, tools
error out. The extension never silently falls back to host execution.

## Backends

- **Linux — bubblewrap (`bwrap`).** Read-only bind of `/` plus read-write binds
  for the writable dirs (cwd + allow-list + scratch). Fresh `/tmp`, `/proc`
  (`--unshare-pid`), and `/dev`; network left on. Install with
  `apt install bubblewrap` / `dnf install bubblewrap` if missing.
- **macOS — `sandbox-exec` (Seatbelt).** An `(allow default)` /
  `(deny file-write*)` profile that re-allows writes only under the writable
  dirs (canonicalized with `realpath`) plus the usual devices. Always present at
  `/usr/bin/sandbox-exec`. (Apple marks `sandbox-exec` deprecated, but it still
  works and needs no entitlements/signing.)

What's *not* provided vs a container: no PID-namespace cutoff on macOS and no
network isolation — this is a write boundary by design.

## Install

```bash
pi install npm:pi-os-guard
# or
pi install git:github.com/Oxel40/pi-os-guard
```

No `npm install` needed — it uses only pi's bundled APIs and Node built-ins.
Requires **bubblewrap** on Linux; macOS works out of the box. Windows is not
supported (yet).

## Commands

```
/guard                 # status (backend, mode, workspace, allow, scratch)
/guard readonly        # switch to read-only  (rebuilds sandbox)
/guard restricted      # switch to restricted (rebuilds sandbox)
/guard off             # use host tools
/guard allow <dir>     # add an extra read-write dir (restricted) and rebuild
/guard backend <name>  # bwrap | sandbox-exec | auto
/guard rebuild         # force-rebuild the sandbox
```

Default startup mode is `readonly`. Override at launch with `pi --guard off`,
`pi --guard readonly`, or `pi --guard restricted`.

## Configuration

Merged from global then project, with env-var overrides on top:

- `~/.pi/agent/extensions/pi-guard.json` (global)
- `<cwd>/.pi/pi-guard.json` (project-local, requires project trust)

```json
{
  "backend": "auto",
  "mode": "readonly",
  "allow": []
}
```

- `backend`: `auto` | `bwrap` | `sandbox-exec` (env `PI_GUARD_BACKEND`)
- `mode`: default startup mode (`readonly` by default; env `PI_GUARD_MODE`)
- `allow`: extra read-write dirs for `restricted` mode

## Concurrent Subagents (`spawn_agents`)

pi-guard provides a `spawn_agents` tool that spawns multiple pi subagents
concurrently, each inheriting the same guard mode. In TUI mode, it shows a
**non-blocking status widget above the input box** — the session remains
usable while it runs, and the widget never takes keyboard focus.

### Features

- **Live status updates**: Each agent shows its current state with colored icons:
  - `○` pending (dim)
  - `◔` starting (muted)
  - spinner (braille frames) running (accent)
  - `✓` completed (success)
  - `✗` failed (error)

- **Event tracking**: See what each agent is doing in real-time:
  - `Calling: bash` while a tool call is in flight, `✓ bash` / `✗ bash (error)` once it finishes
  - `Thinking...` / `Generating response...` during LLM calls

- **Progress summary**: Bottom bar shows `Completed: X | Running: Y | Failed: Z`

- **Abort support**: Ctrl+C cancels the tool call via pi's standard tool-cancellation mechanism (stops all in-flight subagents)

- **Graceful degradation**: Falls back to simple mode when not in TUI (print, JSON, or RPC mode)

### Visual Example

When spawning 3 concurrent agents:

```
  Spawning 3 subagents

 ✓ [1/3] COMPLETED  Analyze the error logs in /var/log/app.log...
 ⠹ [2/3] RUNNING    Fix the failing unit test in test_auth.py...
    └─ Calling: bash
 ○ [3/3] PENDING    Update the README with new installation steps...

  Completed: 1 | Running: 1 | Failed: 0
  Working... (Ctrl+C to cancel)
```

### Usage

The agent automatically uses `spawn_agents` when appropriate, or you can request it:

```
# In a pi session
Please use spawn_agents to run these 3 tasks concurrently:
1. Check system logs for errors
2. Run the test suite
3. Update the documentation
```

Each subagent runs in its own pi process with the same guard mode (readonly/restricted),
so they can work in parallel while respecting the write boundary.

## Lifecycle

Each session builds the sandbox lazily on first use and creates one scratch dir.
There is no long-lived process to reap: the scratch dir is removed on session
shutdown (quit/new/resume/fork/reload). A dir leaked by a hard crash is small and
reaped by the OS temp cleaner.
