@gotgenes/pi-permission-system 11.0.0 → 13.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +9 -11
  3. package/package.json +1 -1
  4. package/schemas/permissions.schema.json +1 -1
  5. package/src/config-modal.ts +3 -7
  6. package/src/extension-config.ts +11 -25
  7. package/src/handlers/gates/bash-path-extractor.ts +0 -12
  8. package/src/handlers/gates/bash-path.ts +12 -10
  9. package/src/handlers/gates/bash-program.ts +52 -11
  10. package/src/handlers/gates/bash-token-classification.ts +1 -1
  11. package/src/handlers/gates/external-directory.ts +8 -2
  12. package/src/handlers/gates/path.ts +4 -2
  13. package/src/handlers/gates/tool-call-gate-pipeline.ts +5 -2
  14. package/src/index.ts +8 -2
  15. package/src/input-normalizer.ts +17 -11
  16. package/src/path-utils.ts +122 -0
  17. package/src/permission-manager.ts +81 -17
  18. package/src/permission-resolver.ts +24 -0
  19. package/src/permission-session.ts +2 -4
  20. package/src/permissions-service.ts +12 -0
  21. package/src/rule.ts +61 -11
  22. package/src/service.ts +24 -0
  23. package/src/tool-access-extractor-registry.ts +68 -0
  24. package/test/bash-external-directory.test.ts +1 -81
  25. package/test/composition-root.test.ts +36 -0
  26. package/test/config-modal.test.ts +7 -10
  27. package/test/config-pipeline.test.ts +90 -0
  28. package/test/extension-config.test.ts +0 -58
  29. package/test/handlers/gates/bash-path.test.ts +45 -2
  30. package/test/handlers/gates/bash-program.test.ts +44 -11
  31. package/test/handlers/gates/external-directory.test.ts +54 -0
  32. package/test/handlers/gates/path.test.ts +72 -0
  33. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +64 -1
  34. package/test/helpers/gate-fixtures.ts +23 -3
  35. package/test/helpers/handler-fixtures.ts +14 -2
  36. package/test/helpers/session-fixtures.ts +14 -0
  37. package/test/input-normalizer.test.ts +52 -0
  38. package/test/path-utils.test.ts +135 -0
  39. package/test/permission-manager-unified.test.ts +134 -0
  40. package/test/permission-resolver.test.ts +69 -0
  41. package/test/permissions-service.test.ts +35 -1
  42. package/test/rule.test.ts +74 -1
  43. package/test/service-lifecycle.test.ts +1 -0
  44. package/test/service.test.ts +53 -0
  45. package/test/tool-access-extractor-registry.test.ts +77 -0
package/test/rule.test.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
  import type { Rule, RuleOrigin, Ruleset } from "#src/rule";
3
- import { evaluate, evaluateFirst, evaluateMostRestrictive } from "#src/rule";
3
+ import {
4
+ evaluate,
5
+ evaluateAnyValue,
6
+ evaluateFirst,
7
+ evaluateMostRestrictive,
8
+ } from "#src/rule";
4
9
 
