# 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 "-" \ --session-dir "" \ --no-builtin-tools \ --no-extensions -e "/scenario-tools.ts" \ --no-skills --skill "/.pi/skills/" \ --no-prompt-templates \ --no-themes \ --approve \ --provider "" --model "" --thinking "" \ --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 ` | `~/.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 ` | `~/.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: ` 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 ```