@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.
- package/CHANGELOG.md +23 -0
- package/package.json +1 -1
- package/src/builtin-tool-input-formatters.ts +82 -0
- package/src/config-loader.ts +53 -46
- package/src/handlers/gates/bash-path-extractor.ts +135 -169
- package/src/handlers/gates/bash-token-classification.ts +105 -0
- package/src/handlers/permission-gate-handler.ts +3 -0
- package/src/index.ts +13 -1
- package/src/permission-prompts.ts +5 -1
- package/src/service.ts +21 -1
- package/src/tool-input-formatter-registry.ts +57 -0
- package/src/tool-preview-formatter.ts +18 -1
- package/test/builtin-tool-input-formatters.test.ts +109 -0
- package/test/config-loader.test.ts +82 -0
- package/test/handlers/before-agent-start.test.ts +2 -20
- package/test/handlers/external-directory-integration.test.ts +43 -81
- package/test/handlers/external-directory-session-dedup.test.ts +2 -29
- package/test/handlers/gates/bash-path.test.ts +5 -26
- package/test/handlers/gates/bash-token-classification.test.ts +241 -0
- package/test/handlers/gates/path.test.ts +3 -12
- package/test/handlers/gates/runner.test.ts +78 -91
- package/test/handlers/input-events.test.ts +42 -95
- package/test/handlers/input.test.ts +3 -71
- package/test/handlers/lifecycle.test.ts +3 -20
- package/test/handlers/tool-call-events.test.ts +30 -127
- package/test/handlers/tool-call.test.ts +21 -110
- package/test/helpers/gate-fixtures.ts +105 -0
- package/test/helpers/handler-fixtures.ts +141 -0
- package/test/helpers/manager-harness.ts +51 -0
- package/test/permission-prompts.test.ts +53 -7
- package/test/permission-session.test.ts +1 -19
- package/test/permission-system.test.ts +4 -40
- package/test/service.test.ts +52 -0
- package/test/tool-input-formatter-registry.test.ts +75 -0
- package/test/tool-preview-formatter.test.ts +73 -0
package/test/service.test.ts
CHANGED
|
@@ -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", () => {
|