@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 +17 -0
- package/README.md +1 -3
- package/config/config.example.json +0 -1
- package/package.json +1 -1
- package/schemas/permissions.schema.json +2 -6
- package/src/config-loader.ts +2 -1
- package/src/extension-config.ts +0 -1
- package/src/permission-manager.ts +2 -1
- package/src/types.ts +1 -1
- package/tests/config-loader.test.ts +5 -4
- package/tests/extension-config.test.ts +7 -2
- package/tests/permission-system.test.ts +72 -8
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": { "
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"default": "ask"
|
|
61
61
|
},
|
|
62
62
|
"special": {
|
|
63
|
-
"description": "Default permission for special checks (
|
|
64
|
-
"markdownDescription": "Default permission for special checks (`
|
|
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`.",
|
package/src/config-loader.ts
CHANGED
|
@@ -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(["
|
|
53
|
+
const SPECIAL_PERMISSION_KEYS = new Set(["external_directory"]);
|
|
53
54
|
|
|
54
55
|
export function stripJsonComments(input: string): string {
|
|
55
56
|
let output = "";
|
package/src/extension-config.ts
CHANGED
|
@@ -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(["
|
|
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 = "
|
|
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(
|
|
147
|
-
expect(result.issues
|
|
148
|
-
expect(result.
|
|
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
|
|
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, "
|
|
2136
|
-
assert.equal(doomResult.matchedPattern,
|
|
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
|
|
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,
|
|
2591
|
-
assert.
|
|
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
|
|
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: {
|
|
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 () => {
|