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.
- package/README.md +48 -381
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
# permission-guard-pi
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
## Quick start
|
|
62
20
|
|
|
63
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
34
|
+
Restart pi — you're done. Config locations and precedence:
|
|
83
35
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
42
|
+
See `examples/global/permission.json`, `examples/project/.pi/permission.json`, `examples/project/.pi/local/permission.json` for annotated starting points.
|
|
90
43
|
|
|
91
|
-
|
|
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
|
-
|
|
46
|
+
Each tool call resolves to `auto-allow`, `prompt`, or `deny`. Two axes drive the decision:
|
|
142
47
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
51
|
+
Rules in plain English:
|
|
172
52
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
86
|
+
npm run typecheck # tsc --noEmit
|
|
87
|
+
npm test # node --test "test/*.test.ts" "extensions/*.test.ts"
|
|
413
88
|
```
|
|
414
89
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
```bash
|
|
418
|
-
npm test
|
|
419
|
-
```
|
|
90
|
+
Layout:
|
|
420
91
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
-
|
|
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.
|
|
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",
|