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 +35 -0
- package/README.md +36 -23
- package/config/config.example.json +3 -1
- package/package.json +71 -71
- package/schemas/permissions.schema.json +9 -1
- package/src/common.ts +20 -0
- package/src/config-modal.ts +14 -34
- package/src/evaluate-permission.ts +58 -0
- package/src/extension-config.ts +18 -19
- package/src/index.ts +561 -144
- package/src/logging.ts +11 -26
- package/src/permanent-approval-store.ts +93 -0
- package/src/permission-dialog.ts +91 -15
- package/src/permission-forwarding.ts +17 -1
- package/src/permission-manager.ts +121 -10
- package/src/session-approval-store.ts +42 -0
- package/src/skill-prompt-sanitizer.ts +51 -5
- package/src/types-shims.d.ts +2 -2
- package/src/wildcard-matcher.ts +79 -77
- package/src/zellij-modal.ts +14 -2
- package/tests/config-modal-theme-red.test.ts +337 -0
- package/tests/config-modal.test.ts +18 -12
- package/tests/directory-resource-matching-red.test.ts +493 -0
- package/tests/edge-red-agent-name-traversal.test.ts +49 -0
- package/tests/edit-decision-deduplication-red.test.ts +252 -0
- package/tests/edit-prompt-compaction-red.test.ts +273 -0
- package/tests/permission-persistence-red.test.ts +643 -0
- package/tests/permission-system.test.ts +532 -78
- package/tests/session-permanent-permission.test.ts +796 -0
- package/tests/types-shims-coverage.test.ts +416 -0
- package/tests/wildcard-multiline.test.ts +562 -0
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 |
|
|
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
|
|
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
|
-
-
|
|
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
|
-
"
|
|
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
|
-
| `
|
|
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
|
-
|
|
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` |
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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;
|
package/src/config-modal.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
+
}
|