@gotgenes/pi-permission-system 5.3.0 → 5.3.2

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 (3) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +50 -876
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.3.2](https://github.com/gotgenes/pi-permission-system/compare/v5.3.1...v5.3.2) (2026-05-06)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * fix ordered list continuation, use realistic quick-start config ([dd26166](https://github.com/gotgenes/pi-permission-system/commit/dd261666e497a280902790a5a50f7daf70a511a4))
14
+ * **retro:** add retro notes for issue [#98](https://github.com/gotgenes/pi-permission-system/issues/98) ([bf2bbc6](https://github.com/gotgenes/pi-permission-system/commit/bf2bbc61c748222ff4bef8234ed8543d7a93ad27))
15
+
16
+ ## [5.3.1](https://github.com/gotgenes/pi-permission-system/compare/v5.3.0...v5.3.1) (2026-05-05)
17
+
18
+
19
+ ### Documentation
20
+
21
+ * add permission frontmatter convention guide for subagent extensions ([c992959](https://github.com/gotgenes/pi-permission-system/commit/c99295960ad31e359fc1fbdea0ad45057d8366d8))
22
+ * add upstream issue template for subagent extension outreach ([79eef27](https://github.com/gotgenes/pi-permission-system/commit/79eef27fbee6786b5ef8c830ed0fce575d067b92))
23
+ * link permission frontmatter guide from README and target architecture ([df261fc](https://github.com/gotgenes/pi-permission-system/commit/df261fc36f9d9c527524b3301959a1be69bf8a51))
24
+ * plan shared permission frontmatter convention for subagent extensions ([#98](https://github.com/gotgenes/pi-permission-system/issues/98)) ([eec5763](https://github.com/gotgenes/pi-permission-system/commit/eec57637810a04c923ddff6e5991a8fbd95f4030))
25
+ * restructure README with inverted pyramid, extract reference docs ([db51142](https://github.com/gotgenes/pi-permission-system/commit/db51142e3bfc2283298c23a7a8517c72179f4611))
26
+ * **retro:** add retro notes for issue [#29](https://github.com/gotgenes/pi-permission-system/issues/29) ([be0620b](https://github.com/gotgenes/pi-permission-system/commit/be0620bc417297d43d5e7c0d04efd14f2844f147))
27
+
8
28
  ## [5.3.0](https://github.com/gotgenes/pi-permission-system/compare/v5.2.1...v5.3.0) (2026-05-05)
9
29
 
10
30
 
package/README.md CHANGED
@@ -2,898 +2,82 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@gotgenes/pi-permission-system?style=flat&logo=npm&logoColor=white)](https://www.npmjs.com/package/@gotgenes/pi-permission-system) [![CI](https://img.shields.io/github/actions/workflow/status/gotgenes/pi-permission-system/ci.yml?style=flat&logo=github&label=CI)](https://github.com/gotgenes/pi-permission-system/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Pi Package](https://img.shields.io/badge/Pi-Package-6366F1?style=flat)](https://pi.mariozechner.at/)
4
4
 
5
- Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
5
+ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) coding agent that provides centralized, deterministic permission gates over tool, bash, MCP, skill, and special operations.
6
6
 
7
7
  > **Fork notice:** This package is a full fork of [MasuRii/pi-permission-system](https://github.com/MasuRii/pi-permission-system), published to npm as `@gotgenes/pi-permission-system`.
8
8
  > It has diverged substantially from upstream in config format, internal architecture, and permission model.
9
- > The `/permission-system` slash command name is the only upstream identity preserved.
10
9
 
11
- ## Features
10
+ ## What It Does
12
11
 
13
- - **Tool Filtering** — Hides disallowed tools from the agent before it starts (reduces "try another tool" behavior)
14
- - **System Prompt Sanitization** Removes denied tool entries from the `Available tools:` system prompt section so the agent only sees tools it can actually call
15
- - **Runtime Enforcement** Blocks/asks/allows at tool call time with UI confirmation dialogs and readable approval summaries
16
- - **Bash Command Control** Wildcard pattern matching for granular bash command permissions
17
- - **MCP Access Control** — Server and tool-level permissions for MCP operations
18
- - **Skill Protection** — Controls which skills can be loaded or read from disk, including multi-block prompt sanitization
19
- - **Per-Agent Overrides** — Agent-specific permission policies via YAML frontmatter
20
- - **Subagent Permission Forwarding** — Forwards `ask` confirmations from non-UI subagents back to the main interactive session
21
- - **File-Based Review Logging** — Writes permission request/denial review entries to a file by default for later auditing
22
- - **Optional Debug Logging** — Keeps verbose extension diagnostics in a separate file when enabled in `config.json`
23
- - **JSON Schema Validation** — Full schema for editor autocomplete and config validation
24
- - **External Directory Guard** — Enforces `special.external_directory` for path-bearing file tools and bash commands that reference paths outside the active working directory
12
+ - **Hides disallowed tools** before the agent starts no wasted turns probing for blocked tools
13
+ - **Enforces allow / ask / deny** at tool-call time with UI confirmation dialogs
14
+ - **Controls bash commands** with wildcard pattern matching (`git *: ask`, `rm -rf *: deny`)
15
+ - **Gates MCP and skill access** at server, tool, and skill-name granularity
16
+ - **Guards external paths** — prompts before file tools or bash commands reach outside `cwd`
17
+ - **Forwards prompts from subagents** — `ask` policies work even in non-UI execution contexts
25
18
 
26
- ## Installation
27
-
28
- ### npm package
19
+ ## Install
29
20
 
30
21
  ```bash
31
- pi install npm:pi-permission-system
22
+ pi install npm:@gotgenes/pi-permission-system
32
23
  ```
33
24
 
34
- ### Local extension folder
35
-
36
- Place this folder in one of the following locations:
37
-
38
- | Scope | Path |
39
- | -------------- | ------------------------------------------------------------------------------ |
40
- | Global default | `~/.pi/agent/extensions/pi-permission-system` (respects `PI_CODING_AGENT_DIR`) |
41
- | Project | `.pi/extensions/pi-permission-system` |
42
-
43
- Pi auto-discovers extensions in these paths.
25
+ ## Quick Start
44
26
 
45
- > **Tip:** All `~/.pi/agent` paths shown in this document are defaults. If the `PI_CODING_AGENT_DIR` environment variable is set, pi uses that directory instead. The extension automatically follows pi's `getAgentDir()` helper, so global policy files, per-agent overrides, session directories, and extension installation paths all resolve under the configured agent directory.
27
+ 1. Create the global config file at `~/.pi/agent/extensions/pi-permission-system/config.json`:
46
28
 
47
- ## Usage
48
-
49
- ### Quick Start
50
-
51
- 1. Create the global config file (default: `~/.pi/agent/extensions/pi-permission-system/config.json`, respects `PI_CODING_AGENT_DIR`):
52
-
53
- ```jsonc
54
- {
55
- "permission": {
56
- "*": "ask",
57
- "read": "allow",
58
- "write": "deny"
59
- }
60
- }
61
- ```
62
-
63
- 1. Start Pi — the extension automatically loads and enforces your policy.
29
+ ```jsonc
30
+ {
31
+ "permission": {
32
+ "*": "allow",
33
+ "bash": {
34
+ "rm -rf *": "deny",
35
+ "sudo *": "ask"
36
+ },
37
+ "external_directory": "ask"
38
+ }
39
+ }
40
+ ```
64
41
 
65
- ### Permission States
42
+ 2. Start Pi — the extension automatically loads and enforces your policy.
66
43
 
67
44
  All permissions use one of three states:
68
45
 
69
- | State | Behavior |
70
- | ------- | ---------------------------------------- |
71
- | `allow` | Permits the action silently |
72
- | `deny` | Blocks the action with an error message |
73
- | `ask` | Prompts the user for confirmation via UI |
74
-
75
- ### Pi Integration Hooks
76
-
77
- The extension integrates via Pi's lifecycle hooks:
78
-
79
- | Hook | Behavior |
80
- | -------------------- | ------------------------------------------------------------------------------------------------- |
81
- | `before_agent_start` | Filters active tools, removes denied tool entries from the system prompt, and hides denied skills |
82
- | `tool_call` | Enforces permissions for every tool invocation |
83
- | `input` | Intercepts `/skill:<name>` requests and enforces skill policy |
84
-
85
- **Additional behaviors:**
46
+ |State|Behavior|
47
+ |---|---|
48
+ |`allow`|Permits the action silently|
49
+ |`deny`|Blocks the action with an error message|
50
+ |`ask`|Prompts the user for confirmation via UI|
86
51
 
87
- - Unknown/unregistered tools are blocked before permission checks (prevents bypass attempts)
88
- - The `Available tools:` system prompt section is rewritten to match the filtered active tool set
89
- - Extension-provided tools like `task`, `mcp`, and third-party tools are handled by exact registered name instead of private built-in hardcodes
90
- - When a subagent hits an `ask` permission without direct UI access, the request can be forwarded to the main interactive session for confirmation
91
- - Generic extension-tool approval prompts include a bounded input preview; built-in file tools use concise human-readable summaries instead of raw multiline JSON
92
- - Permission review logs include bounded `toolInputPreview` values for non-bash/non-MCP tool calls so approvals can be audited without writing raw full payloads
93
- - Path-bearing file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) evaluate `permission.external_directory` before their normal tool permission when an explicit path points outside `ctx.cwd`
94
- - Bash commands are parsed with a full bash AST (`web-tree-sitter` + `tree-sitter-bash`) to extract path-bearing arguments; only genuine command arguments and redirect destinations are checked — heredoc bodies, comments, and quoted string contents are correctly excluded — and paths that resolve outside `ctx.cwd` trigger the same `permission.external_directory` gate before the normal bash pattern check
52
+ When the dialog prompts, you can approve once or approve a pattern for the rest of the session.
53
+ See [docs/session-approvals.md](docs/session-approvals.md) for details on session-scoped rules and pattern suggestions.
95
54
 
96
55
  ## Configuration
97
56
 
98
- ### Config File
99
-
100
- **Location:** one unified config file per scope, following the `pi-autoformat` convention:
101
-
102
- | Scope | Path |
103
- | ------- | ------------------------------------------------------------------------------------------------- |
104
- | Global | `~/.pi/agent/extensions/pi-permission-system/config.json` (respects `PI_CODING_AGENT_DIR`) |
105
- | Project | `<cwd>/.pi/extensions/pi-permission-system/config.json` |
106
-
107
- Project config overrides global config; per-agent frontmatter overrides both.
108
- The `permission` object uses deep-shallow merge: string-vs-string replaces; both-object shallow-merges pattern maps; string-vs-object the override wins entirely.
109
- Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`) use simple replacement.
110
-
111
- The config file combines runtime knobs and permission policy in one object:
112
-
113
- ```jsonc
114
- {
115
- "$schema": "https://raw.githubusercontent.com/gotgenes/pi-permission-system/main/schemas/permissions.schema.json",
116
-
117
- // Runtime knobs
118
- "debugLog": false,
119
- "permissionReviewLog": true,
120
- "yoloMode": false,
121
- "piInfrastructureReadPaths": [], // extra dirs to auto-allow for reads
122
-
123
- // Flat permission policy
124
- "permission": {
125
- "*": "ask", // universal fallback
126
- "read": "allow",
127
- "write": "deny",
128
- "bash": { "git status": "allow", "git *": "ask" },
129
- "mcp": { "mcp_status": "allow" },
130
- "skill": { "*": "ask" },
131
- "external_directory": "ask"
132
- }
133
- }
134
- ```
135
-
136
- #### Runtime knobs
137
-
138
- | Key | Default | Description |
139
- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------- |
140
- | `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
141
- | `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl` |
142
- | `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled |
143
- | `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate (supports `~`) |
144
-
145
- Both logs write to `~/.pi/agent/extensions/pi-permission-system/logs/`.
146
- No debug output is printed to the terminal.
147
-
148
- #### Policy sections
149
-
150
- The config file is a JSON object with these policy sections:
151
-
152
- The `permission` object maps surface names to actions:
153
-
154
- | Key | Value type | Description |
155
- | --- | ---------- | ----------- |
156
- | `"*"` | string | Universal fallback — applies when no surface-specific rule matches |
157
- | tool name (e.g. `read`) | string | Catch-all for that tool surface |
158
- | `bash` | string or object | Bash catch-all or `{ pattern: action }` map |
159
- | `mcp` | string or object | MCP catch-all or `{ pattern: action }` map |
160
- | `skill` | string or object | Skill catch-all or `{ pattern: action }` map |
161
- | `external_directory` | string or object | Controls access to paths outside `cwd`; supports `~/` and `$HOME/` patterns |
162
-
163
- > **Note:** Trailing commas are **not** supported. If parsing fails, the extension falls back to `ask` for all categories.
164
-
165
- ### Global Per-Agent Overrides
166
-
167
- Override global permissions for specific agents via YAML frontmatter in the global Pi agents directory (default: `~/.pi/agent/agents/<agent>.md`, respects `PI_CODING_AGENT_DIR`):
168
-
169
- ```yaml
170
- ---
171
- name: my-agent
172
- permission:
173
- read: allow
174
- write: deny
175
- mcp: allow
176
- bash:
177
- git status: allow
178
- git *: ask
179
- mcp:
180
- chrome_devtools_*: deny
181
- exa_*: allow
182
- skill:
183
- "*": ask
184
- ---
185
- ```
186
-
187
- **MCP behavior:** `permission.mcp` is the coarse entry/fallback permission for a registered `mcp` tool when one is available. More specific `permission.mcp` target rules override that fallback when they match.
188
-
189
- **Limitations:** The frontmatter parser is intentionally minimal. Use only `key: value` scalars and nested maps. Avoid arrays, multi-line scalars, and YAML anchors.
190
-
191
- ### Project-Level Config and Overrides
192
-
193
- Project-local config uses the same format as the global config file.
194
- Per-agent overrides use YAML frontmatter in the project agents directory:
195
-
196
- | Scope | Path |
197
- | ---------------------- | ------------------------------------------------------------- |
198
- | Project config | `<cwd>/.pi/extensions/pi-permission-system/config.json` |
199
- | Project agent override | `<cwd>/.pi/agent/agents/<agent>.md` |
200
-
201
- These project files are resolved from Pi's current session `cwd`, so they are workspace-specific and do **not** move under `PI_CODING_AGENT_DIR`.
202
-
203
- **Precedence order:**
204
-
205
- 1. Global config file
206
- 2. Project config file
207
- 3. Global agent frontmatter
208
- 4. Project agent frontmatter
209
-
210
- Later layers override earlier layers. Within a surface map like `bash` or `mcp`, matching follows **last matching rule wins** — put broad catch-alls first and specific overrides after. The recommended convention is also used by [OpenCode's permission model](https://opencode.ai/docs/permissions/#granular-rules-object-syntax).
211
-
212
- ---
213
-
214
- ## Policy Reference
215
-
216
- ### `permission["*"]` — universal fallback
217
-
218
- The `"*"` key sets the action used when no surface-specific rule matches:
219
-
220
- ```jsonc
221
- {
222
- "permission": {
223
- "*": "ask"
224
- }
225
- }
226
- ```
227
-
228
- Omitting `"*"` defaults to `"ask"` (least privilege).
229
-
230
- ### Tool surfaces
231
-
232
- Any registered tool name can be a surface key. A string value is a catch-all for that surface.
233
-
234
- | Surface example | Description |
235
- | --------------- | ----------- |
236
- | `read`, `write`, `edit`, `grep`, `find`, `ls` | Canonical Pi built-in file tools |
237
- | `bash` | Shell command execution |
238
- | `mcp` | Registered MCP proxy tool |
239
- | `task` | Delegation tool |
240
- | `third_party_tool` | Any other registered extension tool |
241
-
242
- ```jsonc
243
- {
244
- "permission": {
245
- "read": "allow",
246
- "write": "deny",
247
- "third_party_tool": "ask"
248
- }
249
- }
250
- ```
251
-
252
- Unknown or absent tools are not required in the config. If a tool is not registered at runtime, this extension blocks it before permission checks run.
253
-
254
- ### `bash` surface
255
-
256
- Command patterns use `*` wildcards matched against the full command string. **Last matching rule wins** — put broad catch-alls first, specific overrides after.
257
-
258
- ```jsonc
259
- {
260
- "permission": {
261
- "bash": {
262
- "*": "ask",
263
- "git status": "allow",
264
- "git diff": "allow",
265
- "git *": "ask",
266
- "rm -rf *": "deny"
267
- }
268
- }
269
- }
270
- ```
271
-
272
- String shorthand sets a catch-all for all bash commands:
273
-
274
- ```jsonc
275
- {
276
- "permission": { "bash": "allow" }
277
- }
278
- ```
279
-
280
- ### `mcp` surface
281
-
282
- MCP permissions match against derived targets from tool input:
283
-
284
- | Target type | Examples |
285
- | ----------- | -------- |
286
- | Baseline ops | `mcp_status`, `mcp_list`, `mcp_search`, `mcp_describe`, `mcp_connect` |
287
- | Server name | `myServer` |
288
- | Server/tool combo | `myServer:search`, `myServer_search` |
289
- | Generic | `mcp_call` |
290
-
291
- ```jsonc
292
- {
293
- "permission": {
294
- "mcp": {
295
- "*": "ask",
296
- "mcp_status": "allow",
297
- "mcp_list": "allow",
298
- "myServer:*": "ask",
299
- "dangerousServer": "deny"
300
- }
301
- }
302
- }
303
- ```
304
-
305
- > **Note:** Baseline discovery targets auto-allow when any explicit `mcp: allow` rule exists.
306
-
307
- String shorthand grants broad MCP access — useful for per-agent overrides:
308
-
309
- ```yaml
310
- # ~/.pi/agent/agents/researcher.md (respects PI_CODING_AGENT_DIR)
311
- ---
312
- name: researcher
313
- permission:
314
- mcp: allow
315
- ---
316
- ```
317
-
318
- ### `skill` surface
319
-
320
- Skill name patterns use `*` wildcards (note: surface is `skill`, not `skills`):
321
-
322
- ```jsonc
323
- {
324
- "permission": {
325
- "skill": {
326
- "*": "ask",
327
- "dangerous-*": "deny",
328
- "librarian": "allow"
329
- }
330
- }
331
- }
332
- ```
333
-
334
- ### Home directory expansion in patterns
335
-
336
- Pattern keys in any permission surface can start with `~/` or `$HOME/` (or be exactly `~` / `$HOME`).
337
- They are expanded to the OS home directory at match time, so configs are portable across machines and users.
338
-
339
- ```jsonc
340
- {
341
- "permission": {
342
- "external_directory": {
343
- "*": "ask",
344
- "~/development/*": "allow"
345
- }
346
- }
347
- }
348
- ```
349
-
350
- The pattern is stored and displayed as written (e.g. `~/development/*`) in logs and approval dialogs.
351
-
352
- ### `external_directory` surface
353
-
354
- Controls access to paths outside the active working directory.
355
- Use a pattern map to allow specific directories without opening all external access:
356
-
357
- ```jsonc
358
- {
359
- "permission": {
360
- "external_directory": {
361
- "*": "ask",
362
- "~/development/*": "allow"
363
- }
364
- }
365
- }
366
- ```
367
-
368
- `external_directory` is evaluated before the normal tool permission check. For example, `read: "allow"` can permit ordinary reads while `external_directory: "ask"` still requires confirmation before reading `../outside.txt` or an absolute path outside `ctx.cwd`.
369
- Optional-path search tools (`find`, `grep`, `ls`) skip this check when no `path` is provided.
370
-
371
- Bash commands are also covered: the extension extracts path-like tokens from the command string and applies the same gate when any resolve outside `ctx.cwd`.
372
- Quoted strings are stripped first to reduce false positives.
373
- This is a best-effort heuristic — variable expansion, subshells, and escaped quotes are not parsed.
374
- OS device paths (`/dev/null`, `/dev/stdin`, `/dev/stdout`, `/dev/stderr`) are always excluded.
375
-
376
- **Pi infrastructure read auto-allow** — Read-only tools (`read`, `find`, `grep`, `ls`) targeting Pi infrastructure directories are automatically allowed without triggering the gate, even when `external_directory` is `ask` or `deny`.
377
- Infrastructure directories include:
378
-
379
- 1. The agent config directory (`~/.pi/agent/` or `$PI_CODING_AGENT_DIR`)
380
- 2. Git-cloned global packages (`<agentDir>/git/`)
381
- 3. The global `node_modules` root (auto-discovered from the extension's own install path; falls back to `npm root -g` when running from a local development checkout)
382
- 4. Project-local Pi packages (`<cwd>/.pi/npm/` and `<cwd>/.pi/git/`)
383
- 5. Any paths listed in `piInfrastructureReadPaths`
384
-
385
- Write tools (`write`, `edit`) to infrastructure paths are **not** auto-allowed and still go through the gate.
386
-
387
- ---
388
-
389
- ## Common Recipes
390
-
391
- ### Read-Only Mode
392
-
393
- ```jsonc
394
- {
395
- "permission": {
396
- "*": "ask",
397
- "read": "allow",
398
- "grep": "allow",
399
- "find": "allow",
400
- "ls": "allow",
401
- "write": "deny",
402
- "edit": "deny"
403
- }
404
- }
405
- ```
406
-
407
- ### Restricted Bash Surface
408
-
409
- ```jsonc
410
- {
411
- "permission": {
412
- "*": "ask",
413
- "bash": {
414
- "*": "deny",
415
- "git status": "allow",
416
- "git diff": "allow",
417
- "git log *": "allow"
418
- }
419
- }
420
- }
421
- ```
422
-
423
- ### MCP Discovery Only
424
-
425
- ```jsonc
426
- {
427
- "permission": {
428
- "*": "ask",
429
- "mcp": {
430
- "*": "ask",
431
- "mcp_status": "allow",
432
- "mcp_list": "allow",
433
- "mcp_search": "allow",
434
- "mcp_describe": "allow"
435
- }
436
- }
437
- }
438
- ```
439
-
440
- ### Per-Agent Lockdown
441
-
442
- In the global Pi agents directory (default: `~/.pi/agent/agents/reviewer.md`, respects `PI_CODING_AGENT_DIR`):
443
-
444
- ```yaml
445
- ---
446
- permission:
447
- write: deny
448
- edit: deny
449
- bash: deny
450
- ---
451
- ```
452
-
453
- ---
454
-
455
- ## Technical Details
456
-
457
- ### Permission Prompt Summaries
57
+ Config lives in one JSON file per scope:
458
58
 
459
- When a tool permission resolves to `ask`, the prompt is designed to be readable enough for an informed approval decision:
460
-
461
- - `bash` prompts show the command and matched bash pattern when available.
462
- - `mcp` prompts show the derived MCP target and matched rule when available.
463
- - Built-in file tools show concise summaries, such as the target path and edit/write line counts, instead of raw multiline JSON.
464
- - Unknown or third-party extension tools show a bounded single-line JSON preview of the input so users are not asked to approve a blind tool name.
465
-
466
- Example edit approval prompt:
467
-
468
- ```text
469
- Current agent requested tool 'edit' for '.gitignore' (1 replacement: edit #1 replaces 5 lines with 2 lines). Allow this call?
470
- ```
471
-
472
- ### Session-Scoped Approvals
473
-
474
- When any permission resolves to `ask`, the permission dialog offers four options:
475
-
476
- ```text
477
- Yes | Yes, allow "<pattern>" for this session | No | No, provide reason
478
- ```
479
-
480
- Selecting **Yes, allow "\<pattern\>" for this session** approves the current request and records the suggested wildcard pattern as a session rule.
481
- Subsequent requests that match the pattern skip the prompt for the remainder of the session.
482
-
483
- The suggested pattern is surface-specific:
484
-
485
- |Surface|Example request|Suggested session pattern|
486
- |---|---|---|
487
- |bash|`git status --short`|`git status *`|
488
- |mcp (qualified)|`exa:search`|`exa:*`|
489
- |mcp (munged)|`exa_search`|`exa_*`|
490
- |skill|`librarian`|`librarian`|
491
- |tool (read, write, …)|`read`|`*`|
492
- |external_directory|`/other/project/src/foo.ts`|`/other/project/src/*`|
493
-
494
- #### Bash arity table
495
-
496
- Bash pattern suggestions use a curated arity dictionary (`src/bash-arity.ts`) to determine how many tokens define the "human-understandable subcommand."
497
- Longest matching prefix wins, so `npm run` (arity 3) takes precedence over `npm` (arity 2).
498
- Unknown commands default to arity 1 (first word only).
499
-
500
- |Example command|Arity entry matched|Suggested pattern|
501
- |---|---|---|
502
- |`git checkout main`|`git` → 2|`git checkout *`|
503
- |`npm run dev`|`npm run` → 3|`npm run dev*`|
504
- |`npm install lodash`|`npm` → 2|`npm install *`|
505
- |`docker compose up`|`docker compose` → 3|`docker compose up *`|
506
- |`rm -rf node_modules`|`rm` → 1|`rm *`|
507
- |`mytool --verbose`|(unknown) → 1|`mytool *`|
508
-
509
- The arity table covers common CLI tools including git, npm/pnpm/yarn/bun, docker, cargo, go, kubectl, gh, and others.
510
- To add an entry, open `src/bash-arity.ts` and add a key/arity pair to the `ARITY` object.
511
- Put the most specific multi-word prefix first (e.g. `"npm run": 3`) before the shorter fallback (`"npm": 2`).
512
-
513
- Session approvals are ephemeral — they are never persisted to disk and are cleared on `session_shutdown`.
514
- The review log records these decisions: `resolution: "approved_for_session"` when the user approves, and `resolution: "session_approved"` when a later request is matched by an existing session rule.
515
-
516
- ### Subagent Permission Forwarding
517
-
518
- When a delegated or routed subagent runs without direct UI access, `ask` permissions can still be enforced by forwarding the confirmation request through Pi session directories. The main interactive session polls for forwarded requests, shows the confirmation prompt, writes the response, and the subagent resumes once that decision is available.
519
-
520
- This keeps `ask` policies usable even when the original permission check happens inside a non-UI execution context.
521
-
522
- ### Coexistence with Subagent Extensions
523
-
524
- Several pi-subagent extensions implement their own tool restriction mechanisms.
525
- These compose correctly with the permission system because the two operate at different layers: **visibility** (subagent extension) and **policy** (permission system).
526
-
527
- #### The two-layer model
528
-
529
- ```text
530
- ┌─────────────────────────────────────────────────────┐
531
- │ Layer 1 – Visibility (subagent extension) │
532
- │ Controls which tools are registered / active │
533
- │ before the agent session starts. │
534
- ├─────────────────────────────────────────────────────┤
535
- │ Layer 2 – Policy (pi-permission-system) │
536
- │ Controls allow / ask / deny decisions on every │
537
- │ tool call, bash command, MCP operation, etc. │
538
- └─────────────────────────────────────────────────────┘
539
- ```
540
-
541
- #### Known subagent extensions and their mechanisms
542
-
543
- |Extension|Mechanism|Frontmatter key|
544
- |----|----------|----------|
545
- |[nicobailon/pi-subagents](https://github.com/nicobailon/pi-subagents)|`--tools` CLI allowlist passed to subprocess|`tools:` (CSV allowlist)|
546
- |[tintinweb/pi-subagents](https://github.com/tintinweb/pi-subagents)|`session.setActiveToolsByName()` in-process filter|`disallowed_tools:` (CSV denylist)|
547
- |[HazAT/pi-interactive-subagents](https://github.com/HazAT/pi-interactive-subagents)|`PI_DENY_TOOLS` env var + `--tools` CLI allowlist|`deny-tools:` (CSV denylist), `spawning:` (bool)|
548
-
549
- #### Interaction rules
550
-
551
- 1. **Hidden tool → permission system never sees it.**
552
- If a subagent extension removes a tool from the active set, the permission system receives no registration or call event for that tool.
553
- The permission policy for that tool is irrelevant — it is already gone.
554
- 2. **Denied tool → hidden regardless of the subagent extension's allowlist.**
555
- If the permission system denies a tool (via `deny` policy or tool filtering), it is removed from the active set before the agent starts.
556
- A `tools:` allowlist in a subagent extension cannot restore a tool that the permission system has already hidden.
557
- 3. **The two denylist mechanisms are additive, not conflicting.**
558
- A tool blocked by either layer stays blocked.
559
- Neither layer can silently re-enable what the other has blocked.
560
-
561
- #### `permission:` frontmatter is exclusive to this extension
562
-
563
- The `permission:` key in an agent's YAML frontmatter is read exclusively by `pi-permission-system`.
564
- It has no interaction with the `tools:`, `disallowed_tools:`, or `deny-tools:` keys consumed by subagent extensions.
565
- You can freely use both in the same agent file:
566
-
567
- ```yaml
568
- ---
569
- # Subagent extension: allow only bash and read_file in the subprocess
570
- tools: bash,read_file
571
- # pi-permission-system: still enforce ask on bash within those allowed tools
572
- permission:
573
- bash: ask
574
- ---
575
- ```
576
-
577
- In this example the subagent extension restricts visibility to `bash` and `read_file`, and the permission system then gates every `bash` call with an `ask` prompt — both rules apply independently.
578
-
579
- ### Logging
580
-
581
- When the extension prompts, denies, or forwards permission requests, it can append structured JSONL entries under:
582
-
583
- ```text
584
- Default global logs directory: ~/.pi/agent/extensions/pi-permission-system/logs/
585
- Actual global logs directory: $PI_CODING_AGENT_DIR/extensions/pi-permission-system/logs/ when PI_CODING_AGENT_DIR is set
586
- ```
587
-
588
- - `pi-permission-system-permission-review.jsonl` — enabled by default for permission review/audit history, including bounded `toolInputPreview` values for non-bash/non-MCP tool calls
589
- - `pi-permission-system-debug.jsonl` — disabled by default and intended for troubleshooting
590
-
591
- On every session start, the extension emits a `config.resolved` entry to both logs listing the resolved config paths and whether each exists.
592
- This makes it easy to verify which files the extension actually loaded:
593
-
594
- ```jsonc
595
- {
596
- "event": "config.resolved",
597
- "globalConfigPath": "/…/.pi/agent/extensions/pi-permission-system/config.json",
598
- "globalConfigExists": true,
599
- "projectConfigPath": "/…/my-project/.pi/extensions/pi-permission-system/config.json",
600
- "projectConfigExists": false,
601
- "agentsDir": "/…/.pi/agent/agents",
602
- "agentsDirExists": true,
603
- "projectAgentsDir": "/…/my-project/.pi/agent/agents",
604
- "projectAgentsDirExists": false,
605
- "legacyGlobalPolicyDetected": false,
606
- "legacyProjectPolicyDetected": false,
607
- "legacyExtensionConfigDetected": false
608
- }
609
- ```
610
-
611
- ### Architecture
612
-
613
- ```text
614
- index.ts → Root Pi entrypoint shim
615
- src/
616
- ├── index.ts → Extension bootstrap, permission checks, readable prompts, review logging, reload handling, and subagent forwarding
617
- ├── pattern-suggest.ts → Per-surface session approval pattern suggestions
618
- ├── bash-arity.ts → Curated arity dictionary for smarter bash session-approval patterns
619
- ├── session-rules.ts → Ephemeral session-scoped approval rules (Ruleset-based, wildcard patterns across all surfaces)
620
- ├── config-loader.ts → Unified config loader, merger, and legacy-path detection
621
- ├── config-paths.ts → Path derivation for global, project, and legacy config locations
622
- ├── config-reporter.ts → Resolved config path reporting for diagnostic logs
623
- ├── extension-config.ts → Runtime config normalization and defaults
624
- ├── logging.ts → File-only debug/review logging helpers
625
- ├── permission-manager.ts → Global/project policy loading, merging, and resolution with caching
626
- ├── skill-prompt-sanitizer.ts → Skill prompt parsing, multi-block sanitization, and skill-read path matching
627
- ├── bash-filter.ts → Bash command wildcard pattern matching
628
- ├── wildcard-matcher.ts → Shared wildcard pattern compilation and matching
629
- ├── common.ts → Shared utilities (YAML parsing, type guards, etc.)
630
- ├── tool-registry.ts → Registered tool name resolution
631
- └── types.ts → TypeScript type definitions
632
- tests/
633
- ├── permission-system.test.ts → Core permission, layering, forwarding, and policy tests
634
- ├── config-modal.test.ts → Config command and modal behavior tests
635
- └── test-harness.ts → Shared lightweight test helpers
636
- schemas/
637
- └── permissions.schema.json → JSON Schema for policy validation
638
- config/
639
- └── config.example.json → Starter global policy template
640
- ```
641
-
642
- #### Module Organization
643
-
644
- The extension uses a modular architecture with shared utilities:
645
-
646
- | Module | Purpose |
647
- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
648
- | `common.ts` | Shared utilities: `toRecord()`, `getNonEmptyString()`, `isPermissionState()`, `parseSimpleYamlMap()`, `extractFrontmatter()` |
649
- | `wildcard-matcher.ts` | Compile-once wildcard patterns with last-match-wins evaluation: `compileWildcardPatterns()`, `findCompiledWildcardMatch()` |
650
- | `permission-manager.ts` | Policy resolution with file stamp caching for performance |
651
- | `bash-filter.ts` | Uses shared wildcard matcher for bash command patterns |
652
- | `skill-prompt-sanitizer.ts` | Parses all available skill prompt blocks, removes denied skills, and tracks visible skill paths for read protection |
653
-
654
- #### Performance Optimizations
655
-
656
- - **File stamp caching**: Configurations are cached with file modification timestamps to avoid redundant reads
657
- - **Pre-compiled patterns**: Wildcard patterns are compiled to regex once and reused across permission checks
658
- - **Resolved permissions caching**: Merged agent+global permissions are cached per-agent with invalidation on file changes
659
-
660
- ### Threat Model
661
-
662
- **Goal:** Enforce policy at the host level, not the model level.
663
-
664
- **What this stops:**
665
-
666
- - Agent calling tools it shouldn't use (e.g., `write`, dangerous `bash`)
667
- - Tool switching attempts (calling non-existent tool names)
668
- - Accidental escalation via skill loading
669
- - Unapproved path-bearing tool access outside the active working directory when `external_directory` is `ask` or `deny`
670
-
671
- **Limitations:**
672
-
673
- - If a dangerous action is possible via an allowed tool, policy must explicitly restrict it
674
- - This is a permission decision layer, not a sandbox
675
-
676
- ### Schema Validation
677
-
678
- Validate your config against the included schema:
679
-
680
- ```bash
681
- npx --yes ajv-cli@5 validate \
682
- -s ./schemas/permissions.schema.json \
683
- -d ./pi-permissions.valid.json
684
- ```
685
-
686
- **Editor tip:** Add `"$schema": "./schemas/permissions.schema.json"` to your config for autocomplete support.
687
-
688
- ---
689
-
690
- ## Event API
691
-
692
- The extension emits events on Pi's `pi.events` bus so other extensions can observe permission decisions and integrate with the policy system without importing this package.
693
-
694
- ### Stability guarantee
695
-
696
- Fields may be added to any payload, but existing fields will not be removed or renamed without a semver-major version bump.
697
- The protocol version constant is exported from `src/permission-events.ts` and embedded in every RPC reply.
698
-
699
- ### Channel reference
700
-
701
- |Channel|Direction|When|Payload type|
702
- |---|---|---|---|
703
- |`permissions:ready`|Broadcast|Once, immediately after load|`PermissionsReadyEvent`|
704
- |`permissions:decision`|Broadcast|After every gate resolution|`PermissionDecisionEvent`|
705
- |`permissions:rpc:check`|Request|On-demand|`PermissionsCheckRequest`|
706
- |`permissions:rpc:check:reply:<requestId>`|Reply|After each check request|`PermissionsRpcReply<PermissionsCheckReplyData>`|
707
- |`permissions:rpc:prompt`|Request|On-demand|`PermissionsPromptRequest`|
708
- |`permissions:rpc:prompt:reply:<requestId>`|Reply|After prompt is resolved|`PermissionsRpcReply<PermissionsPromptReplyData>`|
709
-
710
- ### Surface 1 — Decision broadcasts
711
-
712
- Every permission gate resolution emits a `permissions:decision` event, regardless of outcome.
713
- This is useful for dashboards, telemetry, or audit overlays.
714
-
715
- ```typescript
716
- pi.events.on("permissions:decision", (raw) => {
717
- const event = raw as import("@gotgenes/pi-permission-system").PermissionDecisionEvent;
718
- console.log(event.surface, event.result, event.resolution);
719
- // e.g. "bash" "allow" "user_approved_for_session"
720
- });
721
- ```
722
-
723
- Payload fields:
724
-
725
- |Field|Type|Description|
726
- |---|---|---|
727
- |`surface`|`string`|Permission surface (`"bash"`, `"read"`, `"mcp"`, `"skill"`, `"external_directory"`, etc.)|
728
- |`value`|`string`|Value evaluated (command, tool name, skill name, path)|
729
- |`result`|`"allow" \| "deny"`|Final outcome|
730
- |`resolution`|`string`|How the outcome was reached (see table below)|
731
- |`origin`|`string \| null`|Config scope that contributed the winning rule|
732
- |`agentName`|`string \| null`|Active agent name when known|
733
- |`matchedPattern`|`string \| null`|Pattern from the winning rule|
734
-
735
- Resolution values:
736
-
737
- |Value|Meaning|
59
+ |Scope|Path|
738
60
  |---|---|
739
- |`policy_allow`|Config rule said allow — no prompt shown|
740
- |`policy_deny`|Config rule said deny — blocked immediately|
741
- |`session_approved`|Covered by a session-level approval from earlier in the same session|
742
- |`infrastructure_auto_allowed`|Read of a Pi infrastructure path — auto-allowed|
743
- |`user_approved`|User approved once via dialog|
744
- |`user_approved_for_session`|User approved for the rest of the session|
745
- |`user_denied`|User denied via dialog|
746
- |`auto_approved`|Yolo mode — approved automatically without dialog|
747
- |`confirmation_unavailable`|State was `ask` but no UI was available — blocked|
748
-
749
- ### Surface 2 — Policy query RPC
750
-
751
- Other extensions can evaluate the current permission policy without importing this package.
752
- The call is synchronous-style: emit a request, listen on a scoped reply channel.
753
-
754
- ```typescript
755
- const requestId = crypto.randomUUID();
756
-
757
- // Listen for the reply first
758
- const unsub = pi.events.on(
759
- `permissions:rpc:check:reply:${requestId}`,
760
- (raw) => {
761
- unsub();
762
- const reply = raw as import("@gotgenes/pi-permission-system").PermissionsRpcReply<
763
- import("@gotgenes/pi-permission-system").PermissionsCheckReplyData
764
- >;
765
- if (reply.success) {
766
- console.log(reply.data?.result); // "allow" | "deny" | "ask"
767
- }
768
- },
769
- );
770
-
771
- // Then emit the request
772
- pi.events.emit("permissions:rpc:check", {
773
- requestId,
774
- surface: "bash",
775
- value: "git push",
776
- agentName: "Worker", // optional
777
- });
778
- ```
779
-
780
- If the extension is not loaded, no reply arrives.
781
- Callers should implement a timeout and treat no-reply as `deny` (graceful degradation).
782
-
783
- Request fields:
784
-
785
- |Field|Required|Description|
786
- |---|---|---|
787
- |`requestId`|Yes|Unique string; scopes the reply channel|
788
- |`surface`|Yes|Permission surface to evaluate|
789
- |`value`|No|Value to evaluate (command, name, path); defaults to `"*"`|
790
- |`agentName`|No|Agent name for per-agent policy resolution|
791
-
792
- Reply data fields (`PermissionsCheckReplyData`):
61
+ |Global|`~/.pi/agent/extensions/pi-permission-system/config.json`|
62
+ |Project|`<cwd>/.pi/extensions/pi-permission-system/config.json`|
793
63
 
794
- |Field|Type|Description|
795
- |---|---|---|
796
- |`result`|`"allow" \| "deny" \| "ask"`|Policy decision (including active session rules)|
797
- |`matchedPattern`|`string \| null`|Matched rule pattern|
798
- |`origin`|`string \| null`|Config scope of the winning rule|
64
+ Project overrides global; per-agent YAML frontmatter overrides both.
799
65
 
800
- ### Surface 3Prompt forwarding RPC
66
+ Within a surface map like `bash` or `mcp`, **last matching rule wins** put broad catch-alls first and specific overrides after.
801
67
 
802
- In-process child sessions (e.g. tintinweb/pi-subagents running via `createAgentSession()`) cannot use file-based permission forwarding because no child process is spawned.
803
- They can instead forward permission prompts to the parent session's UI via this RPC.
68
+ For the full reference all surfaces, runtime knobs, per-agent overrides, merge semantics, and common recipes see [docs/configuration.md](docs/configuration.md).
804
69
 
805
- ```typescript
806
- const requestId = crypto.randomUUID();
70
+ ## Documentation
807
71
 
808
- const unsub = pi.events.on(
809
- `permissions:rpc:prompt:reply:${requestId}`,
810
- (raw) => {
811
- unsub();
812
- const reply = raw as import("@gotgenes/pi-permission-system").PermissionsRpcReply<
813
- import("@gotgenes/pi-permission-system").PermissionsPromptReplyData
814
- >;
815
- if (reply.success && reply.data?.approved) {
816
- // proceed
817
- } else {
818
- // deny — either user denied or no UI was available (error: "no_ui")
819
- }
820
- },
821
- );
822
-
823
- pi.events.emit("permissions:rpc:prompt", {
824
- requestId,
825
- surface: "bash",
826
- value: "rm -rf /tmp/build",
827
- message: "Allow rm -rf /tmp/build?",
828
- agentName: "Explore", // optional
829
- sessionLabel: "Allow rm *", // optional — label for the \"for this session\" option
830
- });
831
- ```
832
-
833
- The handler replies with `{ success: false, error: "no_ui" }` when no interactive session is available.
834
- A successful reply contains:
835
-
836
- |Field|Type|Description|
837
- |---|---|---|
838
- |`approved`|`boolean`|Whether the user approved|
839
- |`state`|`string`|`"approved"`, `"approved_for_session"`, `"denied"`, or `"denied_with_reason"`|
840
- |`denialReason`|`string` (optional)|User-provided denial reason|
841
-
842
- ### Ready event
843
-
844
- The extension emits `permissions:ready` once immediately after it loads.
845
- Consumers that start after the extension can check via a ping-style RPC check — the `permissions:rpc:check` handler is active as long as the extension is loaded.
846
-
847
- ```typescript
848
- pi.events.on("permissions:ready", (raw) => {
849
- const event = raw as import("@gotgenes/pi-permission-system").PermissionsReadyEvent;
850
- console.log("Permission system loaded, protocol version:", event.protocolVersion);
851
- });
852
- ```
853
-
854
- ---
855
-
856
- ## Migration from pre-v2 layout
857
-
858
- Before v2, config was split across two files:
859
-
860
- - Policy: `~/.pi/agent/pi-permissions.jsonc`
861
- - Runtime knobs: `<extension-install-dir>/config.json`
862
-
863
- These are now consolidated into one file.
864
- The extension detects legacy files and merges them with a warning for one release.
865
- To migrate manually:
866
-
867
- ```bash
868
- # Move the global policy file
869
- mkdir -p ~/.pi/agent/extensions/pi-permission-system
870
- mv ~/.pi/agent/pi-permissions.jsonc ~/.pi/agent/extensions/pi-permission-system/config.json
871
-
872
- # If you had project-level policy:
873
- mkdir -p .pi/extensions/pi-permission-system
874
- mv .pi/agent/pi-permissions.jsonc .pi/extensions/pi-permission-system/config.json
875
- ```
876
-
877
- Then add any runtime knobs (`debugLog`, `permissionReviewLog`, `yoloMode`) to the same file.
878
- The old extension-root `config.json` is no longer read from the install directory.
879
-
880
- > **Note:** Logs also moved from `<extension-install-dir>/logs/` to `~/.pi/agent/extensions/pi-permission-system/logs/`.
881
- > Old log files are not deleted or migrated — they remain readable but no new entries are appended.
882
-
883
- ---
884
-
885
- ## Troubleshooting
886
-
887
- | Problem | Cause | Solution |
888
- | ------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
889
- | Config not applied (everything asks) | File not found or parse error | Verify the global config at `~/.pi/agent/extensions/pi-permission-system/config.json` (respects `PI_CODING_AGENT_DIR`); check for trailing commas |
890
- | Per-agent override not applied | Frontmatter parsing issue | Ensure `---` delimiters at file top; keep YAML simple; restart session |
891
- | Tool blocked as unregistered | Unknown tool name | Use a registered `mcp` tool for server tools: `{ "tool": "server:tool" }` |
892
- | `/skill:<name>` blocked | Deny policy or confirmation unavailable | Check merged `skills` policy (global/project/agent layers). Active agent context is optional in the main session; `ask` still requires UI or forwarded confirmation. |
893
- | External file path blocked | `special.external_directory` is `ask` without UI or `deny` | Allow/ask the special permission or keep file tools inside the active working directory. |
894
- | Permission prompt is too verbose | Generic extension tool input is large | Built-in file tools are summarized automatically; third-party tools are capped to a bounded one-line JSON preview. |
895
-
896
- ---
72
+ |Document|Contents|
73
+ |---|---|
74
+ |[docs/configuration.md](docs/configuration.md)|Full policy reference, runtime knobs, per-agent overrides, recipes|
75
+ |[docs/session-approvals.md](docs/session-approvals.md)|Session-scoped rules, pattern suggestions, bash arity table|
76
+ |[docs/event-api.md](docs/event-api.md)|Event bus integration, decision broadcasts, RPC check/prompt|
77
+ |[docs/subagent-integration.md](docs/subagent-integration.md)|Permission forwarding, coexistence with subagent extensions|
78
+ |[docs/guides/permission-frontmatter-for-subagent-extensions.md](docs/guides/permission-frontmatter-for-subagent-extensions.md)|Convention guide for subagent extension authors|
79
+ |[docs/troubleshooting.md](docs/troubleshooting.md)|Common issues, diagnostic logging, threat model|
80
+ |[docs/migration/legacy-to-flat.md](docs/migration/legacy-to-flat.md)|Migration from pre-v2 config layout|
897
81
 
898
82
  ## Development
899
83
 
@@ -911,17 +95,7 @@ pnpm run check # build + lint:all + test
911
95
  ### Pre-commit hooks
912
96
 
913
97
  This project uses [prek](https://prek.j178.dev/) to run Biome and markdownlint on staged files before each commit.
914
- This catches lint and formatting issues locally instead of waiting for CI.
915
-
916
- 1. Install prek ([installation guide](https://prek.j178.dev/installation/)).
917
- 2. Run `pnpm install` — the `prepare` script calls `prek install` automatically.
918
- If prek is not installed, the script prints a warning and continues.
919
- 3. Hooks run automatically on `git commit`.
920
- To skip in emergencies: `git commit --no-verify`.
921
-
922
- The hook configuration lives in `prek.toml` at the repo root.
923
-
924
- ---
98
+ Run `pnpm install` to set up hooks automatically.
925
99
 
926
100
  ## Acknowledgments
927
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.3.0",
3
+ "version": "5.3.2",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [