pi-permission-system 0.2.1 → 0.3.0
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 +33 -0
- package/README.md +28 -27
- package/config.json +2 -1
- package/package.json +5 -4
- package/schemas/permissions.schema.json +2 -0
- package/src/config-modal-test.ts +217 -0
- package/src/config-modal.ts +231 -0
- package/src/extension-config.ts +45 -3
- package/src/index.ts +184 -155
- package/src/permission-dialog.ts +83 -0
- package/src/permission-forwarding.ts +102 -0
- package/src/permission-manager.ts +13 -34
- package/src/test.ts +325 -24
- package/src/types-shims.d.ts +166 -0
- package/src/yolo-mode.ts +23 -0
- package/src/zellij-modal.ts +1001 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Yolo mode for auto-approval when enabled — bypasses permission prompts for streamlined workflows
|
|
12
|
+
- Permission forwarding system for subagent-to-primary IPC communication
|
|
13
|
+
- Configuration modal UI with Zellij integration (`config-modal.ts`, `zellij-modal.ts`)
|
|
14
|
+
- `permission-forwarding.ts` module for subagent permission request routing
|
|
15
|
+
- `yolo-mode.ts` module for automatic permission approval when yolo mode is active
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.62.0
|
|
19
|
+
- Refactored `index.ts` to export new permission resolution utilities
|
|
20
|
+
- Expanded `extension-config.ts` with config normalization for new features
|
|
21
|
+
- Added `types-shims.d.ts` for Zellij modal type definitions
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
- Added comprehensive tests for config modal functionality
|
|
25
|
+
- Added tests for permission forwarding behavior
|
|
26
|
+
|
|
27
|
+
## [0.2.2] - 2026-03-13
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Removed delegation task restriction logic — the `task` tool is no longer restricted to orchestrator agent only
|
|
31
|
+
- Simplified tool permission lookup to use explicit `tools` entries for arbitrary registered tools instead of MCP fallback
|
|
32
|
+
- Renamed `TOOL_PERMISSION_NAMES` to `BUILT_IN_TOOL_PERMISSION_NAMES` to clarify it covers only canonical Pi tools
|
|
33
|
+
- Updated schema descriptions for `tools` and `mcp` fields to guide configuration usage
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
- Removed delegation-specific permission checks (`isDelegationAllowedAgent`, `getDelegationBlockReason`) from permission evaluation
|
|
37
|
+
|
|
38
|
+
### Tests
|
|
39
|
+
- Added comprehensive test coverage for tool permission lookup behavior
|
|
40
|
+
|
|
8
41
|
## [0.2.1] - 2026-03-13
|
|
9
42
|
|
|
10
43
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🔐 pi-permission-system
|
|
2
2
|
|
|
3
|
-
[](package.json)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
6
|
Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
|
|
@@ -79,7 +79,7 @@ The extension integrates via Pi's lifecycle hooks:
|
|
|
79
79
|
**Additional behaviors:**
|
|
80
80
|
- Unknown/unregistered tools are blocked before permission checks (prevents bypass attempts)
|
|
81
81
|
- The `Available tools:` system prompt section is rewritten to match the filtered active tool set
|
|
82
|
-
-
|
|
82
|
+
- Extension-provided tools like `task`, `mcp`, and third-party tools are handled by exact registered name instead of private built-in hardcodes
|
|
83
83
|
- When a subagent hits an `ask` permission without direct UI access, the request can be forwarded to the main interactive session for confirmation
|
|
84
84
|
- When a subagent triggers an `ask` permission without UI access, the request can be forwarded to the main session and answered there
|
|
85
85
|
|
|
@@ -111,14 +111,14 @@ Both logs write to files only under the extension directory. No debug output is
|
|
|
111
111
|
|
|
112
112
|
The policy file is a JSON object with these sections:
|
|
113
113
|
|
|
114
|
-
| Section | Description
|
|
115
|
-
|
|
116
|
-
| `defaultPolicy` | Fallback permissions per category
|
|
117
|
-
| `tools` |
|
|
118
|
-
| `bash` | Command pattern permissions
|
|
119
|
-
| `mcp` | MCP server/tool permissions
|
|
120
|
-
| `skills` | Skill name pattern permissions
|
|
121
|
-
| `special` | Reserved permission checks
|
|
114
|
+
| Section | Description |
|
|
115
|
+
|-----------------|-----------------------------------------------------|
|
|
116
|
+
| `defaultPolicy` | Fallback permissions per category |
|
|
117
|
+
| `tools` | Exact-name tool permissions for registered tools |
|
|
118
|
+
| `bash` | Command pattern permissions |
|
|
119
|
+
| `mcp` | MCP server/tool permissions for calls routed through a registered `mcp` tool |
|
|
120
|
+
| `skills` | Skill name pattern permissions |
|
|
121
|
+
| `special` | Reserved permission checks |
|
|
122
122
|
|
|
123
123
|
> **Note:** Trailing commas are **not** supported. If parsing fails, the extension falls back to `ask` for all categories.
|
|
124
124
|
|
|
@@ -147,7 +147,7 @@ permission:
|
|
|
147
147
|
|
|
148
148
|
**Precedence:** Agent frontmatter overrides global config (shallow-merged per section).
|
|
149
149
|
|
|
150
|
-
**MCP behavior:** `permission.tools.mcp` is the coarse entry/fallback permission for
|
|
150
|
+
**MCP behavior:** `permission.tools.mcp` is the coarse entry/fallback permission for a registered `mcp` tool when one is available. More specific `permission.mcp` target rules override that fallback when they match.
|
|
151
151
|
|
|
152
152
|
**Limitations:** The frontmatter parser is intentionally minimal. Use only `key: value` scalars and nested maps. Avoid arrays, multi-line scalars, and YAML anchors.
|
|
153
153
|
|
|
@@ -173,33 +173,34 @@ Sets fallback permissions when no specific rule matches:
|
|
|
173
173
|
|
|
174
174
|
### `tools`
|
|
175
175
|
|
|
176
|
-
Controls
|
|
176
|
+
Controls tools by exact registered name (no wildcards). This is the recommended standalone format for **all** tool entries, including Pi built-ins and arbitrary third-party extension tools.
|
|
177
177
|
|
|
178
|
-
| Tool
|
|
179
|
-
|
|
180
|
-
| `bash`
|
|
181
|
-
| `read`
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `find` | File discovery |
|
|
186
|
-
| `ls` | Directory listing |
|
|
187
|
-
| `mcp` | MCP proxy tool entry/fallback |
|
|
178
|
+
| Tool name example | Description |
|
|
179
|
+
|-----------------------|-------------|
|
|
180
|
+
| `bash` | Shell command execution (tool-level fallback before `bash` pattern rules) |
|
|
181
|
+
| `read` / `write` | Canonical Pi built-in file tools |
|
|
182
|
+
| `mcp` | Registered MCP proxy tool entry/fallback when available |
|
|
183
|
+
| `task` | Delegation tool handled like any other registered extension tool |
|
|
184
|
+
| `third_party_tool` | Arbitrary registered extension tool |
|
|
188
185
|
|
|
189
186
|
```jsonc
|
|
190
187
|
{
|
|
191
188
|
"tools": {
|
|
192
189
|
"read": "allow",
|
|
193
190
|
"write": "deny",
|
|
194
|
-
"
|
|
195
|
-
"
|
|
191
|
+
"mcp": "allow",
|
|
192
|
+
"third_party_tool": "ask"
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
```
|
|
199
196
|
|
|
197
|
+
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.
|
|
198
|
+
|
|
200
199
|
> **Note:** Setting `tools.bash` affects the *default* for bash commands, but `bash` patterns can provide command-level overrides.
|
|
201
200
|
>
|
|
202
|
-
> **Note:** Setting `tools.mcp` controls coarse access to
|
|
201
|
+
> **Note:** Setting `tools.mcp` controls coarse access to a registered `mcp` tool when one is available. Specific `mcp` rules still override it when a target pattern matches.
|
|
202
|
+
>
|
|
203
|
+
> **Note:** Top-level shorthand is only supported for the canonical Pi built-ins (`bash`, `read`, `write`, `edit`, `grep`, `find`, `ls`) in agent frontmatter. Use `permission.tools.<name>` for `mcp`, `task`, and any third-party tool.
|
|
203
204
|
|
|
204
205
|
### `bash`
|
|
205
206
|
|
|
@@ -241,7 +242,7 @@ MCP permissions match against derived targets from tool input. These rules are m
|
|
|
241
242
|
|
|
242
243
|
#### MCP Tool Fallback via `tools.mcp`
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
A registered `mcp` tool can use `tools.mcp` as an entry permission point. This provides a fallback when no specific MCP pattern matches:
|
|
245
246
|
|
|
246
247
|
```jsonc
|
|
247
248
|
{
|
|
@@ -456,7 +457,7 @@ npx --yes ajv-cli@5 validate \
|
|
|
456
457
|
|---------|-------|----------|
|
|
457
458
|
| Config not applied (everything asks) | File not found or parse error | Verify file at `~/.pi/agent/pi-permissions.jsonc`; check for trailing commas |
|
|
458
459
|
| Per-agent override not applied | Frontmatter parsing issue | Ensure `---` delimiters at file top; keep YAML simple; restart session |
|
|
459
|
-
| Tool blocked as unregistered | Unknown tool name | Use
|
|
460
|
+
| Tool blocked as unregistered | Unknown tool name | Use a registered `mcp` tool for server tools: `{ "tool": "server:tool" }` |
|
|
460
461
|
| `/skill:<name>` blocked | Missing context or deny policy | Requires active agent context; `ask` behaves as block in headless mode |
|
|
461
462
|
|
|
462
463
|
---
|
package/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-permission-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
23
23
|
"lint": "npm run build",
|
|
24
|
-
"test": "bun ./src/test.ts",
|
|
24
|
+
"test": "bun ./src/test.ts && bun ./src/config-modal-test.ts",
|
|
25
25
|
"check": "npm run lint && npm run test"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@mariozechner/pi-coding-agent": "
|
|
58
|
-
"@
|
|
57
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
58
|
+
"@mariozechner/pi-tui": "^0.62.0",
|
|
59
|
+
"@sinclair/typebox": "^0.34.48"
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"tools": {
|
|
34
|
+
"description": "Exact-name permissions for registered tools. Use this map for the canonical Pi built-ins and any extension-provided or third-party tools.",
|
|
34
35
|
"$ref": "#/$defs/permissionMap"
|
|
35
36
|
},
|
|
36
37
|
"bash": {
|
|
37
38
|
"$ref": "#/$defs/permissionMap"
|
|
38
39
|
},
|
|
39
40
|
"mcp": {
|
|
41
|
+
"description": "Pattern-based permissions for targets invoked through a registered `mcp` tool when available.",
|
|
40
42
|
"$ref": "#/$defs/permissionMap"
|
|
41
43
|
},
|
|
42
44
|
"skills": {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { mock } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_EXTENSION_CONFIG,
|
|
9
|
+
loadPermissionSystemConfig,
|
|
10
|
+
savePermissionSystemConfig,
|
|
11
|
+
type PermissionSystemExtensionConfig,
|
|
12
|
+
} from "./extension-config.js";
|
|
13
|
+
|
|
14
|
+
mock.module("@mariozechner/pi-coding-agent", () => ({
|
|
15
|
+
getSettingsListTheme: () => ({}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module("@mariozechner/pi-tui", () => ({
|
|
19
|
+
Box: class {},
|
|
20
|
+
Container: class {
|
|
21
|
+
addChild(): void {}
|
|
22
|
+
render(): string[] {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
invalidate(): void {}
|
|
26
|
+
},
|
|
27
|
+
SettingsList: class {
|
|
28
|
+
handleInput(): void {}
|
|
29
|
+
updateValue(): void {}
|
|
30
|
+
render(): string[] {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
invalidate(): void {}
|
|
34
|
+
},
|
|
35
|
+
Spacer: class {},
|
|
36
|
+
Text: class {},
|
|
37
|
+
truncateToWidth: (text: string) => text,
|
|
38
|
+
visibleWidth: (text: string) => text.length,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { registerPermissionSystemCommand } = await import("./config-modal.js");
|
|
42
|
+
|
|
43
|
+
type Notification = { message: string; level: "info" | "warning" | "error" };
|
|
44
|
+
|
|
45
|
+
type CommandContextStub = {
|
|
46
|
+
hasUI: boolean;
|
|
47
|
+
ui: {
|
|
48
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
49
|
+
custom<T>(renderer: (...args: unknown[]) => unknown, options?: unknown): Promise<T>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function runTest(name: string, testFn: () => void): void {
|
|
54
|
+
testFn();
|
|
55
|
+
console.log(`[PASS] ${name}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runAsyncTest(name: string, testFn: () => Promise<void>): Promise<void> {
|
|
59
|
+
await testFn();
|
|
60
|
+
console.log(`[PASS] ${name}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createCommandContext(
|
|
64
|
+
hasUI: boolean,
|
|
65
|
+
): { ctx: CommandContextStub; notifications: Notification[]; getCustomCalls(): number } {
|
|
66
|
+
const notifications: Notification[] = [];
|
|
67
|
+
let customCalls = 0;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
ctx: {
|
|
71
|
+
hasUI,
|
|
72
|
+
ui: {
|
|
73
|
+
notify(message: string, level: "info" | "warning" | "error") {
|
|
74
|
+
notifications.push({ message, level });
|
|
75
|
+
},
|
|
76
|
+
async custom<T>(_renderer: (...args: unknown[]) => unknown, _options?: unknown): Promise<T> {
|
|
77
|
+
customCalls += 1;
|
|
78
|
+
return undefined as T;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
notifications,
|
|
83
|
+
getCustomCalls: () => customCalls,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lastNotification(notifications: Notification[]): Notification {
|
|
88
|
+
return notifications[notifications.length - 1] as Notification;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
runTest("permission-system command completions expose top-level config actions", () => {
|
|
92
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-command-completions-"));
|
|
93
|
+
const configPath = join(baseDir, "config.json");
|
|
94
|
+
let config: PermissionSystemExtensionConfig = { ...DEFAULT_EXTENSION_CONFIG };
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const controller = {
|
|
98
|
+
getConfig: () => config,
|
|
99
|
+
setConfig: (next: PermissionSystemExtensionConfig) => {
|
|
100
|
+
config = next;
|
|
101
|
+
},
|
|
102
|
+
getConfigPath: () => configPath,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let definition: {
|
|
106
|
+
description: string;
|
|
107
|
+
getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
|
|
108
|
+
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
|
109
|
+
} | null = null;
|
|
110
|
+
|
|
111
|
+
registerPermissionSystemCommand(
|
|
112
|
+
{
|
|
113
|
+
registerCommand(_name: string, nextDefinition: typeof definition) {
|
|
114
|
+
definition = nextDefinition;
|
|
115
|
+
},
|
|
116
|
+
} as never,
|
|
117
|
+
controller as never,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
assert.ok(definition !== null);
|
|
121
|
+
assert.ok(typeof definition?.getArgumentCompletions === "function");
|
|
122
|
+
|
|
123
|
+
const topLevel = definition?.getArgumentCompletions?.("");
|
|
124
|
+
assert.ok(Array.isArray(topLevel));
|
|
125
|
+
assert.ok(topLevel?.some((item) => item.value === "show"));
|
|
126
|
+
assert.ok(topLevel?.some((item) => item.value === "reset"));
|
|
127
|
+
|
|
128
|
+
const filtered = definition?.getArgumentCompletions?.("pa");
|
|
129
|
+
assert.deepEqual(filtered?.map((item) => item.value), ["path"]);
|
|
130
|
+
assert.equal(definition?.getArgumentCompletions?.("path extra"), null);
|
|
131
|
+
assert.equal(definition?.getArgumentCompletions?.("zzz"), null);
|
|
132
|
+
} finally {
|
|
133
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await runAsyncTest("permission-system command handlers manage config summary, persistence, and modal routing", async () => {
|
|
138
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-command-"));
|
|
139
|
+
const configPath = join(baseDir, "config.json");
|
|
140
|
+
let config: PermissionSystemExtensionConfig = {
|
|
141
|
+
debugLog: true,
|
|
142
|
+
permissionReviewLog: false,
|
|
143
|
+
yoloMode: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const initialSave = savePermissionSystemConfig(config, configPath);
|
|
148
|
+
assert.equal(initialSave.success, true);
|
|
149
|
+
|
|
150
|
+
const controller = {
|
|
151
|
+
getConfig: () => config,
|
|
152
|
+
setConfig: (next: PermissionSystemExtensionConfig) => {
|
|
153
|
+
const normalized = loadPermissionSystemConfig(configPath).config;
|
|
154
|
+
const saved = savePermissionSystemConfig(next, configPath);
|
|
155
|
+
assert.equal(saved.success, true);
|
|
156
|
+
config = loadPermissionSystemConfig(configPath).config;
|
|
157
|
+
assert.notDeepEqual(config, normalized);
|
|
158
|
+
},
|
|
159
|
+
getConfigPath: () => configPath,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let registeredName = "";
|
|
163
|
+
let definition: {
|
|
164
|
+
description: string;
|
|
165
|
+
getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
|
|
166
|
+
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
|
167
|
+
} | null = null;
|
|
168
|
+
|
|
169
|
+
registerPermissionSystemCommand(
|
|
170
|
+
{
|
|
171
|
+
registerCommand(name: string, nextDefinition: typeof definition) {
|
|
172
|
+
registeredName = name;
|
|
173
|
+
definition = nextDefinition;
|
|
174
|
+
},
|
|
175
|
+
} as never,
|
|
176
|
+
controller as never,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
assert.equal(registeredName, "permission-system");
|
|
180
|
+
assert.ok(definition !== null);
|
|
181
|
+
assert.ok((definition?.description ?? "").includes("Configure pi-permission-system"));
|
|
182
|
+
|
|
183
|
+
const infoCtx = createCommandContext(true);
|
|
184
|
+
await definition?.handler("show", infoCtx.ctx);
|
|
185
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("yoloMode=on"));
|
|
186
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("debugLog=on"));
|
|
187
|
+
|
|
188
|
+
await definition?.handler("path", infoCtx.ctx);
|
|
189
|
+
assert.equal(lastNotification(infoCtx.notifications).message, `permission-system config: ${configPath}`);
|
|
190
|
+
|
|
191
|
+
await definition?.handler("help", infoCtx.ctx);
|
|
192
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /permission-system"));
|
|
193
|
+
|
|
194
|
+
await definition?.handler("reset", infoCtx.ctx);
|
|
195
|
+
assert.deepEqual(config, DEFAULT_EXTENSION_CONFIG);
|
|
196
|
+
assert.equal(lastNotification(infoCtx.notifications).message, "Permission system settings reset to defaults.");
|
|
197
|
+
|
|
198
|
+
const persisted = JSON.parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
199
|
+
assert.deepEqual(persisted, DEFAULT_EXTENSION_CONFIG);
|
|
200
|
+
|
|
201
|
+
await definition?.handler("unknown", infoCtx.ctx);
|
|
202
|
+
assert.equal(lastNotification(infoCtx.notifications).level, "warning");
|
|
203
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /permission-system"));
|
|
204
|
+
|
|
205
|
+
const headlessCtx = createCommandContext(false);
|
|
206
|
+
await definition?.handler("", headlessCtx.ctx);
|
|
207
|
+
assert.equal(lastNotification(headlessCtx.notifications).message, "/permission-system requires interactive TUI mode.");
|
|
208
|
+
|
|
209
|
+
const modalCtx = createCommandContext(true);
|
|
210
|
+
await definition?.handler("", modalCtx.ctx);
|
|
211
|
+
assert.equal(modalCtx.getCustomCalls(), 1);
|
|
212
|
+
} finally {
|
|
213
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
console.log("All permission-system config-modal tests passed.");
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { SettingItem } from "@mariozechner/pi-tui";
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_EXTENSION_CONFIG, 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
|
+
const COMMAND_ARGUMENTS = [
|
|
19
|
+
{
|
|
20
|
+
value: "show",
|
|
21
|
+
label: "Show active settings",
|
|
22
|
+
description: "Display the current permission-system config summary",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: "path",
|
|
26
|
+
label: "Show config path",
|
|
27
|
+
description: "Display the config.json path used by pi-permission-system",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: "reset",
|
|
31
|
+
label: "Reset defaults",
|
|
32
|
+
description: "Restore default yolo/logging settings and persist them",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: "help",
|
|
36
|
+
label: "Show help",
|
|
37
|
+
description: "Display command usage",
|
|
38
|
+
},
|
|
39
|
+
] as const;
|
|
40
|
+
const USAGE_TEXT = "Usage: /permission-system [show|path|reset|help] (or run /permission-system with no args to open settings modal)";
|
|
41
|
+
|
|
42
|
+
function cloneDefaultConfig(): PermissionSystemExtensionConfig {
|
|
43
|
+
return {
|
|
44
|
+
debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
|
|
45
|
+
permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
|
|
46
|
+
yoloMode: DEFAULT_EXTENSION_CONFIG.yoloMode,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toOnOff(value: boolean): string {
|
|
51
|
+
return value ? "on" : "off";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function summarizeConfig(config: PermissionSystemExtensionConfig): string {
|
|
55
|
+
return [
|
|
56
|
+
`yoloMode=${toOnOff(config.yoloMode)}`,
|
|
57
|
+
`permissionReviewLog=${toOnOff(config.permissionReviewLog)}`,
|
|
58
|
+
`debugLog=${toOnOff(config.debugLog)}`,
|
|
59
|
+
].join(", ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSettingItems(config: PermissionSystemExtensionConfig): SettingItem[] {
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
id: "yoloMode",
|
|
66
|
+
label: "YOLO mode",
|
|
67
|
+
description: "Auto-approve ask-state permission checks, including subagent approval forwarding",
|
|
68
|
+
currentValue: toOnOff(config.yoloMode),
|
|
69
|
+
values: ON_OFF,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "permissionReviewLog",
|
|
73
|
+
label: "Permission review log",
|
|
74
|
+
description: "Write permission request and decision audit events to the extension logs directory",
|
|
75
|
+
currentValue: toOnOff(config.permissionReviewLog),
|
|
76
|
+
values: ON_OFF,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "debugLog",
|
|
80
|
+
label: "Debug logging",
|
|
81
|
+
description: "Write verbose permission-system diagnostics to the extension logs directory",
|
|
82
|
+
currentValue: toOnOff(config.debugLog),
|
|
83
|
+
values: ON_OFF,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applySetting(
|
|
89
|
+
config: PermissionSystemExtensionConfig,
|
|
90
|
+
id: string,
|
|
91
|
+
value: string,
|
|
92
|
+
): PermissionSystemExtensionConfig {
|
|
93
|
+
switch (id) {
|
|
94
|
+
case "yoloMode":
|
|
95
|
+
return { ...config, yoloMode: value === "on" };
|
|
96
|
+
case "permissionReviewLog":
|
|
97
|
+
return { ...config, permissionReviewLog: value === "on" };
|
|
98
|
+
case "debugLog":
|
|
99
|
+
return { ...config, debugLog: value === "on" };
|
|
100
|
+
default:
|
|
101
|
+
return config;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function syncSettingValues(settingsList: SettingValueSyncTarget, config: PermissionSystemExtensionConfig): void {
|
|
106
|
+
settingsList.updateValue("yoloMode", toOnOff(config.yoloMode));
|
|
107
|
+
settingsList.updateValue("permissionReviewLog", toOnOff(config.permissionReviewLog));
|
|
108
|
+
settingsList.updateValue("debugLog", toOnOff(config.debugLog));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getArgumentCompletions(argumentPrefix: string): Array<{ value: string; label: string; description: string }> | null {
|
|
112
|
+
const normalized = argumentPrefix.trim().toLowerCase();
|
|
113
|
+
if (normalized.includes(" ")) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const filtered = COMMAND_ARGUMENTS.filter((item) => item.value.startsWith(normalized));
|
|
118
|
+
return filtered.length > 0 ? [...filtered] : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function openSettingsModal(ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): Promise<void> {
|
|
122
|
+
const overlayOptions = { anchor: "center" as const, width: 82, maxHeight: "85%" as const, margin: 1 };
|
|
123
|
+
|
|
124
|
+
await ctx.ui.custom<void>(
|
|
125
|
+
(tui, theme, _keybindings, done) => {
|
|
126
|
+
let current = controller.getConfig();
|
|
127
|
+
let settingsModal: ZellijSettingsModal | null = null;
|
|
128
|
+
|
|
129
|
+
settingsModal = new ZellijSettingsModal(
|
|
130
|
+
{
|
|
131
|
+
title: "Permission System Settings",
|
|
132
|
+
description: "Local extension options for permission logging and auto-approval behavior",
|
|
133
|
+
settings: buildSettingItems(current),
|
|
134
|
+
onChange: (id, newValue) => {
|
|
135
|
+
current = applySetting(current, id, newValue);
|
|
136
|
+
controller.setConfig(current, ctx);
|
|
137
|
+
current = controller.getConfig();
|
|
138
|
+
if (settingsModal) {
|
|
139
|
+
syncSettingValues(settingsModal, current);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
onClose: () => done(),
|
|
143
|
+
helpText: `/permission-system show • /permission-system reset • ${controller.getConfigPath()}`,
|
|
144
|
+
enableSearch: true,
|
|
145
|
+
},
|
|
146
|
+
theme,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const modal = new ZellijModal(
|
|
150
|
+
settingsModal,
|
|
151
|
+
{
|
|
152
|
+
borderStyle: "rounded",
|
|
153
|
+
titleBar: {
|
|
154
|
+
left: "Permission System Settings",
|
|
155
|
+
right: "pi-permission-system",
|
|
156
|
+
},
|
|
157
|
+
helpUndertitle: {
|
|
158
|
+
text: "Esc: close | ↑↓: navigate | Space: toggle",
|
|
159
|
+
color: "dim",
|
|
160
|
+
},
|
|
161
|
+
overlay: overlayOptions,
|
|
162
|
+
},
|
|
163
|
+
theme,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
render(width: number) {
|
|
168
|
+
return modal.renderModal(width).lines;
|
|
169
|
+
},
|
|
170
|
+
invalidate() {
|
|
171
|
+
modal.invalidate();
|
|
172
|
+
},
|
|
173
|
+
handleInput(data: string) {
|
|
174
|
+
modal.handleInput(data);
|
|
175
|
+
tui.requestRender();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
{ overlay: true, overlayOptions },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function handleArgs(args: string, ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): boolean {
|
|
184
|
+
const normalized = args.trim().toLowerCase();
|
|
185
|
+
if (!normalized) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (normalized === "show") {
|
|
190
|
+
ctx.ui.notify(`permission-system: ${summarizeConfig(controller.getConfig())}`, "info");
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (normalized === "path") {
|
|
195
|
+
ctx.ui.notify(`permission-system config: ${controller.getConfigPath()}`, "info");
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (normalized === "reset") {
|
|
200
|
+
controller.setConfig(cloneDefaultConfig(), ctx);
|
|
201
|
+
ctx.ui.notify("Permission system settings reset to defaults.", "info");
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (normalized === "help") {
|
|
206
|
+
ctx.ui.notify(USAGE_TEXT, "info");
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ctx.ui.notify(USAGE_TEXT, "warning");
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function registerPermissionSystemCommand(pi: ExtensionAPI, controller: PermissionSystemConfigController): void {
|
|
215
|
+
pi.registerCommand("permission-system", {
|
|
216
|
+
description: "Configure pi-permission-system logging and yolo-mode behavior",
|
|
217
|
+
getArgumentCompletions,
|
|
218
|
+
handler: async (args, ctx) => {
|
|
219
|
+
if (handleArgs(args, ctx, controller)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!ctx.hasUI) {
|
|
224
|
+
ctx.ui.notify("/permission-system requires interactive TUI mode.", "warning");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await openSettingsModal(ctx, controller);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|