5
10
  describe("evaluate", () => {
6
11
  const allowBashGit: Rule = {
@@ -393,6 +398,74 @@ describe("evaluateFirst", () => {
393
398
  });
394
399
  });
395
400
 
401
+ describe("evaluateAnyValue", () => {
402
+ const catchAllAllow: Rule = {
403
+ surface: "path",
404
+ pattern: "*",
405
+ action: "allow",
406
+ layer: "config",
407
+ origin: "global",
408
+ };
409
+ const catchAllAsk: Rule = {
410
+ surface: "path",
411
+ pattern: "*",
412
+ action: "ask",
413
+ layer: "config",
414
+ origin: "global",
415
+ };
416
+ const relativeDeny: Rule = {
417
+ surface: "path",
418
+ pattern: "src/*",
419
+ action: "deny",
420
+ layer: "config",
421
+ origin: "global",
422
+ };
423
+ const absoluteAllow: Rule = {
424
+ surface: "path",
425
+ pattern: "/proj/*",
426
+ action: "allow",
427
+ layer: "config",
428
+ origin: "global",
429
+ };
430
+
431
+ test("a later relative rule wins over a catch-all matched by another alias", () => {
432
+ const rules: Ruleset = [catchAllAllow, relativeDeny];
433
+ const result = evaluateAnyValue(
434
+ "path",
435
+ ["/proj/src/foo.ts", "src/foo.ts"],
436
+ rules,
437
+ );
438
+ expect(result.rule).toEqual(relativeDeny);
439
+ expect(result.value).toBe("src/foo.ts");
440
+ });
441
+
442
+ test("uses an absolute alias when no later relative rule matches", () => {
443
+ const rules: Ruleset = [catchAllAsk, absoluteAllow];
444
+ const result = evaluateAnyValue(
445
+ "path",
446
+ ["/proj/src/foo.ts", "src/foo.ts"],
447
+ rules,
448
+ );
449
+ expect(result.rule).toEqual(absoluteAllow);
450
+ expect(result.value).toBe("/proj/src/foo.ts");
451
+ });
452
+
453
+ test("falls back to the first value's default when no rule matches", () => {
454
+ const result = evaluateAnyValue(
455
+ "path",
456
+ ["/proj/src/foo.ts", "src/foo.ts"],
457
+ [],
458
+ );
459
+ expect(result.rule.action).toBe("ask");
460
+ expect(result.value).toBe("/proj/src/foo.ts");
461
+ });
462
+
463
+ test("uses '*' as fallback value when values array is empty", () => {
464
+ const result = evaluateAnyValue("path", [], []);
465
+ expect(result.value).toBe("*");
466
+ });
467
+ });
468
+
396
469
  describe("evaluateMostRestrictive", () => {
397
470
  const denyEnv: Rule = {
398
471
  surface: "path",
@@ -35,6 +35,7 @@ function makeService(): PermissionsService {
35
35
  checkPermission: vi.fn(),
36
36
  getToolPermission: vi.fn(),
37
37
  registerToolInputFormatter: vi.fn(),
38
+ registerToolAccessExtractor: vi.fn(),
38
39
  };
39
40
  }
40
41
 
@@ -6,6 +6,7 @@ import {
6
6
  publishPermissionsService,
7
7
  unpublishPermissionsService,
8
8
  } from "#src/service";
9
+ import { ToolAccessExtractorRegistry } from "#src/tool-access-extractor-registry";
9
10
  import { ToolInputFormatterRegistry } from "#src/tool-input-formatter-registry";
10
11
  import type { PermissionCheckResult } from "#src/types";
11
12
 
@@ -18,6 +19,7 @@ function makeService(
18
19
  checkPermission: vi.fn(),
19
20
  getToolPermission: vi.fn(),
20
21
  registerToolInputFormatter: vi.fn(),
22
+ registerToolAccessExtractor: vi.fn(),
21
23
  ...overrides,
22
24
  };
23
25
  }
@@ -155,6 +157,7 @@ describe("service adapter delegation", () => {
155
157
  return getToolPermissionFn(toolName, agentName);
156
158
  },
157
159
  registerToolInputFormatter: vi.fn(),
160
+ registerToolAccessExtractor: vi.fn(),
158
161
  };
159
162
 
160
163
  publishPermissionsService(service);
@@ -177,6 +180,7 @@ describe("service adapter delegation", () => {
177
180
  return getToolPermissionFn(toolName, agentName);
178
181
  },
179
182
  registerToolInputFormatter: vi.fn(),
183
+ registerToolAccessExtractor: vi.fn(),
180
184
  };
181
185
 
182
186
  publishPermissionsService(service);
@@ -253,3 +257,52 @@ describe("registerToolInputFormatter delegation", () => {
253
257
  ).toThrow("my-tool");
254
258
  });
255
259
  });
