pi-permission-system 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.1] - 2026-06-16
11
+
12
+ ### Added
13
+ - Added resource-qualified path rules for path-bearing built-in tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) using action-scoped `tools` keys such as `read:/home/alice/project/generated/*`.
14
+ - Added resource-qualified `external_directory` rules using `external_directory:<normalized-path>/*` for specific outside-worktree directories.
15
+
16
+ ### Changed
17
+ - Clarified that `Allow Once` approves only the current request, `Allow Always` records an explicit matching approval for the current session only, and plain `Reject` does not become a future default.
18
+ - Clarified that YOLO/auto-response approvals do not create saved approval rules.
19
+
20
+ ## [0.7.0] - 2026-06-01
21
+
22
+ ### Added
23
+ - Added `SessionApprovalStore` for in-memory per-session permission approval tracking with `approveAlways`, `approveOnce`, `hasSessionApproval`, and `evaluate` methods.
24
+ - Added `PermanentApprovalStore` for persistent approval rules with atomic file writes and last-match-wins evaluation, stored at `pi-permission-system-approvals.json`.
25
+ - Added `evaluate-permission.ts` module with `evaluatePermission()` function that evaluates tool+command pairs against multiple rulesets with last-match-wins semantics.
26
+ - Added forwarded permission prompt auto-denial timeout (30 seconds) so unanswered forwarded subagent prompts automatically deny instead of blocking indefinitely.
27
+ - Added `Allow Once`/`Allow Always`/`Reject`/`Reject with Reason` permission decision options replacing the previous `Yes`/`No`/`No, provide reason` labels.
28
+ - Added `PI_DELEGATED_AUTH_RUNTIME_DIR` and `PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR` environment variable support for resolving forwarded permission request directories when launched by delegated auth or policy agent routers.
29
+ - Added notification warning when a forwarded subagent permission prompt is displayed.
30
+ - Added automatic expiration of forwarded permission requests that exceed the 10-minute forwarding timeout before display.
31
+ - Added pruning of hidden structured skill references (table rows and list items) from system prompts when the referenced skill name is not fully allowed.
32
+
33
+ ### Changed
34
+ - Consolidated three separate logging options (`debugLog`, `permissionReviewLog`, `logPlaintextBashCommands`) into a single `debug` config toggle that writes both diagnostics and permission review entries to one `pi-permission-system-debug.jsonl` file.
35
+ - Updated wildcard matcher to normalize backslashes to forward slashes on Windows, add case-insensitive matching on Windows, and support `?` single-character wildcards and trailing ` .*` optional-space patterns.
36
+ - Skill prompt sanitizer now hides all non-allow skills (including `ask`) from the advertised available-skills section instead of only hiding `deny` skills.
37
+ - Migrated test suite from Bun to Node.js with tsx and Node experimental test module mocks.
38
+ - Widened Pi peer dependency compatibility to include `^0.77.0 || ^0.78.0`.
39
+ - Replaced `PermissionDecisionState` values `approved`/`denied`/`denied_with_reason` with `once`/`always`/`reject` in the permission decision API.
40
+
41
+ ### Fixed
42
+ - Fixed forwarded permission response directory resolution when subagents are launched by delegated auth or router components with isolated `PI_CODING_AGENT_DIR`.
43
+ - Fixed wildcard matcher to correctly handle Windows backslash path separators in pattern matching.
44
+
10
45
  ## [0.6.0] - 2026-05-26
11
46
 
12
47
  ### Added
package/README.md CHANGED
@@ -23,6 +23,7 @@ Yes — this extension was designed so OpenCode-style agent permission policies
23
23
 
24
24
  - **Agents are still markdown files with YAML frontmatter.**
25
25
  - **Wildcard permissions still use last-match-wins ordering.**
26
+ - **Resource-qualified path rules are supported for path-bearing tools.** Use action-scoped `tools` keys like `read:/home/alice/project/generated/*` and scoped special keys like `external_directory:/home/alice/shared/*` when you need OpenCode-style directory rules.
26
27
  - **Keep frontmatter simple when porting.** This extension intentionally supports `key: value` scalars and nested maps, not full YAML features like arrays, anchors, or multiline scalars.
