[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
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