@gotgenes/pi-permission-system 3.4.0 → 3.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
@@ -5,6 +5,23 @@ 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.5.0](https://github.com/gotgenes/pi-permission-system/compare/v3.4.0...v3.5.0) (2026-05-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * deprecate doom_loop special permission key ([68e70e7](https://github.com/gotgenes/pi-permission-system/commit/68e70e71b68e5a76a071ef4613da356a91080158))
14
+ * remove doom_loop from type union and config-loader ([bf2f288](https://github.com/gotgenes/pi-permission-system/commit/bf2f2886a800187337e82954e812e6d05e9bd451))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * add architecture documents for current and target permission model ([aab1ac5](https://github.com/gotgenes/pi-permission-system/commit/aab1ac50c4478d2e393c2a796bf6fcc4ec606f79))
20
+ * plan doom_loop deprecation ([#54](https://github.com/gotgenes/pi-permission-system/issues/54)) ([2e730f5](https://github.com/gotgenes/pi-permission-system/commit/2e730f52189dd2996ebbe90dd5d2b3206a45d1f6))
21
+ * plan handler extraction from piPermissionSystemExtension ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([6ecd419](https://github.com/gotgenes/pi-permission-system/commit/6ecd4190fb9a60009eb695b4998ab8a1d1419139))
22
+ * remove doom_loop from schema, example, and README ([7f422e0](https://github.com/gotgenes/pi-permission-system/commit/7f422e086f0052e0d9449dbd0122c57b923b053d))
23
+ * **retro:** add retro notes for issue [#45](https://github.com/gotgenes/pi-permission-system/issues/45) ([14c5559](https://github.com/gotgenes/pi-permission-system/commit/14c55595c5abfaa51f8ec83369452db5f457836c))
24
+
8
25
  ## [3.4.0](https://github.com/gotgenes/pi-permission-system/compare/v3.3.0...v3.4.0) (2026-05-03)
9
26
 
10
27
 
package/README.md CHANGED
@@ -137,7 +137,7 @@ The config file combines runtime knobs and permission policy in one object:
137
137
  "bash": { "git status": "allow", "git *": "ask" },
138
138
  "mcp": { "mcp_status": "allow" },
139
139
  "skills": { "*": "ask" },
140
- "special": { "doom_loop": "deny", "external_directory": "ask" }
140
+ "special": { "external_directory": "ask" }
141
141
  }
142
142
  ```
143
143
 
@@ -353,13 +353,11 @@ Reserved permission checks:
353
353
 
354
354
  | Key | Description |
355
355
  | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
356
- | `doom_loop` | Controls doom loop detection behavior |
357
356
  | `external_directory` | Enforces ask/allow/deny decisions for path-bearing tools and bash commands that reference paths outside the active working directory |
358
357
 
359
358
  ```jsonc
360
359
  {
361
360
  "special": {
362
- "doom_loop": "deny",
363
361
  "external_directory": "ask",
364
362
  },
365
363
  }
@@ -27,7 +27,6 @@
27
27
  "*": "ask"
28
28
  },
29
29
  "special": {
30
- "doom_loop": "deny",
31
30
  "external_directory": "ask"
32
31
  }
33
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -60,8 +60,8 @@
60
60
  "default": "ask"
61
61
  },
62
62
  "special": {
63
- "description": "Default permission for special checks (doom_loop, external_directory) when no explicit rule matches.",
64
- "markdownDescription": "Default permission for special checks (`doom_loop`, `external_directory`) when no explicit rule matches.",
63
+ "description": "Default permission for special checks (external_directory) when no explicit rule matches.",
64
+ "markdownDescription": "Default permission for special checks (`external_directory`) when no explicit rule matches.",
65
65
  "$ref": "#/$defs/permissionState",
66
66
  "default": "ask"
67
67
  }
@@ -122,10 +122,6 @@
122
122
  "type": "object",
123
123
  "additionalProperties": false,
124
124
  "properties": {
125
- "doom_loop": {
126
- "description": "Controls doom loop detection behavior.",
127
- "$ref": "#/$defs/permissionState"
128
- },
129
125
  "external_directory": {
130
126
  "description": "Enforces permission checks for path-bearing file tools (read, write, edit, find, grep, ls) when they target paths outside the active working directory.",
131
127
  "markdownDescription": "Enforces permission checks for path-bearing file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) when they target paths outside the active working directory.\n\nEvaluated **before** the normal tool permission check — so `tools.read: \"allow\"` can permit ordinary reads while `external_directory: \"ask\"` still requires confirmation for paths outside `cwd`.",
@@ -36,6 +36,7 @@ export interface UnifiedConfigLoadResult {
36
36
  }
37
37
 
38
38
  const DEPRECATED_SPECIAL_KEYS: ReadonlySet<string> = new Set([
39
+ "doom_loop",
39
40
  "tool_call_limit",
40
41
  ]);
41
42
 
@@ -49,7 +50,7 @@ const BUILT_IN_TOOL_PERMISSION_NAMES = new Set([
49
50
  "ls",
50
51
  ]);
51
52
 
52
- const SPECIAL_PERMISSION_KEYS = new Set(["doom_loop", "external_directory"]);
53
+ const SPECIAL_PERMISSION_KEYS = new Set(["external_directory"]);
53
54
 
54
55
  export function stripJsonComments(input: string): string {
55
56
  let output = "";
@@ -69,7 +69,6 @@ const PERMISSION_POLICY_KEYS: ReadonlySet<string> = new Set([
69
69
  "skills",
70
70
  "special",
71
71
  "external_directory",
72
- "doom_loop",
73
72
  ]);
74
73
 
75
74
  export function detectMisplacedPermissionKeys(
@@ -46,7 +46,7 @@ const BUILT_IN_TOOL_PERMISSION_NAMES = new Set([
46
46
  "find",
47
47
  "ls",
48
48
  ]);
49
- const SPECIAL_PERMISSION_KEYS = new Set(["doom_loop", "external_directory"]);
49
+ const SPECIAL_PERMISSION_KEYS = new Set(["external_directory"]);
50
50
  const MCP_BASELINE_TARGETS = new Set([
51
51
  "mcp_status",
52
52
  "mcp_list",
@@ -156,6 +156,7 @@ function getConfiguredMcpServerNamesFromPaths(
156
156
  }
157
157
 
158
158
  const DEPRECATED_SPECIAL_KEYS: ReadonlySet<string> = new Set([
159
+ "doom_loop",
159
160
  "tool_call_limit",
160
161
  ]);
161
162
 
package/src/types.ts CHANGED
@@ -15,7 +15,7 @@ export type BashPermissions = Record<string, PermissionState>;
15
15
 
16
16
  export type SkillPermissions = Record<string, PermissionState>;
17
17
 
18
- export type SpecialPermissionName = "doom_loop" | "external_directory";
18
+ export type SpecialPermissionName = "external_directory";
19
19
 
20
20
  export type SpecialPermissions = Record<string, PermissionState>;
21
21
 
@@ -133,7 +133,7 @@ describe("loadUnifiedConfig", () => {
133
133
  expect(result.config.bash).toEqual({ "git *": "ask" });
134
134
  });
135
135
 
136
- it("collects deprecated special key issues", () => {
136
+ it("collects deprecated special key issues (doom_loop and tool_call_limit)", () => {
137
137
  const configPath = join(tempDir, "config.json");
138
138
  writeFileSync(
139
139
  configPath,
@@ -143,9 +143,10 @@ describe("loadUnifiedConfig", () => {
143
143
  );
144
144
 
145
145
  const result = loadUnifiedConfig(configPath);
146
- expect(result.issues).toHaveLength(1);
147
- expect(result.issues[0]).toContain("tool_call_limit");
148
- expect(result.config.special).toEqual({ doom_loop: "deny" });
146
+ expect(result.issues).toHaveLength(2);
147
+ expect(result.issues.some((i) => i.includes("doom_loop"))).toBe(true);
148
+ expect(result.issues.some((i) => i.includes("tool_call_limit"))).toBe(true);
149
+ expect(result.config.special).toBeUndefined();
149
150
  });
150
151
  });
151
152
 
@@ -42,7 +42,6 @@ describe("detectMisplacedPermissionKeys", () => {
42
42
  skills: {},
43
43
  special: {},
44
44
  external_directory: {},
45
- doom_loop: {},
46
45
  });
47
46
  expect(result).toEqual([
48
47
  "defaultPolicy",
@@ -52,10 +51,16 @@ describe("detectMisplacedPermissionKeys", () => {
52
51
  "skills",
53
52
  "special",
54
53
  "external_directory",
55
- "doom_loop",
56
54
  ]);
57
55
  });
58
56
 
57
+ it("does not detect doom_loop as a misplaced permission key (deprecated)", () => {
58
+ const result = detectMisplacedPermissionKeys({
59
+ doom_loop: {},
60
+ });
61
+ expect(result).toEqual([]);
62
+ });
63
+
59
64
  it("ignores unknown keys that are not permission-rule keys", () => {
60
65
  const result = detectMisplacedPermissionKeys({
61
66
  debugLog: true,
@@ -2115,7 +2115,7 @@ permission:
2115
2115
  }
2116
2116
  });
2117
2117
 
2118
- test("external_directory permission is independent of doom_loop in the same special config", () => {
2118
+ test("external_directory permission is unaffected when doom_loop key is present in config (deprecated and ignored)", () => {
2119
2119
  const { manager, cleanup } = createManager({
2120
2120
  defaultPolicy: {
2121
2121
  tools: "allow",
@@ -2131,10 +2131,12 @@ test("external_directory permission is independent of doom_loop in the same spec
2131
2131
  });
2132
2132
 
2133
2133
  try {
2134
+ // doom_loop is deprecated and stripped — falls through to defaultPolicy.tools
2134
2135
  const doomResult = manager.checkPermission("doom_loop", {});
2135
- assert.equal(doomResult.state, "deny");
2136
- assert.equal(doomResult.matchedPattern, "doom_loop");
2136
+ assert.equal(doomResult.state, "allow"); // defaultPolicy.tools, not the stripped doom_loop: "deny"
2137
+ assert.equal(doomResult.matchedPattern, undefined);
2137
2138
 
2139
+ // external_directory still resolves from its own entry
2138
2140
  const extResult = manager.checkPermission("external_directory", {});
2139
2141
  assert.equal(extResult.state, "allow");
2140
2142
  assert.equal(extResult.matchedPattern, "external_directory");
@@ -2583,12 +2585,22 @@ test("normalizeRawPermission emits deprecation issue for special.tool_call_limit
2583
2585
  assert.equal(result.permissions.special?.tool_call_limit, undefined);
2584
2586
  });
2585
2587
 
2586
- test("normalizeRawPermission emits no issues for valid special keys", () => {
2588
+ test("normalizeRawPermission emits deprecation issue for special.doom_loop (string)", () => {
2589
+ const result = normalizeRawPermission({
2590
+ special: { doom_loop: "ask" },
2591
+ });
2592
+ assert.equal(result.configIssues.length, 1);
2593
+ assert.ok(result.configIssues[0].includes("doom_loop"));
2594
+ assert.equal(result.permissions.special?.doom_loop, undefined);
2595
+ });
2596
+
2597
+ test("normalizeRawPermission emits deprecation issue for special.doom_loop (deny)", () => {
2587
2598
  const result = normalizeRawPermission({
2588
2599
  special: { doom_loop: "deny" },
2589
2600
  });
2590
- assert.equal(result.configIssues.length, 0);
2591
- assert.equal(result.permissions.special?.doom_loop, "deny");
2601
+ assert.equal(result.configIssues.length, 1);
2602
+ assert.ok(result.configIssues[0].includes("doom_loop"));
2603
+ assert.equal(result.permissions.special?.doom_loop, undefined);
2592
2604
  });
2593
2605
 
2594
2606
  test("normalizeRawPermission emits no issues when special is absent", () => {
@@ -2609,7 +2621,7 @@ test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit
2609
2621
  bash: {},
2610
2622
  mcp: {},
2611
2623
  skills: {},
2612
- special: { tool_call_limit: "allow" as PermissionState, doom_loop: "deny" },
2624
+ special: { tool_call_limit: "allow" as PermissionState },
2613
2625
  };
2614
2626
  const { manager, cleanup } = createManager(config);
2615
2627
  try {
@@ -2634,7 +2646,7 @@ test("PermissionManager.getConfigIssues returns empty array for clean config", (
2634
2646
  bash: {},
2635
2647
  mcp: {},
2636
2648
  skills: {},
2637
- special: { doom_loop: "deny" },
2649
+ special: { external_directory: "ask" },
2638
2650
  };
2639
2651
  const { manager, cleanup } = createManager(config);
2640
2652
  try {
@@ -2645,6 +2657,58 @@ test("PermissionManager.getConfigIssues returns empty array for clean config", (
2645
2657
  }
2646
2658
  });
2647
2659
 
2660
+ // --- doom_loop config-loader deprecation tests (#54) ---
2661
+
2662
+ test("PermissionManager.getConfigIssues returns deprecation for doom_loop in global config", () => {
2663
+ const config: GlobalPermissionConfig = {
2664
+ defaultPolicy: {
2665
+ tools: "ask",
2666
+ bash: "ask",
2667
+ mcp: "ask",
2668
+ skills: "ask",
2669
+ special: "ask",
2670
+ },
2671
+ tools: {},
2672
+ bash: {},
2673
+ mcp: {},
2674
+ skills: {},
2675
+ special: { doom_loop: "deny" },
2676
+ };
2677
+ const { manager, cleanup } = createManager(config);
2678
+ try {
2679
+ const issues = manager.getConfigIssues();
2680
+ assert.equal(issues.length, 1);
2681
+ assert.ok(issues[0].includes("doom_loop"));
2682
+ } finally {
2683
+ cleanup();
2684
+ }
2685
+ });
2686
+
2687
+ test("checkPermission doom_loop falls through to defaultPolicy.tools when stripped by config-loader", () => {
2688
+ const { manager, cleanup } = createManager({
2689
+ defaultPolicy: {
2690
+ tools: "allow",
2691
+ bash: "ask",
2692
+ mcp: "ask",
2693
+ skills: "ask",
2694
+ special: "deny",
2695
+ },
2696
+ tools: {},
2697
+ bash: {},
2698
+ mcp: {},
2699
+ skills: {},
2700
+ special: { doom_loop: "ask" },
2701
+ });
2702
+ try {
2703
+ const result = manager.checkPermission("doom_loop", {});
2704
+ // doom_loop stripped by config-loader — falls through to defaultPolicy.tools
2705
+ assert.equal(result.state, "allow");
2706
+ assert.equal(result.matchedPattern, undefined);
2707
+ } finally {
2708
+ cleanup();
2709
+ }
2710
+ });
2711
+
2648
2712
  // --- session-scoped approval tests (#45) ---
2649
2713
 
2650
2714
  test("session approval: first prompt with 'Yes, for this session' skips subsequent prompts under same prefix", async () => {