@gotgenes/pi-permission-system 10.0.0 → 10.2.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 +33 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agent-prep-session.ts +28 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +11 -0
- package/src/forwarded-permissions/permission-forwarder.ts +549 -0
- package/src/forwarding-manager.ts +3 -7
- package/src/gate-handler-session.ts +13 -0
- package/src/gate-prompter.ts +14 -0
- package/src/handlers/before-agent-start.ts +2 -3
- package/src/handlers/gates/bash-command.ts +4 -18
- package/src/handlers/gates/bash-external-directory.ts +3 -15
- package/src/handlers/gates/bash-path.ts +3 -16
- package/src/handlers/gates/descriptor.ts +0 -28
- package/src/handlers/gates/path.ts +3 -15
- package/src/handlers/gates/runner.ts +142 -105
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +44 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
- package/src/handlers/lifecycle.ts +9 -9
- package/src/handlers/permission-gate-handler.ts +34 -238
- package/src/index.ts +53 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +83 -27
- package/src/permissions-service.ts +53 -0
- package/src/runtime.ts +1 -37
- package/src/service-lifecycle.ts +49 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-lifecycle-session.ts +24 -0
- package/src/tool-input-preview.ts +0 -62
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +6 -4
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +62 -0
- package/test/forwarding-manager.test.ts +26 -44
- package/test/handlers/before-agent-start.test.ts +45 -21
- package/test/handlers/external-directory-integration.test.ts +83 -114
- package/test/handlers/external-directory-session-dedup.test.ts +102 -55
- package/test/handlers/gates/bash-command.test.ts +49 -90
- package/test/handlers/gates/bash-external-directory.test.ts +54 -95
- package/test/handlers/gates/bash-path.test.ts +54 -157
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +151 -186
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +128 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
- package/test/handlers/input.test.ts +1 -2
- package/test/handlers/lifecycle.test.ts +49 -33
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +212 -17
- package/test/helpers/handler-fixtures.ts +226 -29
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-forwarding.test.ts +0 -282
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +211 -105
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +2 -86
- package/test/service-lifecycle.test.ts +162 -0
- package/test/tool-input-preview.test.ts +0 -111
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/src/forwarded-permissions/polling.ts +0 -411
|
@@ -3,9 +3,11 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import { getEventInput } from "#src/handlers/permission-gate-handler";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
makeBashCommandCheck,
|
|
6
7
|
makeCheckResult,
|
|
7
8
|
makeCtx,
|
|
8
9
|
makeHandler,
|
|
10
|
+
makeSurfaceCheck,
|
|
9
11
|
makeToolCallEvent,
|
|
10
12
|
} from "#test/helpers/handler-fixtures";
|
|
11
13
|
|
|
@@ -65,11 +67,7 @@ describe("handleToolCall", () => {
|
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
it("blocks when tool is not registered", async () => {
|
|
68
|
-
const { handler } = makeHandler({
|
|
69
|
-
toolRegistry: {
|
|
70
|
-
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
71
|
-
},
|
|
72
|
-
});
|
|
70
|
+
const { handler } = makeHandler({ tools: ["read"] });
|
|
73
71
|
const result = await handler.handleToolCall(
|
|
74
72
|
makeToolCallEvent("unknown-tool"),
|
|
75
73
|
makeCtx(),
|
|
@@ -170,16 +168,11 @@ describe("handleToolCall — external-directory gate", () => {
|
|
|
170
168
|
.fn()
|
|
171
169
|
.mockReturnValue(makeCheckResult({ state: "deny" })),
|
|
172
170
|
},
|
|
173
|
-
|
|
174
|
-
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
175
|
-
},
|
|
171
|
+
tools: ["read"],
|
|
176
172
|
});
|
|
177
|
-
const event = {
|
|
178
|
-
type: "tool_call",
|
|
179
|
-
toolCallId: "tc-ext",
|
|
180
|
-
name: "read",
|
|
173
|
+
const event = makeToolCallEvent("read", {
|
|
181
174
|
input: { path: "/outside/project/file.ts" },
|
|
182
|
-
};
|
|
175
|
+
});
|
|
183
176
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
184
177
|
expect(result).toMatchObject({ block: true });
|
|
185
178
|
});
|
|
@@ -195,16 +188,11 @@ describe("handleToolCall — bash external-directory gate", () => {
|
|
|
195
188
|
.fn()
|
|
196
189
|
.mockReturnValue(makeCheckResult({ state: "deny" })),
|
|
197
190
|
},
|
|
198
|
-
|
|
199
|
-
getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
200
|
-
},
|
|
191
|
+
tools: ["bash"],
|
|
201
192
|
});
|
|
202
|
-
const event = {
|
|
203
|
-
type: "tool_call",
|
|
204
|
-
toolCallId: "tc-bash-ext",
|
|
205
|
-
name: "bash",
|
|
193
|
+
const event = makeToolCallEvent("bash", {
|
|
206
194
|
input: { command: "cat /outside/project/file.ts" },
|
|
207
|
-
};
|
|
195
|
+
});
|
|
208
196
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
209
197
|
expect(result).toMatchObject({ block: true });
|
|
210
198
|
});
|
|
@@ -214,44 +202,24 @@ describe("handleToolCall — bash external-directory gate", () => {
|
|
|
214
202
|
|
|
215
203
|
describe("handleToolCall — path gate (tools)", () => {
|
|
216
204
|
it("blocks a read of .env when path surface denies *.env", async () => {
|
|
217
|
-
const checkPermission = vi
|
|
218
|
-
.fn()
|
|
219
|
-
.mockImplementation(
|
|
220
|
-
(surface: string, _input: unknown, _agentName?: string) => {
|
|
221
|
-
if (surface === "path") {
|
|
222
|
-
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
223
|
-
}
|
|
224
|
-
return makeCheckResult();
|
|
225
|
-
},
|
|
226
|
-
);
|
|
227
205
|
const { handler } = makeHandler({
|
|
228
|
-
session: {
|
|
229
|
-
|
|
230
|
-
|
|
206
|
+
session: {
|
|
207
|
+
checkPermission: makeSurfaceCheck({
|
|
208
|
+
path: { state: "deny", matchedPattern: "*.env" },
|
|
209
|
+
}),
|
|
231
210
|
},
|
|
211
|
+
tools: ["read"],
|
|
232
212
|
});
|
|
233
|
-
const event = {
|
|
234
|
-
type: "tool_call",
|
|
235
|
-
toolCallId: "tc-path",
|
|
236
|
-
name: "read",
|
|
237
|
-
input: { path: ".env" },
|
|
238
|
-
};
|
|
213
|
+
const event = makeToolCallEvent("read", { input: { path: ".env" } });
|
|
239
214
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
240
215
|
expect(result).toMatchObject({ block: true });
|
|
241
216
|
});
|
|
242
217
|
|
|
243
218
|
it("allows a read when path surface allows", async () => {
|
|
244
|
-
const { handler } = makeHandler({
|
|
245
|
-
|
|
246
|
-
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
const event = {
|
|
250
|
-
type: "tool_call",
|
|
251
|
-
toolCallId: "tc-path-ok",
|
|
252
|
-
name: "read",
|
|
219
|
+
const { handler } = makeHandler({ tools: ["read"] });
|
|
220
|
+
const event = makeToolCallEvent("read", {
|
|
253
221
|
input: { path: "src/index.ts" },
|
|
254
|
-
};
|
|
222
|
+
});
|
|
255
223
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
256
224
|
expect(result).toEqual({});
|
|
257
225
|
});
|
|
@@ -261,28 +229,15 @@ describe("handleToolCall — path gate (tools)", () => {
|
|
|
261
229
|
|
|
262
230
|
describe("handleToolCall — bash path gate", () => {
|
|
263
231
|
it("blocks a bash command accessing .env when path surface denies", async () => {
|
|
264
|
-
const checkPermission = vi
|
|
265
|
-
.fn()
|
|
266
|
-
.mockImplementation(
|
|
267
|
-
(surface: string, _input: unknown, _agentName?: string) => {
|
|
268
|
-
if (surface === "path") {
|
|
269
|
-
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
270
|
-
}
|
|
271
|
-
return makeCheckResult();
|
|
272
|
-
},
|
|
273
|
-
);
|
|
274
232
|
const { handler } = makeHandler({
|
|
275
|
-
session: {
|
|
276
|
-
|
|
277
|
-
|
|
233
|
+
session: {
|
|
234
|
+
checkPermission: makeSurfaceCheck({
|
|
235
|
+
path: { state: "deny", matchedPattern: "*.env" },
|
|
236
|
+
}),
|
|
278
237
|
},
|
|
238
|
+
tools: ["bash"],
|
|
279
239
|
});
|
|
280
|
-
const event = {
|
|
281
|
-
type: "tool_call",
|
|
282
|
-
toolCallId: "tc-bash-path",
|
|
283
|
-
name: "bash",
|
|
284
|
-
input: { command: "cat .env" },
|
|
285
|
-
};
|
|
240
|
+
const event = makeToolCallEvent("bash", { input: { command: "cat .env" } });
|
|
286
241
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
287
242
|
expect(result).toMatchObject({ block: true });
|
|
288
243
|
});
|
|
@@ -292,108 +247,44 @@ describe("handleToolCall — bash path gate", () => {
|
|
|
292
247
|
|
|
293
248
|
describe("handleToolCall — bash command chain gate", () => {
|
|
294
249
|
it("blocks a chain when a later sub-command is denied (#301)", async () => {
|
|
295
|
-
const checkPermission = vi
|
|
296
|
-
.fn()
|
|
297
|
-
.mockImplementation((surface: string, input: unknown) => {
|
|
298
|
-
if (surface === "bash") {
|
|
299
|
-
const command = (input as { command?: string }).command ?? "";
|
|
300
|
-
return /^npm\b/.test(command)
|
|
301
|
-
? makeCheckResult({
|
|
302
|
-
state: "deny",
|
|
303
|
-
source: "bash",
|
|
304
|
-
command,
|
|
305
|
-
matchedPattern: "npm *",
|
|
306
|
-
})
|
|
307
|
-
: makeCheckResult({
|
|
308
|
-
state: "allow",
|
|
309
|
-
source: "bash",
|
|
310
|
-
command,
|
|
311
|
-
matchedPattern: "echo *",
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
return makeCheckResult({ state: "allow" });
|
|
315
|
-
});
|
|
316
250
|
const { handler } = makeHandler({
|
|
317
|
-
session: {
|
|
318
|
-
|
|
319
|
-
|
|
251
|
+
session: {
|
|
252
|
+
checkPermission: makeBashCommandCheck({
|
|
253
|
+
deny: /^npm\b/,
|
|
254
|
+
denyMatched: "npm *",
|
|
255
|
+
allowMatched: "echo *",
|
|
256
|
+
}),
|
|
320
257
|
},
|
|
258
|
+
tools: ["bash"],
|
|
321
259
|
});
|
|
322
|
-
const event = {
|
|
323
|
-
type: "tool_call",
|
|
324
|
-
toolCallId: "tc-bash-chain",
|
|
325
|
-
name: "bash",
|
|
260
|
+
const event = makeToolCallEvent("bash", {
|
|
326
261
|
input: { command: "echo start && npm install compromised-package" },
|
|
327
|
-
};
|
|
262
|
+
});
|
|
328
263
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
329
264
|
expect(result).toMatchObject({ block: true });
|
|
330
265
|
});
|
|
331
266
|
|
|
332
267
|
it("blocks a command nested inside command substitution (#306)", async () => {
|
|
333
|
-
const checkPermission = vi
|
|
334
|
-
.fn()
|
|
335
|
-
.mockImplementation((surface: string, input: unknown) => {
|
|
336
|
-
if (surface === "bash") {
|
|
337
|
-
const command = (input as { command?: string }).command ?? "";
|
|
338
|
-
return /^rm\b/.test(command)
|
|
339
|
-
? makeCheckResult({
|
|
340
|
-
state: "deny",
|
|
341
|
-
source: "bash",
|
|
342
|
-
command,
|
|
343
|
-
matchedPattern: "rm *",
|
|
344
|
-
})
|
|
345
|
-
: makeCheckResult({
|
|
346
|
-
state: "allow",
|
|
347
|
-
source: "bash",
|
|
348
|
-
command,
|
|
349
|
-
matchedPattern: "echo *",
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
return makeCheckResult({ state: "allow" });
|
|
353
|
-
});
|
|
354
268
|
const { handler } = makeHandler({
|
|
355
|
-
session: {
|
|
356
|
-
|
|
357
|
-
|
|
269
|
+
session: {
|
|
270
|
+
checkPermission: makeBashCommandCheck({
|
|
271
|
+
deny: /^rm\b/,
|
|
272
|
+
denyMatched: "rm *",
|
|
273
|
+
allowMatched: "echo *",
|
|
274
|
+
}),
|
|
358
275
|
},
|
|
276
|
+
tools: ["bash"],
|
|
359
277
|
});
|
|
360
|
-
const event = {
|
|
361
|
-
type: "tool_call",
|
|
362
|
-
toolCallId: "tc-bash-substitution",
|
|
363
|
-
name: "bash",
|
|
278
|
+
const event = makeToolCallEvent("bash", {
|
|
364
279
|
input: { command: "echo $(rm -rf foo)" },
|
|
365
|
-
};
|
|
280
|
+
});
|
|
366
281
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
367
282
|
expect(result).toMatchObject({ block: true });
|
|
368
283
|
});
|
|
369
284
|
|
|
370
285
|
it("allows a single non-chained bash command", async () => {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
.mockImplementation((surface: string, input: unknown) => {
|
|
374
|
-
if (surface === "bash") {
|
|
375
|
-
const command = (input as { command?: string }).command ?? "";
|
|
376
|
-
return makeCheckResult({
|
|
377
|
-
state: "allow",
|
|
378
|
-
source: "bash",
|
|
379
|
-
command,
|
|
380
|
-
matchedPattern: "echo *",
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
return makeCheckResult({ state: "allow" });
|
|
384
|
-
});
|
|
385
|
-
const { handler } = makeHandler({
|
|
386
|
-
session: { checkPermission },
|
|
387
|
-
toolRegistry: {
|
|
388
|
-
getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
const event = {
|
|
392
|
-
type: "tool_call",
|
|
393
|
-
toolCallId: "tc-bash-single",
|
|
394
|
-
name: "bash",
|
|
395
|
-
input: { command: "echo hi" },
|
|
396
|
-
};
|
|
286
|
+
const { handler } = makeHandler({ tools: ["bash"] });
|
|
287
|
+
const event = makeToolCallEvent("bash", { input: { command: "echo hi" } });
|
|
397
288
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
398
289
|
expect(result).toEqual({});
|
|
399
290
|
});
|
|
@@ -2,16 +2,36 @@
|
|
|
2
2
|
* Shared gate-level test fixtures for gate descriptor and runner tests.
|
|
3
3
|
*/
|
|
4
4
|
import { vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "#src/handlers/gates/
|
|
5
|
+
import type { DecisionReporter } from "#src/decision-reporter";
|
|
6
|
+
import type { DenialContext } from "#src/denial-messages";
|
|
7
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
8
|
+
import type { GateDescriptor } from "#src/handlers/gates/descriptor";
|
|
9
|
+
import { GateRunner } from "#src/handlers/gates/runner";
|
|
10
|
+
import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
|
|
11
|
+
import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
10
12
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
13
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
14
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
15
|
+
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
16
|
+
import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
|
|
11
17
|
import type { PermissionCheckResult } from "#src/types";
|
|
12
18
|
|
|
13
19
|
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
14
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Permission resolver mock with an optional default check result.
|
|
23
|
+
*
|
|
24
|
+
* Returns a plain object whose `resolve` is a `vi.fn` so callers retain full
|
|
25
|
+
* mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
|
|
26
|
+
*/
|
|
27
|
+
export function makeResolver(defaultCheck?: PermissionCheckResult) {
|
|
28
|
+
const resolve = vi.fn<PermissionResolver["resolve"]>();
|
|
29
|
+
if (defaultCheck) {
|
|
30
|
+
resolve.mockReturnValue(defaultCheck);
|
|
31
|
+
}
|
|
32
|
+
return { resolve };
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
/**
|
|
16
36
|
* Gate descriptor factory with runner-test defaults.
|
|
17
37
|
*
|
|
@@ -48,21 +68,105 @@ export function makeDescriptor(
|
|
|
48
68
|
};
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Reporter mock with independently inspectable vi.fn() stubs.
|
|
73
|
+
*/
|
|
74
|
+
export function makeReporter(
|
|
75
|
+
overrides: Partial<DecisionReporter> = {},
|
|
76
|
+
): DecisionReporter {
|
|
54
77
|
return {
|
|
55
|
-
checkPermission: vi
|
|
56
|
-
.fn()
|
|
57
|
-
.mockReturnValue(makeCheckResult({ matchedPattern: "*" })),
|
|
58
|
-
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
59
|
-
recordSessionApproval: vi.fn(),
|
|
60
78
|
writeReviewLog: vi.fn(),
|
|
61
79
|
emitDecision: vi.fn(),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
...overrides,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gate runner factory for `GateRunner` unit tests.
|
|
86
|
+
*
|
|
87
|
+
* Builds one `GateRunner` from four role mocks and returns `{ runner, deps }`
|
|
88
|
+
* so tests can both invoke `runner.run(...)` and assert on the individual
|
|
89
|
+
* mock call records (`deps.reporter.*`, `deps.resolve`, etc.).
|
|
90
|
+
*/
|
|
91
|
+
export function makeGateRunner(
|
|
92
|
+
overrides: {
|
|
93
|
+
resolveResult?: PermissionCheckResult;
|
|
94
|
+
resolve?: PermissionResolver["resolve"];
|
|
95
|
+
recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
|
|
96
|
+
canConfirm?: GatePrompter["canConfirm"];
|
|
97
|
+
promptPermission?: GatePrompter["promptPermission"];
|
|
98
|
+
reporter?: Partial<DecisionReporter>;
|
|
99
|
+
} = {},
|
|
100
|
+
) {
|
|
101
|
+
const reporter = makeReporter(overrides.reporter);
|
|
102
|
+
const resolve =
|
|
103
|
+
overrides.resolve ??
|
|
104
|
+
vi
|
|
105
|
+
.fn<PermissionResolver["resolve"]>()
|
|
106
|
+
.mockReturnValue(
|
|
107
|
+
overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
|
|
108
|
+
);
|
|
109
|
+
const recordSessionApproval =
|
|
110
|
+
overrides.recordSessionApproval ??
|
|
111
|
+
(vi.fn() as SessionApprovalRecorder["recordSessionApproval"]);
|
|
112
|
+
const canConfirm =
|
|
113
|
+
overrides.canConfirm ??
|
|
114
|
+
(vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
|
|
115
|
+
const promptPermission =
|
|
116
|
+
overrides.promptPermission ??
|
|
117
|
+
vi
|
|
118
|
+
.fn<GatePrompter["promptPermission"]>()
|
|
119
|
+
.mockResolvedValue({ approved: true, state: "approved" });
|
|
120
|
+
const runner = new GateRunner(
|
|
121
|
+
{ resolve },
|
|
122
|
+
{ recordSessionApproval },
|
|
123
|
+
{ canConfirm, promptPermission },
|
|
124
|
+
reporter,
|
|
125
|
+
);
|
|
126
|
+
return {
|
|
127
|
+
runner,
|
|
128
|
+
deps: {
|
|
129
|
+
resolve,
|
|
130
|
+
recordSessionApproval,
|
|
131
|
+
canConfirm,
|
|
132
|
+
promptPermission,
|
|
133
|
+
reporter,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Gate descriptor variant with write-surface defaults and a caller-supplied
|
|
140
|
+
* denialContext.
|
|
141
|
+
*
|
|
142
|
+
* Use instead of `makeDescriptor` when the test exercises denial-message
|
|
143
|
+
* formatting — the write surface and its matching promptDetails/logContext
|
|
144
|
+
* keep the message helpers' field access consistent.
|
|
145
|
+
*/
|
|
146
|
+
export function makeDenialDescriptor(
|
|
147
|
+
denialContext: DenialContext,
|
|
148
|
+
overrides: Partial<GateDescriptor> = {},
|
|
149
|
+
): GateDescriptor {
|
|
150
|
+
return {
|
|
151
|
+
surface: "write",
|
|
152
|
+
input: {},
|
|
153
|
+
denialContext,
|
|
154
|
+
promptDetails: {
|
|
155
|
+
source: "tool_call",
|
|
156
|
+
agentName: null,
|
|
157
|
+
message: "Allow tool 'write'?",
|
|
158
|
+
toolCallId: "tc-1",
|
|
159
|
+
toolName: "write",
|
|
160
|
+
},
|
|
161
|
+
logContext: {
|
|
162
|
+
source: "tool_call",
|
|
163
|
+
toolCallId: "tc-1",
|
|
164
|
+
toolName: "write",
|
|
165
|
+
},
|
|
166
|
+
decision: {
|
|
167
|
+
surface: "write",
|
|
168
|
+
value: "write",
|
|
169
|
+
},
|
|
66
170
|
...overrides,
|
|
67
171
|
};
|
|
68
172
|
}
|
|
@@ -86,6 +190,31 @@ export function makeTcc(
|
|
|
86
190
|
};
|
|
87
191
|
}
|
|
88
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Resolver whose `resolve` dispatches on `input.path`, falling back to a
|
|
195
|
+
* default result for any path not in the map.
|
|
196
|
+
*
|
|
197
|
+
* Use when a test needs different results for different path tokens without
|
|
198
|
+
* writing a full `mockImplementation` block.
|
|
199
|
+
*
|
|
200
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
201
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
|
|
202
|
+
*/
|
|
203
|
+
export function makePathDispatchResolver(
|
|
204
|
+
byPath: Record<string, PermissionCheckResult>,
|
|
205
|
+
defaultResult: PermissionCheckResult,
|
|
206
|
+
) {
|
|
207
|
+
const resolve = vi.fn<PermissionResolver["resolve"]>();
|
|
208
|
+
resolve.mockImplementation((_surface, input) => {
|
|
209
|
+
const path = (input as Record<string, unknown>).path;
|
|
210
|
+
if (typeof path === "string" && path in byPath) {
|
|
211
|
+
return byPath[path];
|
|
212
|
+
}
|
|
213
|
+
return defaultResult;
|
|
214
|
+
});
|
|
215
|
+
return { resolve };
|
|
216
|
+
}
|
|
217
|
+
|
|
89
218
|
/**
|
|
90
219
|
* Path-surface check result factory.
|
|
91
220
|
*
|
|
@@ -103,3 +232,69 @@ export function makeGateCheckResult(
|
|
|
103
232
|
...overrides,
|
|
104
233
|
};
|
|
105
234
|
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Mock of `ToolCallGateInputs` for `ToolCallGatePipeline` unit tests.
|
|
238
|
+
*
|
|
239
|
+
* Each method is a `vi.fn()` stub so callers retain full mock access
|
|
240
|
+
* (`mock.calls`, `mockReturnValue`, etc.) on the returned object.
|
|
241
|
+
* Pass `overrides` to replace individual stubs without rebuilding the whole
|
|
242
|
+
* mock from scratch.
|
|
243
|
+
*/
|
|
244
|
+
export function makeGateInputs(
|
|
245
|
+
overrides: {
|
|
246
|
+
resolve?: PermissionResolver["resolve"];
|
|
247
|
+
getActiveSkillEntries?: () => SkillPromptEntry[];
|
|
248
|
+
getInfrastructureReadDirs?: () => string[];
|
|
249
|
+
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
250
|
+
} = {},
|
|
251
|
+
): ToolCallGateInputs {
|
|
252
|
+
return {
|
|
253
|
+
resolve:
|
|
254
|
+
overrides.resolve ??
|
|
255
|
+
vi.fn<PermissionResolver["resolve"]>().mockReturnValue(makeCheckResult()),
|
|
256
|
+
getActiveSkillEntries:
|
|
257
|
+
overrides.getActiveSkillEntries ??
|
|
258
|
+
vi.fn<() => SkillPromptEntry[]>(() => []),
|
|
259
|
+
getInfrastructureReadDirs:
|
|
260
|
+
overrides.getInfrastructureReadDirs ?? vi.fn<() => string[]>(() => []),
|
|
261
|
+
getToolPreviewLimits:
|
|
262
|
+
overrides.getToolPreviewLimits ??
|
|
263
|
+
vi.fn<() => ToolPreviewFormatterOptions>(() => ({
|
|
264
|
+
toolInputPreviewMaxLength: 500,
|
|
265
|
+
toolTextSummaryMaxLength: 100,
|
|
266
|
+
toolInputLogPreviewMaxLength: 200,
|
|
267
|
+
})),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Mock of `SkillInputGateInputs` for `SkillInputGatePipeline` unit tests.
|
|
273
|
+
*
|
|
274
|
+
* Returns a plain object with a `checkPermission` `vi.fn()` stub so callers
|
|
275
|
+
* retain full mock access (`mockReturnValue`, `mock.calls`, etc.).
|
|
276
|
+
*/
|
|
277
|
+
export function makeSkillInputInputs(
|
|
278
|
+
overrides: { checkPermission?: SkillInputGateInputs["checkPermission"] } = {},
|
|
279
|
+
): SkillInputGateInputs {
|
|
280
|
+
return {
|
|
281
|
+
checkPermission:
|
|
282
|
+
overrides.checkPermission ??
|
|
283
|
+
vi
|
|
284
|
+
.fn<SkillInputGateInputs["checkPermission"]>()
|
|
285
|
+
.mockReturnValue(makeCheckResult()),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Mock `GateNotifier` for `SkillInputGatePipeline` unit tests.
|
|
291
|
+
*
|
|
292
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
293
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.) — annotating with
|
|
294
|
+
* `GateNotifier` would erase `Mock<...>` methods from the inferred type.
|
|
295
|
+
*/
|
|
296
|
+
export function makeNotifier() {
|
|
297
|
+
return {
|
|
298
|
+
warn: vi.fn<(message: string) => void>(),
|
|
299
|
+
};
|
|
300
|
+
}
|