@gotgenes/pi-permission-system 16.2.0 → 16.2.1
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 +15 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/access-intent/bash/command-enumeration.ts +79 -2
- package/src/access-intent/bash/parser.ts +2 -0
- package/src/access-intent/bash/program.ts +3 -0
- package/src/builtin-tool-input-formatters.ts +1 -1
- package/src/config-loader.ts +7 -7
- package/src/forwarded-permissions/permission-forwarder.ts +1 -1
- package/src/handlers/gates/bash-command.ts +15 -1
- package/src/handlers/gates/bash-external-directory.ts +1 -1
- package/src/handlers/gates/bash-path.ts +1 -1
- package/src/handlers/gates/skill-read.ts +1 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +1 -1
- package/src/handlers/permission-gate-handler.ts +1 -2
- package/src/handlers/tool-call-boundary.ts +1 -2
- package/src/input-normalizer.ts +1 -1
- package/src/mcp-targets.ts +1 -1
- package/src/normalize.ts +1 -1
- package/src/path-utils.ts +1 -1
- package/src/permission-manager.ts +1 -1
- package/src/permission-prompts.ts +1 -1
- package/src/policy-loader.ts +2 -2
- package/src/tool-input-prompt-formatters.ts +1 -1
- package/src/tool-preview-formatter.ts +1 -1
- package/src/tool-registry.ts +1 -1
- package/src/{common.ts → value-guards.ts} +0 -66
- package/src/yaml-frontmatter.ts +65 -0
- package/test/access-intent/bash/node-text.test.ts +1 -0
- package/test/access-intent/bash/program.test.ts +67 -0
- package/test/handlers/external-directory-integration.test.ts +40 -153
- package/test/handlers/external-directory-session-dedup.test.ts +38 -262
- package/test/handlers/gates/bash-command.test.ts +63 -0
- package/test/handlers/gates/bash-external-directory.test.ts +1 -1
- package/test/handlers/gates/bash-path.test.ts +1 -1
- package/test/helpers/external-directory-fixtures.ts +269 -0
- package/test/{common.test.ts → value-guards.test.ts} +2 -96
- package/test/yaml-frontmatter.test.ts +91 -0
|
@@ -435,6 +435,73 @@ describe("BashProgram", () => {
|
|
|
435
435
|
expect((await BashProgram.parse("", cwd)).commands()).toEqual([]);
|
|
436
436
|
expect((await BashProgram.parse(" ", cwd)).commands()).toEqual([]);
|
|
437
437
|
});
|
|
438
|
+
|
|
439
|
+
it("strips a leading env-var assignment prefix", async () => {
|
|
440
|
+
const program = await BashProgram.parse(
|
|
441
|
+
"AWS_PROFILE=prod aws ec2 terminate-instances --instance-ids i-1",
|
|
442
|
+
cwd,
|
|
443
|
+
);
|
|
444
|
+
expect(program.commands()).toEqual([
|
|
445
|
+
{ text: "aws ec2 terminate-instances --instance-ids i-1" },
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("strips multiple leading env-var assignments", async () => {
|
|
450
|
+
const program = await BashProgram.parse("A=1 B=2 aws s3 ls", cwd);
|
|
451
|
+
expect(program.commands()).toEqual([{ text: "aws s3 ls" }]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("strips the env-var prefix of each command in a chain", async () => {
|
|
455
|
+
const program = await BashProgram.parse(
|
|
456
|
+
"X=1 aws sts get-caller-identity && ls",
|
|
457
|
+
cwd,
|
|
458
|
+
);
|
|
459
|
+
expect(program.commands()).toEqual([
|
|
460
|
+
{ text: "aws sts get-caller-identity" },
|
|
461
|
+
{ text: "ls" },
|
|
462
|
+
]);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("keeps a pure assignment with no command unchanged", async () => {
|
|
466
|
+
const program = await BashProgram.parse("FOO=bar", cwd);
|
|
467
|
+
expect(program.commands()).toEqual([{ text: "FOO=bar" }]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("opaque-payload wrappers", () => {
|
|
471
|
+
it.each([
|
|
472
|
+
['bash -c "rm -rf /"', 'bash -c "rm -rf /"'],
|
|
473
|
+
['sh -c "rm -rf /"', 'sh -c "rm -rf /"'],
|
|
474
|
+
['dash -c "rm -rf /"', 'dash -c "rm -rf /"'],
|
|
475
|
+
['zsh -c "rm -rf /"', 'zsh -c "rm -rf /"'],
|
|
476
|
+
['ksh -c "rm -rf /"', 'ksh -c "rm -rf /"'],
|
|
477
|
+
['eval "rm -rf /"', 'eval "rm -rf /"'],
|
|
478
|
+
['/bin/bash -c "rm -rf /"', '/bin/bash -c "rm -rf /"'],
|
|
479
|
+
['bash -ec "rm -rf /"', 'bash -ec "rm -rf /"'],
|
|
480
|
+
])("flags %s as opaque", async (command, text) => {
|
|
481
|
+
const program = await BashProgram.parse(command, cwd);
|
|
482
|
+
expect(program.commands()).toEqual([{ text, opaque: true }]);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("flags an env-prefixed wrapper as opaque after stripping the prefix", async () => {
|
|
486
|
+
const program = await BashProgram.parse(
|
|
487
|
+
'AWS_PROFILE=prod bash -c "rm -rf /"',
|
|
488
|
+
cwd,
|
|
489
|
+
);
|
|
490
|
+
expect(program.commands()).toEqual([
|
|
491
|
+
{ text: 'bash -c "rm -rf /"', opaque: true },
|
|
492
|
+
]);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it.each([
|
|
496
|
+
"bash script.sh",
|
|
497
|
+
"bash",
|
|
498
|
+
"ls -la",
|
|
499
|
+
"grep -c foo file",
|
|
500
|
+
])("does not flag %s as opaque", async (command) => {
|
|
501
|
+
const program = await BashProgram.parse(command, cwd);
|
|
502
|
+
expect(program.commands()).toEqual([{ text: command }]);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
438
505
|
});
|
|
439
506
|
|
|
440
507
|
it("derives both slices from a single parse", async () => {
|
|
@@ -12,15 +12,25 @@
|
|
|
12
12
|
import { describe, expect, it, vi } from "vitest";
|
|
13
13
|
|
|
14
14
|
import { EXTENSION_TAG } from "#src/denial-messages";
|
|
15
|
-
import type { GatePrompter } from "#src/gate-prompter";
|
|
16
15
|
import { formatExternalDirectoryAskPrompt } from "#src/handlers/gates/external-directory-messages";
|
|
17
16
|
import type { PermissionCheckResult } from "#src/types";
|
|
18
|
-
|
|
17
|
+
import {
|
|
18
|
+
ALL_PATH_BEARING_TOOLS,
|
|
19
|
+
ALL_TOOLS,
|
|
20
|
+
blockReviewEntries,
|
|
21
|
+
EXT_DIR_CWD,
|
|
22
|
+
EXTERNAL_PATH,
|
|
23
|
+
findExtDirDecision,
|
|
24
|
+
makeApprovingPrompter,
|
|
25
|
+
makeDenyingPrompter,
|
|
26
|
+
makeExtDirCheck,
|
|
27
|
+
makeUnavailablePrompter,
|
|
28
|
+
OPTIONAL_PATH_TOOLS,
|
|
29
|
+
} from "#test/helpers/external-directory-fixtures";
|
|
19
30
|
import {
|
|
20
31
|
getDecisionEvents,
|
|
21
32
|
makeCtx,
|
|
22
33
|
makeHandler,
|
|
23
|
-
makeSurfaceCheck,
|
|
24
34
|
makeToolCallEvent,
|
|
25
35
|
} from "#test/helpers/handler-fixtures";
|
|
26
36
|
|
|
@@ -31,42 +41,6 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
|
31
41
|
return { ...original };
|
|
32
42
|
});
|
|
33
43
|
|
|
34
|
-
// ── Constants ──────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const CWD = "/test/project";
|
|
37
|
-
const EXTERNAL_PATH = "/outside/project/file.ts";
|
|
38
|
-
|
|
39
|
-
/** All PATH_BEARING_TOOLS members. */
|
|
40
|
-
const ALL_PATH_BEARING_TOOLS = ["read", "write", "edit", "find", "grep", "ls"];
|
|
41
|
-
|
|
42
|
-
/** Tools where path is optional. */
|
|
43
|
-
const OPTIONAL_PATH_TOOLS = ["find", "grep", "ls"];
|
|
44
|
-
|
|
45
|
-
/** Full tool set used as the default registry in ext-dir tests. */
|
|
46
|
-
const ALL_TOOLS = [...ALL_PATH_BEARING_TOOLS, "bash"];
|
|
47
|
-
|
|
48
|
-
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Builds a `checkPermission` mock for external-directory integration tests.
|
|
52
|
-
*
|
|
53
|
-
* Routes `external_directory` to `externalDirectoryState`, `path` to allow
|
|
54
|
-
* with `source: "special"` (so the cross-cutting path gate is transparent),
|
|
55
|
-
* and every other surface to `toolState` (default: allow).
|
|
56
|
-
*/
|
|
57
|
-
function makeExtDirCheck(
|
|
58
|
-
externalDirectoryState: "allow" | "deny" | "ask",
|
|
59
|
-
toolState: "allow" | "deny" | "ask" = "allow",
|
|
60
|
-
) {
|
|
61
|
-
return makeSurfaceCheck(
|
|
62
|
-
{
|
|
63
|
-
external_directory: { state: externalDirectoryState },
|
|
64
|
-
path: { state: "allow", source: "special" },
|
|
65
|
-
},
|
|
66
|
-
{ state: toolState },
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
44
|
// ── Regression guard: helper presence ──────────────────────────────────────
|
|
71
45
|
|
|
72
46
|
describe("external_directory helper regression guard", () => {
|
|
@@ -95,7 +69,7 @@ describe("external_directory path scope", () => {
|
|
|
95
69
|
tools: ALL_TOOLS,
|
|
96
70
|
});
|
|
97
71
|
const event = makeToolCallEvent("read", {
|
|
98
|
-
input: { path: `${
|
|
72
|
+
input: { path: `${EXT_DIR_CWD}/src/index.ts` },
|
|
99
73
|
});
|
|
100
74
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
101
75
|
// Should not be blocked — the external_directory gate is skipped,
|
|
@@ -179,11 +153,7 @@ describe("external_directory policy state — allow", () => {
|
|
|
179
153
|
});
|
|
180
154
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
181
155
|
await handler.handleToolCall(event, makeCtx());
|
|
182
|
-
|
|
183
|
-
const extDirDecision = decisions.find(
|
|
184
|
-
(d) => d.surface === "external_directory",
|
|
185
|
-
);
|
|
186
|
-
expect(extDirDecision).toMatchObject({
|
|
156
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
187
157
|
surface: "external_directory",
|
|
188
158
|
result: "allow",
|
|
189
159
|
resolution: "policy_allow",
|
|
@@ -197,11 +167,7 @@ describe("external_directory policy state — allow", () => {
|
|
|
197
167
|
});
|
|
198
168
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
199
169
|
await handler.handleToolCall(event, makeCtx());
|
|
200
|
-
|
|
201
|
-
const blockEntries = reviewCalls.filter(
|
|
202
|
-
([eventName]: string[]) => eventName === "permission_request.blocked",
|
|
203
|
-
);
|
|
204
|
-
expect(blockEntries).toHaveLength(0);
|
|
170
|
+
expect(blockReviewEntries(logger)).toHaveLength(0);
|
|
205
171
|
});
|
|
206
172
|
});
|
|
207
173
|
|
|
@@ -220,12 +186,7 @@ describe("external_directory — allow external reads, gate external writes (#14
|
|
|
220
186
|
it("prompts for write to external path when external_directory allows but write is ask", async () => {
|
|
221
187
|
const { handler, prompter } = makeHandler({
|
|
222
188
|
session: { checkPermission: makeExtDirCheck("allow", "ask") },
|
|
223
|
-
prompter:
|
|
224
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
225
|
-
prompt: vi
|
|
226
|
-
.fn<GatePrompter["prompt"]>()
|
|
227
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
228
|
-
},
|
|
189
|
+
prompter: makeApprovingPrompter(),
|
|
229
190
|
tools: ALL_TOOLS,
|
|
230
191
|
});
|
|
231
192
|
const event = makeToolCallEvent("write", {
|
|
@@ -259,11 +220,8 @@ describe("external_directory — allow external reads, gate external writes (#14
|
|
|
259
220
|
});
|
|
260
221
|
await handler.handleToolCall(event, makeCtx());
|
|
261
222
|
const decisions = getDecisionEvents(events);
|
|
262
|
-
const extDirDecision = decisions.find(
|
|
263
|
-
(d) => d.surface === "external_directory",
|
|
264
|
-
);
|
|
265
223
|
const writeDecision = decisions.find((d) => d.surface === "write");
|
|
266
|
-
expect(
|
|
224
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
267
225
|
surface: "external_directory",
|
|
268
226
|
result: "allow",
|
|
269
227
|
resolution: "policy_allow",
|
|
@@ -308,12 +266,9 @@ describe("external_directory policy state — deny", () => {
|
|
|
308
266
|
});
|
|
309
267
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
310
268
|
await handler.handleToolCall(event, makeCtx());
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
);
|
|
315
|
-
expect(blockEntries.length).toBeGreaterThanOrEqual(1);
|
|
316
|
-
expect(blockEntries[0][1]).toMatchObject({
|
|
269
|
+
const entries = blockReviewEntries(logger);
|
|
270
|
+
expect(entries.length).toBeGreaterThanOrEqual(1);
|
|
271
|
+
expect(entries[0][1]).toMatchObject({
|
|
317
272
|
resolution: "policy_denied",
|
|
318
273
|
});
|
|
319
274
|
});
|
|
@@ -325,11 +280,7 @@ describe("external_directory policy state — deny", () => {
|
|
|
325
280
|
});
|
|
326
281
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
327
282
|
await handler.handleToolCall(event, makeCtx());
|
|
328
|
-
|
|
329
|
-
const extDirDecision = decisions.find(
|
|
330
|
-
(d) => d.surface === "external_directory",
|
|
331
|
-
);
|
|
332
|
-
expect(extDirDecision).toMatchObject({
|
|
283
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
333
284
|
surface: "external_directory",
|
|
334
285
|
result: "deny",
|
|
335
286
|
resolution: "policy_deny",
|
|
@@ -343,12 +294,7 @@ describe("external_directory policy state — ask", () => {
|
|
|
343
294
|
it("does not block when user approves", async () => {
|
|
344
295
|
const { handler } = makeHandler({
|
|
345
296
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
346
|
-
prompter:
|
|
347
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
348
|
-
prompt: vi
|
|
349
|
-
.fn<GatePrompter["prompt"]>()
|
|
350
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
351
|
-
},
|
|
297
|
+
prompter: makeApprovingPrompter(),
|
|
352
298
|
tools: ALL_TOOLS,
|
|
353
299
|
});
|
|
354
300
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
@@ -359,21 +305,12 @@ describe("external_directory policy state — ask", () => {
|
|
|
359
305
|
it("emits user_approved decision when user approves", async () => {
|
|
360
306
|
const { handler, events } = makeHandler({
|
|
361
307
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
362
|
-
prompter:
|
|
363
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
364
|
-
prompt: vi
|
|
365
|
-
.fn<GatePrompter["prompt"]>()
|
|
366
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
367
|
-
},
|
|
308
|
+
prompter: makeApprovingPrompter(),
|
|
368
309
|
tools: ALL_TOOLS,
|
|
369
310
|
});
|
|
370
311
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
371
312
|
await handler.handleToolCall(event, makeCtx());
|
|
372
|
-
|
|
373
|
-
const extDirDecision = decisions.find(
|
|
374
|
-
(d) => d.surface === "external_directory",
|
|
375
|
-
);
|
|
376
|
-
expect(extDirDecision).toMatchObject({
|
|
313
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
377
314
|
surface: "external_directory",
|
|
378
315
|
result: "allow",
|
|
379
316
|
resolution: "user_approved",
|
|
@@ -383,12 +320,7 @@ describe("external_directory policy state — ask", () => {
|
|
|
383
320
|
it("blocks when user denies", async () => {
|
|
384
321
|
const { handler } = makeHandler({
|
|
385
322
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
386
|
-
prompter:
|
|
387
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
388
|
-
prompt: vi
|
|
389
|
-
.fn<GatePrompter["prompt"]>()
|
|
390
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
391
|
-
},
|
|
323
|
+
prompter: makeDenyingPrompter(),
|
|
392
324
|
tools: ALL_TOOLS,
|
|
393
325
|
});
|
|
394
326
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
@@ -399,21 +331,12 @@ describe("external_directory policy state — ask", () => {
|
|
|
399
331
|
it("emits user_denied decision when user denies", async () => {
|
|
400
332
|
const { handler, events } = makeHandler({
|
|
401
333
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
402
|
-
prompter:
|
|
403
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
404
|
-
prompt: vi
|
|
405
|
-
.fn<GatePrompter["prompt"]>()
|
|
406
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
407
|
-
},
|
|
334
|
+
prompter: makeDenyingPrompter(),
|
|
408
335
|
tools: ALL_TOOLS,
|
|
409
336
|
});
|
|
410
337
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
411
338
|
await handler.handleToolCall(event, makeCtx());
|
|
412
|
-
|
|
413
|
-
const extDirDecision = decisions.find(
|
|
414
|
-
(d) => d.surface === "external_directory",
|
|
415
|
-
);
|
|
416
|
-
expect(extDirDecision).toMatchObject({
|
|
339
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
417
340
|
surface: "external_directory",
|
|
418
341
|
result: "deny",
|
|
419
342
|
resolution: "user_denied",
|
|
@@ -423,14 +346,7 @@ describe("external_directory policy state — ask", () => {
|
|
|
423
346
|
it("block reason includes denialReason when user provides one", async () => {
|
|
424
347
|
const { handler } = makeHandler({
|
|
425
348
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
426
|
-
prompter:
|
|
427
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
428
|
-
prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
|
|
429
|
-
approved: false,
|
|
430
|
-
state: "denied",
|
|
431
|
-
denialReason: "not needed",
|
|
432
|
-
}),
|
|
433
|
-
},
|
|
349
|
+
prompter: makeDenyingPrompter("not needed"),
|
|
434
350
|
tools: ALL_TOOLS,
|
|
435
351
|
});
|
|
436
352
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
@@ -442,10 +358,7 @@ describe("external_directory policy state — ask", () => {
|
|
|
442
358
|
it("blocks with confirmation_unavailable when no UI is available", async () => {
|
|
443
359
|
const { handler } = makeHandler({
|
|
444
360
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
445
|
-
prompter:
|
|
446
|
-
canConfirm: vi.fn().mockReturnValue(false),
|
|
447
|
-
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
448
|
-
},
|
|
361
|
+
prompter: makeUnavailablePrompter(),
|
|
449
362
|
tools: ALL_TOOLS,
|
|
450
363
|
});
|
|
451
364
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
@@ -462,20 +375,14 @@ describe("external_directory policy state — ask", () => {
|
|
|
462
375
|
it("writes review-log entry with confirmation_unavailable when no UI", async () => {
|
|
463
376
|
const { handler, logger } = makeHandler({
|
|
464
377
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
465
|
-
prompter:
|
|
466
|
-
canConfirm: vi.fn().mockReturnValue(false),
|
|
467
|
-
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
468
|
-
},
|
|
378
|
+
prompter: makeUnavailablePrompter(),
|
|
469
379
|
tools: ALL_TOOLS,
|
|
470
380
|
});
|
|
471
381
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
472
382
|
await handler.handleToolCall(event, makeCtx({ hasUI: false }));
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
);
|
|
477
|
-
expect(blockEntries.length).toBeGreaterThanOrEqual(1);
|
|
478
|
-
expect(blockEntries[0][1]).toMatchObject({
|
|
383
|
+
const entries = blockReviewEntries(logger);
|
|
384
|
+
expect(entries.length).toBeGreaterThanOrEqual(1);
|
|
385
|
+
expect(entries[0][1]).toMatchObject({
|
|
479
386
|
resolution: "confirmation_unavailable",
|
|
480
387
|
});
|
|
481
388
|
});
|
|
@@ -483,19 +390,12 @@ describe("external_directory policy state — ask", () => {
|
|
|
483
390
|
it("emits confirmation_unavailable decision when no UI", async () => {
|
|
484
391
|
const { handler, events } = makeHandler({
|
|
485
392
|
session: { checkPermission: makeExtDirCheck("ask") },
|
|
486
|
-
prompter:
|
|
487
|
-
canConfirm: vi.fn().mockReturnValue(false),
|
|
488
|
-
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
489
|
-
},
|
|
393
|
+
prompter: makeUnavailablePrompter(),
|
|
490
394
|
tools: ALL_TOOLS,
|
|
491
395
|
});
|
|
492
396
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
493
397
|
await handler.handleToolCall(event, makeCtx({ hasUI: false }));
|
|
494
|
-
|
|
495
|
-
const extDirDecision = decisions.find(
|
|
496
|
-
(d) => d.surface === "external_directory",
|
|
497
|
-
);
|
|
498
|
-
expect(extDirDecision).toMatchObject({
|
|
398
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
499
399
|
surface: "external_directory",
|
|
500
400
|
result: "deny",
|
|
501
401
|
resolution: "confirmation_unavailable",
|
|
@@ -547,9 +447,7 @@ describe("external_directory per-agent override", () => {
|
|
|
547
447
|
const result1 = await handler1.handleToolCall(event, makeCtx());
|
|
548
448
|
expect(result1).toEqual({ action: "allow" });
|
|
549
449
|
|
|
550
|
-
|
|
551
|
-
const extDir1 = decisions1.find((d) => d.surface === "external_directory");
|
|
552
|
-
expect(extDir1).toMatchObject({
|
|
450
|
+
expect(findExtDirDecision(events1)).toMatchObject({
|
|
553
451
|
result: "allow",
|
|
554
452
|
resolution: "policy_allow",
|
|
555
453
|
agentName: "special-agent",
|
|
@@ -578,10 +476,7 @@ describe("external_directory decision event fields", () => {
|
|
|
578
476
|
});
|
|
579
477
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
580
478
|
await handler.handleToolCall(event, makeCtx());
|
|
581
|
-
const
|
|
582
|
-
const extDirDecision = decisions.find(
|
|
583
|
-
(d) => d.surface === "external_directory",
|
|
584
|
-
);
|
|
479
|
+
const extDirDecision = findExtDirDecision(events);
|
|
585
480
|
expect(extDirDecision).toBeDefined();
|
|
586
481
|
expect(extDirDecision!.value).toBe(EXTERNAL_PATH);
|
|
587
482
|
});
|
|
@@ -596,11 +491,7 @@ describe("external_directory decision event fields", () => {
|
|
|
596
491
|
});
|
|
597
492
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
598
493
|
await handler.handleToolCall(event, makeCtx());
|
|
599
|
-
|
|
600
|
-
const extDirDecision = decisions.find(
|
|
601
|
-
(d) => d.surface === "external_directory",
|
|
602
|
-
);
|
|
603
|
-
expect(extDirDecision).toMatchObject({
|
|
494
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
604
495
|
agentName: "my-agent",
|
|
605
496
|
});
|
|
606
497
|
});
|
|
@@ -612,11 +503,7 @@ describe("external_directory decision event fields", () => {
|
|
|
612
503
|
});
|
|
613
504
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
614
505
|
await handler.handleToolCall(event, makeCtx());
|
|
615
|
-
|
|
616
|
-
const extDirDecision = decisions.find(
|
|
617
|
-
(d) => d.surface === "external_directory",
|
|
618
|
-
);
|
|
619
|
-
expect(extDirDecision).toMatchObject({
|
|
506
|
+
expect(findExtDirDecision(events)).toMatchObject({
|
|
620
507
|
agentName: null,
|
|
621
508
|
});
|
|
622
509
|
});
|