27
28
 
28
29
  ### Minimal Pi agent example
@@ -53,7 +54,7 @@ Your agent instructions go here.
53
54
  | Wildcard precedence | Same last-declared-match-wins behavior | High | Broad rules first, specific overrides later. |
54
55
  | `bash` permission rules | `permission.bash` | High | Command-pattern gating ports cleanly. |
55
56
  | Per-tool permission rules like `read`, `grep`, `list`, `task`, or arbitrary extension tool names | `permission.tools` | Medium-High | Pi groups registered tool names under `tools`, including built-ins and extension tools. |
56
- | `external_directory` | `permission.special.external_directory` | Medium | Same idea, different location. |
57
+ | `external_directory` | `permission.special.external_directory` or `permission.special.external_directory:<path>/*` | Medium-High | Coarse fallback stays supported; add resource-qualified rules for specific outside-worktree directories. |
57
58
  | `doom_loop` | `permission.special.doom_loop` | Medium | Same idea, different location. |
58
59
  | `skill` permission rules | `permission.skills` | Medium | Same purpose, but Pi uses a dedicated plural `skills` section. |
59
60
  | MCP-related access | `permission.mcp` for proxy targets, `permission.tools` for direct registered tools | Medium | This is the biggest Pi-specific difference: proxy MCP targets and direct tool names are intentionally split. |
@@ -99,8 +100,7 @@ If you are coming from OpenCode, you usually do **not** need to rewrite your who
99
100
  - **Per-Agent Overrides** — Agent-specific permission policies via YAML frontmatter
100
101
  - **Subagent Permission Forwarding** — Forwards `ask` confirmations from non-UI subagents back to the main interactive session
101
102
  - **Runtime YOLO Control** — Lets users toggle yolo mode from the settings modal and lets other extensions toggle it through the runtime API
102
- - **File-Based Review Logging** — Writes permission request/denial review entries to a file by default, with raw bash command text redacted unless `logPlaintextBashCommands` is enabled
103
- - **Optional Debug Logging** — Keeps verbose extension diagnostics in a separate file when enabled in `config.json`
103
+ - **File-Based Debug Logging** — Writes verbose diagnostics and permission request/denial review entries to one debug file when enabled in `config.json`, including the responsible agent and raw tool-call input
104
104
  - **JSON Schema Validation** — Full schema for editor autocomplete and config validation
105
105
  - **External Directory Guard** — Enforces `special.external_directory` for path-bearing file tools that target paths outside the active working directory
106
106
 
@@ -159,6 +159,8 @@ All permissions use one of three states:
159
159
  | `deny` | Blocks the action with an error message |
160
160
  | `ask` | Prompts the user for confirmation via UI |
161
161
 
162
+ When an `ask` permission prompts, the confirmation UI offers `Allow Once`, `Allow Always`, `Reject`, and `Reject with Reason`. `Allow Once` approves only the current request. `Allow Always` records an explicit matching approval for the current session, while plain `Reject` and `Reject with Reason` deny only the current request and do not silently become future defaults. YOLO/auto-response approvals also do not create saved approval rules; after YOLO mode is disabled, matching `ask` requests require approval again. A configured `deny` remains a hard boundary and is not relaxed by prior one-shot, auto-response, or saved approvals.
163
+
162
164
  ### Pi Integration Hooks
163
165
 
164
166
  The extension integrates via Pi's lifecycle hooks:
@@ -175,7 +177,7 @@ The extension integrates via Pi's lifecycle hooks:
175
177
  - Extension-provided tools like `task`, `mcp`, and third-party tools are handled through the same registered-tool permission layer instead of private built-in hardcodes
176
178
  - When a subagent hits an `ask` permission without direct UI access, the request can be forwarded to the main interactive session for confirmation
177
179
  - Generic extension-tool approval prompts include a bounded input preview; built-in file tools use concise human-readable summaries instead of raw multiline JSON
