@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.
- package/CHANGELOG.md +43 -0
- package/README.md +9 -11
- package/package.json +1 -1
- package/schemas/permissions.schema.json +1 -1
- package/src/config-modal.ts +3 -7
- package/src/extension-config.ts +11 -25
- package/src/handlers/gates/bash-path-extractor.ts +0 -12
- package/src/handlers/gates/bash-path.ts +12 -10
- package/src/handlers/gates/bash-program.ts +52 -11
- package/src/handlers/gates/bash-token-classification.ts +1 -1
- package/src/handlers/gates/external-directory.ts +8 -2
- package/src/handlers/gates/path.ts +4 -2
- package/src/handlers/gates/tool-call-gate-pipeline.ts +5 -2
- package/src/index.ts +8 -2
- package/src/input-normalizer.ts +17 -11
- package/src/path-utils.ts +122 -0
- package/src/permission-manager.ts +81 -17
- package/src/permission-resolver.ts +24 -0
- package/src/permission-session.ts +2 -4
- package/src/permissions-service.ts +12 -0
- package/src/rule.ts +61 -11
- package/src/service.ts +24 -0
- package/src/tool-access-extractor-registry.ts +68 -0
- package/test/bash-external-directory.test.ts +1 -81
- package/test/composition-root.test.ts +36 -0
- package/test/config-modal.test.ts +7 -10
- package/test/config-pipeline.test.ts +90 -0
- package/test/extension-config.test.ts +0 -58
- package/test/handlers/gates/bash-path.test.ts +45 -2
- package/test/handlers/gates/bash-program.test.ts +44 -11
- package/test/handlers/gates/external-directory.test.ts +54 -0
- package/test/handlers/gates/path.test.ts +72 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +64 -1
- package/test/helpers/gate-fixtures.ts +23 -3
- package/test/helpers/handler-fixtures.ts +14 -2
- package/test/helpers/session-fixtures.ts +14 -0
- package/test/input-normalizer.test.ts +52 -0
- package/test/path-utils.test.ts +135 -0
- package/test/permission-manager-unified.test.ts +134 -0
- package/test/permission-resolver.test.ts +69 -0
- package/test/permissions-service.test.ts +35 -1
- package/test/rule.test.ts +74 -1
- package/test/service-lifecycle.test.ts +1 -0
- package/test/service.test.ts +53 -0
- 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 {
|
|
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",
|
package/test/service.test.ts
CHANGED
|
@@ -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
|
+
});
|