@gotgenes/pi-permission-system 8.2.0 → 8.3.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 (35) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +1 -1
  3. package/src/builtin-tool-input-formatters.ts +82 -0
  4. package/src/config-loader.ts +53 -46
  5. package/src/handlers/gates/bash-path-extractor.ts +135 -169
  6. package/src/handlers/gates/bash-token-classification.ts +105 -0
  7. package/src/handlers/permission-gate-handler.ts +3 -0
  8. package/src/index.ts +13 -1
  9. package/src/permission-prompts.ts +5 -1
  10. package/src/service.ts +21 -1
  11. package/src/tool-input-formatter-registry.ts +57 -0
  12. package/src/tool-preview-formatter.ts +18 -1
  13. package/test/builtin-tool-input-formatters.test.ts +109 -0
  14. package/test/config-loader.test.ts +82 -0
  15. package/test/handlers/before-agent-start.test.ts +2 -20
  16. package/test/handlers/external-directory-integration.test.ts +43 -81
  17. package/test/handlers/external-directory-session-dedup.test.ts +2 -29
  18. package/test/handlers/gates/bash-path.test.ts +5 -26
  19. package/test/handlers/gates/bash-token-classification.test.ts +241 -0
  20. package/test/handlers/gates/path.test.ts +3 -12
  21. package/test/handlers/gates/runner.test.ts +78 -91
  22. package/test/handlers/input-events.test.ts +42 -95
  23. package/test/handlers/input.test.ts +3 -71
  24. package/test/handlers/lifecycle.test.ts +3 -20
  25. package/test/handlers/tool-call-events.test.ts +30 -127
  26. package/test/handlers/tool-call.test.ts +21 -110
  27. package/test/helpers/gate-fixtures.ts +105 -0
  28. package/test/helpers/handler-fixtures.ts +141 -0
  29. package/test/helpers/manager-harness.ts +51 -0
  30. package/test/permission-prompts.test.ts +53 -7
  31. package/test/permission-session.test.ts +1 -19
  32. package/test/permission-system.test.ts +4 -40
  33. package/test/service.test.ts +52 -0
  34. package/test/tool-input-formatter-registry.test.ts +75 -0
  35. package/test/tool-preview-formatter.test.ts +73 -0
@@ -6,6 +6,7 @@ import {
6
6
  publishPermissionsService,
7
7
  unpublishPermissionsService,
8
8
  } from "#src/service";
9
+ import { ToolInputFormatterRegistry } from "#src/tool-input-formatter-registry";
9
10
  import type { PermissionCheckResult } from "#src/types";
10
11
 
11
12
  // ── helpers ────────────────────────────────────────────────────────────────
@@ -16,6 +17,7 @@ function makeService(
16
17
  return {
17
18
  checkPermission: vi.fn(),
18
19
  getToolPermission: vi.fn(),
20
+ registerToolInputFormatter: vi.fn(),
19
21
  ...overrides,
20
22
  };
21
23
  }
@@ -136,6 +138,7 @@ describe("service adapter delegation", () => {
136
138
  getToolPermission(toolName, agentName) {
137
139
  return getToolPermissionFn(toolName, agentName);
138
140
  },
141
+ registerToolInputFormatter: vi.fn(),
139
142
  };
140
143
 
141
144
  publishPermissionsService(service);
@@ -157,6 +160,7 @@ describe("service adapter delegation", () => {
157
160
  getToolPermission(toolName, agentName) {
158
161
  return getToolPermissionFn(toolName, agentName);
159
162
  },
163
+ registerToolInputFormatter: vi.fn(),
160
164
  };
161
165
 
162
166
  publishPermissionsService(service);
@@ -182,3 +186,51 @@ describe("service adapter delegation", () => {
182
186
  expect(checkPermission).toHaveBeenCalledWith("read", {}, undefined, []);
183
187
  });
184
188
  });