178
- - Permission review logs include redacted prompt/input metadata for auditing; raw bash command text is omitted unless `logPlaintextBashCommands` is enabled.
180
+ - Debug review entries include the responsible agent, raw prompt, raw tool-call input, command, target, and decision metadata for auditing.
179
181
  - Path-bearing file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) evaluate `special.external_directory` before their normal tool permission when an explicit path points outside `ctx.cwd`
180
182
  - `read` calls under global and project Pi skill directories are checked against `skills` policy even when the skill entry is inferred from the path rather than an active prompt block.
181
183
  - Structured edit payloads are summarized by operation and line count in prompts so permission decisions do not require raw multiline JSON.
@@ -188,29 +190,25 @@ The extension integrates via Pi's lifecycle hooks:
188
190
 
189
191
  Set `PI_PERMISSION_SYSTEM_CONFIG_PATH` to point this extension at a specific config file when the default global path is not appropriate.
190
192
 
191
- The extension creates this file automatically when it is missing. It controls extension-local logging behavior and yolo mode defaults:
193
+ The extension creates this file automatically when it is missing. It controls extension-local debug logging behavior and yolo mode defaults:
192
194
 
193
195
  ```json
194
196
  {
195
- "debugLog": false,
196
- "permissionReviewLog": true,
197
- "logPlaintextBashCommands": false,
197
+ "debug": false,
198
198
  "yoloMode": false
199
199
  }
200
200
  ```
201
201
 
202
202
  | Key | Default | Description |
203
203
  |-----|---------|-------------|
204
- | `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
205
- | `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl` |
206
- | `logPlaintextBashCommands` | `false` | Opts in to storing raw bash command strings in review logs; when disabled, bash commands are redacted and only safe metadata is retained |
204
+ | `debug` | `false` | Enables verbose diagnostics and permission review entries in `logs/pi-permission-system-debug.jsonl` |
207
205
  | `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled |
208
206
 
209
- Both logs write to files only under the extension directory by default. Set `PI_PERMISSION_SYSTEM_LOGS_DIR` to redirect review/debug logs to a specific directory. No debug output is printed to the terminal.
207
+ Debug output writes only under the extension directory by default. Set `PI_PERMISSION_SYSTEM_LOGS_DIR` to redirect the debug file to a specific directory. No debug output is printed to the terminal.
210
208
 
211
209
  ### Runtime YOLO Control
212
210
 
213
- Use `/permission-system` to open the settings modal and inspect or change yolo mode interactively.
211
+ Use `/permission-system` to open the settings modal and inspect or change yolo mode interactively. In interactive TUI mode, the settings modal uses Pi's renderer-provided theme and does not require a separate global `initTheme()` call before opening.
214
212
 
215
213
  Other extensions can toggle yolo mode immediately through the shared runtime API:
216
214
 
@@ -347,6 +345,20 @@ Controls tools by registered name pattern. This is the recommended standalone fo
347
345
 
348
346
  Unknown or absent tools are not required in the config. If another extension is not installed, its tool simply will not be registered at runtime, and this extension will block attempts to call that missing tool before permission checks run. Wildcard `tools` rules apply to direct tools from any extension; no adapter-specific naming is required.
349
347
 
348
+ Path-bearing built-ins (`read`, `write`, `edit`, `find`, `grep`, `ls`) can also use action/resource keys in `tools` with normalized absolute paths. Use this when a tool should be allowed or denied only for a specific directory resource:
349
+
350
+ ```jsonc
351
+ {
352
+ "tools": {
353
+ "read": "ask",
354
+ "read:/home/alice/project/generated/*": "allow",
355
+ "write": "deny"
356
+ }
357
+ }
358
+ ```
359
+
360
+ Action-scoped resource rules still respect normal permission guardrails: matching uses the same wildcard/last-match behavior as other tool rules, and outside-worktree paths must also satisfy the `special.external_directory` check.
361
+
350
362
  > **Note:** Setting `tools.bash` affects the *default* for bash commands, but `bash` patterns can provide command-level overrides.
351
363
  >
