pi-permission-system 0.7.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. |
@@ -158,7 +159,7 @@ All permissions use one of three states:
158
159
  | `deny` | Blocks the action with an error message |
159
160
  | `ask` | Prompts the user for confirmation via UI |
160
161
 
161
- When an `ask` permission prompts, the confirmation UI offers `Allow Once`, `Allow Always`, `Reject`, and `Reject with Reason`. `Allow Once` records an in-memory approval for the current runtime, `Allow Always` persists a matching approval rule for future sessions, and rejected decisions can include an optional reason shown back to the agent.
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.
162
163
 
163
164
  ### Pi Integration Hooks
164
165
 
@@ -207,7 +208,7 @@ Debug output writes only under the extension directory by default. Set `PI_PERMI
207
208
 
208
209
  ### Runtime YOLO Control
209
210
 
210
- 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.
211
212
 
212
213
  Other extensions can toggle yolo mode immediately through the shared runtime API:
213
214
 
@@ -344,6 +345,20 @@ Controls tools by registered name pattern. This is the recommended standalone fo
344
345
 
345
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.
346
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
+
347
362
  > **Note:** Setting `tools.bash` affects the *default* for bash commands, but `bash` patterns can provide command-level overrides.
348
363
  >
349
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_*`.
@@ -439,18 +454,20 @@ Reserved permission checks:
439
454
  | Key | Description |
440
455
  |----------------------|------------------------------------------|
441
456
  | `doom_loop` | Controls doom loop detection behavior |
442
- | `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 |
443
459
 
444
460
  ```jsonc
445
461
  {
446
462
  "special": {
447
463
  "doom_loop": "deny",
448
- "external_directory": "ask"
464
+ "external_directory": "ask",
465
+ "external_directory:/home/alice/shared/*": "allow"
449
466
  }
450
467
  }
451
468
  ```
452
469
 
453
- `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.
454
471
 
455
472
  ---
456
473
 
@@ -642,7 +659,7 @@ npx --yes ajv-cli@5 validate \
642
659
  | Per-agent override not applied | Frontmatter parsing issue | Ensure `---` delimiters at file top; keep YAML simple; restart session |
643
660
  | Tool blocked as unregistered | Unknown tool name | Use a registered `mcp` tool for server tools: `{ "tool": "server:tool" }` |
