pi-permission-system 0.4.8 → 0.5.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 CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-05-22
11
+
12
+ ### Added
13
+ - Added `logPlaintextBashCommands` as an opt-in extension setting and settings-modal control; review logs redact raw bash command strings by default while retaining safe metadata.
14
+ - Added structured edit summaries for `replace`, `append`, `prepend`, `replace_text`, and `delete` payloads in permission prompts.
15
+ - Added skill-read enforcement for direct `read` calls under global and project Pi skill directories by inferring the skill name from the requested path when prompt entries are absent.
16
+
17
+ ### Changed
18
+ - Hardened subagent permission forwarding with watched request directories, async request/response reads, and nonce-bound responses.
19
+ - Updated package metadata and lockfile version to `0.5.0` and migrated Pi peer dependency metadata to the `@earendil-works` scope.
20
+
21
+ ## [0.4.9] - 2026-05-05
22
+
23
+ ### Changed
24
+ - Permission-system config parsing now accepts JSONC comments and trailing commas across both policy files and the extension config, and invalid config warnings are emitted once per session as a compact single-line message with explicit fallback behavior.
25
+
26
+ ### Fixed
27
+ - Stopped repeating identical invalid-config warnings in the TUI when the same broken permission policy is re-evaluated during the same session or reload cycle (thanks to @jviel-beta for issue #20).
28
+
10
29
  ## [0.4.8] - 2026-05-04
11
30
 
12
31
  ### Added
package/README.md CHANGED
@@ -95,11 +95,11 @@ If you are coming from OpenCode, you usually do **not** need to rewrite your who
95
95
  - **Runtime Enforcement** — Blocks/asks/allows at tool call time with UI confirmation dialogs and readable approval summaries
96
96
  - **Bash Command Control** — Wildcard pattern matching for granular bash command permissions
97
97
  - **MCP Access Control** — Server and tool-level permissions for MCP operations
98
- - **Skill Protection** — Controls which skills can be loaded or read from disk, including multi-block prompt sanitization
98
+ - **Skill Protection** — Controls which skills can be loaded or read from disk, including multi-block prompt sanitization and path-inferred reads under Pi skill directories
99
99
  - **Per-Agent Overrides** — Agent-specific permission policies via YAML frontmatter
100
100
  - **Subagent Permission Forwarding** — Forwards `ask` confirmations from non-UI subagents back to the main interactive session
101
101
  - **Runtime YOLO Control** — Lets users toggle yolo mode from the settings modal and lets other extensions toggle it through the runtime API
102
- - **File-Based Review Logging** — Writes permission request/denial review entries to a file by default for later auditing
102
+ - **File-Based Review Logging** — Writes permission request/denial review entries to a file by default, with raw bash command text redacted unless `logPlaintextBashCommands` is enabled
103
103
  - **Optional Debug Logging** — Keeps verbose extension diagnostics in a separate file when enabled in `config.json`
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
@@ -167,7 +167,7 @@ The extension integrates via Pi's lifecycle hooks:
167
167
  |----------------------|-------------------------------------------------------------------------------------------|
168
168
  | `before_agent_start` | Filters active tools, removes denied tool entries from the system prompt, and hides denied skills |
169
169
  | `tool_call` | Enforces permissions for every tool invocation |
170
- | `input` | Intercepts `/skill:<name>` requests and enforces skill policy |
170
+ | `input` | Tracks explicit `/skill:<name>` requests so user-invoked skill loads can proceed while agent-initiated reads remain policy-gated |
171
171
 
172
172
  **Additional behaviors:**
173
173
  - Unknown/unregistered tools are blocked before permission checks (prevents bypass attempts)
@@ -175,8 +175,10 @@ The extension integrates via Pi's lifecycle hooks:
175
175
  - 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
176
  - When a subagent hits an `ask` permission without direct UI access, the request can be forwarded to the main interactive session for confirmation
177
177
  - Generic extension-tool approval prompts include a bounded input preview; built-in file tools use concise human-readable summaries instead of raw multiline JSON
178
- - Permission review logs include requested bash command text plus redacted prompt/input metadata for auditing without writing raw prompts or generic tool payload previews
178
+ - Permission review logs include redacted prompt/input metadata for auditing; raw bash command text is omitted unless `logPlaintextBashCommands` is enabled.
179
179
  - 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
+ - `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
+ - Structured edit payloads are summarized by operation and line count in prompts so permission decisions do not require raw multiline JSON.
180
182
 
181
183
  ## Configuration
182
184
 
@@ -192,6 +194,7 @@ The extension creates this file automatically when it is missing. It controls ex
192
194
  {
193
195
  "debugLog": false,
194
196
  "permissionReviewLog": true,
197
+ "logPlaintextBashCommands": false,
195
198
  "yoloMode": false
196
199
  }
197
200
  ```
@@ -200,6 +203,7 @@ The extension creates this file automatically when it is missing. It controls ex
200
203
  |-----|---------|-------------|
201
204
  | `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
202
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 |
203
207
  | `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled |
204
208
 
205
209
  Both logs write to files only under the extension directory by default. Set `PI_PERMISSION_SYSTEM_LOGS_DIR` to redirect review/debug logs to a specific directory. No debug output is printed to the terminal.
@@ -246,7 +250,7 @@ The policy file is a JSON object with these sections:
246
250
  | `skills` | Skill name pattern permissions |
247
251
  | `special` | Reserved permission checks such as external directory access |
248
252
 
249
- > **Note:** Trailing commas are **not** supported. If parsing fails, the extension falls back to `ask` for all categories.
253
+ > **Note:** JSONC comments and trailing commas are supported. If parsing still fails, the extension falls back to `ask` for all categories and shows a warning in the TUI when available.
250
254
 
251
255
  ### Global Per-Agent Overrides
252
256
 
@@ -430,6 +434,7 @@ Skill name patterns use `*` wildcards:
430
434
  }