352
364
  > **Note:** Setting `tools.mcp` controls coarse access to a registered `mcp` proxy tool when one is available. Specific `mcp` rules still override it when a proxy target pattern matches. Direct MCP tools registered by extensions are regular registered tools and should be controlled with `tools` patterns such as `context7_*` or `github_*`.
@@ -442,18 +454,20 @@ Reserved permission checks:
442
454
  | Key | Description |
443
455
  |----------------------|------------------------------------------|
444
456
  | `doom_loop` | Controls doom loop detection behavior |
445
- | `external_directory` | Enforces ask/allow/deny decisions for path-bearing built-in tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) when they target paths outside the active working directory |
457
+ | `external_directory` | Coarse fallback for ask/allow/deny decisions on path-bearing built-in tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) when they target paths outside the active working directory |
458
+ | `external_directory:<path>/*` | Resource-qualified external-directory rule for a specific normalized outside-worktree directory |
446
459
 
447
460
  ```jsonc
448
461
  {
449
462
  "special": {
450
463
  "doom_loop": "deny",
451
- "external_directory": "ask"
464
+ "external_directory": "ask",
465
+ "external_directory:/home/alice/shared/*": "allow"
452
466
  }
453
467
  }
454
468
  ```
455
469
 
456
- `external_directory` is evaluated before the normal tool permission check. For example, `tools.read: "allow"` can permit ordinary reads while `special.external_directory: "ask"` still requires confirmation before reading `../outside.txt` or an absolute path outside `ctx.cwd`. Optional-path search tools (`find`, `grep`, `ls`) skip this check when no `path` is provided because they default to the active working directory.
470
+ `external_directory` is evaluated before the normal tool permission check. For example, `tools.read: "allow"` can permit ordinary reads while `special.external_directory: "ask"` still requires confirmation before reading `../outside.txt` or an absolute path outside `ctx.cwd`. Add `external_directory:<normalized-absolute-directory>/*` when a known outside directory should be allowed or denied without changing the coarse fallback. Optional-path search tools (`find`, `grep`, `ls`) skip this check when no `path` is provided because they default to the active working directory.
457
471
 
458
472
  ---
459
473
 
@@ -554,21 +568,20 @@ Actual global logs directory: $PI_CODING_AGENT_DIR/extensions/pi-permission-syst
554
568
  Override logs directory: $PI_PERMISSION_SYSTEM_LOGS_DIR when set
555
569
  ```
556
570
 
557
- - `pi-permission-system-permission-review.jsonl` — enabled by default for permission review/audit history, including metadata hashes and lengths for prompts, commands, denial reasons, and tool input previews instead of raw sensitive content
558
- - `pi-permission-system-debug.jsonl` — disabled by default and intended for troubleshooting
571
+ - `pi-permission-system-debug.jsonl` — disabled by default; includes troubleshooting diagnostics and permission review/audit entries with responsible agent metadata, raw prompts, raw tool-call inputs, commands, targets, and decisions
559
572
 
560
573
  ### Architecture
561
574
 
562
575
  ```
563
576
  index.ts → Root Pi entrypoint shim
564
577
  src/
565
- ├── index.ts → Extension bootstrap, permission checks, readable prompts, review logging, reload handling, and subagent forwarding
578
+ ├── index.ts → Extension bootstrap, permission checks, readable prompts, debug review entries, reload handling, and subagent forwarding
566
579
  ├── before-agent-start-cache.ts → Caches prompt/tool filtering state between before_agent_start runs
567
580
  ├── bash-filter.ts → Bash command wildcard pattern matching
568
581
  ├── common.ts → Shared utilities (YAML parsing, type guards, etc.)
569
582
  ├── config-modal.ts → `/permission-system` modal registration and settings UI wiring
570
583
  ├── extension-config.ts → Extension-local config loading and default creation
571
- ├── logging.ts → File-only debug/review logging helpers
584
+ ├── logging.ts → File-only debug logging helpers
572
585
  ├── model-option-compatibility.ts → Guards unsupported provider/model options
573
586
  ├── permission-dialog.ts → Interactive permission approval UI helpers
574
587
  ├── permission-forwarding.ts → Subagent-to-parent permission forwarding utilities
