permission-guard-pi 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +48 -381
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,24 +1,12 @@
1
1
  # permission-guard-pi
2
2
 
3
- Layered, configurable file-access control for pi-coding-agent.
3
+ A permission gate for [pi-coding-agent](https://github.com/earendil-works/pi) that gates `read` / `write` / `edit` / `bash` calls behind a small, declarative config. Project-inner files are silent by default; sensitive paths (SSH keys, `.env`, private keys, credential stores) and cross-project writes prompt; hard blocks are declarative.
4
4
 
5
- Instead of a one-size-fits-all "block everything sensitive" rule, this
6
- extension lets you declare per-path permissions in JSONC config files and
7
- enforce them across all pi tools (read / write / edit / bash) — with
8
- persistent, project-scoped memory and an audit trail.
9
-
10
- ## Features
11
-
12
- - **Three-tier permissions**: `read_write` / `read_only` / `unread`
13
- - **Three config scopes**: global (~), project (repo), local (machine-specific)
14
- - **Dual region**: automatic project-inner / project-outer classification
15
- - **Two-tier memory**: session-level (per pi session) + persistent (survives restarts)
16
- - **Default sensitive-path fallback**: SSH keys, `.env`, cloud creds, private keys
17
- - **Cross-source conflict detection**: unread vs read_write prompts for confirmation
18
- - **Bash target extraction**: parses `rm`, `mv`, `cp`, `cat`, redirections, `curl -o`, etc.
19
- - **LLM prompt guardrails**: seeds bash-safety hints into the system prompt so the model reaches for `read`/`grep`/`find` first
20
- - **JSONL audit log**: every permission decision is traceable
21
- - **Zero runtime dependencies**: only `@earendil-works/pi-coding-agent` peerDep
5
+ - JSONC config, three scopes (`global` / `project` / `local`)
6
+ - Session + persistent grants, project-scoped (`~/.pi/agent/extensions/permission-guard-pi/trusted.json`)
7
+ - Default sensitive-path fallback when nothing is declared
8
+ - JSONL audit log with provenance (`authorization` + `grant_source`)
9
+ - Zero runtime deps — one peer: `@earendil-works/pi-coding-agent`
22
10
 
23
11
  ## Install
24
12
 
@@ -26,368 +14,53 @@ persistent, project-scoped memory and an audit trail.
26
14
  pi install npm:permission-guard-pi
27
15
  ```
28
16
 
29
- Or from a local checkout:
30
-
31
- ```bash
32
- pi install ./path/to/permission-guard-pi
33
- ```
34
-
35
- ## Upgrading from `@permission-system:pi-extension`
36
-
37
- This package was renamed from `@permission-system:pi-extension`. Runtime
38
- data now lives under a new path:
39
-
40
- | Before | After |
41
- |--------|-------|
42
- | `~/.pi/agent/extensions/@permission-system:pi-extension/` | `~/.pi/agent/extensions/permission-guard-pi/` |
43
-
44
- Historical `trusted.json` and `logs/permission-review.jsonl` at the old
45
- path are NOT migrated — remove them if you want a clean break, or copy
46
- them over manually:
47
-
48
- ```bash
49
- mv ~/.pi/agent/extensions/@permission-system:pi-extension ~/.pi/agent/extensions/permission-guard-pi
50
- ```
51
-
52
- Re-install after the rename:
53
-
54
- ```bash
55
- pi remove @permission-system:pi-extension
56
- pi install npm:permission-guard-pi
57
- ```
58
-
59
- ## Configuration
17
+ Local checkout: `pi install ./path/to/permission-guard-pi`.
60
18
 
61
- Three JSONC config files are checked, in this order of precedence:
19
+ ## Quick start
62
20
 
63
- | Scope | Location | Priority |
64
- |-----------|-------------------------------------------|----------|
65
- | local | `<projectRoot>/.pi/local/permission.json` | Highest |
66
- | project | `<projectRoot>/.pi/permission.json` | Medium |
67
- | global | `~/.pi/agent/permission.json` | Lowest |
68
-
69
- Missing files are treated as empty. Malformed files are ignored with a
70
- warning at session start.
71
-
72
- ### Format
21
+ Declare once:
73
22
 
74
23
  ```jsonc
24
+ // <projectRoot>/.pi/permission.json (JSONC: comments + trailing commas OK)
75
25
  {
76
26
  "paths": {
77
- "<path-pattern>": "read_write" | "read_only" | "unread"
27
+ "src/secrets.txt": "unread", // hard deny, no prompt
28
+ "**/*.pem": "read_write",
29
+ "vendor/**": "read_only"
78
30
  }
79
31
  }
80
32
  ```
81
33
 
82
- ### Path patterns
34
+ Restart pi — you're done. Config locations and precedence:
83
35
 
84
- - **Absolute paths** (`/...`, `~/...`, `C:\...`): match exactly, or match
85
- a glob pattern if it contains `*`, `?`, or `[...]`.
86
- - **Relative paths**: match only inside `<projectRoot>`. Supports `**` for
87
- any depth, `*` for a single segment, `?` for one char, and `[abc]` classes.
36
+ | Scope | Path | Priority |
37
+ |----------|---------------------------------------------|----------|
38
+ | local | `<projectRoot>/.pi/local/permission.json` | highest |
39
+ | project | `<projectRoot>/.pi/permission.json` | medium |
40
+ | global | `~/.pi/agent/permission.json` | lowest |
88
41
 
89
- Example:
42
+ See `examples/global/permission.json`, `examples/project/.pi/permission.json`, `examples/project/.pi/local/permission.json` for annotated starting points.
90
43
 
91
- ```jsonc
92
- {
93
- "paths": {
94
- "~/.ssh": "read_write", // exact match
95
- "**/*.pem": "read_write", // project-scoped glob
96
- "/var/log/**/*.log": "read_only", // absolute glob
97
- "src/secrets.txt": "unread" // hard deny
98
- }
99
- }
100
- ```
101
-
102
- ## Permission semantics
103
-
104
- | Permission | Read | Write | Delete | Notes |
105
- |------------|------|-------|--------|-------|
106
- | `read_write` (inner) | auto-allow | auto-allow | auto-allow | Inside project, no prompt |
107
- | `read_write` (outer) | prompt | prompt | prompt | Three-tier prompt, can be persisted |
108
- | `read_only` | auto-allow | prompt | prompt | Write/delete require approval |
109
- | `unread` | **deny** | **deny** | **deny** | Hard block, no prompt, not overrideable |
110
- | *(unconfigured, inner, non-sensitive)* | auto-allow | auto-allow | auto-allow | Default, keeps pi's normal behavior |
111
- | *(unconfigured, inner, sensitive)* | prompt | prompt | prompt | **Three-tier** (can persist) |
112
- | *(unconfigured, outer, non-sensitive)* | auto-allow | prompt | prompt | Three-tier (can persist) |
113
- | *(unconfigured, outer, sensitive)* | prompt | prompt | prompt | **Two-tier** (no "always allow") |
114
- | *(default `/tmp/**`)* | auto-allow | auto-allow | auto-allow | Built-in scratch-dir default; sensitive files still prompt |
115
-
116
- ## Prompt tiers
117
-
118
- When a prompt appears, it can be either **three-tier** or **two-tier**:
119
-
120
- - **Three-tier**: Allow this call only / Allow for this session / **Always allow** / Deny
121
- - **Two-tier**: Allow this call only / Allow for this session / Deny
122
-
123
- Three-tier appears when:
124
- - A config entry is declared (`read_write` on outer, or `read_only`/`unread` conflict)
125
- - The action is on a non-sensitive unconfigured outer path (writing to `/Users/x/Downloads/`, etc.)
126
-
127
- Default `/tmp/**` behavior:
128
- - Ordinary, non-sensitive `/tmp/**` and `/private/tmp/**` reads/writes/deletes are auto-allowed without prompting.
129
- - Sensitive files inside `/tmp` still prompt; explicit `read_only`/`unread` config still overrides the default.
130
-
131
- Two-tier appears when:
132
- - The path matches a built-in sensitive rule (SSH keys, `.env`, `.pem`, etc.) — we can't safely persist these
133
- - A bash command can't be reliably parsed (command substitution `$(...)`, backtick, process substitution `<(...)`, heredocs, forwarders like `xargs` in non-first pipeline stages, unquoted globs, unresolved variables, etc.) — we can't predict what will run
134
-
135
- "Always allow" persists to `~/.pi/agent/extensions/permission-guard-pi/trusted.json`
136
- under a key of `<projectHash>|<action>|<path>`. It's project-scoped, so
137
- grants in one project never leak into another.
138
-
139
- To revoke a grant, edit `trusted.json` and remove the entry.
44
+ ## How decisions work
140
45
 
141
- ## Sensitive path fallback
46
+ Each tool call resolves to `auto-allow`, `prompt`, or `deny`. Two axes drive the decision:
142
47
 
143
- When a path is not declared in any config AND matches the built-in sensitive
144
- list, the extension prompts. The prompt tier depends on region:
145
-
146
- - **Inside project** (tier=three): "Always allow" is offered because the
147
- path is within your project scope. Declare an explicit config entry to
148
- override the fallback entirely.
149
- - **Outside project** (tier=two): no "Always allow" — system-sensitive
150
- paths should not be silently persisted.
151
-
152
- Sensitive paths:
153
-
154
- - SSH keys (`id_rsa`, `id_ed25519`, etc.)
155
- - Private keys (`.pem`, `.key`, `.p12`, `.pfx`, `.jks`, `.kdbx`)
156
- - Env files (`.env`, `.env.*` but not `.env.example`)
157
- - Credential stores (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube`, `~/.docker`, `~/.netrc`)
158
- - Secret configs (`secrets.yaml`, `credentials`, `.npmrc`, `.netrc`)
159
-
160
- To allow a specific sensitive path without per-call prompts, add an explicit
161
- declaration to your config:
162
-
163
- ```jsonc
164
- {
165
- "paths": {
166
- "~/.ssh": "read_write" // Now no prompt for SSH key access.
167
- }
168
- }
169
- ```
48
+ - **Region** — `project_inner` (inside `<projectRoot>`) vs `project_outer`.
49
+ - **Sensitive** — matches the built-in list in `src/lib/sensitive.ts` (SSH keys, `.env`, private keys, credential stores, `/etc/shadow`, cron files, …).
170
50
 
171
- ## Bash target extraction
51
+ Rules in plain English:
172
52
 
173
- For bash commands, the extension parses the command line to identify which
174
- files will be read or written. The parser is quote-aware: characters inside
175
- single quotes are literal, and inside double quotes `\$`, `\`` , `\"`, and `\\`
176
- are literal but bare `$(...)` still executes (matching bash semantics).
53
+ - Explicit `read_write` → silent inside project, prompt outside.
54
+ - Explicit `read_only` → silent reads, prompted writes/deletes.
55
+ - Explicit `unread` → hard deny, never prompts.
56
+ - Unconfigured inside project → silent for non-sensitive paths, prompt for sensitive.
57
+ - Unconfigured outside project → silent reads, prompted writes; sensitive paths always prompt (and can never be persisted).
177
58
 
178
- **Pipeline handling**: pipelines (`|`) are split into independent stages and
179
- extracted per-stage. `find /etc | sort | head` produces read target `/etc`.
180
- Forwarders (`xargs`, `parallel`, `pforeach`) in non-first pipeline stages
181
- remain unreliable because they consume stdin as arguments to downstream tools.
59
+ Prompts come in two tiers: **three-tier** offers "Always allow" (persists to `trusted.json`, project-scoped by hash), **two-tier** does not. Sensitive paths outside the project, and any bash construct the extractor can't statically reason about, use two-tier. Revoke a grant by editing `trusted.json`.
182
60
 
183
- **Writes**: `rm`, `mv` (dest), `cp` (dest), `tee`, `install`, `chmod`, `chown`, `dd of=`, `curl -o`, `wget -O`, redirections (`>`, `>>`, `2>`)
184
- **Reads**: `cat`, `head`, `tail`, `wc`, `grep`, `mv` (src), `cp` (src)
185
- **Variables**: `$HOME` and `$PWD` are expanded (quote-aware). Other variables trigger a fallback prompt.
61
+ Bash commands are statically parsed to extract read/write targets; anything complex (subshells, heredocs, `bash -c`, interpreters, remote tools, unquoted globs outside the project) falls through to a two-tier prompt. Silent allow is a small whitelist of dev-tool commands (`git status`/`log`/`diff`, `npm test`, `cargo check`, `tsc --noEmit`, …).
186
62
 
187
- **Silent commands** (0 target, no prompt): development-tool commands whose
188
- filesystem side effects are constrained to the project worktree. The
189
- whitelist is a **command + allowed subcommand** pair — the command name
190
- must match AND the invocation form must be read-only:
191
-
192
- | Command | Allowed subcommands | Example (silent) | Example (prompts) |
193
- |---|---|---|---|
194
- | `git` | `status`, `log`, `diff`, `branch`, `tag`, `remote`, `config`, `rev-parse`, `rev-list`, `shortlog`, `blame`, `show`, `ls-files` | `git status`, `git log --oneline -n 10`, `git rev-list HEAD` | `git log /etc/passwd` |
195
- | `npm` | `test`, `lint`, `audit` | `npm test` | `npm run build` |
196
- | `yarn` | `test`, `lint`, `audit` | `yarn lint` | `yarn add <pkg>` |
197
- | `pnpm` | `test`, `lint`, `audit` | `pnpm audit` | `pnpm dlx <pkg>` |
198
- | `tsc` | *(none — flag-only)* | `tsc --noEmit` | `tsc <file>` |
199
- | `cargo` | `check`, `metadata` | `cargo check` | `cargo run` |
200
- | `go` | `version`, `env` | `go version` | `go run ./cmd/main.go` |
201
- | `openspec` | *(any)* | `openspec status --change foo` | — |
202
- | `make`, `npx`, `rg`, `jq` | *(none — moved out of the silent whitelist; always prompt)* | — | `make -f /tmp/x.mk`, `rg pattern /etc/shadow` |
203
-
204
- **How a silent match is enforced**: if the leading command of a simple
205
- single-command invocation is in the whitelist but the invocation form
206
- doesn't match an allowed subcommand (e.g. `git log /etc/passwd` — `log`
207
- is allowed but a path follows; `npm run build` — `run` isn't in npm's
208
- allow-set), the command is classified as `unreliable_command` and
209
- prompts rather than silently falling through as 0-target. For `git`,
210
- revision references (`HEAD`, `main`, `HEAD~1`) are allowed after
211
- subcommands like `log`/`diff`/`show`. Pipeline/compound/redirect
212
- invocations (`git log | grep`, `git status && echo`, `git status 2>/dev/null`)
213
- are evaluated per-stage by the extractor rather than by this rule.
214
-
215
- **Commands moved out of the silent whitelist** (see below): `make`,
216
- `npx`, `rg`, `jq` (and `cargo`'s non-check subcommands) all appear in
217
- **Unreliable commands** now. They were historically silent, but they can
218
- read/write outside the worktree — `make -f /tmp/x.mk` runs an arbitrary
219
- makefile, `rg pattern /etc/shadow` reads outside the worktree, and
220
- `npx <pkg>` runs an arbitrary package. They always prompt.
221
-
222
- **Unreliable commands** (unconditionally prompts; tier=two with no
223
- "Always allow"): commands that can execute arbitrary code, read/write
224
- arbitrary files, or exfiltrate over the network. Command-name exact
225
- match on any command fragment's first token (after `env` / `command`
226
- wrapper prefixes) triggers this — `/usr/bin/sed` matches by basename,
227
- `env FOO=1 sed -i …` catches the inner `sed`:
228
-
229
- - **Script interpreters**: `sed`, `awk`, `perl`, `python`, `python2`,
230
- `python3`, `node`, `nodejs`, `ruby`, `php`, `lua`, `deno`, `bun`
231
- - **System script executors**: `osascript`, `powershell`, `pwsh`
232
- - **Network / remote tools**: `scp`, `sftp`, `rsync`, `ssh`, `telnet`,
233
- `nc`, `netcat`, `ncat`, `socat`
234
- - **Container / cloud orchestration**: `kubectl`, `docker`, `podman`,
235
- `nerdctl`, `crictl`
236
- - **Shell meta-commands**: `eval`, `exec`, `trap`
237
- - **Formerly-silent with path/script arguments**: `rg`, `jq`, `make`,
238
- `cargo`, `npx` (moved out of the silent whitelist)
239
-
240
- **Unreliable constructs** (always two-tier prompt with no "Always allow"):
241
- - Command substitution `$(...)` and backtick `` `...` `` (outside single quotes)
242
- - Process substitution `<(...)` / `>(...)`
243
- - Heredocs `<<EOF`
244
- - Forwarders (`xargs`, `parallel`, `pforeach`) in non-first pipeline stages
245
- - Unquoted glob metacharacters (`*`, `?`, `[`) outside quotes
246
- - Unresolved user variables (other than `$HOME`/`$PWD`)
247
- - Control-flow keywords (`while`, `for`, `until`, `case`, `if`, `then`, `else`, `elif`, `fi`, `do`, `done`, `esac`, `in`) as standalone tokens outside quotes — loop variables and branch conditions make execution non-statically-determinable
248
- - Shell forwarders (`bash -c`, `sh -c`, `dash -c`, `zsh -c`) **or** `bash` / `sh` / `dash` / `zsh` **with a script-path argument** (`bash /tmp/x.sh`, `bash -x /tmp/x.sh`) — the command to run is either a string or a file whose contents are invisible to static analysis (also catches `env KEY=VAL bash -c "..."` and option clusters like `-xc`)
249
- - `source <path>` and `.` (dot-source) with a path argument — same as `bash <path>`
250
- - Input redirection `<` (e.g. `bash < /tmp/x.sh`, `diff < /etc/passwd < /etc/hosts`) — extracted as a read target. Note: `bash < /tmp/x.sh` still triggers `shell_forwarder` because `bash` executes stdin contents.
251
-
252
- Note: pipelines like `cat /etc/passwd | base64` are **not** unreliable —
253
- they split into two stages, each parsed independently. The `cat` stage
254
- contributes read target `/etc/passwd`; the `base64` stage contributes none
255
- (base64 is not in the command table).
256
-
257
- ## Prompt guardrails
258
-
259
- Complex bash commands that the extractor cannot parse always hit the
260
- two-tier prompt with no "Always allow" option. That's the right safety
261
- posture, but it's painful to hit repeatedly. Rather than rely on the LLM
262
- to discover that after the fact, the extension seeds a short guardrail
263
- list into the system prompt at `before_agent_start`.
264
-
265
- The guardrails are organized in three layers — silent-first, so the LLM
266
- reaches for a no-prompt path before falling back to bash constructs
267
- that will prompt:
268
-
269
- - **Layer 1 (decision layer)**: one sentence explaining that prompts
270
- interrupt the user, so the LLM should **Minimize prompts** — each one
271
- interrupts the user. This is the motivation — without it, the LLM
272
- only avoids pitfalls reactively.
273
- - **Layer 2 (silent tool catalog)**: prefer `read`/`grep`/`find` and
274
- read-only forms of git/test/dev-tool commands. In-project read-only
275
- globs (`ls openspc/*`, `find . -name '*.ts'`, `cat src/a.ts`,
276
- `wc -l src/*.ts`, `git log src/`) are auto-allowed.
277
- - **Layer 3 (popup pitfalls)**: constructs that always prompt, each
278
- with a `→` alternative — forwarders, command/heredoc substitution,
279
- control-flow keywords, unquoted globs outside project or in write/
280
- delete commands, shell forwarders, interpreters & remote tools,
281
- non-read-only dev-tool forms, and **Sensitive system paths** outside
282
- the project (`/etc`, `/usr`, `/System`, `~/.ssh`, `~/.aws`) which
283
- usually prompt or hard-deny.
284
-
285
- **How it works**
286
-
287
- - Injected via `systemPromptOptions.promptGuidelines` (a mutable array on
288
- the `before_agent_start` event), not via `systemPrompt`. This appends a
289
- delta instead of replacing the whole prompt, so pi's default tools list,
290
- skills, cwd, and any other extension's guidance all stay intact.
291
- - Three-layer organization (silent-first): Layer 1 explains *why* to
292
- minimize prompts, Layer 2 lists the no-prompt options, Layer 3
293
- catches what's left. Positive guidance comes first so it wins the
294
- LLM's attention budget before the pitfall list.
295
- - Idempotent: each bullet is deduped by exact-match string, so repeat
296
- dispatches in the same turn don't pile up.
297
- - Non-destructive: pre-existing entries in `promptGuidelines` are
298
- preserved, so other extensions' rules survive alongside these.
299
- - Fail-safe: the injection is wrapped in try/catch. A malformed event or
300
- a missing `systemPromptOptions` is silently ignored — guardrails never
301
- break the agent loop.
302
-
303
- **Scope and limits**
304
-
305
- - These are *soft* guardrails. They steer the model, they don't enforce
306
- anything. The bash extractor's hard rules still decide what runs.
307
- - The bullets are hard-coded in `extensions/index.ts`. Edit the
308
- `BASH_GUARDRAILS` array there to customize.
309
- - Layer 3's "Sensitive system paths" bullet is a reminder, not an
310
- enforcement — the actual sensitive-path list lives in
311
- `src/lib/sensitive.ts` and is unchanged by the guardrail text.
312
- - The rules are always injected, for every session. If you want them off
313
- for a specific project, remove the `installPromptGuardrails(pi)` call
314
- from `registerPermissionGate`.
315
-
316
- ## Cross-source conflicts
317
-
318
- If two config sources declare different permissions for the same path:
319
-
320
- - **`read_only` vs `read_write`** (no unread): the local source wins
321
- silently. No prompt.
322
- - **`unread` involved**: the extension prompts for confirmation on each
323
- access, marking the prompt as a "cross-source conflict".
324
-
325
- ## Audit log
326
-
327
- Every permission decision is written to:
328
-
329
- ```
330
- ~/.pi/agent/extensions/permission-guard-pi/logs/permission-review.jsonl
331
- ```
332
-
333
- Each line is a JSON object:
334
-
335
- ```json
336
- {
337
- "ts": "2026-10-12T10:00:00.000Z",
338
- "event": "allow_persistent",
339
- "tool": "write",
340
- "project": "a1b2c3d4e5f6",
341
- "projectRoot": "/Users/x/project",
342
- "path": "/opt/shared/secrets/x",
343
- "command": null,
344
- "region": "project_outer",
345
- "declaration": "read_write",
346
- "permission": "read_write",
347
- "toolCallId": "tc-123",
348
- "note": "saved to ~/.pi/agent/extensions/permission-guard-pi/trusted.json"
349
- }
350
- ```
351
-
352
- `event` is one of: `allow_once`, `allow_session`, `allow_persistent`,
353
- `deny`, `hard_deny`, `conflict_prompt`, `auto_allow`.
354
-
355
- ### Authorization provenance
356
-
357
- Every audit record also carries two optional fields that tell you *who* made
358
- the decision and *what rule / memory entry* fired. Consumers can filter on
359
- either axis without parsing the free-form `note` field.
360
-
361
- - `authorization`: `"by_person" | "presumed" | undefined`
362
- - `grant_source`:
363
- `"config_declaration" | "allow_rule" | "grant_memory" | "bash_grant_memory" | "default" | undefined`
364
-
365
- **event → authorization mapping** (deterministic, guaranteed by the implementation):
366
-
367
- | event | authorization |
368
- |---|---|
369
- | `allow_once` | `by_person` |
370
- | `allow_session` | `by_person` |
371
- | `allow_persistent` | `by_person` |
372
- | `deny` | `by_person` |
373
- | `conflict_prompt` | `by_person` |
374
- | `hard_deny` | `presumed` |
375
- | `auto_allow` | from the decision (see below) |
376
-
377
- `authorization` answers "was a human in the loop for this decision?".
378
- `grant_source` answers "which rule / memory entry actually fired?" —
379
- the fine-grained origin:
380
-
381
- - `config_declaration` — a config rule (read_write / read_only) matched.
382
- - `allow_rule` — a tool+pattern allow rule matched (non-sensitive target).
383
- - `grant_memory` — a path-based grant-memory entry hit.
384
- - `bash_grant_memory` — a bash-command grant-memory entry hit.
385
- - `default` — region/sensitive/default fallback (no config, no rule).
386
-
387
- **Backward compatibility.** Records written before this field existed
388
- have neither `authorization` nor `grant_source`. Consumers MUST treat
389
- their absence as `undefined` and fall back to `event` semantics for
390
- compatibility — do not try to backfill fields into existing log lines.
63
+ > Full parser semantics, silent-command whitelist, and known blind spots: [`docs/security/permission-model.md`](./docs/security/permission-model.md).
391
64
 
392
65
  ## Uninstall
393
66
 
@@ -395,43 +68,37 @@ compatibility — do not try to backfill fields into existing log lines.
395
68
  pi remove permission-guard-pi
396
69
  ```
397
70
 
398
- Your configs and `trusted.json` remain on disk. Delete them manually if
399
- you want a full clean:
71
+ Configs and `trusted.json` stay on disk. Full clean:
400
72
 
401
73
  ```bash
402
74
  rm -rf ~/.pi/agent/extensions/permission-guard-pi/
403
- rm ~/.pi/agent/permission.json # if you created one
75
+ rm ~/.pi/agent/permission.json # if you created one
404
76
  ```
405
77
 
406
78
  ## Development
407
79
 
80
+ Requires Node.js ≥ 22.18.
81
+
408
82
  ```bash
409
83
  git clone <this repo>
410
84
  cd permission-guard-pi
411
85
  npm install
412
- npm test
86
+ npm run typecheck # tsc --noEmit
87
+ npm test # node --test "test/*.test.ts" "extensions/*.test.ts"
413
88
  ```
414
89
 
415
- Tests use the Node.js built-in test runner (requires Node.js >= 22.18):
416
-
417
- ```bash
418
- npm test
419
- ```
90
+ Layout:
420
91
 
421
- Which runs:
422
-
423
- ```
424
- node --test "test/*.test.ts" "extensions/*.test.ts"
425
- ```
92
+ - `extensions/index.ts` — pi integration layer (session init, prompt flow, audit wiring)
93
+ - `src/lib/*.ts` — pure functions (config, decision, bash extraction, sensitive matching, trust, audit)
94
+ - `test/` — Node.js built-in test runner
95
+ - `openspec/specs/` — per-subsystem specs
96
+ - `docs/security/permission-model.md` — security boundary contract
426
97
 
427
98
  ## Contributing
428
99
 
429
- Contributions are welcome. Please:
430
-
431
- - Add tests before implementation (TDD)
432
- - Keep the decision engine (`src/lib/decision.ts`) free of IO/UI dependencies
433
- - Update the specs in `openspec/specs/` when adding behavior
434
-
435
- ## License
100
+ - TDD: add the failing test first, then the implementation.
101
+ - Keep `src/lib/decision.ts` free of IO/UI — it's the only piece that should be trivially testable in isolation.
102
+ - Update `openspec/specs/` whenever you add or change behavior.
436
103
 
437
104
  MIT. See [LICENSE](./LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "permission-guard-pi",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Layered, configurable file-access control for pi-coding-agent. Dual region (project_inner / project_outer) x three-tier permissions (read_write / read_only / unread), with session- and persistent-grant memory, audit log, and default-sensitive-path fallback.",
5
5
  "keywords": [
6
6
  "pi-package",