@gotgenes/pi-permission-system 3.10.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.
@@ -1,30 +1,39 @@
1
1
  import { describe, expect, test } from "vitest";
2
- import { normalizeConfig } from "../src/normalize";
2
+ import { normalizeFlatConfig } from "../src/normalize";
3
3
 
4
- describe("normalizeConfig", () => {
5
- describe("tools entries", () => {
6
- test("converts tools entries to tool-name-as-surface rules", () => {
7
- const result = normalizeConfig({
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("tools.bash is excluded (handled as fallback override)", () => {
17
- const result = normalizeConfig({
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("tools.mcp is excluded (handled as fallback override)", () => {
26
- const result = normalizeConfig({
27
- tools: { mcp: "ask", read: "allow" },
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("bash entries", () => {
36
- test("converts bash entries to surface 'bash' rules", () => {
37
- const result = normalizeConfig({
38
- bash: { "git *": "allow", "rm -rf *": "deny" },
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
- describe("mcp entries", () => {
48
- test("converts mcp entries to surface 'mcp' rules", () => {
49
- const result = normalizeConfig({
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: "exa:*", action: "allow" },
60
+ { surface: "mcp", pattern: "*", action: "ask" },
54
61
  { surface: "mcp", pattern: "mcp_status", action: "allow" },
55
62
  ]);
56
63
  });
57
- });
58
64
 
59
- describe("skills entries", () => {
60
- test("converts skills entries to surface 'skill' rules", () => {
61
- const result = normalizeConfig({
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
- describe("special entries", () => {
72
- test("converts special entries to surface 'special' with key as pattern", () => {
73
- const result = normalizeConfig({
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: "special", pattern: "external_directory", action: "ask" },
80
+ { surface: "bash", pattern: "git *", action: "allow" },
78
81
  ]);
79
82
  });
80
83
  });
81
84
 
82
- describe("ordering", () => {
83
- test("tools.bash excluded; bash entries come after tools", () => {
84
- const result = normalizeConfig({
85
- tools: { bash: "allow", read: "deny" },
86
- bash: { "git *": "ask" },
87
- });
88
- expect(result).toEqual([
89
- { surface: "read", pattern: "*", action: "deny" },
90
- { surface: "bash", pattern: "git *", action: "ask" },
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: "exa:*", action: "allow" },
106
- { surface: "skill", pattern: "librarian", action: "allow" },
107
- { surface: "special", pattern: "external_directory", action: "ask" },
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 missing sections", () => {
113
- test("empty config produces empty ruleset", () => {
114
- expect(normalizeConfig({})).toEqual([]);
109
+ describe("empty and edge cases", () => {
110
+ test("empty permission object produces empty ruleset", () => {
111
+ expect(normalizeFlatConfig({})).toEqual([]);
115
112
  });
116
113
 
117
- test("undefined sections are skipped", () => {
118
- expect(normalizeConfig({ tools: undefined })).toEqual([]);
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
  });