@gotgenes/pi-permission-system 3.11.0 → 4.0.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 +35 -0
- package/README.md +135 -168
- package/config/config.example.json +11 -21
- package/package.json +1 -1
- package/schemas/permissions.schema.json +34 -102
- package/src/config-loader.ts +87 -118
- package/src/defaults.ts +6 -62
- package/src/extension-config.ts +3 -4
- package/src/normalize.ts +22 -60
- package/src/permission-manager.ts +244 -348
- package/src/synthesize.ts +17 -82
- package/src/types.ts +12 -18
- package/tests/config-loader.test.ts +113 -63
- package/tests/defaults.test.ts +8 -101
- package/tests/extension-config.test.ts +12 -4
- package/tests/normalize.test.ts +67 -64
- package/tests/permission-system.test.ts +153 -714
- package/tests/session-start.test.ts +1 -7
- package/tests/synthesize.test.ts +46 -219
|
@@ -24,7 +24,7 @@ describe("detectMisplacedPermissionKeys", () => {
|
|
|
24
24
|
expect(result).toEqual([]);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it("returns misplaced key names when permission-rule keys are present", () => {
|
|
27
|
+
it("returns misplaced key names when legacy permission-rule keys are present", () => {
|
|
28
28
|
const result = detectMisplacedPermissionKeys({
|
|
29
29
|
debugLog: true,
|
|
30
30
|
defaultPolicy: { tools: "ask" },
|
|
@@ -33,7 +33,7 @@ describe("detectMisplacedPermissionKeys", () => {
|
|
|
33
33
|
expect(result).toEqual(["defaultPolicy", "bash"]);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it("detects all known permission-rule keys", () => {
|
|
36
|
+
it("detects all known legacy permission-rule keys", () => {
|
|
37
37
|
const result = detectMisplacedPermissionKeys({
|
|
38
38
|
defaultPolicy: {},
|
|
39
39
|
tools: {},
|
|
@@ -54,13 +54,21 @@ describe("detectMisplacedPermissionKeys", () => {
|
|
|
54
54
|
]);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it("does not detect doom_loop as a misplaced permission key
|
|
57
|
+
it("does not detect doom_loop as a misplaced permission key", () => {
|
|
58
58
|
const result = detectMisplacedPermissionKeys({
|
|
59
59
|
doom_loop: {},
|
|
60
60
|
});
|
|
61
61
|
expect(result).toEqual([]);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it("does not flag the new flat-format permission key as misplaced", () => {
|
|
65
|
+
const result = detectMisplacedPermissionKeys({
|
|
66
|
+
debugLog: false,
|
|
67
|
+
permission: { "*": "ask" },
|
|
68
|
+
});
|
|
69
|
+
expect(result).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
64
72
|
it("ignores unknown keys that are not permission-rule keys", () => {
|
|
65
73
|
const result = detectMisplacedPermissionKeys({
|
|
66
74
|
debugLog: true,
|
|
@@ -109,7 +117,7 @@ describe("loadPermissionSystemConfig", () => {
|
|
|
109
117
|
expect(result.warning).toBeDefined();
|
|
110
118
|
expect(result.warning).toContain("defaultPolicy");
|
|
111
119
|
expect(result.warning).toContain("bash");
|
|
112
|
-
expect(result.warning).toContain("
|
|
120
|
+
expect(result.warning).toContain("permission");
|
|
113
121
|
});
|
|
114
122
|
|
|
115
123
|
it("still returns the valid extension config fields when misplaced keys are present", () => {
|
package/tests/normalize.test.ts
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeFlatConfig } from "../src/normalize";
|
|
3
3
|
|
|
4
|
-
describe("
|
|
5
|
-
describe("
|
|
6
|
-
test("
|
|
7
|
-
const result =
|
|
8
|
-
tools: { read: "allow", write: "deny" },
|
|
9
|
-
});
|
|
4
|
+
describe("normalizeFlatConfig", () => {
|
|
5
|
+
describe("string shorthand", () => {
|
|
6
|
+
test("string value produces a single catch-all rule for the surface", () => {
|
|
7
|
+
const result = normalizeFlatConfig({ read: "allow" });
|
|
10
8
|
expect(result).toEqual([
|
|
11
9
|
{ surface: "read", pattern: "*", action: "allow" },
|
|
12
|
-
{ surface: "write", pattern: "*", action: "deny" },
|
|
13
10
|
]);
|
|
14
11
|
});
|
|
15
12
|
|
|
16
|
-
test("
|
|
17
|
-
const result =
|
|
18
|
-
tools: { bash: "allow", read: "allow" },
|
|
19
|
-
});
|
|
13
|
+
test("string shorthand works for multiple surfaces", () => {
|
|
14
|
+
const result = normalizeFlatConfig({ read: "allow", write: "deny" });
|
|
20
15
|
expect(result).toEqual([
|
|
21
16
|
{ surface: "read", pattern: "*", action: "allow" },
|
|
17
|
+
{ surface: "write", pattern: "*", action: "deny" },
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("universal fallback '*' becomes a catch-all rule with surface '*'", () => {
|
|
22
|
+
const result = normalizeFlatConfig({ "*": "ask" });
|
|
23
|
+
expect(result).toEqual([{ surface: "*", pattern: "*", action: "ask" }]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("external_directory string shorthand maps directly to its surface", () => {
|
|
27
|
+
const result = normalizeFlatConfig({ external_directory: "ask" });
|
|
28
|
+
expect(result).toEqual([
|
|
29
|
+
{ surface: "external_directory", pattern: "*", action: "ask" },
|
|
22
30
|
]);
|
|
23
31
|
});
|
|
24
32
|
|
|
25
|
-
test("
|
|
26
|
-
const result =
|
|
27
|
-
|
|
33
|
+
test("invalid string values (non-PermissionState) are ignored", () => {
|
|
34
|
+
const result = normalizeFlatConfig({
|
|
35
|
+
read: "allow",
|
|
36
|
+
write: "invalid" as never,
|
|
28
37
|
});
|
|
29
38
|
expect(result).toEqual([
|
|
30
39
|
{ surface: "read", pattern: "*", action: "allow" },
|
|
@@ -32,90 +41,84 @@ describe("normalizeConfig", () => {
|
|
|
32
41
|
});
|
|
33
42
|
});
|
|
34
43
|
|
|
35
|
-
describe("
|
|
36
|
-
test("
|
|
37
|
-
const result =
|
|
38
|
-
bash: { "
|
|
44
|
+
describe("object pattern map", () => {
|
|
45
|
+
test("object value produces one rule per pattern", () => {
|
|
46
|
+
const result = normalizeFlatConfig({
|
|
47
|
+
bash: { "*": "ask", "git *": "allow" },
|
|
39
48
|
});
|
|
40
49
|
expect(result).toEqual([
|
|
50
|
+
{ surface: "bash", pattern: "*", action: "ask" },
|
|
41
51
|
{ surface: "bash", pattern: "git *", action: "allow" },
|
|
42
|
-
{ surface: "bash", pattern: "rm -rf *", action: "deny" },
|
|
43
52
|
]);
|
|
44
53
|
});
|
|
45
|
-
});
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
mcp: { "exa:*": "allow", mcp_status: "allow" },
|
|
55
|
+
test("mcp object map produces rules with surface 'mcp'", () => {
|
|
56
|
+
const result = normalizeFlatConfig({
|
|
57
|
+
mcp: { "*": "ask", mcp_status: "allow" },
|
|
51
58
|
});
|
|
52
59
|
expect(result).toEqual([
|
|
53
|
-
{ surface: "mcp", pattern: "
|
|
60
|
+
{ surface: "mcp", pattern: "*", action: "ask" },
|
|
54
61
|
{ surface: "mcp", pattern: "mcp_status", action: "allow" },
|
|
55
62
|
]);
|
|
56
63
|
});
|
|
57
|
-
});
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
skills: { "*": "ask", librarian: "allow" },
|
|
65
|
+
test("skill object map produces rules with surface 'skill'", () => {
|
|
66
|
+
const result = normalizeFlatConfig({
|
|
67
|
+
skill: { "*": "ask", librarian: "allow" },
|
|
63
68
|
});
|
|
64
69
|
expect(result).toEqual([
|
|
65
70
|
{ surface: "skill", pattern: "*", action: "ask" },
|
|
66
71
|
{ surface: "skill", pattern: "librarian", action: "allow" },
|
|
67
72
|
]);
|
|
68
73
|
});
|
|
69
|
-
});
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
special: { external_directory: "ask" },
|
|
75
|
+
test("invalid action values in object map are ignored", () => {
|
|
76
|
+
const result = normalizeFlatConfig({
|
|
77
|
+
bash: { "git *": "allow", "rm -rf *": "bad" as never },
|
|
75
78
|
});
|
|
76
79
|
expect(result).toEqual([
|
|
77
|
-
{ surface: "
|
|
80
|
+
{ surface: "bash", pattern: "git *", action: "allow" },
|
|
78
81
|
]);
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
describe("
|
|
83
|
-
test("
|
|
84
|
-
const result =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("full ordering: tools → bash → mcp → skills → special", () => {
|
|
95
|
-
const result = normalizeConfig({
|
|
96
|
-
tools: { read: "allow" },
|
|
97
|
-
bash: { "git *": "allow" },
|
|
98
|
-
mcp: { "exa:*": "allow" },
|
|
99
|
-
skills: { librarian: "allow" },
|
|
100
|
-
special: { external_directory: "ask" },
|
|
85
|
+
describe("mixed surfaces", () => {
|
|
86
|
+
test("full mixed config produces rules in insertion order", () => {
|
|
87
|
+
const result = normalizeFlatConfig({
|
|
88
|
+
"*": "ask",
|
|
89
|
+
read: "allow",
|
|
90
|
+
write: "deny",
|
|
91
|
+
bash: { "*": "ask", "git *": "allow" },
|
|
92
|
+
mcp: { mcp_status: "allow" },
|
|
93
|
+
skill: { "*": "ask" },
|
|
94
|
+
external_directory: "ask",
|
|
101
95
|
});
|
|
102
96
|
expect(result).toEqual([
|
|
97
|
+
{ surface: "*", pattern: "*", action: "ask" },
|
|
103
98
|
{ surface: "read", pattern: "*", action: "allow" },
|
|
99
|
+
{ surface: "write", pattern: "*", action: "deny" },
|
|
100
|
+
{ surface: "bash", pattern: "*", action: "ask" },
|
|
104
101
|
{ surface: "bash", pattern: "git *", action: "allow" },
|
|
105
|
-
{ surface: "mcp", pattern: "
|
|
106
|
-
{ surface: "skill", pattern: "
|
|
107
|
-
{ surface: "
|
|
102
|
+
{ surface: "mcp", pattern: "mcp_status", action: "allow" },
|
|
103
|
+
{ surface: "skill", pattern: "*", action: "ask" },
|
|
104
|
+
{ surface: "external_directory", pattern: "*", action: "ask" },
|
|
108
105
|
]);
|
|
109
106
|
});
|
|
110
107
|
});
|
|
111
108
|
|
|
112
|
-
describe("empty and
|
|
113
|
-
test("empty
|
|
114
|
-
expect(
|
|
109
|
+
describe("empty and edge cases", () => {
|
|
110
|
+
test("empty permission object produces empty ruleset", () => {
|
|
111
|
+
expect(normalizeFlatConfig({})).toEqual([]);
|
|
115
112
|
});
|
|
116
113
|
|
|
117
|
-
test("
|
|
118
|
-
|
|
114
|
+
test("non-object values (null, array) nested in map are skipped", () => {
|
|
115
|
+
const result = normalizeFlatConfig({
|
|
116
|
+
bash: null as never,
|
|
117
|
+
read: "allow",
|
|
118
|
+
});
|
|
119
|
+
expect(result).toEqual([
|
|
120
|
+
{ surface: "read", pattern: "*", action: "allow" },
|
|
121
|
+
]);
|
|
119
122
|
});
|
|
120
123
|
});
|
|
121
124
|
});
|