Files
pi-agent-config/docs/isolation-baseline.md
T
Kai 07dd648b5f feat: scenarios for curator/memo-inbox/pi-grok, deploy and backup tooling
Scenarios
- memo-inbox: mirrored by copying; the live directory was not moved or modified
  and the service was not restarted. All four tracked files match byte for byte
  (pi-diff.sh reports SAME). Marked deploy = "mirror" so deploy-scenario.sh
  refuses --apply: applying a mirror would invert the direction of truth and
  could change a service in daily use.
- curator: target configuration, not yet deployed. .pi/SYSTEM.md replaces pi's
  coding-assistant prompt; durable role text is in .pi/APPEND_SYSTEM.md;
  profile.toml is the single source of truth for the launch contract.
- pi-grok: registered only. It is genuinely a coding agent, so the isolation
  baseline does not apply in full.

Corrections to the documentation, found by testing rather than by reading
- AGENTS.override.md does NOT block parent-directory context files; it only
  shadows its own directory. Verified: with an override file in the workspace, a
  marker in /tmp/AGENTS.md still reached the system prompt. The only effective
  switch is --no-context-files, so durable role text must live in
  .pi/APPEND_SYSTEM.md, which is a system-prompt file and unaffected by -nc.
  Verified end state: no coding-assistant framing, no pi-docs block, own
  identity and role text present, no parent pollution, only own skills/tools.
- PI_CODING_AGENT_DIR isolates settings/models/auth/trust/extensions/skills/
  prompts/themes under the agent directory -- stronger than the --no-* flags
  because it also repoints credentials -- but does NOT cover ~/.agents/skills.
  Measured: find-skills, modsearch and summarize still leak. So it complements
  --no-skills rather than replacing it.
- --append-system-prompt accepts a file path, which pi-grok relies on.
- cwd is what anchors .pi discovery: a probe that forgot cwd silently lost
  .pi/SYSTEM.md and kept the coding-assistant persona.

Tooling (all dry-run by default; none of them restarts a service)
- pi-diff.sh: compares tracked config against the live install in both
  directions, with a key-redacted comparison for models.json
- deploy-scenario.sh: installs a workspace and renders profile.toml into
  .pi/launch.json, then checks that every referenced path exists
- deploy-runtime.sh: renders models.json from its template, refusing placeholder
  or missing keys. Verified byte-identical to the live file
- pi-backup.sh / pi-restore.sh: archives outside the repo, sha256 manifest
  verified before any restore, live paths preserved rather than overwritten

Fixed while testing: pi-backup.sh compared the destination against the repo root
literally, so a relative --dest ./backups wrote credential archives into the work
tree. Now canonicalised with realpath; ./backups, an absolute in-repo path and
./docs/../backups are all refused.
2026-08-26 23:17:12 -07:00

176 lines
8.4 KiB
Markdown

