@gotgenes/pi-permission-system 3.11.0 → 4.0.1

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.
@@ -27,13 +27,7 @@ describe("session_start handler consolidation", () => {
27
27
  mkdirSync(dirname(globalConfigPath), { recursive: true });
28
28
 
29
29
  const config: ScopeConfig = {
30
- defaultPolicy: {
31
- tools: "ask",
32
- bash: "ask",
33
- mcp: "ask",
34
- skills: "ask",
35
- special: "ask",
36
- },
30
+ permission: { "*": "ask" },
37
31
  };
38
32
  writeFileSync(
39
33
  globalConfigPath,
@@ -4,42 +4,15 @@ import {
4
4
  composeRuleset,
5
5
  synthesizeBaseline,
6
6
  synthesizeDefaults,
7
- synthesizeOverrides,
8
7
  } from "../src/synthesize";
9
- import type { PermissionDefaultPolicy } from "../src/types";
10
-
11
- const ALL_ASK: PermissionDefaultPolicy = {
12
- tools: "ask",
13
- bash: "ask",
14
- mcp: "ask",
15
- skills: "ask",
16
- special: "ask",
17
- };
18
-
19
- const ALL_ALLOW: PermissionDefaultPolicy = {
20
- tools: "allow",
21
- bash: "allow",
22
- mcp: "allow",
23
- skills: "allow",
24
- special: "allow",
25
- };
26
8
 
27
9
  // ── synthesizeDefaults ─────────────────────────────────────────────────────
28
10
 
29
11
  describe("synthesizeDefaults", () => {
30
- test("emits 5 catch-all rules with layer 'default'", () => {
31
- const rules = synthesizeDefaults(ALL_ASK);
32
- expect(rules).toHaveLength(5);
33
- for (const rule of rules) {
34
- expect(rule.layer).toBe("default");
35
- expect(rule.pattern).toBe("*");
36
- }
37
- });
38
-
39
- test("emits universal catch-all for tools default", () => {
40
- const rules = synthesizeDefaults(ALL_ASK);
41
- const universal = rules.find((r) => r.surface === "*");
42
- expect(universal).toEqual({
12
+ test("emits a single universal catch-all rule with layer 'default'", () => {
13
+ const rules = synthesizeDefaults("ask");
14
+ expect(rules).toHaveLength(1);
15
+ expect(rules[0]).toEqual({
43
16
  surface: "*",
44
17
  pattern: "*",
45
18
  action: "ask",
@@ -47,124 +20,23 @@ describe("synthesizeDefaults", () => {
47
20
  });
48
21
  });
49
22
 
50
- test("emits per-surface catch-alls for bash, mcp, skill, special", () => {
51
- const rules = synthesizeDefaults(ALL_ASK);
52
- const surfaces = rules.map((r) => r.surface);
53
- expect(surfaces).toContain("bash");
54
- expect(surfaces).toContain("mcp");
55
- expect(surfaces).toContain("skill");
56
- expect(surfaces).toContain("special");
57
- });
58
-
59
- test("reflects non-ask actions correctly", () => {
60
- const rules = synthesizeDefaults(ALL_ALLOW);
61
- for (const rule of rules) {
62
- expect(rule.action).toBe("allow");
63
- }
64
- });
65
-
66
- test("mixed defaults produce correct per-surface actions", () => {
67
- const mixed: PermissionDefaultPolicy = {
68
- tools: "allow",
69
- bash: "deny",
70
- mcp: "ask",
71
- skills: "allow",
72
- special: "deny",
73
- };
74
- const rules = synthesizeDefaults(mixed);
75
- const get = (surface: string) => rules.find((r) => r.surface === surface);
76
- expect(get("*")?.action).toBe("allow"); // tools default
77
- expect(get("bash")?.action).toBe("deny");
78
- expect(get("mcp")?.action).toBe("ask");
79
- expect(get("skill")?.action).toBe("allow");
80
- expect(get("special")?.action).toBe("deny");
81
- });
82
-
83
- test("default rules catch any surface via the universal '*' entry", () => {
84
- const rules = synthesizeDefaults(ALL_ASK);
85
- // A brand-new surface "future_tool" should be caught by the universal rule.
86
- const result = evaluate("future_tool", "*", rules);
87
- expect(result.action).toBe("ask");
88
- expect(result.layer).toBe("default");
89
- });
90
-
91
- test("specific surface default beats universal default (last-match-wins)", () => {
92
- const mixed: PermissionDefaultPolicy = {
93
- tools: "allow",
94
- bash: "deny",
95
- mcp: "ask",
96
- skills: "ask",
97
- special: "ask",
98
- };
99
- const rules = synthesizeDefaults(mixed);
100
- // For bash, the specific bash rule (later in array) beats the universal rule.
101
- const result = evaluate("bash", "git status", rules);
102
- expect(result.action).toBe("deny");
103
- expect(result.layer).toBe("default");
104
- });
105
- });
106
-
107
- // ── synthesizeOverrides ────────────────────────────────────────────────────
108
-
109
- describe("synthesizeOverrides", () => {
110
- test("returns empty ruleset for empty input", () => {
111
- expect(synthesizeOverrides([])).toEqual([]);
112
- });
113
-
114
- test("returns empty ruleset when no scope has overrides", () => {
115
- expect(synthesizeOverrides([{}, {}, {}])).toEqual([]);
116
- });
117
-
118
- test("emits a bash override rule for each scope that defines tools.bash", () => {
119
- const rules = synthesizeOverrides([{ bash: "allow" }]);
120
- expect(rules).toEqual([
121
- { surface: "bash", pattern: "*", action: "allow", layer: "override" },
122
- ]);
123
- });
124
-
125
- test("emits an mcp override rule for each scope that defines tools.mcp", () => {
126
- const rules = synthesizeOverrides([{ mcp: "deny" }]);
127
- expect(rules).toEqual([
128
- { surface: "mcp", pattern: "*", action: "deny", layer: "override" },
129
- ]);
130
- });
131
-
132
- test("emits both bash and mcp override rules when both are defined in a scope", () => {
133
- const rules = synthesizeOverrides([{ bash: "allow", mcp: "deny" }]);
134
- expect(rules).toHaveLength(2);
135
- const bash = rules.find((r) => r.surface === "bash");
136
- const mcp = rules.find((r) => r.surface === "mcp");
137
- expect(bash?.action).toBe("allow");
138
- expect(mcp?.action).toBe("deny");
23
+ test("reflects the supplied PermissionState as the action", () => {
24
+ expect(synthesizeDefaults("allow")[0].action).toBe("allow");
25
+ expect(synthesizeDefaults("deny")[0].action).toBe("deny");
26
+ expect(synthesizeDefaults("ask")[0].action).toBe("ask");
139
27
  });
140
28
 
141
- test("later scopes produce later rules (higher priority via last-match-wins)", () => {
142
- const rules = synthesizeOverrides([{ bash: "deny" }, { bash: "allow" }]);
143
- const result = evaluate("bash", "git status", rules);
144
- expect(result.action).toBe("allow"); // later scope wins
29
+ test("universal rule catches any surface via wildcardMatch", () => {
30
+ const rules = synthesizeDefaults("ask");
31
+ expect(evaluate("read", "*", rules).action).toBe("ask");
32
+ expect(evaluate("bash", "git status", rules).action).toBe("ask");
33
+ expect(evaluate("external_directory", "*", rules).action).toBe("ask");
34
+ expect(evaluate("future_surface", "*", rules).action).toBe("ask");
145
35
  });
146
36
 
147
- test("skips undefined fields and emits nothing for them", () => {
148
- const rules = synthesizeOverrides([
149
- { bash: undefined, mcp: "allow" },
150
- { bash: "deny", mcp: undefined },
151
- ]);
152
- const bashRules = rules.filter((r) => r.surface === "bash");
153
- const mcpRules = rules.filter((r) => r.surface === "mcp");
154
- expect(bashRules).toHaveLength(1);
155
- expect(mcpRules).toHaveLength(1);
156
- expect(bashRules[0].action).toBe("deny");
157
- expect(mcpRules[0].action).toBe("allow");
158
- });
159
-
160
- test("override rules all have layer 'override'", () => {
161
- const rules = synthesizeOverrides([
162
- { bash: "allow", mcp: "deny" },
163
- { bash: "deny" },
164
- ]);
165
- for (const rule of rules) {
166
- expect(rule.layer).toBe("override");
167
- }
37
+ test("universal rule has layer 'default'", () => {
38
+ const rules = synthesizeDefaults("allow");
39
+ expect(evaluate("read", "*", rules).layer).toBe("default");
168
40
  });
169
41
  });
170
42
 
@@ -247,19 +119,6 @@ describe("synthesizeBaseline", () => {
247
119
  expect(synthesizeBaseline(configRules)).toEqual([]);
248
120
  });
249
121
 
250
- test("baseline is NOT synthesized when defaults.mcp === 'allow' but no config allow rules", () => {
251
- // defaults.mcp === 'allow' is handled by the synthesized default catch-all, not baseline.
252
- const configRules = [
253
- {
254
- surface: "mcp",
255
- pattern: "*",
256
- action: "deny" as const,
257
- layer: "config" as const,
258
- },
259
- ];
260
- expect(synthesizeBaseline(configRules)).toEqual([]);
261
- });
262
-
263
122
  test("baseline auto-allows mcp_status when an mcp allow rule exists", () => {
264
123
  const configRules = [
265
124
  {
@@ -280,38 +139,21 @@ describe("synthesizeBaseline", () => {
280
139
 
281
140
  describe("composeRuleset", () => {
282
141
  test("returns concatenation of all layers in order", () => {
283
- const defaults = synthesizeDefaults(ALL_ASK);
142
+ const defaults = synthesizeDefaults("ask");
284
143
  const baseline = synthesizeBaseline([
285
144
  { surface: "mcp", pattern: "exa:*", action: "allow", layer: "config" },
286
145
  ]);
287
- const overrides = synthesizeOverrides([{ bash: "allow" }]);
288
146
  const config = [
289
147
  { surface: "bash", pattern: "rm -rf *", action: "deny" as const },
290
148
  ];
291
- const composed = composeRuleset(defaults, baseline, overrides, config);
149
+ const composed = composeRuleset(defaults, baseline, config);
292
150
  expect(composed.length).toBe(
293
- defaults.length + baseline.length + overrides.length + config.length,
151
+ defaults.length + baseline.length + config.length,
294
152
  );
295
153
  });
296
154
 
297
155
  test("defaults come first (lowest priority), config comes last (highest priority)", () => {
298
- const defaults = [
299
- {
300
- surface: "bash",
301
- pattern: "*",
302
- action: "ask" as const,
303
- layer: "default" as const,
304
- },
305
- ];
306
- const baseline: never[] = [];
307
- const overrides = [
308
- {
309
- surface: "bash",
310
- pattern: "*",
311
- action: "allow" as const,
312
- layer: "override" as const,
313
- },
314
- ];
156
+ const defaults = synthesizeDefaults("ask");
315
157
  const config = [
316
158
  {
317
159
  surface: "bash",
@@ -320,45 +162,30 @@ describe("composeRuleset", () => {
320
162
  layer: "config" as const,
321
163
  },
322
164
  ];
323
- const composed = composeRuleset(defaults, baseline, overrides, config);
324
- // Last-match-wins: config is last → deny wins for any bash command.
165
+ const composed = composeRuleset(defaults, [], config);
325
166
  const result = evaluate("bash", "echo hello", composed);
326
167
  expect(result.action).toBe("deny");
327
168
  expect(result.layer).toBe("config");
328
169
  });
329
170
 
330
- test("override beats default when no config rule exists", () => {
331
- const defaults = [
332
- {
333
- surface: "bash",
334
- pattern: "*",
335
- action: "ask" as const,
336
- layer: "default" as const,
337
- },
338
- ];
339
- const overrides = [
171
+ test("config beats default for matching patterns", () => {
172
+ const defaults = synthesizeDefaults("ask");
173
+ const config = [
340
174
  {
341
- surface: "bash",
175
+ surface: "read",
342
176
  pattern: "*",
343
177
  action: "allow" as const,
344
- layer: "override" as const,
178
+ layer: "config" as const,
345
179
  },
346
180
  ];
347
- const composed = composeRuleset(defaults, [], overrides, []);
348
- const result = evaluate("bash", "echo hello", composed);
181
+ const composed = composeRuleset(defaults, [], config);
182
+ const result = evaluate("read", "*", composed);
349
183
  expect(result.action).toBe("allow");
350
- expect(result.layer).toBe("override");
184
+ expect(result.layer).toBe("config");
351
185
  });
352
186
 
353
- test("baseline beats default but override beats baseline", () => {
354
- const defaults = [
355
- {
356
- surface: "mcp",
357
- pattern: "*",
358
- action: "ask" as const,
359
- layer: "default" as const,
360
- },
361
- ];
187
+ test("baseline beats default but config beats baseline", () => {
188
+ const defaults = synthesizeDefaults("ask");
362
189
  const baseline = [
363
190
  {
364
191
  surface: "mcp",
@@ -367,28 +194,28 @@ describe("composeRuleset", () => {
367
194
  layer: "baseline" as const,
368
195
  },
369
196
  ];
370
- const overrides = [
197
+ const config = [
371
198
  {
372
199
  surface: "mcp",
373
- pattern: "*",
200
+ pattern: "mcp_status",
374
201
  action: "deny" as const,
375
- layer: "override" as const,
202
+ layer: "config" as const,
376
203
  },
377
204
  ];
378
- const composed = composeRuleset(defaults, baseline, overrides, []);
379
- // override beats baseline for mcp_status
205
+ const composed = composeRuleset(defaults, baseline, config);
380
206
  const result = evaluate("mcp", "mcp_status", composed);
381
207
  expect(result.action).toBe("deny");
382
- expect(result.layer).toBe("override");
208
+ expect(result.layer).toBe("config");
383
209
  });
384
210
 
385
- test("config beats override for specific patterns", () => {
386
- const overrides = [
211
+ test("config beats baseline for specific patterns", () => {
212
+ const defaults = synthesizeDefaults("ask");
213
+ const baseline = [
387
214
  {
388
215
  surface: "mcp",
389
- pattern: "*",
390
- action: "deny" as const,
391
- layer: "override" as const,
216
+ pattern: "mcp_status",
217
+ action: "allow" as const,
218
+ layer: "baseline" as const,
392
219
  },
393
220
  ];
394
221
  const config = [
@@ -399,15 +226,15 @@ describe("composeRuleset", () => {
399
226
  layer: "config" as const,
400
227
  },
401
228
  ];
402
- const composed = composeRuleset([], [], overrides, config);
229
+ const composed = composeRuleset(defaults, baseline, config);
403
230
  const result = evaluate("mcp", "exa_web_search", composed);
404
231
  expect(result.action).toBe("allow");
405
232
  expect(result.layer).toBe("config");
406
233
  });
407
234
 
408
235
  test("handles empty layers gracefully", () => {
409
- const defaults = synthesizeDefaults(ALL_ASK);
410
- const composed = composeRuleset(defaults, [], [], []);
236
+ const defaults = synthesizeDefaults("ask");
237
+ const composed = composeRuleset(defaults, [], []);
411
238
  expect(composed).toEqual(defaults);
412
239
  });
413
240
  });