644
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. |
645
- | 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/*`. |
646
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. |
647
664
 
648
665
  ---
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-permission-system",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -22,7 +22,7 @@
22
22
  "build": "npm run typecheck",
23
23
  "lint": "npm run typecheck",
24
24
  "validate:artifacts": "node ./scripts/validate-artifacts.mjs",
25
- "test": "bun ./tests/permission-system.test.ts && bun ./tests/config-modal.test.ts",
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
26
  "check": "npm run lint && npm run validate:artifacts && npm run test"
27
27
  },
28
28
  "keywords": [
@@ -61,9 +61,9 @@
61
61
  },
62
62
  "peerDependencies": {
63
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"
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
67
  },
68
68
  "dependencies": {
69
69
  "jsonc-parser": "^3.3.1"
@@ -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;
@@ -1,136 +1,136 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import type { SettingItem } from "@earendil-works/pi-tui";
3
-
4
- import type { PermissionSystemExtensionConfig } from "./extension-config.js";
5
- import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.js";
6
-
7
- interface PermissionSystemConfigController {
8
- getConfig(): PermissionSystemExtensionConfig;
9
- setConfig(next: PermissionSystemExtensionConfig, ctx: ExtensionCommandContext): void;
10
- getConfigPath(): string;
11
- }
12
-
13
- interface SettingValueSyncTarget {
14
- updateValue(id: string, value: string): void;
15
- }
16
-
17
- const ON_OFF = ["on", "off"];
18
-
19
- function toOnOff(value: boolean): string {
20
- return value ? "on" : "off";
21
- }
22
-
23
- function buildSettingItems(config: PermissionSystemExtensionConfig): SettingItem[] {
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
- },
32
- {
33
- id: "yoloMode",
34
- label: "YOLO mode",
35
- description: "Auto-approve ask-state permission checks, including subagent approval forwarding",
36
- currentValue: toOnOff(config.yoloMode),
37
- values: ON_OFF,
38
- },
39
- ];
40
- }
41
-
42
- function applySetting(
43
- config: PermissionSystemExtensionConfig,
44
- id: string,
45
- value: string,
46
- ): PermissionSystemExtensionConfig {
47
- switch (id) {
48
- case "debug":
49
- return { ...config, debug: value === "on" };
50
- case "yoloMode":
51
- return { ...config, yoloMode: value === "on" };
52
- default:
53
- return config;
54
- }
55
- }
56
-
57
- function syncSettingValues(settingsList: SettingValueSyncTarget, config: PermissionSystemExtensionConfig): void {
58
- settingsList.updateValue("debug", toOnOff(config.debug));
59
- settingsList.updateValue("yoloMode", toOnOff(config.yoloMode));
60
- }
61
-
62
- export async function openPermissionSystemSettingsModal(ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): Promise<void> {
63
- const overlayOptions = { anchor: "center" as const, width: 82, maxHeight: "85%" as const, margin: 1 };
64
-
65
- await ctx.ui.custom<void>(
66
- (tui, theme, _keybindings, done) => {
67
- let current = controller.getConfig();
68
- let settingsModal: ZellijSettingsModal | null = null;
69
-
70
- settingsModal = new ZellijSettingsModal(
71
- {
72
- title: "Permission System Settings",
73
- description: "Local extension options for debug logging and auto-approval behavior",
74
- settings: buildSettingItems(current),
75
- onChange: (id, newValue) => {
76
- current = applySetting(current, id, newValue);
77
- controller.setConfig(current, ctx);
78
- current = controller.getConfig();
79
- if (settingsModal) {
80
- syncSettingValues(settingsModal, current);
81
- }
82
- },
83
- onClose: () => done(),
84
- helpText: `Config file: ${controller.getConfigPath()}`,
85
- enableSearch: true,
86
- },
87
- theme,
88
- );
89
-
90
- const modal = new ZellijModal(
91
- settingsModal,
92
- {
93
- borderStyle: "rounded",
94
- titleBar: {
95
- left: "Permission System Settings",
96
- right: "pi-permission-system",
97
- },
98
- helpUndertitle: {
99
- text: "Esc: close | ↑↓: navigate | Space: toggle",
100
- color: "dim",
101
- },
102
- overlay: overlayOptions,
103
- },
104
- theme,
105
- );
106
-
107
- return {
108
- render(width: number) {
109
- return modal.renderModal(width).lines;
110
- },
111
- invalidate() {
112
- modal.invalidate();
113
- },
114
- handleInput(data: string) {
115
- modal.handleInput(data);
116
- tui.requestRender();
117
- },
118
- };
119
- },
120
- { overlay: true, overlayOptions },
121
- );
122
- }
123
-
124
- export function registerPermissionSystemCommand(pi: ExtensionAPI, controller: PermissionSystemConfigController): void {
125
- pi.registerCommand("permission-system", {
126
- description: "Configure pi-permission-system debug logging and yolo-mode behavior",
127
- handler: async (_args, ctx) => {
128
- if (!ctx.hasUI) {
129
- ctx.ui.notify("/permission-system requires interactive TUI mode.", "warning");
130
- return;
131
- }
132
-
133
- await openPermissionSystemSettingsModal(ctx, controller);
134
- },
135
- });
136
- }
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { SettingItem } from "@earendil-works/pi-tui";
3
+
4
+ import type { PermissionSystemExtensionConfig } from "./extension-config.js";
5
+ import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.js";
6
+
7
+ interface PermissionSystemConfigController {
8
+ getConfig(): PermissionSystemExtensionConfig;
9
+ setConfig(next: PermissionSystemExtensionConfig, ctx: ExtensionCommandContext): void;
10
+ getConfigPath(): string;
11
+ }
12
+
13
+ interface SettingValueSyncTarget {
14
+ updateValue(id: string, value: string): void;
15
+ }
16
+
17
+ const ON_OFF = ["on", "off"];
18
+
19
+ function toOnOff(value: boolean): string {
20
+ return value ? "on" : "off";
21
+ }
22
+
23
+ function buildSettingItems(config: PermissionSystemExtensionConfig): SettingItem[] {
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
+ },
32
+ {
33
+ id: "yoloMode",
34
+ label: "YOLO mode",
35
+ description: "Auto-approve ask-state permission checks, including subagent approval forwarding",
36
+ currentValue: toOnOff(config.yoloMode),
37
+ values: ON_OFF,
38
+ },
39
+ ];
40
+ }
41
+
42
+ function applySetting(
43
+ config: PermissionSystemExtensionConfig,
44
+ id: string,
45
+ value: string,
46
+ ): PermissionSystemExtensionConfig {
47
+ switch (id) {
48
+ case "debug":
49
+ return { ...config, debug: value === "on" };
50
+ case "yoloMode":
51
+ return { ...config, yoloMode: value === "on" };
52
+ default:
53
+ return config;
54
+ }
55
+ }
56
+
57
+ function syncSettingValues(settingsList: SettingValueSyncTarget, config: PermissionSystemExtensionConfig): void {
58
+ settingsList.updateValue("debug", toOnOff(config.debug));
59
+ settingsList.updateValue("yoloMode", toOnOff(config.yoloMode));
60
+ }
61
+
62
+ export async function openPermissionSystemSettingsModal(ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): Promise<void> {
63
+ const overlayOptions = { anchor: "center" as const, width: 82, maxHeight: "85%" as const, margin: 1 };
64
+
65
+ await ctx.ui.custom<void>(
66
+ (tui, theme, _keybindings, done) => {
67
+ let current = controller.getConfig();
68
+ let settingsModal: ZellijSettingsModal | null = null;
69
+
70
+ settingsModal = new ZellijSettingsModal(
71
+ {
72
+ title: "Permission System Settings",
73
+ description: "Local extension options for debug logging and auto-approval behavior",
74
+ settings: buildSettingItems(current),
75
+ onChange: (id, newValue) => {
76
+ current = applySetting(current, id, newValue);
77
+ controller.setConfig(current, ctx);
78
+ current = controller.getConfig();
79
+ if (settingsModal) {
80
+ syncSettingValues(settingsModal, current);
81
+ }
82
+ },
83
+ onClose: () => done(),
84
+ helpText: `Config file: ${controller.getConfigPath()}`,
85
+ enableSearch: true,
86
+ },
87
+ theme,
88
+ );
89
+
90
+ const modal = new ZellijModal(
91
+ settingsModal,
92
+ {
93
+ borderStyle: "rounded",
94
+ titleBar: {
95
+ left: "Permission System Settings",
96
+ right: "pi-permission-system",
97
+ },
98
+ helpUndertitle: {
99
+ text: "Esc: close | ↑↓: navigate | Space: toggle",
100
+ color: "dim",
101
+ },
102
+ overlay: overlayOptions,
103
+ },
104
+ theme,
105
+ );
106
+
107
+ return {
108
+ render(width: number) {
109
+ return modal.renderModal(width).lines;
110
+ },
111
+ invalidate() {
112
+ modal.invalidate();
113
+ },
114
+ handleInput(data: string) {
115
+ modal.handleInput(data);
116
+ tui.requestRender();
117
+ },
118
+ };
119
+ },
120
+ { overlay: true, overlayOptions },
121
+ );
122
+ }
123
+
124
+ export function registerPermissionSystemCommand(pi: ExtensionAPI, controller: PermissionSystemConfigController): void {
125
+ pi.registerCommand("permission-system", {
126
+ description: "Configure pi-permission-system debug logging and yolo-mode behavior",
127
+ handler: async (_args, ctx) => {
128
+ if (!ctx.hasUI) {
129
+ ctx.ui.notify("/permission-system requires interactive TUI mode.", "warning");
130
+ return;
131
+ }
132
+
133
+ await openPermissionSystemSettingsModal(ctx, controller);
134
+ },
135
+ });
136
+ }