@gotgenes/pi-permission-system 3.8.0 → 3.10.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
@@ -5,6 +5,43 @@ 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
+ ## [3.10.0](https://github.com/gotgenes/pi-permission-system/compare/v3.9.0...v3.10.0) (2026-05-04)
9
+
10
+
11
+ ### Features
12
+
13
+ * migrate tool_call external_directory to SessionRules ([42c2bd9](https://github.com/gotgenes/pi-permission-system/commit/42c2bd91dbc35c6e4343133fb907f43a6a2550bf))
14
+ * remove SessionApprovalCache ([9d5a5be](https://github.com/gotgenes/pi-permission-system/commit/9d5a5be8251491a66b2826183ca22cbd5a232374))
15
+ * replace SessionApprovalCache with SessionRules in runtime ([4cec9c5](https://github.com/gotgenes/pi-permission-system/commit/4cec9c553779afa8a5fb62bf2ffbd35a43af3e23))
16
+
17
+
18
+ ### Documentation
19
+
20
+ * plan replace SessionApprovalCache with session Ruleset ([#57](https://github.com/gotgenes/pi-permission-system/issues/57)) ([ed1cefe](https://github.com/gotgenes/pi-permission-system/commit/ed1cefec2fd81542084460eb02cd3706b7093c07))
21
+ * **retro:** add retro notes for issue [#56](https://github.com/gotgenes/pi-permission-system/issues/56) ([f97f65c](https://github.com/gotgenes/pi-permission-system/commit/f97f65c448bd907866042bf9804378f441ae7c36))
22
+ * update session approval references ([#57](https://github.com/gotgenes/pi-permission-system/issues/57)) ([40e5e89](https://github.com/gotgenes/pi-permission-system/commit/40e5e89bf29b404b36fedaa48c896391d30574f6))
23
+
24
+ ## [3.9.0](https://github.com/gotgenes/pi-permission-system/compare/v3.8.0...v3.9.0) (2026-05-03)
25
+
26
+
27
+ ### Features
28
+
29
+ * add normalizeConfig and defaults modules ([84f9c3e](https://github.com/gotgenes/pi-permission-system/commit/84f9c3ef1c665e8d55b694ecf8bbec2dff41b093))
30
+ * evaluate() accepts optional defaultAction parameter ([69dde81](https://github.com/gotgenes/pi-permission-system/commit/69dde81d05722065307b04102a8af6935df0e17c))
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * remove unused imports flagged by biome ([62704a3](https://github.com/gotgenes/pi-permission-system/commit/62704a3b5c833af74a3bd27942ffc4247c92c12c))
36
+
37
+
38
+ ### Documentation
39
+
40
+ * mark [#42](https://github.com/gotgenes/pi-permission-system/issues/42) and [#43](https://github.com/gotgenes/pi-permission-system/issues/43) complete in target architecture ([04430f2](https://github.com/gotgenes/pi-permission-system/commit/04430f2ea000e9607541262a71ad2be633dc7bb6))
41
+ * mark [#56](https://github.com/gotgenes/pi-permission-system/issues/56) complete in target architecture ([2fe95c5](https://github.com/gotgenes/pi-permission-system/commit/2fe95c577ec97e78c1390db91020294ba25662e0))
42
+ * plan unify Rule type and normalize config into flat Ruleset ([#56](https://github.com/gotgenes/pi-permission-system/issues/56)) ([61e8c48](https://github.com/gotgenes/pi-permission-system/commit/61e8c4800f39a173b69f2773e8b2f09fa9c7318b))
43
+ * **retro:** add retro notes for issue [#43](https://github.com/gotgenes/pi-permission-system/issues/43) ([bd6aea6](https://github.com/gotgenes/pi-permission-system/commit/bd6aea6ed2e1dfdad1ef610f9abb8319d87460cd))
44
+
8
45
  ## [3.8.0](https://github.com/gotgenes/pi-permission-system/compare/v3.7.0...v3.8.0) (2026-05-03)
9
46
 
10
47
 
package/README.md CHANGED
@@ -529,7 +529,7 @@ This makes it easy to verify which files the extension actually loaded:
529
529
  index.ts → Root Pi entrypoint shim
530
530
  src/
531
531
  ├── index.ts → Extension bootstrap, permission checks, readable prompts, review logging, reload handling, and subagent forwarding
532
- ├── session-approval-cache.ts → Ephemeral session-scoped approval cache for external-directory access
532
+ ├── session-rules.ts → Ephemeral session-scoped approval rules (Ruleset-based, external-directory access)
533
533
  ├── config-loader.ts → Unified config loader, merger, and legacy-path detection
534
534
  ├── config-paths.ts → Path derivation for global, project, and legacy config locations
535
535
  ├── config-reporter.ts → Resolved config path reporting for diagnostic logs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "3.8.0",
3
+ "version": "3.10.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,60 @@
1
+ import type { PermissionDefaultPolicy, PermissionState } from "./types";
2
+
3
+ /** Hardcoded fallback — every surface defaults to "ask" (least privilege). */
4
+ export const DEFAULT_POLICY: PermissionDefaultPolicy = {
5
+ tools: "ask",
6
+ bash: "ask",
7
+ mcp: "ask",
8
+ skills: "ask",
9
+ special: "ask",
10
+ };
11
+
12
+ /**
13
+ * Map a surface name used in evaluate() to the corresponding
14
+ * defaultPolicy key. Surfaces not listed here fall through to
15
+ * either "tools" or "special" via getSurfaceDefault().
16
+ */
17
+ const SURFACE_TO_DEFAULT_KEY: Record<string, keyof PermissionDefaultPolicy> = {
18
+ bash: "bash",
19
+ mcp: "mcp",
20
+ skill: "skills",
21
+ };
22
+
23
+ /**
24
+ * Resolve the default action for a surface, consulting merged defaults.
25
+ *
26
+ * - "bash", "mcp", "skill" → dedicated defaultPolicy key
27
+ * - special-key surfaces (e.g. "external_directory") → defaults.special
28
+ * - everything else (tool-name surfaces) → defaults.tools
29
+ */
30
+ export function getSurfaceDefault(
31
+ surface: string,
32
+ defaults: PermissionDefaultPolicy,
33
+ specialKeys: ReadonlySet<string>,
34
+ ): PermissionState {
35
+ const key = SURFACE_TO_DEFAULT_KEY[surface];
36
+ if (key) return defaults[key];
37
+ if (specialKeys.has(surface)) return defaults.special;
38
+ return defaults.tools;
39
+ }
40
+
41
+ /**
42
+ * Merge zero or more partial default policies on top of DEFAULT_POLICY.
43
+ * Later partials override earlier ones (shallow spread per key).
44
+ */
45
+ export function mergeDefaults(
46
+ ...partials: ReadonlyArray<Partial<PermissionDefaultPolicy> | undefined>
47
+ ): PermissionDefaultPolicy {
48
+ const merged: PermissionDefaultPolicy = { ...DEFAULT_POLICY };
49
+
50
+ for (const partial of partials) {
51
+ if (!partial) continue;
52
+ if (partial.tools) merged.tools = partial.tools;
53
+ if (partial.bash) merged.bash = partial.bash;
54
+ if (partial.mcp) merged.mcp = partial.mcp;
55
+ if (partial.skills) merged.skills = partial.skills;
56
+ if (partial.special) merged.special = partial.special;
57
+ }
58
+
59
+ return merged;
60
+ }
@@ -76,6 +76,6 @@ export async function handleSessionShutdown(deps: HandlerDeps): Promise<void> {
76
76
  deps.runtime.activeSkillEntries = [];
77
77
  deps.runtime.lastActiveToolsCacheKey = null;
78
78
  deps.runtime.lastPromptStateCacheKey = null;
79
- deps.runtime.sessionApprovalCache.clear();
79
+ deps.runtime.sessionRules.clear();
80
80
  deps.stopForwardedPermissionPolling();
81
81
  }
@@ -29,7 +29,8 @@ import {
29
29
  formatUnknownToolReason,
30
30
  formatUserDeniedReason,
31
31
  } from "../permission-prompts";
32
- import { deriveApprovalPrefix } from "../session-approval-cache";
32
+ import { evaluate } from "../rule";
33
+ import { deriveApprovalPattern } from "../session-rules";
33
34
  import { findSkillPathMatch } from "../skill-prompt-sanitizer";
34
35
  import { getPermissionLogContext } from "../tool-input-preview";
35
36
  import {
@@ -169,12 +170,15 @@ export async function handleToolCall(
169
170
  externalDirectoryPath,
170
171
  ctx.cwd,
171
172
  );
172
- const sessionPrefix = deps.runtime.sessionApprovalCache.findMatchingPrefix(
173
+ const sessionRuleset = deps.runtime.sessionRules.getRuleset();
174
+ const sessionMatch = evaluate(
173
175
  "external_directory",
174
176
  normalizedExtPath,
177
+ sessionRuleset,
175
178
  );
179
+ const isSessionApproved = sessionRuleset.includes(sessionMatch);
176
180
 
177
- if (sessionPrefix) {
181
+ if (isSessionApproved) {
178
182
  deps.runtime.writeReviewLog("permission_request.session_approved", {
179
183
  source: "tool_call",
180
184
  toolCallId: (event as { toolCallId: string }).toolCallId,
@@ -182,7 +186,7 @@ export async function handleToolCall(
182
186
  agentName,
183
187
  path: externalDirectoryPath,
184
188
  resolution: "session_approved",
185
- sessionApprovalPrefix: sessionPrefix,
189
+ sessionApprovalPattern: sessionMatch.pattern,
186
190
  });
187
191
  // Fall through to normal permission check
188
192
  } else {
@@ -245,8 +249,8 @@ export async function handleToolCall(
245
249
  }
246
250
 
247
251
  if (extDirDecision?.state === "approved_for_session") {
248
- const prefix = deriveApprovalPrefix(normalizedExtPath);
249
- deps.runtime.sessionApprovalCache.approve("external_directory", prefix);
252
+ const pattern = deriveApprovalPattern(normalizedExtPath);
253
+ deps.runtime.sessionRules.approve("external_directory", pattern);
250
254
  }
251
255
  }
252
256
  // Fall through to normal permission check
@@ -261,9 +265,12 @@ export async function handleToolCall(
261
265
  ctx.cwd,
262
266
  );
263
267
  if (externalPaths.length > 0) {
268
+ const bashSessionRuleset = deps.runtime.sessionRules.getRuleset();
264
269
  const uncoveredPaths = externalPaths.filter(
265
270
  (p) =>
266
- !deps.runtime.sessionApprovalCache.has("external_directory", p),
271
+ !bashSessionRuleset.includes(
272
+ evaluate("external_directory", p, bashSessionRuleset),
273
+ ),
267
274
  );
268
275
 
269
276
  if (uncoveredPaths.length === 0) {
@@ -339,11 +346,8 @@ export async function handleToolCall(
339
346
 
340
347
  if (bashExtDecision?.state === "approved_for_session") {
341
348
  for (const extPath of uncoveredPaths) {
342
- const prefix = deriveApprovalPrefix(extPath);
343
- deps.runtime.sessionApprovalCache.approve(
344
- "external_directory",
345
- prefix,
346
- );
349
+ const pattern = deriveApprovalPattern(extPath);
350
+ deps.runtime.sessionRules.approve("external_directory", pattern);
347
351
  }
348
352
  }
349
353
  }
package/src/index.ts CHANGED
@@ -11,10 +11,7 @@ import {
11
11
  handleSessionStart,
12
12
  handleToolCall,
13
13
  } from "./handlers";
14
- import {
15
- type PermissionPromptDecision,
16
- requestPermissionDecisionFromUi,
17
- } from "./permission-dialog";
14
+ import { requestPermissionDecisionFromUi } from "./permission-dialog";
18
15
  import {
19
16
  createExtensionRuntime,
20
17
  createPermissionManagerForCwd,
@@ -0,0 +1,70 @@
1
+ import type { Rule, Ruleset } from "./rule";
2
+ import type { PermissionState } from "./types";
3
+
4
+ /**
5
+ * Subset of UnifiedPermissionConfig covering only policy fields.
6
+ * Used as the input shape for normalizeConfig().
7
+ */
8
+ export interface NormalizableConfig {
9
+ tools?: Record<string, PermissionState>;
10
+ bash?: Record<string, PermissionState>;
11
+ mcp?: Record<string, PermissionState>;
12
+ skills?: Record<string, PermissionState>;
13
+ special?: Record<string, PermissionState>;
14
+ }
15
+
16
+ /**
17
+ * Keys in the `tools` map that serve as fallback defaults for their
18
+ * respective pattern-based surfaces rather than as tool-level rules.
19
+ *
20
+ * `tools.bash` sets the bash default (fallback when no bash pattern matches).
21
+ * `tools.mcp` sets the tool-level MCP fallback.
22
+ *
23
+ * These are NOT normalized into the Ruleset — they are extracted by the
24
+ * caller and handled as separate fallbacks to preserve the semantic that
25
+ * specific bash/mcp patterns always have priority.
26
+ */
27
+ export const TOOL_SURFACE_OVERRIDE_KEYS: ReadonlySet<string> = new Set([
28
+ "bash",
29
+ "mcp",
30
+ ]);
31
+
32
+ /**
33
+ * Convert the on-disk config shape into a flat Ruleset.
34
+ *
35
+ * Ordering within a scope:
36
+ * 1. tools entries (tool-name-as-surface, pattern "*") — excluding bash/mcp
37
+ * 2. bash entries (surface "bash", pattern = command glob)
38
+ * 3. mcp entries (surface "mcp", pattern = target glob)
39
+ * 4. skills entries (surface "skill", pattern = skill glob)
40
+ * 5. special entries (surface "special", pattern = key name)
41
+ *
42
+ * `tools.bash` and `tools.mcp` are excluded — see TOOL_SURFACE_OVERRIDE_KEYS.
43
+ * `defaultPolicy` is NOT included — handled separately by the caller.
44
+ */
45
+ export function normalizeConfig(config: NormalizableConfig): Ruleset {
46
+ const rules: Rule[] = [];
47
+
48
+ for (const [name, action] of Object.entries(config.tools ?? {})) {
49
+ if (TOOL_SURFACE_OVERRIDE_KEYS.has(name)) continue;
50
+ rules.push({ surface: name, pattern: "*", action });
51
+ }
52
+
53
+ for (const [pattern, action] of Object.entries(config.bash ?? {})) {
54
+ rules.push({ surface: "bash", pattern, action });
55
+ }
56
+
57
+ for (const [pattern, action] of Object.entries(config.mcp ?? {})) {
58
+ rules.push({ surface: "mcp", pattern, action });
59
+ }
60
+
61
+ for (const [pattern, action] of Object.entries(config.skills ?? {})) {
62
+ rules.push({ surface: "skill", pattern, action });
63
+ }
64
+
65
+ for (const [name, action] of Object.entries(config.special ?? {})) {
66
+ rules.push({ surface: "special", pattern: name, action });
67
+ }
68
+
69
+ return rules;
70
+ }