431
435
  ```
432
436
 
437
+ Skill-read enforcement also applies when a `read` path is under the global Pi skills directory (`~/.pi/agent/skills` or `PI_CODING_AGENT_DIR/skills`) or the active project's `.pi/agent/skills` directory. In that case the skill name is inferred from the path and checked against `skills` policy even if no active prompt block listed the skill; direct user `/skill:<name>` requests are allowed to proceed for that requested skill.
433
438
  ### `special`
434
439
 
435
440
  Reserved permission checks:
@@ -637,7 +642,7 @@ npx --yes ajv-cli@5 validate \
637
642
 
638
643
  | Problem | Cause | Solution |
639
644
  |---------|-------|----------|
640
- | Config not applied (everything asks) | File not found or parse error | Verify the global Pi policy file (default: `~/.pi/agent/pi-permissions.jsonc`, respects `PI_CODING_AGENT_DIR`); check for trailing commas |
645
+ | Config not applied (everything asks) | File not found or parse error | Verify the global Pi policy file (default: `~/.pi/agent/pi-permissions.jsonc`, respects `PI_CODING_AGENT_DIR`); check the TUI warning for the parse location/message |
641
646
  | Per-agent override not applied | Frontmatter parsing issue | Ensure `---` delimiters at file top; keep YAML simple; restart session |
642
647
  | Tool blocked as unregistered | Unknown tool name | Use a registered `mcp` tool for server tools: `{ "tool": "server:tool" }` |
643
648
  | `/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. |
