[READ-ONLY] Mirror of https://github.com/andrioid/pi-fence. Fence wrapper for Pi coding agent. Wraps bash calls and intercepts reads/writes according to fence config
0

Configure Feed

Select the types of activity you want to include in your feed.

pi-fence / README.md
24 kB 611 lines
1# pi-fence 2 3A Pi extension that enforces one shared [`fence.jsonc`][fence] policy 4across **two enforcement layers**: 5 61. **Bash subprocesses**, wrapped via the [`fence`][fence] binary 7 (Seatbelt on macOS, bubblewrap + Landlock on Linux). Covers the LLM 8 `bash` tool and user `!` / `!!` commands. 92. **Pi's own file tools** (`read`, `write`, `edit`, `grep`, `find`, 10 `ls`), gated by an in-process `tool_call` interceptor that consumes 11 the same resolved policy. 12 13Both layers read from `fence config show`, so they cannot disagree. 14 15[fence]: https://github.com/Use-Tusk/fence 16 17## Status 18 19| Layer | Status | Notes | 20|---|---|---| 21| **Bash + user_bash** | shipped | Pass 0 | 22| **Pi file tools** (read/write/edit/grep/find/ls) | shipped | Pass 1 — [`PLAN.md`](./PLAN.md) §5 | 23| **Interactive grants + persist + audit log** | shipped | Pass 2 — [`PLAN.md`](./PLAN.md) §8 | 24 25## What it does 26 27When pi-fence is loaded: 28 291. The built-in `bash` tool is replaced by a fenced version that 30 prefixes every command with `exec fence -c <cmd>`. The agent sees 31 identical stdout/stderr/exit-code behavior; the kernel-level 32 sandbox enforces filesystem and network policy on the spawned 33 process tree. User shell commands (`!`, `!!`) go through the same 34 wrapping. 352. Pi's `read`, `write`, `edit`, `grep`, `find`, `ls` tools each fire 36 a `tool_call` event that pi-fence intercepts. The path is 37 `realpath`'d (defeating symlink escape) and matched against the 38 resolved policy. A block returns a structured JSON payload to the 39 LLM: 40 41 ``` 42 pi-fence: blocked 43 { 44 "tool": "read", 45 "path": "/private/etc/passwd", 46 "matched_rule": "filesystem.defaultDenyRead", 47 "matched_pattern": null, 48 "policy_source": "/Users/me/proj/fence.jsonc", 49 "suggestion": "If this access is legitimate, ask the user to grant 50 it. They can use /fence or add the path to filesystem.allowRead 51 in fence.jsonc." 52 } 53 ``` 54 553. On a block, if a TUI is available, you're prompted with four 56 options: `[d] deny` (default), `[a] allow once`, `[s] session`, 57 `[p] persist`. Picking persist edits (or scaffolds) the project's 58 `fence.jsonc`. Picking session records the grant in session storage 59 so it survives fork/resume. 604. Every block decision is appended to `~/.pi/agent/pi-fence.log` for 61 forensics. Daily rotation, 7-day retention. 625. On first run in a git project that has no `fence.jsonc`, pi-fence 63 prompts to scaffold one tuned for agent workflows (mise, npm, 64 language-toolchain installs). Decline once and pi-fence stays out 65 of the way until you ask via `/fence scaffold` or `/fence init`. 66 See [Project scaffolding](#project-scaffolding-default-behavior). 676. The footer status indicator reflects the active state: 68 - `🔒 fenced (bash + fs)` — both layers active 69 - `🔒 fenced (bash) — no policy` — fence available, no 70 `fence.jsonc` (FS layer is dormant; bash uses fence defaults) 71 - `⚠ unfenced (--no-fence)` — bypassed via the per-session flag 72 - `⚠ unfenced (fence missing)` — `fence` binary not on PATH; 73 symmetric fail-open with a warning notification 74 75## Requirements 76 77- **Pi** ≥ 0.72 78- **`fence`** on PATH ([install instructions][fence-install]) 79- macOS or Linux 80 81[fence-install]: https://github.com/Use-Tusk/fence#install 82 83## Install 84 85The directory is auto-discovered by Pi when placed at 86`~/.pi/agent/extensions/pi-fence/`. 87 88```bash 89cd ~/.pi/agent/extensions/pi-fence 90npm install # installs dev deps for type-checking only 91``` 92 93No runtime npm dependencies; the extension only needs the `fence` binary 94on PATH. 95 96## Usage 97 98Just start Pi as usual. The footer status will show `🔒 fenced` if all 99prerequisites are met. 100 101To bypass for a single session: 102 103```bash 104pi --no-fence 105``` 106 107## Configuration 108 109All policy lives in `fence.jsonc` (or `fence.json`), the same file the 110`fence` binary itself consumes. Search order: 111 1121. `<project>/fence.jsonc` (project-local) 1132. `~/.config/fence/fence.jsonc` (user-global) 114 115If neither exists, fence runs with default-deny network and default-deny 116writes — see fence's [configuration reference][fence-config]. 117 118### Project scaffolding (default behavior) 119 120The first time pi-fence runs in a git project that has no `fence.jsonc`, 121it prompts: 122 123``` 124pi-fence: scaffold fence.jsonc? 125 126This project has no fence policy. Pi-fence ships a starter tuned for 127agent workflows: mise / npm / language-toolchain installs work, project 128secrets (.env, *.pem, *.key) stay protected, common destructive 129commands (git push, npm publish, sudo, ...) are denied. You can edit 130the file freely afterwards; pi-fence won't overwrite it. 131 132Create /path/to/repo/fence.jsonc? [Y/n] 133``` 134 135Accept and you get a self-contained `fence.jsonc` with a vetted policy 136(see [`policy/starter-template.ts`](./policy/starter-template.ts) for 137the authoritative source). The file is yours to commit, edit, or 138delete. Pi-fence treats its presence as authoritative and will not 139re-prompt or overwrite it. 140 141Decline and pi-fence stays out of the way for the rest of the session. 142You can re-run the prompt later via `/fence scaffold`, or get a 143minimal `extends: "code"` stub via `/fence init`. 144 145Why not just `extends: "code"`? The `code` template ships a 146`denyWrite: ["**/*.pem", "**/*.key", ...]` that's globally scoped, so 147it false-positives on legitimate package-manager extractions like Go's 148`crypto/tls/testdata/*.pem` and breaks `mise install go@…`, 149`mise install node@…`, native-module npm installs, etc. Fence's slice 150fields are append-only on `extends`, so a child config can't narrow 151those patterns. Pi-fence's starter sidesteps the problem by authoring 152the whole policy directly, with denyWrite scoped to the project tree 153and the agent-relevant CDNs (mise infrastructure, nodejs.org, 154dl.google.com, hashicorp, rust toolchains) in the network allowlist. 155For the full diff vs. the upstream `code` template, see the comment 156block in [`policy/starter-template.ts`](./policy/starter-template.ts). 157 158#### What the starter contains 159 160Network: every domain in fence's `code` template (LLM providers, 161GitHub, npmjs, pypi, crates.io, formulae.brew.sh, ...) plus the 162language-toolchain CDNs `*.jdx.dev`, `nodejs.org`, `dl.google.com`, 163`static.rust-lang.org`, `releases.hashicorp.com`. Same `deniedDomains` 164(metadata services, statsig, sentry). 165 166Filesystem: same `denyRead` (credentials, ssh keys, cloud creds). 167Same `allowWrite` plus `~/Library/Caches/**` (where mise's cache lives 168on macOS). `denyWrite` patterns scoped to `./**/*` instead of `**/*` — 169the agent still can't write `.env`/`*.pem`/`*.key` into your project, 170but tarball test fixtures extracted into caches no longer 171false-positive. 172 173Command: identical `deny` list to `code` — `git push`, `npm publish`, 174`sudo`, destructive `gh` subcommands, etc. 175 176#### Drift from upstream `code` 177 178The starter is a pinned snapshot of fence's `code` template plus the 179adjustments above. When fence ships a new version of `code` (a new 180allowed domain, a new writable cache dir), pi-fence's snapshot does 181not auto-update. Periodic resync recipe: 182 183```bash 184diff <(jq -S '.network, .filesystem, .command' \ 185 < fence.jsonc 2>/dev/null) \ 186 <(fence config show --template code | jq -S '.network, .filesystem, .command') 187``` 188 189The template is versioned via `STARTER_TEMPLATE_VERSION` in 190[`policy/starter-template.ts`](./policy/starter-template.ts) so you 191can tell at a glance which generation a project's `fence.jsonc` was 192scaffolded from. 193 194#### Bypass / opt-out 195 196- One session: `pi --no-fence`. 197- This project: decline the scaffold prompt; pi-fence stays out of 198 the way until you accept (or run `/fence scaffold`/`/fence init`). 199- Don't want pi-fence loaded at all: remove 200 `~/.pi/agent/extensions/pi-fence`. 201 202### Manual configuration 203 204If you'd rather hand-roll your fence.jsonc, the standard fence form 205works fine and pi-fence will respect it: 206 207```jsonc 208{ 209 "$schema": "https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json", 210 "extends": "code", 211 "network": { 212 "allowedDomains": ["private-registry.company.com"] 213 }, 214 "filesystem": { 215 "denyRead": ["./secrets/**"] 216 } 217} 218``` 219 220If you go this route on macOS and use mise, expect the issues 221described above (mise install of source toolchains fails on the 222broad pem/key deny). Either scope your own `denyWrite` to `./**/*` 223patterns and add `~/Library/Caches/**` to `allowWrite`, or accept 224that toolchain installs need to happen outside pi-fence (`pi --no-fence` 225or a separate terminal). 226 227[fence-agents]: https://github.com/Use-Tusk/fence/blob/main/docs/agents.md 228[fence-config]: https://github.com/Use-Tusk/fence/blob/main/docs/configuration.md 229 230### Going stricter (optional): `defaultDenyRead` 231 232If you want reads to be default-deny instead of default-allow, fence 233ships the `code-strict` template (`extends: "code-strict"`) which sets 234`defaultDenyRead: true` plus a curated `allowRead` covering the 235project and common tool config dirs. 236 237**Caveat with `defaultDenyRead` alone**: fence's bash layer treats 238`defaultDenyRead: true` as "deny reads outside `allowRead` *plus* a 239built-in allowlist of system paths" (`/etc`, `/usr`, `/System`, 240language-version-manager dirs, etc.). So `cat /etc/passwd` still 241succeeds at the bash layer. 242 243The Pi-tool layer (the `read` tool) does **not** honor that built-in 244allowlist — it blocks `read("/etc/passwd")` directly. Result: the 245layers appear to disagree under bare `defaultDenyRead`. 246 247To make both layers reject system-path reads, also add 248`strictDenyRead: true`: 249 250```jsonc 251{ 252 "extends": "code-strict", 253 "filesystem": { 254 "strictDenyRead": true // tighten bash layer to match Pi-tool layer 255 } 256} 257``` 258 259Most users do not need this. Stick with `extends: "code"` unless you 260have a specific reason to deny reads. 261 262> Why pi-fence doesn't auto-mirror fence's implicit defaults: there's 263> no documented API for them today (only debug-output parsing). We 264> chose to keep the Pi-tool layer strict by-the-book and let users opt 265> into `strictDenyRead` for symmetry. See PLAN.md §Layer alignment for 266> the trade-off discussion. 267 268## Slash commands 269 270- `/fence` — show the resolved policy, including the chain (project 271 config + any extends), case-sensitivity detection, and the current 272 session grants. 273- `/fence reload` — re-run `fence config show` and refresh the cached 274 policy. Use after editing `fence.jsonc` mid-session. 275- `/fence scaffold` — write pi-fence's starter `fence.jsonc` to the 276 project root. Refuses to clobber an existing config; use this if 277 you previously declined the session_start prompt or want to upgrade 278 a minimal `extends: "code"` stub. 279- `/fence init` — proxy to `fence config init` in cwd. Writes the 280 minimal `extends: "code"` stub fence ships by default. Prefer 281 `/fence scaffold` for projects you actually want pi-fence to 282 manage. 283- `/fence log [N]` — tail the last `N` audit log entries (default 20, 284 cap 500). 285 286## Grants 287 288When the FS layer blocks a tool call and a TUI is available, you'll see: 289 290``` 291pi-fence: read denied 292 path: /Users/me/other-repo/package.json 293 requested: ./other-repo/package.json (only shown when realpath differs) 294 rule: filesystem.defaultDenyRead 295 296 [d] deny ← default-focused 297 [a] allow once 298 [s] allow for this session 299 [p] persist to fence.jsonc 300``` 301 302The four options: 303 304- **deny**: structured error returned to the LLM, no record kept. 305- **allow once**: this single tool call only. Logged to audit as 306 `grant-once`. 307- **session**: matches future calls in this session for the same 308 `(read|write, realpath)` pair. Persisted via `pi.appendEntry` so it 309 survives fork/resume. Cleared on `/new`. Logged as `grant-session`. 310- **persist**: session grant *plus* writes the path into the 311 project-root `fence.jsonc` (see below). Requires a second 312 confirmation. Logged as `grant-persist`. 313 314A 500ms anti-fatfinger delay drains buffered keystrokes before the 315prompt accepts input — a chain of denials can't be grant-bombed by a 316lingering Enter. 317 318### Persist target rules 319 320Persist always writes to `<project-root>/fence.jsonc`, where project 321root = `git rev-parse --show-toplevel`, falling back to cwd. We never 322edit `~/.config/fence/fence.jsonc` (the global) and we never walk 323upward looking for an existing file. If no project file exists, we 324scaffold one with `extends: "@base"` (when a global config is in the 325resolution chain) or `extends: "code"` otherwise. 326 327Edits go through `jsonc-parser` so comments and formatting are 328preserved; the result is validated against a vendored copy of 329[fence's JSON schema][fence-schema]. Atomic write via tmp + rename; 330last 3 backups kept as `fence.jsonc.bak.<unix-ts>`. 331 332[fence-schema]: https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json 333 334### Mandatory denies (non-overridable) 335 336A short list of paths is denied for writes regardless of your 337`fence.jsonc`. They cannot be granted via any prompt or persist: 338 339- `~/.pi/agent/extensions/**` — Pi auto-loads TS extensions at startup 340- `~/.pi/agent/skills/**`, `~/.agents/skills/**` — skills become part 341 of the system prompt 342- `~/.pi/agent/prompts/**` — prompt templates same effect 343- `~/.config/fence/**` — editing global fence config silently broadens 344 policy machine-wide 345- `~/.pi/agent/pi-fence.log*` — audit log integrity 346- `<pi-fence install dir>/**` — belt-and-braces against self-modification 347 348The rationale is that a write to any of these gives the agent 349*persistent influence over future Pi sessions* — effectively, prompt 350injection or arbitrary code execution that survives even after this 351session ends. See [`PLAN.md`](./PLAN.md) §11. 352 353This enforcement is full-strength on the Pi-tool layer (which we 354control). The bash layer inherits the same denies via the resolved 355config fence reads. 356 357### Headless mode 358 359When `ctx.hasUI === false` (RPC and `--print` modes), no prompts are 360shown. Every block returns the structured error directly to the LLM 361and the audit log records it. Set policy upfront via `fence.jsonc` for 362headless agents. 363 364## Audit log 365 366Location: `~/.pi/agent/pi-fence.log`. One JSON object per line: 367 368```jsonc 369{ 370 "ts": "2026-05-10T14:30:01.123Z", 371 "session": "<session-id>", 372 "decision": "block" | "grant-once" | "grant-session" | "grant-persist", 373 "tool": "read" | "write", 374 "path": "/Users/me/.ssh/id_rsa", 375 "rule": "filesystem.defaultDenyRead", 376 "pattern": null 377} 378``` 379 380Daily rotation: on the first append of a UTC day, an existing 381`pi-fence.log` is renamed to `pi-fence.log.YYYY-MM-DD`. Files older 382than 7 days are pruned. Use `/fence log [N]` to tail. 383 384Note: only Pi-tool-layer decisions are logged here. Bash-side denials 385live in fence's own monitoring (`fence -m`). 386 387## Footgun lint 388 389At session start, pi-fence runs a one-shot lint of your resolved 390policy and emits warnings (never blocks) for: 391 392- cwd === `$HOME` (every relative pattern becomes very permissive) 393- `allowRead` matching `~/.ssh/id_rsa`, `~/.aws/credentials`, 394 `/etc/passwd`, `~/.bash_history`, `/etc/shadow` 395- `allowWrite` matching `/etc/hosts`, `/etc/passwd`, `~/.bashrc`, 396 `~/.zshrc`, `~/.profile` 397- Top-level `**` patterns 398- Brace expansion in patterns (`~/.{ssh,aws}/**`) — picomatch 399 expands these but fence's Go matcher does not, so the layers would 400 silently disagree 401- Bare `**` segments without slash neighbors (likely typos for `*`) 402 403## Trade-offs and known limits 404 405Pi-fence is **defense-in-depth against agent error and casual probing**. 406It is not a containment boundary against malicious code or an adversarial 407LLM. The items below are real and intentionally not addressed in code. 408Each is something to understand before relying on this extension. 409 410### 1. Delayed-execution payloads 411 412The agent legitimately writes `package.json` (allowed by 413`allowWrite: ["."]`). It includes a malicious `postinstall` hook. Later, 414*you* run `npm install` in a normal terminal — outside any sandbox. 415Game over. 416 417This is fundamentally outside pi-fence's lane. Fence sandboxes the 418agent's bash, not your subsequent shell sessions. Mitigations: 419 420- Run package managers through fence yourself: `fence -- npm install` 421- Use `--ignore-scripts` (`npm ci --ignore-scripts`, 422 `pnpm install --ignore-scripts`) 423- Diff-review dependency changes the agent introduces 424 425### 2. Cache staleness between bash and Pi-tool layers 426 427The bash layer reads `fence.jsonc` per invocation; the Pi-tool layer 428caches at session start. If you edit `fence.jsonc` mid-session, bash 429gets the new policy immediately but the Pi-tool layer keeps the stale 430view until `/fence reload`. 431 432Worst case: tightening `denyRead` mid-session leaves a brief window where 433the Pi-tool layer still allows it. 434 435A future enhancement may auto-reload via `fs.watch`. Until then, run 436`/fence reload` after editing config. 437 438### 3. Adversarial LLM 439 440Pi-fence assumes the LLM is *fallible*, not *adversarial*. A jailbroken 441or prompt-injected model can: 442 443- Construct grant requests that look legitimate but aren't 444- Tell you to run `--no-fence` "to fix the issue" 445- Chain operations that reach denied outcomes via allowed primitives 446- Exfiltrate data via its own response text — invisible to every layer 447 of pi-fence 448 449If your threat model includes a malicious model served from an 450untrusted provider, you need VM-level isolation (Firecracker, gVisor, 451full container) — not pi-fence. 452 453### 4. Headless `--no-fence` is silent by default 454 455In `-p` print mode and RPC mode, `ctx.ui.notify` may be swallowed. A CI 456runner that picks up `--no-fence` from a pasted command line gets 457unsandboxed bash with no visible warning. 458 459A future enhancement may refuse to start in headless mode if 460`--no-fence` is set or fence is missing, unless an explicit 461`--allow-unfenced` flag is also present. Tracked but not implemented. 462 463### 5. Grant rate-limiting is per-turn 464 465The grant flow rate-limits to 3 distinct denials per agent turn, 466resetting on `turn_end`. A chatty agent doing one tool call per turn 467never hits the limit. A rolling 60-second window would close this; 468filter for v2. 469 470### 6. Unicode normalization 471 472`café` in NFC and `café` in NFD are different byte strings, but the 473same file on macOS APFS. A deny rule written one way may not match a 474path requested the other way. Mostly theoretical. 475 476### 7. TOCTOU between realpath check and operation 477 478We `realpath` a path, decide, then the tool reads. A racing process 479could swap a symlink between the check and the open. Defeating this 480requires concurrent host-level malware, outside pi-fence's threat model. 481 482### 8. Audit log integrity 483 484The audit log at `~/.pi/agent/pi-fence.log` is plain append-only text 485— not cryptographically signed and not OS-level append-only. Mandatory 486denies (see [`PLAN.md`](./PLAN.md) §11) cover the log path so the 487agent cannot persist a grant to it via the normal flow, but a 488sufficiently determined attacker can still rewrite it. 489 490If forensic-grade auditing matters, use OS-level append-only flags 491(`chattr +a` on Linux, ext attrs on macOS). 492 493### 9. Schema and binary version drift 494 495Pi-fence vendors a pinned `fence.schema.json` for config validation 496(used during `/fence`-driven persists). Your installed `fence` binary 497may be older or newer. Schema-valid configs can still be rejected by 498the binary, or vice versa. Tested with fence 0.1.54. 499 500### 10. Trust in the `fence` binary 501 502If `fence` on your PATH is replaced with a malicious shim, the entire 503bash sandbox is gone, and the Pi-tool layer's policy comes from whatever 504the shim's `fence config show` returns. Same trust model as any binary 505on PATH. 506 507Install fence from official sources (`brew tap use-tusk/tap`, signed 508GitHub releases, or build from source) and verify your installation. 509 510### 11. MCP server tools (out of scope) 511 512If you have MCP servers configured in Pi, their tools run as subprocesses 513of Pi — neither bash subprocesses (invisible to fence) nor Pi's built-in 514file tools (invisible to pi-fence's Pi-tool layer once pass 1 ships). 515 516There is no plan to extend pi-fence to MCP servers in v1. If you use 517MCP, treat those tools as ungated and configure their access at the 518MCP-server level. 519 520### 12. Pi-internal reads and writes 521 522Pi reads files for itself outside of tool calls — AGENTS.md, skill 523files, session storage, image attachments, extension code. Pi-fence 524governs the **agent's** tool calls, not Pi's own filesystem access. 525 526The mandatory denies in [`PLAN.md`](./PLAN.md) §11 prevent the agent 527from writing to the most dangerous of these locations 528(`~/.pi/agent/extensions/`, `~/.pi/agent/skills/`, 529`~/.config/fence/`). Other Pi-internal paths are unprotected by design. 530 531### 13. Pi-fence ships an opinionated policy snapshot 532 533The scaffolded `fence.jsonc` is a vendored snapshot of fence's `code` 534template plus the adjustments described in 535[Project scaffolding](#project-scaffolding-default-behavior). When fence 536upgrades the `code` template upstream (e.g. adds a new domain to 537`allowedDomains`), pi-fence's snapshot does not auto-update. You may 538be running on a slightly stale baseline. 539 540This is the trade for solving the `**/*.pem` deny problem at all: 541fence's slice fields are append-only on `extends`, so the only way 542to narrow an inherited deny is to author the policy from scratch. 543Pi-fence pays for that by owning a small ongoing sync burden against 544upstream `code`. 545 546Practical mitigations: 547 548- The starter is intentionally short and human-readable; review before 549 committing to a shared repo. 550- Bump pi-fence periodically; new releases ship a refreshed snapshot. 551- Run the diff recipe in 552 [Project scaffolding](#project-scaffolding-default-behavior) when 553 fence ships a `code` update you care about. 554- If you want fence's `code` template *as fence ships it today*, run 555 `/fence init` instead of `/fence scaffold` and accept the mise/npm 556 toolchain-install gap. 557 558## Threat model summary 559 560**Pi-fence stops:** 561 562- Accidental over-reach by the LLM via routine tool calls 563- Casual probing via bash (caught by fence at the kernel level) 564- Symlink-escape tricks (we `realpath` for both reads and writes — pass 1) 565- Path-traversal via `..` 566- Stale config drift between layers (impossible — same source) 567- Persistent prompt-injection / arbitrary code execution via writes to 568 Pi's extension or skill directories (mandatory denies) 569 570**Pi-fence does NOT stop:** 571 572- Determined attackers with kernel-level escapes 573- Content-based exfiltration to allowed network destinations 574- Delayed payloads in files the user later runs outside fence 575- TOCTOU races against host-level malware 576- Adversarial LLMs that work around denials via creative chains 577- MCP-server tool access 578- Pi's own internal filesystem and network access 579 580## Roadmap 581 582See [`PLAN.md`](./PLAN.md) for the full design and build plan. 583 584- **Pass 0** (shipped): bash + user_bash wrapping, status footer, 585 `--no-fence` flag. 586- **Pass 1** (shipped): Pi-tool-layer enforcement for 587 read/write/edit/grep/find/ls, structured block messages, `/fence` 588 command surface, mandatory denies, footgun lint. 589- **Pass 2** (shipped): interactive grants 590 (once / session / persist / deny), persist-to-`fence.jsonc` flow, 591 audit log with daily rotation. 592- **Pass 3** (shipped): project scaffolding with vetted starter 593 template, `/fence scaffold` slash command, mise/toolchain CDNs in 594 the network allowlist, project-scoped denyWrite patterns. 595- **Future** (no pass scheduled): rolling-window rate limiter, 596 `--allow-unfenced` headless guard, `fs.watch`-based auto-reload, 597 Linux append-only audit log via `chattr +a`, drift detection vs. 598 upstream `code` template. 599 600## License 601 602MIT. Same as Pi. 603 604## Credits 605 606Built on [`fence`][fence] by Use-Tusk, which itself credits Anthropic's 607[`sandbox-runtime`][sr] as inspiration. Both are sandbox wrappers around 608OS-level isolation primitives — pi-fence wires `fence` into Pi's 609extension event surface. 610 611[sr]: https://github.com/anthropic-experimental/sandbox-runtime