@@ -646,20 +659,20 @@ npx --yes ajv-cli@5 validate \
646
659
  | Per-agent override not applied | Frontmatter parsing issue | Ensure `---` delimiters at file top; keep YAML simple; restart session |
647
660
  | Tool blocked as unregistered | Unknown tool name | Use a registered `mcp` tool for server tools: `{ "tool": "server:tool" }` |
648
661
  | `/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. |
649
- | 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. |
662
+ | External file path blocked | `special.external_directory` is `ask` without UI or a matching rule resolves to `deny` | Keep file tools inside the active working directory, set an appropriate coarse fallback, or add a scoped rule such as `external_directory:/home/alice/shared/*`. |
650
663
  | 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. |
651
664
 
652
665
  ---
653
666
 
654
667
  ## Development
655
668
 
656
- Runtime checks require Node.js 20+; the test suite requires Bun 1.1+.
669
+ Runtime checks require Node.js 20+; the test suite runs through Node.js with tsx and Node's experimental test module mocks (validated on Node.js 24).
657
670
 
658
671
  ```bash
659
672
  npm run build # Run TypeScript type checks
660
673
  npm run lint # Run local static checks
661
674
  npm run validate:artifacts # Validate JSON/schema/example artifacts
662
- npm run test # Run Bun tests from ./tests
675
+ npm run test # Run Node/tsx tests from ./tests
663
676
  npm run check # Run static, artifact, and test checks
664
677
  ```
665
678
 
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "tools": {
10
10
  "read": "allow",
11
+ "read:/home/alice/project/generated/*": "allow",
11
12
  "write": "deny"
12
13
  },
13
14
  "bash": {
@@ -22,6 +23,7 @@
22
23
  },
23
24
  "special": {
24
25
  "doom_loop": "deny",
25
- "external_directory": "ask"
26
+ "external_directory": "ask",
27
+ "external_directory:/home/alice/shared/*": "allow"
26
28
  }
27
29
  }
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
- {
2
- "name": "pi-permission-system",
3
- "version": "0.6.0",
4
- "description": "Permission enforcement extension for the Pi coding agent.",
5
- "type": "module",
6
- "main": "./index.ts",
7
- "exports": {
8
- ".": "./index.ts"
9
- },
10
- "files": [
11
- "index.ts",
12
- "src",
13
- "tests",
14
- "config/config.example.json",
15
- "schemas/permissions.schema.json",
16
- "README.md",
17
- "CHANGELOG.md",
18
- "LICENSE"
19
- ],
20
- "scripts": {
21
- "typecheck": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noEmit",
22
- "build": "npm run typecheck",
23
- "lint": "npm run typecheck",
24
- "validate:artifacts": "node ./scripts/validate-artifacts.mjs",
25
- "test": "bun ./tests/permission-system.test.ts && bun ./tests/config-modal.test.ts",
26
- "check": "npm run lint && npm run validate:artifacts && npm run test"
27
- },
28
- "keywords": [
29
- "pi-package",
30
- "pi",
31
- "pi-extension",
32
- "pi-coding-agent",
33
- "coding-agent",
34
- "permissions",
35
- "policy",
36
- "access-control",
37
- "authorization",
38
- "security"
39
- ],
40
- "author": "MasuRii",
41
- "license": "MIT",
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/MasuRii/pi-permission-system.git"
45
- },
46
- "homepage": "https://github.com/MasuRii/pi-permission-system#readme",
47
- "bugs": {
48
- "url": "https://github.com/MasuRii/pi-permission-system/issues"
49
- },
50
- "engines": {
51
- "node": ">=20",
52
- "bun": ">=1.1.0"
53
- },
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "pi": {
58
- "extensions": [
59
- "./index.ts"
60
- ]
61
- },
62
- "peerDependencies": {
63
- "@sinclair/typebox": "^0.34.49",
64
- "@earendil-works/pi-ai": "^0.74.0 || ^0.75.0",
65
- "@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0",
66
- "@earendil-works/pi-tui": "^0.74.0 || ^0.75.0"
67
- },
68
- "dependencies": {
69
- "jsonc-parser": "^3.3.1"
70
- }
71
- }
1
+ {
2
+ "name": "pi-permission-system",
3
+ "version": "0.7.1",
4
+ "description": "Permission enforcement extension for the Pi coding agent.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "tests",
14
+ "config/config.example.json",
15
+ "schemas/permissions.schema.json",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "typecheck": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noEmit",
22
+ "build": "npm run typecheck",
23
+ "lint": "npm run typecheck",
24
+ "validate:artifacts": "node ./scripts/validate-artifacts.mjs",
25
+ "test": "bun ./tests/permission-system.test.ts && bun ./tests/config-modal.test.ts && bun ./tests/edit-prompt-compaction-red.test.ts && bun ./tests/edit-decision-deduplication-red.test.ts",
26
+ "check": "npm run lint && npm run validate:artifacts && npm run test"
27
+ },
28
+ "keywords": [
29
+ "pi-package",
30
+ "pi",
31
+ "pi-extension",
32
+ "pi-coding-agent",
33
+ "coding-agent",
34
+ "permissions",
35
+ "policy",
36
+ "access-control",
37
+ "authorization",
38
+ "security"
39
+ ],
40
+ "author": "MasuRii",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/MasuRii/pi-permission-system.git"
45
+ },
46
+ "homepage": "https://github.com/MasuRii/pi-permission-system#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/MasuRii/pi-permission-system/issues"
49
+ },
50
+ "engines": {
51
+ "node": ">=20",
52
+ "bun": ">=1.1.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "pi": {
58
+ "extensions": [
59
+ "./index.ts"
60
+ ]
61
+ },
62
+ "peerDependencies": {
63
+ "@sinclair/typebox": "^0.34.49",
64
+ "@earendil-works/pi-ai": "^0.74.0 || ^0.75.0 || ^0.78.0 || ^0.79.0",
65
+ "@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.78.0 || ^0.79.0",
66
+ "@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.78.0 || ^0.79.0"
67
+ },
68
+ "dependencies": {
69
+ "jsonc-parser": "^3.3.1"
70
+ }
71
+ }
@@ -31,7 +31,7 @@
31
31
  }
32
32
  },
33
33
  "tools": {
34
- "description": "Pattern-based permissions for registered tools. Use exact names or `*` wildcards for canonical Pi built-ins and any extension-provided or third-party tools.",
34
+ "description": "Pattern-based permissions for registered tools. Use exact names or `*` wildcards for canonical Pi built-ins and any extension-provided or third-party tools. Path-bearing built-ins also accept resource-qualified keys such as `read:/home/alice/project/generated/*` or `edit:c:/Users/alice/project/generated/*`.",
35
35
  "$ref": "#/$defs/permissionMap"
36
36
  },
37
37
  "bash": {
@@ -45,6 +45,7 @@
45
45
  "$ref": "#/$defs/permissionMap"
46
46
  },
47
47
  "special": {
48
+ "description": "Reserved permission checks. The coarse `external_directory` key remains supported, and resource-qualified keys like `external_directory:/home/alice/shared/*` or `external_directory:c:/Users/alice/shared/*` can scope outside-worktree file access by normalized directory resource.",
48
49
  "type": "object",
49
50
  "additionalProperties": false,
50
51
  "properties": {
@@ -52,6 +53,13 @@
52
53
  "$ref": "#/$defs/permissionState"
53
54
  },
54
55
  "external_directory": {
56
+ "description": "Coarse fallback for path-bearing file tools when they target paths outside the active working directory.",
57
+ "$ref": "#/$defs/permissionState"
58
+ }
59
+ },
60
+ "patternProperties": {
61
+ "^external_directory:.+$": {
62
+ "description": "Resource-qualified external directory rule. Use `external_directory:<normalized-absolute-directory>/*` to allow, deny, or ask for a specific outside directory.",
55
63
  "$ref": "#/$defs/permissionState"
56
64
  }
57
65
  }
package/src/common.ts CHANGED
@@ -43,6 +43,26 @@ export function normalizePathForComparison(pathValue: string, cwd: string): stri
43
43
  return process.platform === "win32" ? normalizedAbsolutePath.toLowerCase() : normalizedAbsolutePath;
44
44
  }
45
45
 
46
+ export function normalizePathResourceForPermission(pathValue: string, cwd: string): string {
47
+ const normalizedPath = normalizePathForComparison(pathValue, cwd).replaceAll("\\", "/");
48
+ if (!normalizedPath) {
49
+ return "";
50
+ }
51
+
52
+ const driveRootMatch = normalizedPath.match(/^([a-z]):\/+$/iu);
53
+ if (driveRootMatch?.[1]) {
54
+ return `${driveRootMatch[1].toLowerCase()}:/`;
55
+ }
56
+
57
+ if (/^\/+$/u.test(normalizedPath)) {
58
+ return "/";
59
+ }
60
+
61
+ return normalizedPath
62
+ .replace(/^([A-Z]):/u, (_, drive: string) => `${drive.toLowerCase()}:`)
63
+ .replace(/\/+$/u, "");
64
+ }
65
+
46
66
  export function isPathWithinDirectory(pathValue: string, directory: string): boolean {
47
67
  if (!pathValue || !directory) {
48
68
  return false;
@@ -22,6 +22,13 @@ function toOnOff(value: boolean): string {
22
22
 
23
23
  function buildSettingItems(config: PermissionSystemExtensionConfig): SettingItem[] {
24
24
  return [
25
+ {
26
+ id: "debug",
27
+ label: "Debug logging",
28
+ description: "Write diagnostics and permission review entries to the extension debug file",
29
+ currentValue: toOnOff(config.debug),
30
+ values: ON_OFF,
31
+ },
25
32
  {
26
33
  id: "yoloMode",
27
34
  label: "YOLO mode",
@@ -29,27 +36,6 @@ function buildSettingItems(config: PermissionSystemExtensionConfig): SettingItem
29
36
  currentValue: toOnOff(config.yoloMode),
30
37
  values: ON_OFF,
31
38
  },
32
- {
33
- id: "permissionReviewLog",
34
- label: "Permission review log",
35
- description: "Write permission request and decision audit events to the extension logs directory",
36
- currentValue: toOnOff(config.permissionReviewLog),
37
- values: ON_OFF,
38
- },
39
- {
40
- id: "logPlaintextBashCommands",
41
- label: "Plaintext bash commands in review log",
42
- description: "Opt in to storing raw bash command strings; disabled stores only safe command metadata",
43
- currentValue: toOnOff(config.logPlaintextBashCommands),
44
- values: ON_OFF,
45
- },
46
- {
47
- id: "debugLog",
48
- label: "Debug logging",
49
- description: "Write verbose permission-system diagnostics to the extension logs directory",
50
- currentValue: toOnOff(config.debugLog),
51
- values: ON_OFF,
52
- },
53
39
  ];
54
40
  }
55
41
 
@@ -59,27 +45,21 @@ function applySetting(
59
45
  value: string,
60
46
  ): PermissionSystemExtensionConfig {
61
47
  switch (id) {
48
+ case "debug":
49
+ return { ...config, debug: value === "on" };
62
50
  case "yoloMode":
63
51
  return { ...config, yoloMode: value === "on" };
64
- case "permissionReviewLog":
65
- return { ...config, permissionReviewLog: value === "on" };
66
- case "logPlaintextBashCommands":
67
- return { ...config, logPlaintextBashCommands: value === "on" };
68
- case "debugLog":
69
- return { ...config, debugLog: value === "on" };
70
52
  default:
71
53
  return config;
72
54
  }
73
55
  }
74
56
 
75
57
  function syncSettingValues(settingsList: SettingValueSyncTarget, config: PermissionSystemExtensionConfig): void {
58
+ settingsList.updateValue("debug", toOnOff(config.debug));
76
59
  settingsList.updateValue("yoloMode", toOnOff(config.yoloMode));
77
- settingsList.updateValue("permissionReviewLog", toOnOff(config.permissionReviewLog));
78
- settingsList.updateValue("logPlaintextBashCommands", toOnOff(config.logPlaintextBashCommands));
79
- settingsList.updateValue("debugLog", toOnOff(config.debugLog));
80
60
  }
81
61
 
82
- async function openSettingsModal(ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): Promise<void> {
62
+ export async function openPermissionSystemSettingsModal(ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): Promise<void> {
83
63
  const overlayOptions = { anchor: "center" as const, width: 82, maxHeight: "85%" as const, margin: 1 };
84
64
 
85
65
  await ctx.ui.custom<void>(
@@ -90,7 +70,7 @@ async function openSettingsModal(ctx: ExtensionCommandContext, controller: Permi
90
70
  settingsModal = new ZellijSettingsModal(
91
71
  {
92
72
  title: "Permission System Settings",
93
- description: "Local extension options for permission logging and auto-approval behavior",
73
+ description: "Local extension options for debug logging and auto-approval behavior",
94
74
  settings: buildSettingItems(current),
95
75
  onChange: (id, newValue) => {
96
76
  current = applySetting(current, id, newValue);
@@ -143,14 +123,14 @@ async function openSettingsModal(ctx: ExtensionCommandContext, controller: Permi
143
123
 
144
124
  export function registerPermissionSystemCommand(pi: ExtensionAPI, controller: PermissionSystemConfigController): void {
145
125
  pi.registerCommand("permission-system", {
146
- description: "Configure pi-permission-system logging and yolo-mode behavior",
126
+ description: "Configure pi-permission-system debug logging and yolo-mode behavior",
147
127
  handler: async (_args, ctx) => {
148
128
  if (!ctx.hasUI) {
149
129
  ctx.ui.notify("/permission-system requires interactive TUI mode.", "warning");
150
130
  return;
151
131
  }
152
132
 
153
- await openSettingsModal(ctx, controller);
133
+ await openPermissionSystemSettingsModal(ctx, controller);
154
134
  },
155
135
  });
156
136
  }
@@ -0,0 +1,58 @@
1
+ import type { PermissionState } from "./types.js";
2
+ import { compileWildcardPattern } from "./wildcard-matcher.js";
3
+
4
+ export type PatternPermissionRule = {
5
+ tool: string;
6
+ pattern: string;
7
+ action: PermissionState;
8
+ };
9
+
10
+ export type PatternPermissionEvaluation = {
11
+ action: PermissionState;
12
+ matchedPattern?: string;
13
+ matchedTool?: string;
14
+ };
15
+
16
+ function isPatternPermissionRule(value: unknown): value is PatternPermissionRule {
17
+ if (!value || typeof value !== "object") {
18
+ return false;
19
+ }
20
+
21
+ const candidate = value as Partial<PatternPermissionRule>;
22
+ return typeof candidate.tool === "string"
23
+ && typeof candidate.pattern === "string"
24
+ && (candidate.action === "allow" || candidate.action === "deny" || candidate.action === "ask");
25
+ }
26
+
27
+ function normalizeRuleset(value: unknown): PatternPermissionRule[] {
28
+ return Array.isArray(value) ? value.filter(isPatternPermissionRule) : [];
29
+ }
30
+
31
+ export function evaluatePermission(
32
+ tool: string,
33
+ command: string,
34
+ ...rulesets: unknown[]
35
+ ): PatternPermissionEvaluation {
36
+ const rules = rulesets.flatMap(normalizeRuleset);
37
+
38
+ for (let index = rules.length - 1; index >= 0; index -= 1) {
39
+ const rule = rules[index];
40
+ const toolPattern = compileWildcardPattern(rule.tool, rule.action);
41
+ if (!toolPattern.regex.test(tool)) {
42
+ continue;
43
+ }
44
+
45
+ const commandPattern = compileWildcardPattern(rule.pattern, rule.action);
46
+ if (!commandPattern.regex.test(command)) {
47
+ continue;
48
+ }
49
+
50
+ return {
51
+ action: rule.action,
52
+ matchedPattern: rule.pattern,
53
+ matchedTool: rule.tool,
54
+ };
55
+ }
56
+
57
+ return { action: "ask" };
58
+ }