package/package.json CHANGED
@@ -1,68 +1,71 @@
1
- {
2
- "name": "pi-permission-system",
3
- "version": "0.4.8",
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
- "@mariozechner/pi-ai": "^0.72.0",
64
- "@mariozechner/pi-coding-agent": "^0.72.0",
65
- "@mariozechner/pi-tui": "^0.72.0",
66
- "@sinclair/typebox": "^0.34.49"
67
- }
68
- }
1
+ {
2
+ "name": "pi-permission-system",
3
+ "version": "0.5.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.75.4",
65
+ "@earendil-works/pi-coding-agent": "^0.75.4",
66
+ "@earendil-works/pi-tui": "^0.75.4"
67
+ },
68
+ "dependencies": {
69
+ "jsonc-parser": "^3.3.1"
70
+ }
71
+ }
@@ -1,5 +1,5 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import type { SettingItem } from "@mariozechner/pi-tui";
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { SettingItem } from "@earendil-works/pi-tui";
3
3
 
4
4
  import type { PermissionSystemExtensionConfig } from "./extension-config.js";
5
5
  import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.js";
@@ -36,6 +36,13 @@ function buildSettingItems(config: PermissionSystemExtensionConfig): SettingItem
36
36
  currentValue: toOnOff(config.permissionReviewLog),
37
37
  values: ON_OFF,