# Isolation Baseline for a Dedicated Pi Agent
> Applies to every scenario in `scenarios/`. Mechanisms verified against
> pi 0.84.3 — see [`pi-runtime-notes.md`](pi-runtime-notes.md) for the source
> quotes and [`evidence/`](evidence/) for reproduction.
A "dedicated agent" is not achieved by narrowing what the model is *told*. It is
achieved by narrowing what the process *loads* and what it *can call*. Pi offers
four independent layers. Use all four; each one covers a different failure mode.
---
## The four layers
```
┌──────────────────────────────────────────────────────────────┐
│ L1 Loading what enters the process at all │
│ --no-extensions -e / --no-skills --skill / │
│ --no-prompt-templates / --no-themes / -nc / project trust │
├──────────────────────────────────────────────────────────────┤
│ L2 Personality who the agent is │
│ .pi/SYSTEM.md (replace) · .pi/APPEND_SYSTEM.md · -nc │
│ AGENTS.md only if parent-dir layering is acceptable │
├──────────────────────────────────────────────────────────────┤
│ L3 Capability which tools exist and are active │
│ --no-builtin-tools · --tools · --exclude-tools │
│ pi.setActiveTools() · same-name registerTool override │
├──────────────────────────────────────────────────────────────┤
│ L4 Invocation per-call authorization │
│ pi.on("tool_call") -> { block: true, reason } │
└──────────────────────────────────────────────────────────────┘
```
Layers 3 and 4 must be enforced **inside the extension**, not only on the
command line, so that a flag mistake in a systemd unit cannot silently widen the
agent's reach.
---
## Baseline flag set
```bash
pi --mode rpc \
--session-id "<scenario>-<subject>" \
--session-dir "<dedicated session dir>" \
--no-builtin-tools \
--no-extensions -e "<abs>/scenario-tools.ts" \
--no-skills --skill "<abs>/.pi/skills/<name>" \
--no-prompt-templates \
--no-themes \
--approve \
--provider "<provider>" --model "<model>" --thinking "<level>" \
--name "<display name>"
```
Launched with:
| Property | Value | Reason |
|---|---|---|
| `cwd` | the scenario workspace | bounds `AGENTS.md` and `.pi` discovery |
| `env` | explicit minimal allowlist | keeps backend API keys out of the node process |
| `start_new_session=True` | yes | lets the parent kill the whole process group |
| timeout handling | `killpg`, not `kill` | pi is a node CLI and may leave children |
### Why each flag
| Flag | Failure mode it prevents |
|---|---|
| `--no-extensions` + `-e <abs>` | `~/.pi/agent/extensions/*.ts` (currently `herdr-agent-state.ts`, `pi-memo-trust.ts`) being injected into an unrelated agent. The absolute `-e` path also loads before project trust resolves, so the extension works without trusting the directory. |
| `--no-skills` + `--skill <abs>` | `~/.agents/skills/*` leaking in. Probed leak: `["find-skills","modsearch","summarize"]``find-skills` actively instructs the agent to install more skills. |
| `--no-prompt-templates` | `~/.pi/agent/prompts/*.md` becoming callable slash commands. |
| `--no-themes` | pointless discovery I/O and a source of run-to-run variation. |
| `--no-builtin-tools` | `bash`/`edit`/`write`/`grep`/`find`/`ls` being active. Chosen over `--tools` because it imposes no registry-level allowlist, so an extension may register tools dynamically. Built-ins stay registered-but-inactive; the extension governs the active set. |
| `--approve` | required to load `.pi/SYSTEM.md` and `.pi/settings.json`. Combined with `--no-skills` it does **not** re-open skill auto-discovery. |
| `--session-id` + `--session-dir` | sessions colliding with the user's interactive history. |
### Do not use `--no-tools`
It disables extension tools as well, which removes `read`, which removes the
skills block from the system prompt (see `pi-runtime-notes.md` §1). Any
`SKILL.md` then becomes dead weight. This was a real, long-lived defect in the
Curator scenario.
---
## Required extension responsibilities
A scenario extension must do all five:
```typescript
const ALLOWED_TOOLS = ["read", /* … scenario tools … */];
export default function scenarioGuard(pi: ExtensionAPI) {
// 1. path-restricted `read` override — shadows the built-in by name
pi.registerTool({ name: "read", /* allowlist-checked implementation */ });
// 2. scenario tools, each with promptSnippet so they appear in the prose list
pi.registerTool({ name: "…", promptSnippet: "…", promptGuidelines: [ "…" ] });
// 3. declarative active set, reasserted after resource discovery
const restrict = () => pi.setActiveTools(ALLOWED_TOOLS);
pi.on("session_start", async () => restrict());
pi.on("resources_discover", async () => restrict());
// 4. per-call gate — defence in depth against layer-3 mistakes
pi.on("tool_call", async (event) => {
if (!ALLOWED_TOOLS.includes(event.toolName)) {
return { block: true, reason: `<scenario> blocks tool: ${event.toolName}` };
}
});
// 5. every tool truncates its own output (50 KB / 2000 line caps)
}
```
The `read` override must still permit the skill directory, otherwise skill
bodies cannot be loaded (progressive disclosure, `pi-runtime-notes.md` §2).
---
## Measured effect
Same workspace, same extension, only flags differ:
| Configuration | system prompt | active tools | skills visible | coding-assistant persona | pi-docs paths |
|---|---:|---|---|---|---|
| `--no-tools` (Curator, as found) | 1859 | *(none)* | **no** | yes | yes |
| `--no-builtin-tools`, no isolation | 3413 | own + `read` | 3 foreign | yes | yes |
| `+ --no-skills --skill` | 2619 | own + `read` | **1, correct** | yes | yes |
| `+ .pi/SYSTEM.md --approve` | **960** | own + `read` | 1, correct | **no** | **no** |
Full baseline is 72 % smaller than the unisolated default *and* strictly more
capable, because the skill is finally reachable.
---
## Residual risk
1. **No kernel boundary.** Pi ships no sandbox; extensions run with the process's
full permissions. The `read` override is application-level. A future step is
bubblewrap (`examples/extensions/sandbox/`) or a container.
2. **`env` discipline is the only secret boundary.** Without an explicit `env=`
allowlist the node process inherits every `*_API_KEY` in the unit file.
3. **Parent-directory context files.** Nothing prevents a future
`~/AGENTS.md` from layering into every scenario, and `AGENTS.override.md`
does **not** prevent it -- it only shadows its own directory (verified: a
marker in `/tmp/AGENTS.md` still reached the prompt). The only effective
switch is `-nc`, which means durable role text must live in
`.pi/APPEND_SYSTEM.md` rather than `AGENTS.md`.
4. **Workspace should be read-only to the service.** `--approve` trusts
everything project-local, so a writable `.pi/` is a code-execution path.
Enforce with systemd `ReadOnlyPaths=`.
5. **Prompt injection is not solved by any of this.** Untrusted text must be
delimited and labelled as data, and any write must be gated by deterministic
code outside the model.
---
## Per-scenario conformance
| Scenario | L1 loading | L2 personality | L3 capability | L4 invocation | `env` minimal |
|---|---|---|---|---|---|
| `curator` | target | target | target | target | target |
| `memo-inbox` | **gap** | **gap** | ok (`setActiveTools`) | ok (`tool_call`) | **gap** |
| `pi-grok` | not assessed | not assessed | not assessed | not assessed | not assessed |
memo-inbox already implements L3 and L4 correctly and is the reference for those
layers; its L1/L2/`env` gaps are tracked as a follow-up and must not be changed
in the same pass that migrates its configuration into this repository.
Check conformance with:
```bash
scripts/pi-diff.sh <scenario>
```