@gotgenes/pi-permission-system 5.11.1 → 5.14.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
@@ -5,6 +5,85 @@ 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
+ ## [5.14.1](https://github.com/gotgenes/pi-permission-system/compare/v5.14.0...v5.14.1) (2026-05-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * show tool name instead of bare wildcard in session-approval label ([1a65c30](https://github.com/gotgenes/pi-permission-system/commit/1a65c3017f25012c3a7ced63f26d40fcecea81d3))
14
+ * surface-prefixed session-approval labels for all permission surfaces ([759da03](https://github.com/gotgenes/pi-permission-system/commit/759da03be9c0d847ec6de58e161ef2e7cbbc70b8))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * **retro:** add retro notes for issue [#122](https://github.com/gotgenes/pi-permission-system/issues/122) ([7867db2](https://github.com/gotgenes/pi-permission-system/commit/7867db22054df00e13aa6c88347238dadaa63166))
20
+
21
+ ## [5.14.0](https://github.com/gotgenes/pi-permission-system/compare/v5.13.0...v5.14.0) (2026-05-09)
22
+
23
+
24
+ ### Features
25
+
26
+ * support ? single-character wildcard in permission patterns ([#122](https://github.com/gotgenes/pi-permission-system/issues/122)) ([7b56f49](https://github.com/gotgenes/pi-permission-system/commit/7b56f4979a3479912dcc1d903f52a517587b95f6))
27
+
28
+
29
+ ### Documentation
30
+
31
+ * document ? wildcard and update OpenCode compatibility ([#122](https://github.com/gotgenes/pi-permission-system/issues/122)) ([31ace5f](https://github.com/gotgenes/pi-permission-system/commit/31ace5f36a11299750994c2b9e6084dbecc240ba))
32
+ * plan ? single-character wildcard support ([#122](https://github.com/gotgenes/pi-permission-system/issues/122)) ([a7a2963](https://github.com/gotgenes/pi-permission-system/commit/a7a296337809ee7c107f86b87b64bfeb3709467f))
33
+ * **retro:** add retro notes for issue [#1](https://github.com/gotgenes/pi-permission-system/issues/1) ([b1c66f1](https://github.com/gotgenes/pi-permission-system/commit/b1c66f18d1aed5cc95a1fccb1a9c6c0f44f5cd11))
34
+
35
+ ## [5.13.0](https://github.com/gotgenes/pi-permission-system/compare/v5.12.0...v5.13.0) (2026-05-08)
36
+
37
+
38
+ ### Features
39
+
40
+ * warn that this is a pnpm project on global npm pass-throughs ([f643149](https://github.com/gotgenes/pi-permission-system/commit/f64314981126331ec96ec6a20418440eff1738e7))
41
+
42
+
43
+ ### Bug Fixes
44
+
45
+ * pass through npm install/uninstall -g in PATH shim ([eaf4256](https://github.com/gotgenes/pi-permission-system/commit/eaf4256446b2c1ec3ecba98d11cc75b1406931af))
46
+ * prevent double-loading extension in dev via project settings ([6c39f33](https://github.com/gotgenes/pi-permission-system/commit/6c39f33890e61b5e0fde41d178aad9df93592cc9))
47
+
48
+
49
+ ### Documentation
50
+
51
+ * plan external_directory integration tests ([#1](https://github.com/gotgenes/pi-permission-system/issues/1)) ([695ffeb](https://github.com/gotgenes/pi-permission-system/commit/695ffeb6d7a648df3dd9e18a01c20ff22266857b))
52
+ * **retro:** add retro notes for double-prompt investigation ([37734a5](https://github.com/gotgenes/pi-permission-system/commit/37734a57aee972aa4c732d92eeda424dd40443ee))
53
+ * **retro:** add retro notes for issue [#123](https://github.com/gotgenes/pi-permission-system/issues/123) ([5dbea33](https://github.com/gotgenes/pi-permission-system/commit/5dbea3379963b6231a4a101c889a4594705b12b6))
54
+
55
+
56
+ ### Miscellaneous Chores
57
+
58
+ * switch ask tool from pi-ask-user to @eko24ive/pi-ask ([0087458](https://github.com/gotgenes/pi-permission-system/commit/00874585724d79fbeda1fca84b998db3bcd1a043))
59
+
60
+ ## [5.12.0](https://github.com/gotgenes/pi-permission-system/compare/v5.11.2...v5.12.0) (2026-05-08)
61
+
62
+
63
+ ### Features
64
+
65
+ * support trailing wildcard optionality ([#123](https://github.com/gotgenes/pi-permission-system/issues/123)) ([c25b0b5](https://github.com/gotgenes/pi-permission-system/commit/c25b0b5c59e5d5739a3b6c99de18444a4e7820ec))
66
+
67
+
68
+ ### Documentation
69
+
70
+ * plan trailing wildcard optionality ([#123](https://github.com/gotgenes/pi-permission-system/issues/123)) ([a6a50d2](https://github.com/gotgenes/pi-permission-system/commit/a6a50d28284bb624a3695d361b5b9f2a9861f470))
71
+ * **retro:** add retro notes for issue [#113](https://github.com/gotgenes/pi-permission-system/issues/113) ([7412740](https://github.com/gotgenes/pi-permission-system/commit/7412740c876c42cc1cd63f95ee4cb0ea0de72e68))
72
+ * update wildcard optionality docs ([#123](https://github.com/gotgenes/pi-permission-system/issues/123)) ([562adaf](https://github.com/gotgenes/pi-permission-system/commit/562adaff9726096004dc4f5214550af2dc2fc118))
73
+
74
+ ## [5.11.2](https://github.com/gotgenes/pi-permission-system/compare/v5.11.1...v5.11.2) (2026-05-08)
75
+
76
+
77
+ ### Documentation
78
+
79
+ * plan removal of legacy path defaults from logging and extension-config ([#113](https://github.com/gotgenes/pi-permission-system/issues/113)) ([27ec0d8](https://github.com/gotgenes/pi-permission-system/commit/27ec0d8668c8c006f94c544e55d10c0eb272ddab))
80
+
81
+
82
+ ### Miscellaneous Chores
83
+
84
+ * approve @google/genai build scripts in pnpm-workspace.yaml ([23b177f](https://github.com/gotgenes/pi-permission-system/commit/23b177f8c5e00d5bc9812fa826c34844cc665d7a))
85
+ * upgrade pnpm to 11.0.8 and update deps ([31eb848](https://github.com/gotgenes/pi-permission-system/commit/31eb848539ff411cb7b6f422cd0244d6c9765ac7))
86
+
8
87
  ## [5.11.1](https://github.com/gotgenes/pi-permission-system/compare/v5.11.0...v5.11.1) (2026-05-08)
9
88
 
10
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.11.1",
3
+ "version": "5.14.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -52,10 +52,10 @@
52
52
  "@earendil-works/pi-tui": "*"
53
53
  },
54
54
  "devDependencies": {
55
- "@biomejs/biome": "^2.4.13",
55
+ "@biomejs/biome": "^2.4.14",
56
56
  "@earendil-works/pi-coding-agent": "^0.74.0",
57
57
  "@earendil-works/pi-tui": "^0.74.0",
58
- "@types/node": "^25.6.0",
58
+ "@types/node": "^25.6.2",
59
59
  "markdownlint-cli2": "^0.22.1",
60
60
  "typescript": "6.0.3",
61
61
  "vitest": "^4.1.5"
@@ -1,11 +1,4 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- renameSync,
6
- unlinkSync,
7
- writeFileSync,
8
- } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
9
2
  import { dirname, join } from "node:path";
10
3
  import { fileURLToPath } from "node:url";
11
4
 
@@ -21,17 +14,6 @@ export interface PermissionSystemExtensionConfig {
21
14
  piInfrastructureReadPaths?: string[];
22
15
  }
23
16
 
24
- export interface PermissionSystemConfigLoadResult {
25
- config: PermissionSystemExtensionConfig;
26
- created: boolean;
27
- warning?: string;
28
- }
29
-
30
- export interface PermissionSystemConfigSaveResult {
31
- success: boolean;
32
- error?: string;
33
- }
34
-
35
17
  export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
36
18
  debugLog: false,
37
19
  permissionReviewLog: true,
@@ -43,25 +25,6 @@ export function resolveExtensionRoot(moduleUrl = import.meta.url): string {
43
25
  }
44
26
 
45
27
  export const EXTENSION_ROOT = resolveExtensionRoot();
46
- export const CONFIG_PATH = join(EXTENSION_ROOT, "config.json");
47
- export const LOGS_DIR = join(EXTENSION_ROOT, "logs");
48
- export const DEBUG_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-debug.jsonl`);
49
- export const PERMISSION_REVIEW_LOG_PATH = join(
50
- LOGS_DIR,
51
- `${EXTENSION_ID}-permission-review.jsonl`,
52
- );
53
-
54
- function cloneDefaultConfig(): PermissionSystemExtensionConfig {
55
- return {
56
- debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
57
- permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
58
- yoloMode: DEFAULT_EXTENSION_CONFIG.yoloMode,
59
- };
60
- }
61
-
62
- function createDefaultConfigContent(): string {
63
- return `${JSON.stringify(DEFAULT_EXTENSION_CONFIG, null, 2)}\n`;
64
- }
65
28
 
66
29
  const PERMISSION_POLICY_KEYS: ReadonlySet<string> = new Set([
67
30
  "defaultPolicy",
@@ -100,108 +63,8 @@ export function normalizePermissionSystemConfig(
100
63
  return result;
101
64
  }
102
65
 
103
- function ensureConfigDirectory(configPath: string): void {
104
- mkdirSync(dirname(configPath), { recursive: true });
105
- }
106
-
107
- export function ensurePermissionSystemConfig(configPath = CONFIG_PATH): {
108
- created: boolean;
109
- warning?: string;
110
- } {
111
- if (existsSync(configPath)) {
112
- return { created: false };
113
- }
114
-
115
- try {
116
- ensureConfigDirectory(configPath);
117
- writeFileSync(configPath, createDefaultConfigContent(), "utf-8");
118
- return { created: true };
119
- } catch (error) {
120
- const message = error instanceof Error ? error.message : String(error);
121
- return {
122
- created: false,
123
- warning: `Failed to initialize permission-system config at '${configPath}': ${message}`,
124
- };
125
- }
126
- }
127
-
128
- export function loadPermissionSystemConfig(
129
- configPath = CONFIG_PATH,
130
- ): PermissionSystemConfigLoadResult {
131
- const ensureResult = ensurePermissionSystemConfig(configPath);
132
-
133
- try {
134
- const raw = readFileSync(configPath, "utf-8");
135
- const parsed = JSON.parse(raw) as unknown;
136
- const config = normalizePermissionSystemConfig(parsed);
137
- const misplacedKeys = detectMisplacedPermissionKeys(toRecord(parsed));
138
-
139
- const warnings: string[] = [];
140
- if (ensureResult.warning) {
141
- warnings.push(ensureResult.warning);
142
- }
143
- if (misplacedKeys.length > 0) {
144
- warnings.push(
145
- `config.json contains legacy permission-rule keys that are ignored: ${misplacedKeys.join(", ")}.\n` +
146
- 'Use the flat permission format: { "permission": { "*": "ask", "read": "allow", ... } }.\n' +
147
- "See config/config.example.json for the new format.",
148
- );
149
- }
150
-
151
- return {
152
- config,
153
- created: ensureResult.created,
154
- warning: warnings.length > 0 ? warnings.join("\n") : undefined,
155
- };
156
- } catch (error) {
157
- const message = error instanceof Error ? error.message : String(error);
158
- return {
159
- config: cloneDefaultConfig(),
160
- created: ensureResult.created,
161
- warning:
162
- ensureResult.warning ??
163
- `Failed to read permission-system config at '${configPath}': ${message}`,
164
- };
165
- }
166
- }
167
-
168
- export function savePermissionSystemConfig(
169
- config: PermissionSystemExtensionConfig,
170
- configPath = CONFIG_PATH,
171
- ): PermissionSystemConfigSaveResult {
172
- const normalized = normalizePermissionSystemConfig(config);
173
- const tmpPath = `${configPath}.tmp`;
174
-
175
- try {
176
- ensureConfigDirectory(configPath);
177
- writeFileSync(tmpPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
178
- renameSync(tmpPath, configPath);
179
- return { success: true };
180
- } catch (error) {
181
- try {
182
- if (existsSync(tmpPath)) {
183
- unlinkSync(tmpPath);
184
- }
185
- } catch {
186
- // Ignore cleanup failures.
187
- }
188
-
189
- const message = error instanceof Error ? error.message : String(error);
190
- return {
191
- success: false,
192
- error: `Failed to save permission-system config at '${configPath}': ${message}`,
193
- };
194
- }
195
- }
196
-
197
- export function getPermissionSystemConfigPath(
198
- configPath = CONFIG_PATH,
199
- ): string {
200
- return configPath;
201
- }
202
-
203
66
  export function ensurePermissionSystemLogsDirectory(
204
- logsDir = LOGS_DIR,
67
+ logsDir: string,
205
68
  ): string | undefined {
206
69
  try {
207
70
  mkdirSync(logsDir, { recursive: true });
package/src/logging.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  import { appendFileSync } from "node:fs";
2
2
 
3
3
  import {
4
- DEBUG_LOG_PATH,
5
4
  EXTENSION_ID,
6
- ensurePermissionSystemLogsDirectory,
7
- LOGS_DIR,
8
- PERMISSION_REVIEW_LOG_PATH,
9
5
  type PermissionSystemExtensionConfig,
10
6
  } from "./extension-config";
11
7
 
@@ -48,19 +44,15 @@ export interface PermissionSystemLogger {
48
44
 
49
45
  interface PermissionSystemLoggerOptions {
50
46
  getConfig: () => PermissionSystemExtensionConfig;
51
- debugLogPath?: string;
52
- reviewLogPath?: string;
53
- ensureLogsDirectory?: () => string | undefined;
47
+ debugLogPath: string;
48
+ reviewLogPath: string;
49
+ ensureLogsDirectory: () => string | undefined;
54
50
  }
55
51
 
56
52
  export function createPermissionSystemLogger(
57
53
  options: PermissionSystemLoggerOptions,
58
54
  ): PermissionSystemLogger {
59
- const debugLogPath = options.debugLogPath ?? DEBUG_LOG_PATH;
60
- const reviewLogPath = options.reviewLogPath ?? PERMISSION_REVIEW_LOG_PATH;
61
- const ensureLogsDirectory =
62
- options.ensureLogsDirectory ??
63
- (() => ensurePermissionSystemLogsDirectory(LOGS_DIR));
55
+ const { debugLogPath, reviewLogPath, ensureLogsDirectory } = options;
64
56
 
65
57
  const writeLine = (
66
58
  stream: "debug" | "review",
@@ -57,8 +57,21 @@ export function suggestMcpPattern(target: string): string {
57
57
  return "*";
58
58
  }
59
59
 
60
- function buildLabel(pattern: string): string {
61
- return `Yes, allow "${pattern}" for this session`;
60
+ /** Surface-aware human-readable labels for the session-approval option. */
61
+ function buildLabel(pattern: string, surface: string): string {
62
+ switch (surface) {
63
+ case "bash":
64
+ return `Yes, allow bash "${pattern}" for this session`;
65
+ case "mcp":
66
+ return `Yes, allow mcp tool "${pattern}" for this session`;
67
+ case "skill":
68
+ return `Yes, allow skill "${pattern}" for this session`;
69
+ case "external_directory":
70
+ return `Yes, allow access to external directory "${pattern}" for this session`;
71
+ default:
72
+ // Tool surfaces (read, write, edit, grep, find, ls, extension tools)
73
+ return `Yes, allow tool "${surface}" for this session`;
74
+ }
62
75
  }
63
76
 
64
77
  /**
@@ -92,5 +105,5 @@ export function suggestSessionPattern(
92
105
  break;
93
106
  }
94
107
 
95
- return { surface, pattern, label: buildLabel(pattern) };
108
+ return { surface, pattern, label: buildLabel(pattern, surface) };
96
109
  }
@@ -21,11 +21,18 @@ export function compileWildcardPattern<TState>(
21
21
  state: TState,
22
22
  ): CompiledWildcardPattern<TState> {
23
23
  const expanded = expandHomePath(pattern);
24
- const escaped = expanded
24
+ let escaped = expanded
25
25
  .split("*")
26
- .map((part) => escapeRegExp(part))
26
+ .map((part) => escapeRegExp(part).replaceAll("\\?", "."))
27
27
  .join(".*");
28
28
 
29
+ // If the pattern ends with " *" (space + wildcard), make the trailing
30
+ // space-and-arguments portion optional so that e.g. "git *" matches both
31
+ // "git status" and bare "git". Mirrors OpenCode wildcard semantics.
32
+ if (escaped.endsWith(" .*")) {
33
+ escaped = escaped.slice(0, -3) + "( .*)?";
34
+ }
35
+
29
36
  return {
30
37
  pattern,
31
38
  state,
@@ -62,7 +69,8 @@ export function findCompiledWildcardMatch<TState>(
62
69
 
63
70
  /**
64
71
  * Test whether `value` matches `pattern` using wildcard rules.
65
- * `*` in the pattern matches any sequence of characters (including empty).
72
+ * `*` matches any sequence of characters (including empty).
73
+ * `?` matches exactly one character.
66
74
  * Used by evaluate() for rule matching.
67
75
  */
68
76
  export function wildcardMatch(pattern: string, value: string): boolean {
@@ -1,14 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { test, vi } from "vitest";
6
6
  import { registerPermissionSystemCommand } from "../src/config-modal";
7
7
  import {
8
8
  DEFAULT_EXTENSION_CONFIG,
9
- loadPermissionSystemConfig,
9
+ normalizePermissionSystemConfig,
10
10
  type PermissionSystemExtensionConfig,
11
- savePermissionSystemConfig,
12
11
  } from "../src/extension-config";
13
12
  import type { Rule } from "../src/rule";
14
13
 
@@ -136,17 +135,28 @@ test("permission-system command handlers manage config summary, persistence, and
136
135
  };
137
136
 
138
137
  try {
139
- const initialSave = savePermissionSystemConfig(config, configPath);
140
- assert.equal(initialSave.success, true);
138
+ writeFileSync(
139
+ configPath,
140
+ `${JSON.stringify(normalizePermissionSystemConfig(config), null, 2)}\n`,
141
+ "utf-8",
142
+ );
141
143
 
142
144
  const controller = {
143
145
  getConfig: () => config,
144
146
  setConfig: (next: PermissionSystemExtensionConfig) => {
145
- const normalized = loadPermissionSystemConfig(configPath).config;
146
- const saved = savePermissionSystemConfig(next, configPath);
147
- assert.equal(saved.success, true);
148
- config = loadPermissionSystemConfig(configPath).config;
149
- assert.notDeepEqual(config, normalized);
147
+ const currentConfig = normalizePermissionSystemConfig(
148
+ JSON.parse(readFileSync(configPath, "utf-8")) as unknown,
149
+ );
150
+ const normalized = normalizePermissionSystemConfig(next);
151
+ writeFileSync(
152
+ configPath,
153
+ `${JSON.stringify(normalized, null, 2)}\n`,
154
+ "utf-8",
155
+ );
156
+ config = normalizePermissionSystemConfig(
157
+ JSON.parse(readFileSync(configPath, "utf-8")) as unknown,
158
+ );
159
+ assert.notDeepEqual(config, currentConfig);
150
160
  },
151
161
  getConfigPath: () => configPath,
152
162
  };
@@ -100,6 +100,7 @@ test("config.resolved entry appears in review log via logger", () => {
100
100
  const logsDir = join(tempDir, "logs");
101
101
  mkdirSync(logsDir, { recursive: true });
102
102
  const reviewLogPath = join(logsDir, "review.jsonl");
103
+ const debugLogPath = join(logsDir, "debug.jsonl");
103
104
 
104
105
  const globalConfigPath = join(tempDir, "pi-permissions.jsonc");
105
106
  writeFileSync(globalConfigPath, "{}", "utf-8");
@@ -116,6 +117,7 @@ test("config.resolved entry appears in review log via logger", () => {
116
117
  permissionReviewLog: true,
117
118
  yoloMode: false,
118
119
  }),
120
+ debugLogPath,
119
121
  reviewLogPath,
120
122
  ensureLogsDirectory: () => undefined,
121
123
  });
@@ -1,11 +1,7 @@
1
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
1
+ import { describe, expect, it } from "vitest";
5
2
 
6
3
  import {
7
4
  detectMisplacedPermissionKeys,
8
- loadPermissionSystemConfig,
9
5
  normalizePermissionSystemConfig,
10
6
  } from "../src/extension-config";
11
7
 
@@ -78,61 +74,6 @@ describe("detectMisplacedPermissionKeys", () => {
78
74
  });
79
75
  });
80
76
 
81
- describe("loadPermissionSystemConfig", () => {
82
- let tempDir: string;
83
- let configPath: string;
84
-
85
- beforeEach(() => {
86
- tempDir = mkdtempSync(join(tmpdir(), "perm-config-test-"));
87
- configPath = join(tempDir, "config.json");
88
- });
89
-
90
- afterEach(() => {
91
- rmSync(tempDir, { recursive: true, force: true });
92
- });
93
-
94
- it("returns no warning for a clean config", () => {
95
- writeFileSync(
96
- configPath,
97
- JSON.stringify({
98
- debugLog: false,
99
- permissionReviewLog: true,
100
- yoloMode: false,
101
- }),
102
- );
103
- const result = loadPermissionSystemConfig(configPath);
104
- expect(result.warning).toBeUndefined();
105
- });
106
-
107
- it("returns a warning naming misplaced permission-rule keys", () => {
108
- writeFileSync(
109
- configPath,
110
- JSON.stringify({
111
- debugLog: true,
112
- defaultPolicy: { tools: "ask" },
113
- bash: { "git status": "allow" },
114
- }),
115
- );
116
- const result = loadPermissionSystemConfig(configPath);
117
- expect(result.warning).toBeDefined();
118
- expect(result.warning).toContain("defaultPolicy");
119
- expect(result.warning).toContain("bash");
120
- expect(result.warning).toContain("permission");
121
- });
122
-
123
- it("still returns the valid extension config fields when misplaced keys are present", () => {
124
- writeFileSync(
125
- configPath,
126
- JSON.stringify({
127
- debugLog: true,
128
- bash: { "git status": "allow" },
129
- }),
130
- );
131
- const result = loadPermissionSystemConfig(configPath);
132
- expect(result.config.debugLog).toBe(true);
133
- });
134
- });
135
-
136
77
  describe("normalizePermissionSystemConfig", () => {
137
78
  it("normalizes a valid config object", () => {
138
79
  const result = normalizePermissionSystemConfig({