38
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
+ },
39
46
  {
40
47
  id: "debugLog",
41
48
  label: "Debug logging",
@@ -56,6 +63,8 @@ function applySetting(
56
63
  return { ...config, yoloMode: value === "on" };
57
64
  case "permissionReviewLog":
58
65
  return { ...config, permissionReviewLog: value === "on" };
66
+ case "logPlaintextBashCommands":
67
+ return { ...config, logPlaintextBashCommands: value === "on" };
59
68
  case "debugLog":
60
69
  return { ...config, debugLog: value === "on" };
61
70
  default:
@@ -66,6 +75,7 @@ function applySetting(
66
75
  function syncSettingValues(settingsList: SettingValueSyncTarget, config: PermissionSystemExtensionConfig): void {
67
76
  settingsList.updateValue("yoloMode", toOnOff(config.yoloMode));
68
77
  settingsList.updateValue("permissionReviewLog", toOnOff(config.permissionReviewLog));
78
+ settingsList.updateValue("logPlaintextBashCommands", toOnOff(config.logPlaintextBashCommands));
69
79
  settingsList.updateValue("debugLog", toOnOff(config.debugLog));
70
80
  }
71
81
 
@@ -1,164 +1,170 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- import { toRecord } from "./common.js";
6
-
7
- export const EXTENSION_ID = "pi-permission-system";
8
-
9
- export interface PermissionSystemExtensionConfig {
10
- debugLog: boolean;
11
- permissionReviewLog: boolean;
12
- yoloMode: boolean;
13
- }
14
-
15
- export interface PermissionSystemConfigLoadResult {
16
- config: PermissionSystemExtensionConfig;
17
- created: boolean;
18
- warning?: string;
19
- }
20
-
21
- export interface PermissionSystemConfigSaveResult {
22
- success: boolean;
23
- error?: string;
24
- }
25
-
26
- export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
27
- debugLog: false,
28
- permissionReviewLog: true,
29
- yoloMode: false,
30
- };
31
-
32
- export function resolveExtensionRoot(moduleUrl = import.meta.url): string {
33
- return join(dirname(fileURLToPath(moduleUrl)), "..");
34
- }
35
-
36
- export const EXTENSION_ROOT = resolveExtensionRoot();
37
- export const CONFIG_PATH = join(EXTENSION_ROOT, "config.json");
38
- export const LOGS_DIR = join(EXTENSION_ROOT, "logs");
39
- export const DEBUG_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-debug.jsonl`);
40
- export const PERMISSION_REVIEW_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-permission-review.jsonl`);
41
- export const CONFIG_PATH_ENV_KEY = "PI_PERMISSION_SYSTEM_CONFIG_PATH";
42
- export const LOGS_DIR_ENV_KEY = "PI_PERMISSION_SYSTEM_LOGS_DIR";
43
-
44
- export function getPermissionSystemConfigPath(configPath?: string): string {
45
- const overridePath = process.env[CONFIG_PATH_ENV_KEY]?.trim();
46
- return configPath || overridePath || CONFIG_PATH;
47
- }
48
-
49
- export function getPermissionSystemLogsDir(logsDir?: string): string {
50
- const overrideDir = process.env[LOGS_DIR_ENV_KEY]?.trim();
51
- return logsDir || overrideDir || LOGS_DIR;
52
- }
53
-
54
- export function getPermissionSystemDebugLogPath(logsDir = getPermissionSystemLogsDir()): string {
55
- return join(logsDir, `${EXTENSION_ID}-debug.jsonl`);
56
- }
57
-
58
- export function getPermissionSystemReviewLogPath(logsDir = getPermissionSystemLogsDir()): string {
59
- return join(logsDir, `${EXTENSION_ID}-permission-review.jsonl`);
60
- }
61
-
62
- export function cloneDefaultConfig(): PermissionSystemExtensionConfig {
63
- return {
64
- debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
65
- permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
66
- yoloMode: DEFAULT_EXTENSION_CONFIG.yoloMode,
67
- };
68
- }
69
-
70
- function createDefaultConfigContent(): string {
71
- return `${JSON.stringify(DEFAULT_EXTENSION_CONFIG, null, 2)}\n`;
72
- }
73
-
74
- export function normalizePermissionSystemConfig(raw: unknown): PermissionSystemExtensionConfig {
75
- const record = toRecord(raw);
76
- return {
77
- debugLog: record.debugLog === true,
78
- permissionReviewLog: record.permissionReviewLog !== false,
79
- yoloMode: record.yoloMode === true,
80
- };
81
- }
82
-
83
- function ensureConfigDirectory(configPath: string): void {
84
- mkdirSync(dirname(configPath), { recursive: true });
85
- }
86
-
87
- export function ensurePermissionSystemConfig(configPath = getPermissionSystemConfigPath()): { created: boolean; warning?: string } {
88
- if (existsSync(configPath)) {
89
- return { created: false };
90
- }
91
-
92
- try {
93
- ensureConfigDirectory(configPath);
94
- writeFileSync(configPath, createDefaultConfigContent(), "utf-8");
95
- return { created: true };
96
- } catch (error) {
97
- const message = error instanceof Error ? error.message : String(error);
98
- return {
99
- created: false,
100
- warning: `Failed to initialize permission-system config at '${configPath}': ${message}`,
101
- };
102
- }
103
- }
104
-
105
- export function loadPermissionSystemConfig(configPath = getPermissionSystemConfigPath()): PermissionSystemConfigLoadResult {
106
- const ensureResult = ensurePermissionSystemConfig(configPath);
107
-
108
- try {
109
- const raw = readFileSync(configPath, "utf-8");
110
- const parsed = JSON.parse(raw) as unknown;
111
- const config = normalizePermissionSystemConfig(parsed);
112
- return {
113
- config,
114
- created: ensureResult.created,
115
- warning: ensureResult.warning,
116
- };
117
- } catch (error) {
118
- const message = error instanceof Error ? error.message : String(error);
119
- return {
120
- config: cloneDefaultConfig(),
121
- created: ensureResult.created,
122
- warning: ensureResult.warning ?? `Failed to read permission-system config at '${configPath}': ${message}`,
123
- };
124
- }
125
- }
126
-
127
- export function savePermissionSystemConfig(
128
- config: PermissionSystemExtensionConfig,
129
- configPath = getPermissionSystemConfigPath(),
130
- ): PermissionSystemConfigSaveResult {
131
- const normalized = normalizePermissionSystemConfig(config);
132
- const tmpPath = `${configPath}.tmp`;
133
-
134
- try {
135
- ensureConfigDirectory(configPath);
136
- writeFileSync(tmpPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
137
- renameSync(tmpPath, configPath);
138
- return { success: true };
139
- } catch (error) {
140
- try {
141
- if (existsSync(tmpPath)) {
142
- unlinkSync(tmpPath);
143
- }
144
- } catch {
145
- // Ignore cleanup failures.
146
- }
147
-
148
- const message = error instanceof Error ? error.message : String(error);
149
- return {
150
- success: false,
151
- error: `Failed to save permission-system config at '${configPath}': ${message}`,
152
- };
153
- }
154
- }
155
-
156
- export function ensurePermissionSystemLogsDirectory(logsDir = getPermissionSystemLogsDir()): string | undefined {
157
- try {
158
- mkdirSync(logsDir, { recursive: true });
159
- return undefined;
160
- } catch (error) {
161
- const message = error instanceof Error ? error.message : String(error);
162
- return `Failed to create permission-system log directory '${logsDir}': ${message}`;
163
- }
164
- }
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { toRecord } from "./common.js";
6
+ import { formatJsoncConfigLoadWarning, parseJsoncConfig } from "./jsonc-config.js";
7
+
8
+ export const EXTENSION_ID = "pi-permission-system";
9
+
10
+ export interface PermissionSystemExtensionConfig {
11
+ debugLog: boolean;
12
+ permissionReviewLog: boolean;
13
+ logPlaintextBashCommands: boolean;
14
+ yoloMode: boolean;
15
+ }
16
+
17
+ export interface PermissionSystemConfigLoadResult {
18
+ config: PermissionSystemExtensionConfig;
19
+ created: boolean;
20
+ warning?: string;
21
+ }
22
+
23
+ export interface PermissionSystemConfigSaveResult {
24
+ success: boolean;
25
+ error?: string;
26
+ }
27
+
28
+ export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
29
+ debugLog: false,
30
+ permissionReviewLog: true,
31
+ logPlaintextBashCommands: false,
32
+ yoloMode: false,
33
+ };
34
+
35
+ export function resolveExtensionRoot(moduleUrl = import.meta.url): string {
36
+ return join(dirname(fileURLToPath(moduleUrl)), "..");
37
+ }
38
+
39
+ export const EXTENSION_ROOT = resolveExtensionRoot();
40
+ export const CONFIG_PATH = join(EXTENSION_ROOT, "config.json");
41
+ export const LOGS_DIR = join(EXTENSION_ROOT, "logs");
42
+ export const DEBUG_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-debug.jsonl`);
43
+ export const PERMISSION_REVIEW_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-permission-review.jsonl`);
44
+ export const CONFIG_PATH_ENV_KEY = "PI_PERMISSION_SYSTEM_CONFIG_PATH";
45
+ export const LOGS_DIR_ENV_KEY = "PI_PERMISSION_SYSTEM_LOGS_DIR";
46
+
47
+ export function getPermissionSystemConfigPath(configPath?: string): string {
48
+ const overridePath = process.env[CONFIG_PATH_ENV_KEY]?.trim();
49
+ return configPath || overridePath || CONFIG_PATH;
50
+ }
51
+
52
+ export function getPermissionSystemLogsDir(logsDir?: string): string {
53
+ const overrideDir = process.env[LOGS_DIR_ENV_KEY]?.trim();
54
+ return logsDir || overrideDir || LOGS_DIR;
55
+ }
56
+
57
+ export function getPermissionSystemDebugLogPath(logsDir = getPermissionSystemLogsDir()): string {
58
+ return join(logsDir, `${EXTENSION_ID}-debug.jsonl`);
59
+ }
60
+
61
+ export function getPermissionSystemReviewLogPath(logsDir = getPermissionSystemLogsDir()): string {
62
+ return join(logsDir, `${EXTENSION_ID}-permission-review.jsonl`);
63
+ }
64
+
65
+ export function cloneDefaultConfig(): PermissionSystemExtensionConfig {
66
+ return {
67
+ debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
68
+ permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
69
+ logPlaintextBashCommands: DEFAULT_EXTENSION_CONFIG.logPlaintextBashCommands,
70
+ yoloMode: DEFAULT_EXTENSION_CONFIG.yoloMode,
71
+ };
72
+ }
73
+
74
+ function createDefaultConfigContent(): string {
75
+ return `${JSON.stringify(DEFAULT_EXTENSION_CONFIG, null, 2)}\n`;
76
+ }
77
+
78
+ export function normalizePermissionSystemConfig(raw: unknown): PermissionSystemExtensionConfig {
79
+ const record = toRecord(raw);
80
+ return {
81
+ debugLog: record.debugLog === true,
82
+ permissionReviewLog: record.permissionReviewLog !== false,
83
+ logPlaintextBashCommands: record.logPlaintextBashCommands === true,
84
+ yoloMode: record.yoloMode === true,
85
+ };
86
+ }
87
+
88
+ function ensureConfigDirectory(configPath: string): void {
89
+ mkdirSync(dirname(configPath), { recursive: true });
90
+ }
91
+
92
+ export function ensurePermissionSystemConfig(configPath = getPermissionSystemConfigPath()): { created: boolean; warning?: string } {
93
+ if (existsSync(configPath)) {
94
+ return { created: false };
95
+ }
96
+
97
+ try {
98
+ ensureConfigDirectory(configPath);
99
+ writeFileSync(configPath, createDefaultConfigContent(), "utf-8");
100
+ return { created: true };
101
+ } catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ return {
104
+ created: false,
105
+ warning: `Failed to initialize permission-system config at '${configPath}': ${message}`,
106
+ };
107
+ }
108
+ }
109
+
110
+ export function loadPermissionSystemConfig(configPath = getPermissionSystemConfigPath()): PermissionSystemConfigLoadResult {
111
+ const ensureResult = ensurePermissionSystemConfig(configPath);
112
+
113
+ try {
114
+ const raw = readFileSync(configPath, "utf-8");
115
+ const parsed = parseJsoncConfig(raw, configPath, "permission-system config");
116
+ const config = normalizePermissionSystemConfig(parsed);
117
+ return {
118
+ config,
119
+ created: ensureResult.created,
120
+ warning: ensureResult.warning,
121
+ };
122
+ } catch (error) {
123
+ return {
124
+ config: cloneDefaultConfig(),
125
+ created: ensureResult.created,
126
+ warning: ensureResult.warning
127
+ ?? formatJsoncConfigLoadWarning(configPath, error, "permission-system config", "using default extension config")
128
+ ?? undefined,
129
+ };
130
+ }
131
+ }
132
+
133
+ export function savePermissionSystemConfig(
134
+ config: PermissionSystemExtensionConfig,
135
+ configPath = getPermissionSystemConfigPath(),
136
+ ): PermissionSystemConfigSaveResult {
137
+ const normalized = normalizePermissionSystemConfig(config);
138
+ const tmpPath = `${configPath}.tmp`;
139
+
140
+ try {
141
+ ensureConfigDirectory(configPath);
142
+ writeFileSync(tmpPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
143
+ renameSync(tmpPath, configPath);
144
+ return { success: true };
145
+ } catch (error) {
146
+ try {
147
+ if (existsSync(tmpPath)) {
148
+ unlinkSync(tmpPath);
149
+ }
150
+ } catch {
151
+ // Ignore cleanup failures.
152
+ }
153
+
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ return {
156
+ success: false,
157
+ error: `Failed to save permission-system config at '${configPath}': ${message}`,
158
+ };
159
+ }
160
+ }
161
+
162
+ export function ensurePermissionSystemLogsDirectory(logsDir = getPermissionSystemLogsDir()): string | undefined {
163
+ try {
164
+ mkdirSync(logsDir, { recursive: true });
165
+ return undefined;
166
+ } catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ return `Failed to create permission-system log directory '${logsDir}': ${message}`;
169
+ }
170
+ }