260
+
261
+ // ── registerToolAccessExtractor delegation (#352) ────────────────────────
262
+
263
+ describe("registerToolAccessExtractor delegation", () => {
264
+ afterEach(() => {
265
+ const current = getPermissionsService();
266
+ if (current) {
267
+ unpublishPermissionsService(current);
268
+ }
269
+ });
270
+
271
+ it("delegates to the registry and returns its disposer", () => {
272
+ const registry = new ToolAccessExtractorRegistry();
273
+ const extractor = () => "/etc/hosts";
274
+
275
+ const service = makeService({
276
+ registerToolAccessExtractor(toolName, ext) {
277
+ return registry.register(toolName, ext);
278
+ },
279
+ });
280
+
281
+ publishPermissionsService(service);
282
+ const dispose = getPermissionsService()!.registerToolAccessExtractor(
283
+ "ffgrep",
284
+ extractor,
285
+ );
286
+
287
+ expect(registry.get("ffgrep")).toBe(extractor);
288
+
289
+ dispose();
290
+ expect(registry.get("ffgrep")).toBeUndefined();
291
+ });
292
+
293
+ it("throws when an extractor is already registered for the tool name", () => {
294
+ const registry = new ToolAccessExtractorRegistry();
295
+ registry.register("ffgrep", () => undefined);
296
+
297
+ const service = makeService({
298
+ registerToolAccessExtractor(toolName, ext) {
299
+ return registry.register(toolName, ext);
300
+ },
301
+ });
302
+
303
+ publishPermissionsService(service);
304
+ expect(() =>
305
+ getPermissionsService()!.registerToolAccessExtractor("ffgrep", () => ""),
306
+ ).toThrow("ffgrep");
307
+ });
308
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "vitest";
2
+
3
+ import {
4
+ type ToolAccessExtractor,
5
+ ToolAccessExtractorRegistry,
6
+ } from "#src/tool-access-extractor-registry";
7
+
8
+ const noopExtractor: ToolAccessExtractor = () => "/tmp/x";
9
+
10
+ describe("ToolAccessExtractorRegistry", () => {
11
+ describe("register", () => {
12
+ test("stores an extractor so get() returns it", () => {
13
+ const registry = new ToolAccessExtractorRegistry();
14
+ registry.register("my-tool", noopExtractor);
15
+ expect(registry.get("my-tool")).toBe(noopExtractor);
16
+ });
17
+
18
+ test("returns a disposer that removes the extractor", () => {
19
+ const registry = new ToolAccessExtractorRegistry();
20
+ const dispose = registry.register("my-tool", noopExtractor);
21
+ dispose();
22
+ expect(registry.get("my-tool")).toBeUndefined();
23
+ });
24
+
25
+ test("throws when an extractor is already registered for the same tool name", () => {
26
+ const registry = new ToolAccessExtractorRegistry();
27
+ registry.register("my-tool", noopExtractor);
28
+ expect(() => registry.register("my-tool", () => undefined)).toThrow(
29
+ "my-tool",
30
+ );
31
+ });
32
+
33
+ test("allows registering different tool names independently", () => {
34
+ const registry = new ToolAccessExtractorRegistry();
35
+ const extractorA: ToolAccessExtractor = () => "/a";
36
+ const extractorB: ToolAccessExtractor = () => "/b";
37
+ registry.register("tool-a", extractorA);
38
+ registry.register("tool-b", extractorB);
39
+ expect(registry.get("tool-a")).toBe(extractorA);
40
+ expect(registry.get("tool-b")).toBe(extractorB);
41
+ });
42
+ });
43
+
44
+ describe("disposer identity guard", () => {
45
+ test("stale disposer does not evict a later registration", () => {
46
+ const registry = new ToolAccessExtractorRegistry();
47
+ const first: ToolAccessExtractor = () => "/first";
48
+ const second: ToolAccessExtractor = () => "/second";
49
+
50
+ const disposeFirst = registry.register("my-tool", first);
51
+ disposeFirst(); // removes first
52
+
53
+ registry.register("my-tool", second); // second registration is now valid
54
+ disposeFirst(); // calling stale disposer again — must not remove second
55
+
56
+ expect(registry.get("my-tool")).toBe(second);
57
+ });
58
+ });
59
+
60
+ describe("get", () => {
61
+ test("returns undefined for an unregistered tool name", () => {
62
+ const registry = new ToolAccessExtractorRegistry();
63
+ expect(registry.get("unknown")).toBeUndefined();
64
+ });
65
+
66
+ test("the registered extractor is callable and returns its path", () => {
67
+ const registry = new ToolAccessExtractorRegistry();
68
+ const extractor: ToolAccessExtractor = (input) =>
69
+ typeof input.target === "string" ? input.target : undefined;
70
+ registry.register("ffgrep", extractor);
71
+ expect(registry.get("ffgrep")?.({ target: "/etc/hosts" })).toBe(
72
+ "/etc/hosts",
73
+ );
74
+ expect(registry.get("ffgrep")?.({ other: true })).toBeUndefined();
75
+ });
76
+ });
77
+ });