@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.
@@ -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 (deprecated)", () => {
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("pi-permissions.jsonc");
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", () => {
@@ -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
  });