189
+
190
+ // ── registerToolInputFormatter delegation ─────────────────────────────────
191
+
192
+ describe("registerToolInputFormatter delegation", () => {
193
+ afterEach(() => {
194
+ unpublishPermissionsService();
195
+ });
196
+
197
+ it("delegates to the registry and returns its disposer", () => {
198
+ const registry = new ToolInputFormatterRegistry();
199
+ const formatter = () => "preview";
200
+
201
+ const service = makeService({
202
+ registerToolInputFormatter(toolName, fmt) {
203
+ return registry.register(toolName, fmt);
204
+ },
205
+ });
206
+
207
+ publishPermissionsService(service);
208
+ const dispose = getPermissionsService()!.registerToolInputFormatter(
209
+ "my-tool",
210
+ formatter,
211
+ );
212
+
213
+ // Registry received the registration
214
+ expect(registry.get("my-tool")).toBe(formatter);
215
+
216
+ // Disposer returned from service removes it from the registry
217
+ dispose();
218
+ expect(registry.get("my-tool")).toBeUndefined();
219
+ });
220
+
221
+ it("throws when a formatter is already registered for the tool name", () => {
222
+ const registry = new ToolInputFormatterRegistry();
223
+ registry.register("my-tool", () => undefined);
224
+
225
+ const service = makeService({
226
+ registerToolInputFormatter(toolName, fmt) {
227
+ return registry.register(toolName, fmt);
228
+ },
229
+ });
230
+
231
+ publishPermissionsService(service);
232
+ expect(() =>
233
+ getPermissionsService()!.registerToolInputFormatter("my-tool", () => ""),
234
+ ).toThrow("my-tool");
235
+ });
236
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "vitest";
2
+
3
+ import {
4
+ type ToolInputFormatter,
5
+ ToolInputFormatterRegistry,
6
+ } from "#src/tool-input-formatter-registry";
7
+
8
+ const noopFormatter: ToolInputFormatter = () => "preview";
9
+
10
+ describe("ToolInputFormatterRegistry", () => {
11
+ describe("register", () => {
12
+ test("stores a formatter so get() returns it", () => {
13
+ const registry = new ToolInputFormatterRegistry();
14
+ registry.register("my-tool", noopFormatter);
15
+ expect(registry.get("my-tool")).toBe(noopFormatter);
16
+ });
17
+
18
+ test("returns a disposer that removes the formatter", () => {
19
+ const registry = new ToolInputFormatterRegistry();
20
+ const dispose = registry.register("my-tool", noopFormatter);
21
+ dispose();
22
+ expect(registry.get("my-tool")).toBeUndefined();
23
+ });
24
+
25
+ test("throws when a formatter is already registered for the same tool name", () => {
26
+ const registry = new ToolInputFormatterRegistry();
27
+ registry.register("my-tool", noopFormatter);
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 ToolInputFormatterRegistry();
35
+ const formatterA: ToolInputFormatter = () => "a";
36
+ const formatterB: ToolInputFormatter = () => "b";
37
+ registry.register("tool-a", formatterA);
38
+ registry.register("tool-b", formatterB);
39
+ expect(registry.get("tool-a")).toBe(formatterA);
40
+ expect(registry.get("tool-b")).toBe(formatterB);
41
+ });
42
+ });
43
+
44
+ describe("disposer identity guard", () => {
45
+ test("stale disposer does not evict a later registration", () => {
46
+ const registry = new ToolInputFormatterRegistry();
47
+ const first: ToolInputFormatter = () => "first";
48
+ const second: ToolInputFormatter = () => "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 ToolInputFormatterRegistry();
63
+ expect(registry.get("unknown")).toBeUndefined();
64
+ });
65
+
66
+ test("the registered formatter is callable and returns its result", () => {
67
+ const registry = new ToolInputFormatterRegistry();
68
+ const fmt: ToolInputFormatter = (input) =>
69
+ typeof input.cmd === "string" ? `runs ${input.cmd}` : undefined;
70
+ registry.register("run", fmt);
71
+ expect(registry.get("run")?.({ cmd: "ls" })).toBe("runs ls");
72
+ expect(registry.get("run")?.({ other: true })).toBeUndefined();
73
+ });
74
+ });
75
+ });
@@ -1,5 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
2
 
3
+ import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
4
+
3
5
  // Mock logging collaborator before importing the module under test.
4
6
  vi.mock("../src/logging.js", () => ({
5
7
  safeJsonStringify: vi.fn((value: unknown) => JSON.stringify(value)),
@@ -201,6 +203,77 @@ describe("ToolPreviewFormatter.formatToolInputForPrompt", () => {
201
203
  });
202
204
  });
203
205
 
206
+ // ── formatToolInputForPrompt (custom formatter seam) ───────────────────────
207
+
208
+ describe("ToolPreviewFormatter.formatToolInputForPrompt — custom formatter seam", () => {
209
+ function makeLookup(
210
+ toolName: string,
211
+ result: string | undefined,
212
+ ): ToolInputFormatterLookup {
213
+ return {
214
+ get: (name) => (name === toolName ? () => result : undefined),
215
+ };
216
+ }
217
+
218
+ test("uses a custom formatter's string result verbatim, bypassing the switch", () => {
219
+ const lookup = makeLookup("my-tool", "custom preview");
220
+ const f = new ToolPreviewFormatter(
221
+ {
222
+ toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
223
+ toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
224
+ toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
225
+ },
226
+ lookup,
227
+ );
228
+ expect(f.formatToolInputForPrompt("my-tool", {})).toBe("custom preview");
229
+ });
230
+
231
+ test("falls through to the built-in switch when custom formatter returns undefined", () => {
232
+ mockedStringify.mockReturnValue('{"x":1}');
233
+ const lookup = makeLookup("unknown-tool", undefined);
234
+ const f = new ToolPreviewFormatter(
235
+ {
236
+ toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
237
+ toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
238
+ toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
239
+ },
240
+ lookup,
241
+ );
242
+ // Falls through to JSON default for unknown tools
243
+ expect(f.formatToolInputForPrompt("unknown-tool", { x: 1 })).toContain(
244
+ '{"x":1}',
245
+ );
246
+ });
247
+
248
+ test("custom formatter for a built-in tool overrides the built-in preview", () => {
249
+ const lookup = makeLookup("read", "custom read summary");
250
+ const f = new ToolPreviewFormatter(
251
+ {
252
+ toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
253
+ toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
254
+ toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
255
+ },
256
+ lookup,
257
+ );
258
+ // Would normally use formatReadInputForPrompt; custom overrides it
259
+ expect(f.formatToolInputForPrompt("read", { path: "/foo.ts" })).toBe(
260
+ "custom read summary",
261
+ );
262
+ });
263
+
264
+ test("absent lookup preserves current behaviour for all tool types", () => {
265
+ const f = new ToolPreviewFormatter({
266
+ toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
267
+ toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
268
+ toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
269
+ });
270
+ // Built-in path still works
271
+ expect(f.formatToolInputForPrompt("read", { path: "/foo.ts" })).toContain(
272
+ "/foo.ts",
273
+ );
274
+ });
275
+ });
276
+
204
277
  // ── formatGenericToolInputForLog ──────────────────────────────────────────
205
278
 
206
279
  describe("ToolPreviewFormatter.formatGenericToolInputForLog", () => {