@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/agent-prep-session.ts +28 -0
  5. package/src/decision-reporter.ts +41 -0
  6. package/src/denial-messages.ts +11 -0
  7. package/src/forwarded-permissions/permission-forwarder.ts +549 -0
  8. package/src/forwarding-manager.ts +3 -7
  9. package/src/gate-handler-session.ts +13 -0
  10. package/src/gate-prompter.ts +14 -0
  11. package/src/handlers/before-agent-start.ts +2 -3
  12. package/src/handlers/gates/bash-command.ts +4 -18
  13. package/src/handlers/gates/bash-external-directory.ts +3 -15
  14. package/src/handlers/gates/bash-path.ts +3 -16
  15. package/src/handlers/gates/descriptor.ts +0 -28
  16. package/src/handlers/gates/path.ts +3 -15
  17. package/src/handlers/gates/runner.ts +142 -105
  18. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  19. package/src/handlers/gates/skill-input.ts +44 -0
  20. package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
  21. package/src/handlers/lifecycle.ts +9 -9
  22. package/src/handlers/permission-gate-handler.ts +34 -238
  23. package/src/index.ts +53 -69
  24. package/src/mcp-targets.ts +56 -46
  25. package/src/permission-manager.ts +69 -3
  26. package/src/permission-prompter.ts +7 -58
  27. package/src/permission-resolver.ts +17 -0
  28. package/src/permission-session.ts +83 -27
  29. package/src/permissions-service.ts +53 -0
  30. package/src/runtime.ts +1 -37
  31. package/src/service-lifecycle.ts +49 -0
  32. package/src/session-approval-recorder.ts +6 -0
  33. package/src/session-lifecycle-session.ts +24 -0
  34. package/src/tool-input-preview.ts +0 -62
  35. package/src/tool-input-prompt-formatters.ts +63 -0
  36. package/src/tool-preview-formatter.ts +6 -4
  37. package/test/decision-reporter.test.ts +112 -0
  38. package/test/denial-messages.test.ts +62 -0
  39. package/test/forwarding-manager.test.ts +26 -44
  40. package/test/handlers/before-agent-start.test.ts +45 -21
  41. package/test/handlers/external-directory-integration.test.ts +83 -114
  42. package/test/handlers/external-directory-session-dedup.test.ts +102 -55
  43. package/test/handlers/gates/bash-command.test.ts +49 -90
  44. package/test/handlers/gates/bash-external-directory.test.ts +54 -95
  45. package/test/handlers/gates/bash-path.test.ts +54 -157
  46. package/test/handlers/gates/path.test.ts +38 -105
  47. package/test/handlers/gates/runner.test.ts +151 -186
  48. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  49. package/test/handlers/gates/skill-input.test.ts +128 -0
  50. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
  51. package/test/handlers/input.test.ts +1 -2
  52. package/test/handlers/lifecycle.test.ts +49 -33
  53. package/test/handlers/tool-call-events.test.ts +1 -1
  54. package/test/handlers/tool-call.test.ts +44 -153
  55. package/test/helpers/gate-fixtures.ts +212 -17
  56. package/test/helpers/handler-fixtures.ts +226 -29
  57. package/test/mcp-targets.test.ts +55 -0
  58. package/test/permission-forwarder.test.ts +295 -0
  59. package/test/permission-forwarding.test.ts +0 -282
  60. package/test/permission-manager-unified.test.ts +159 -1
  61. package/test/permission-prompter.test.ts +33 -44
  62. package/test/permission-session.test.ts +211 -105
  63. package/test/permissions-service.test.ts +151 -0
  64. package/test/runtime.test.ts +2 -86
  65. package/test/service-lifecycle.test.ts +162 -0
  66. package/test/tool-input-preview.test.ts +0 -111
  67. package/test/tool-input-prompt-formatters.test.ts +115 -0
  68. package/src/forwarded-permissions/polling.ts +0 -411
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
+ import type { AgentPrepSession } from "#src/agent-prep-session";
3
4
  import {
4
5
  AgentPrepHandler,
5
6
  shouldExposeTool,
6
7
  } from "#src/handlers/before-agent-start";
7
- import type { PermissionSession } from "#src/permission-session";
8
8
  import type { ToolRegistry } from "#src/tool-registry";
9
9
 
10
- import { makeCtx } from "#test/helpers/handler-fixtures";
10
+ import { makeCheckResult, makeCtx } from "#test/helpers/handler-fixtures";
11
11
 
12
12
  // ── SDK stubs ──────────────────────────────────────────────────────────────
13
13
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
@@ -26,24 +26,48 @@ function makeEvent(systemPrompt = "You are an assistant.") {
26
26
  }
27
27
 
28
28
  function makeSession(
29
- overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
30
- ): PermissionSession {
29
+ overrides: Partial<AgentPrepSession> = {},
30
+ ): AgentPrepSession {
31
31
  return {
32
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
33
- activate: vi.fn(),
34
- refreshConfig: vi.fn(),
35
- resolveAgentName: vi.fn().mockReturnValue(null),
36
- getToolPermission: vi.fn().mockReturnValue("allow"),
37
- checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
38
- shouldUpdateActiveTools: vi.fn().mockReturnValue(true),
39
- commitActiveToolsCacheKey: vi.fn(),
40
- getPolicyCacheStamp: vi.fn().mockReturnValue("stamp-1"),
41
- shouldUpdatePromptState: vi.fn().mockReturnValue(true),
42
- commitPromptStateCacheKey: vi.fn(),
43
- setActiveSkillEntries: vi.fn(),
44
- getActiveSkillEntries: vi.fn().mockReturnValue([]),
45
- ...overrides,
46
- } as unknown as PermissionSession;
32
+ activate: overrides.activate ?? vi.fn<AgentPrepSession["activate"]>(),
33
+ refreshConfig:
34
+ overrides.refreshConfig ?? vi.fn<AgentPrepSession["refreshConfig"]>(),
35
+ resolveAgentName:
36
+ overrides.resolveAgentName ??
37
+ vi.fn<AgentPrepSession["resolveAgentName"]>().mockReturnValue(null),
38
+ checkPermission:
39
+ overrides.checkPermission ??
40
+ vi
41
+ .fn<AgentPrepSession["checkPermission"]>()
42
+ .mockReturnValue(makeCheckResult()),
43
+ getToolPermission:
44
+ overrides.getToolPermission ??
45
+ vi.fn<AgentPrepSession["getToolPermission"]>().mockReturnValue("allow"),
46
+ shouldUpdateActiveTools:
47
+ overrides.shouldUpdateActiveTools ??
48
+ vi
49
+ .fn<AgentPrepSession["shouldUpdateActiveTools"]>()
50
+ .mockReturnValue(true),
51
+ commitActiveToolsCacheKey:
52
+ overrides.commitActiveToolsCacheKey ??
53
+ vi.fn<AgentPrepSession["commitActiveToolsCacheKey"]>(),
54
+ getPolicyCacheStamp:
55
+ overrides.getPolicyCacheStamp ??
56
+ vi
57
+ .fn<AgentPrepSession["getPolicyCacheStamp"]>()
58
+ .mockReturnValue("stamp-1"),
59
+ shouldUpdatePromptState:
60
+ overrides.shouldUpdatePromptState ??
61
+ vi
62
+ .fn<AgentPrepSession["shouldUpdatePromptState"]>()
63
+ .mockReturnValue(true),
64
+ commitPromptStateCacheKey:
65
+ overrides.commitPromptStateCacheKey ??
66
+ vi.fn<AgentPrepSession["commitPromptStateCacheKey"]>(),
67
+ setActiveSkillEntries:
68
+ overrides.setActiveSkillEntries ??
69
+ vi.fn<AgentPrepSession["setActiveSkillEntries"]>(),
70
+ };
47
71
  }
48
72
 
49
73
  function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
@@ -55,11 +79,11 @@ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
55
79
  }
56
80
 
57
81
  function makeHandler(overrides?: {
58
- session?: Partial<Record<keyof PermissionSession, unknown>>;
82
+ session?: Partial<AgentPrepSession>;
59
83
  toolRegistry?: Partial<ToolRegistry>;
60
84
  }): {
61
85
  handler: AgentPrepHandler;
62
- session: PermissionSession;
86
+ session: AgentPrepSession;
63
87
  toolRegistry: ToolRegistry;
64
88
  } {
65
89
  const session = makeSession(overrides?.session);
@@ -8,20 +8,18 @@
8
8
  * Regression guard: importing the four external-directory message helpers
9
9
  * ensures the test file fails to load if any helper is removed.
10
10
  */
11
+
11
12
  import { describe, expect, it, vi } from "vitest";
12
13
 
13
14
  import { EXTENSION_TAG } from "#src/denial-messages";
14
- import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
15
15
  import { formatExternalDirectoryAskPrompt } from "#src/handlers/gates/external-directory-messages";
16
- import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
17
- import type { PermissionSession } from "#src/permission-session";
18
- import type { ToolRegistry } from "#src/tool-registry";
19
- import type { PermissionCheckResult, PermissionState } from "#src/types";
16
+ import type { PermissionCheckResult } from "#src/types";
20
17
 
21
18
  import {
22
19
  getDecisionEvents,
23
20
  makeCtx,
24
- makeEvents,
21
+ makeHandler,
22
+ makeSurfaceCheck,
25
23
  makeToolCallEvent,
26
24
  } from "#test/helpers/handler-fixtures";
27
25
 
@@ -37,93 +35,35 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
37
35
  const CWD = "/test/project";
38
36
  const EXTERNAL_PATH = "/outside/project/file.ts";
39
37
 
40
- // ── Helpers ────────────────────────────────────────────────────────────────
41
-
42
- function makeCheckPermission(
43
- externalDirectoryState: PermissionState,
44
- toolState: PermissionState = "allow",
45
- ) {
46
- return vi
47
- .fn()
48
- .mockImplementation((surface: string): PermissionCheckResult => {
49
- if (surface === "external_directory") {
50
- return {
51
- state: externalDirectoryState,
52
- toolName: surface,
53
- source: "tool",
54
- origin: "builtin",
55
- };
56
- }
57
- // The cross-cutting path gate runs before ext-dir; keep it transparent.
58
- if (surface === "path") {
59
- return {
60
- state: "allow",
61
- toolName: surface,
62
- source: "special",
63
- origin: "builtin",
64
- };
65
- }
66
- return {
67
- state: toolState,
68
- toolName: surface,
69
- source: "tool",
70
- origin: "builtin",
71
- };
72
- });
73
- }
74
-
75
- function makeSession(
76
- overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
77
- ): PermissionSession {
78
- return {
79
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
80
- activate: vi.fn(),
81
- resolveAgentName: vi.fn().mockReturnValue(null),
82
- checkPermission: makeCheckPermission("deny"),
83
- getToolPermission: vi.fn().mockReturnValue("allow"),
84
- getSessionRuleset: vi.fn().mockReturnValue([]),
85
- recordSessionApproval: vi.fn(),
86
- getActiveSkillEntries: vi.fn().mockReturnValue([]),
87
- getInfrastructureDirs: vi.fn().mockReturnValue([]),
88
- getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
89
- config: DEFAULT_EXTENSION_CONFIG,
90
- canPrompt: vi.fn().mockReturnValue(true),
91
- prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
92
- ...overrides,
93
- } as unknown as PermissionSession;
94
- }
95
-
96
38
  /** All PATH_BEARING_TOOLS members. */
97
39
  const ALL_PATH_BEARING_TOOLS = ["read", "write", "edit", "find", "grep", "ls"];
98
40
 
99
41
  /** Tools where path is optional. */
100
42
  const OPTIONAL_PATH_TOOLS = ["find", "grep", "ls"];
101
43
 
102
- function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
103
- return {
104
- getAll: vi
105
- .fn()
106
- .mockReturnValue(
107
- [...ALL_PATH_BEARING_TOOLS, "bash"].map((name) => ({ name })),
108
- ),
109
- setActive: vi.fn(),
110
- ...overrides,
111
- };
112
- }
44
+ /** Full tool set used as the default registry in ext-dir tests. */
45
+ const ALL_TOOLS = [...ALL_PATH_BEARING_TOOLS, "bash"];
46
+
47
+ // ── Helpers ────────────────────────────────────────────────────────────────
113
48
 
114
- function makeHandler(overrides?: {
115
- session?: Partial<Record<keyof PermissionSession, unknown>>;
116
- toolRegistry?: Partial<ToolRegistry>;
117
- }): {
118
- handler: PermissionGateHandler;
119
- events: ReturnType<typeof makeEvents>;
120
- session: PermissionSession;
121
- } {
122
- const session = makeSession(overrides?.session);
123
- const events = makeEvents();
124
- const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
125
- const handler = new PermissionGateHandler(session, events, toolRegistry);
126
- return { handler, events, session };
49
+ /**
50
+ * Builds a `checkPermission` mock for external-directory integration tests.
51
+ *
52
+ * Routes `external_directory` to `externalDirectoryState`, `path` to allow
53
+ * with `source: "special"` (so the cross-cutting path gate is transparent),
54
+ * and every other surface to `toolState` (default: allow).
55
+ */
56
+ function makeExtDirCheck(
57
+ externalDirectoryState: "allow" | "deny" | "ask",
58
+ toolState: "allow" | "deny" | "ask" = "allow",
59
+ ) {
60
+ return makeSurfaceCheck(
61
+ {
62
+ external_directory: { state: externalDirectoryState },
63
+ path: { state: "allow", source: "special" },
64
+ },
65
+ { state: toolState },
66
+ );
127
67
  }
128
68
 
129
69
  // ── Regression guard: helper presence ──────────────────────────────────────
@@ -150,20 +90,22 @@ describe("external_directory helper regression guard", () => {
150
90
  describe("external_directory path scope", () => {
151
91
  it("skips external_directory check when path is inside CWD", async () => {
152
92
  const { handler } = makeHandler({
153
- session: { checkPermission: makeCheckPermission("deny") },
93
+ session: { checkPermission: makeExtDirCheck("deny") },
94
+ tools: ALL_TOOLS,
154
95
  });
155
96
  const event = makeToolCallEvent("read", {
156
97
  input: { path: `${CWD}/src/index.ts` },
157
98
  });
158
99
  const result = await handler.handleToolCall(event, makeCtx());
159
100
  // Should not be blocked — the external_directory gate is skipped,
160
- // and the tool gate sees "allow" (default toolState in makeCheckPermission)
101
+ // and the tool gate sees "allow" (default toolState in makeExtDirCheck)
161
102
  expect(result).toEqual({});
162
103
  });
163
104
 
164
105
  it("fires external_directory check when path is outside CWD", async () => {
165
106
  const { handler } = makeHandler({
166
- session: { checkPermission: makeCheckPermission("deny") },
107
+ session: { checkPermission: makeExtDirCheck("deny") },
108
+ tools: ALL_TOOLS,
167
109
  });
168
110
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
169
111
  const result = await handler.handleToolCall(event, makeCtx());
@@ -172,7 +114,8 @@ describe("external_directory path scope", () => {
172
114
 
173
115
  it("skips external_directory check for non-path-bearing tool (bash)", async () => {
174
116
  const { handler } = makeHandler({
175
- session: { checkPermission: makeCheckPermission("deny", "allow") },
117
+ session: { checkPermission: makeExtDirCheck("deny", "allow") },
118
+ tools: ALL_TOOLS,
176
119
  });
177
120
  const event = makeToolCallEvent("bash", {
178
121
  input: { command: `cat ${EXTERNAL_PATH}` },
@@ -191,7 +134,8 @@ describe("external_directory path scope", () => {
191
134
  ALL_PATH_BEARING_TOOLS,
192
135
  )("blocks %s with an out-of-cwd path when external_directory is deny", async (toolName) => {
193
136
  const { handler } = makeHandler({
194
- session: { checkPermission: makeCheckPermission("deny") },
137
+ session: { checkPermission: makeExtDirCheck("deny") },
138
+ tools: ALL_TOOLS,
195
139
  });
196
140
  const event = makeToolCallEvent(toolName, {
197
141
  input: { path: EXTERNAL_PATH },
@@ -204,7 +148,8 @@ describe("external_directory path scope", () => {
204
148
  OPTIONAL_PATH_TOOLS,
205
149
  )("skips external_directory check for %s when path is omitted", async (toolName) => {
206
150
  const { handler } = makeHandler({
207
- session: { checkPermission: makeCheckPermission("deny") },
151
+ session: { checkPermission: makeExtDirCheck("deny") },
152
+ tools: ALL_TOOLS,
208
153
  });
209
154
  // No path in input — external_directory gate should not fire
210
155
  const event = makeToolCallEvent(toolName);
@@ -218,7 +163,8 @@ describe("external_directory path scope", () => {
218
163
  describe("external_directory policy state — allow", () => {
219
164
  it("falls through to tool gate when external_directory is allow", async () => {
220
165
  const { handler } = makeHandler({
221
- session: { checkPermission: makeCheckPermission("allow") },
166
+ session: { checkPermission: makeExtDirCheck("allow") },
167
+ tools: ALL_TOOLS,
222
168
  });
223
169
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
224
170
  const result = await handler.handleToolCall(event, makeCtx());
@@ -227,7 +173,8 @@ describe("external_directory policy state — allow", () => {
227
173
 
228
174
  it("emits decision event with policy_allow on external_directory surface", async () => {
229
175
  const { handler, events } = makeHandler({
230
- session: { checkPermission: makeCheckPermission("allow") },
176
+ session: { checkPermission: makeExtDirCheck("allow") },
177
+ tools: ALL_TOOLS,
231
178
  });
232
179
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
233
180
  await handler.handleToolCall(event, makeCtx());
@@ -244,7 +191,8 @@ describe("external_directory policy state — allow", () => {
244
191
 
245
192
  it("does not write a block review-log entry when external_directory is allow", async () => {
246
193
  const { handler, session } = makeHandler({
247
- session: { checkPermission: makeCheckPermission("allow") },
194
+ session: { checkPermission: makeExtDirCheck("allow") },
195
+ tools: ALL_TOOLS,
248
196
  });
249
197
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
250
198
  await handler.handleToolCall(event, makeCtx());
@@ -261,7 +209,8 @@ describe("external_directory policy state — allow", () => {
261
209
  describe("external_directory — allow external reads, gate external writes (#144)", () => {
262
210
  it("allows read of external path when external_directory and read are both allow", async () => {
263
211
  const { handler } = makeHandler({
264
- session: { checkPermission: makeCheckPermission("allow", "allow") },
212
+ session: { checkPermission: makeExtDirCheck("allow", "allow") },
213
+ tools: ALL_TOOLS,
265
214
  });
266
215
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
267
216
  const result = await handler.handleToolCall(event, makeCtx());
@@ -274,9 +223,10 @@ describe("external_directory — allow external reads, gate external writes (#14
274
223
  .mockResolvedValue({ approved: true, state: "approved" });
275
224
  const { handler } = makeHandler({
276
225
  session: {
277
- checkPermission: makeCheckPermission("allow", "ask"),
226
+ checkPermission: makeExtDirCheck("allow", "ask"),
278
227
  prompt,
279
228
  },
229
+ tools: ALL_TOOLS,
280
230
  });
281
231
  const event = makeToolCallEvent("write", {
282
232
  input: { path: EXTERNAL_PATH },
@@ -289,7 +239,8 @@ describe("external_directory — allow external reads, gate external writes (#14
289
239
 
290
240
  it("blocks write to external path when external_directory allows but write is deny", async () => {
291
241
  const { handler } = makeHandler({
292
- session: { checkPermission: makeCheckPermission("allow", "deny") },
242
+ session: { checkPermission: makeExtDirCheck("allow", "deny") },
243
+ tools: ALL_TOOLS,
293
244
  });
294
245
  const event = makeToolCallEvent("write", {
295
246
  input: { path: EXTERNAL_PATH },
@@ -300,7 +251,8 @@ describe("external_directory — allow external reads, gate external writes (#14
300
251
 
301
252
  it("emits separate decision events for external_directory and write surfaces", async () => {
302
253
  const { handler, events } = makeHandler({
303
- session: { checkPermission: makeCheckPermission("allow", "deny") },
254
+ session: { checkPermission: makeExtDirCheck("allow", "deny") },
255
+ tools: ALL_TOOLS,
304
256
  });
305
257
  const event = makeToolCallEvent("write", {
306
258
  input: { path: EXTERNAL_PATH },
@@ -327,7 +279,8 @@ describe("external_directory — allow external reads, gate external writes (#14
327
279
  describe("external_directory policy state — deny", () => {
328
280
  it("blocks with reason containing the external path", async () => {
329
281
  const { handler } = makeHandler({
330
- session: { checkPermission: makeCheckPermission("deny") },
282
+ session: { checkPermission: makeExtDirCheck("deny") },
283
+ tools: ALL_TOOLS,
331
284
  });
332
285
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
333
286
  const result = await handler.handleToolCall(event, makeCtx());
@@ -337,7 +290,8 @@ describe("external_directory policy state — deny", () => {
337
290
 
338
291
  it("block reason contains extension attribution", async () => {
339
292
  const { handler } = makeHandler({
340
- session: { checkPermission: makeCheckPermission("deny") },
293
+ session: { checkPermission: makeExtDirCheck("deny") },
294
+ tools: ALL_TOOLS,
341
295
  });
342
296
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
343
297
  const result = await handler.handleToolCall(event, makeCtx());
@@ -347,7 +301,8 @@ describe("external_directory policy state — deny", () => {
347
301
 
348
302
  it("writes review-log entry with resolution policy_denied", async () => {
349
303
  const { handler, session } = makeHandler({
350
- session: { checkPermission: makeCheckPermission("deny") },
304
+ session: { checkPermission: makeExtDirCheck("deny") },
305
+ tools: ALL_TOOLS,
351
306
  });
352
307
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
353
308
  await handler.handleToolCall(event, makeCtx());
@@ -364,7 +319,8 @@ describe("external_directory policy state — deny", () => {
364
319
 
365
320
  it("emits decision event with policy_deny on external_directory surface", async () => {
366
321
  const { handler, events } = makeHandler({
367
- session: { checkPermission: makeCheckPermission("deny") },
322
+ session: { checkPermission: makeExtDirCheck("deny") },
323
+ tools: ALL_TOOLS,
368
324
  });
369
325
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
370
326
  await handler.handleToolCall(event, makeCtx());
@@ -386,11 +342,12 @@ describe("external_directory policy state — ask", () => {
386
342
  it("does not block when user approves", async () => {
387
343
  const { handler } = makeHandler({
388
344
  session: {
389
- checkPermission: makeCheckPermission("ask"),
345
+ checkPermission: makeExtDirCheck("ask"),
390
346
  prompt: vi
391
347
  .fn()
392
348
  .mockResolvedValue({ approved: true, state: "approved" }),
393
349
  },
350
+ tools: ALL_TOOLS,
394
351
  });
395
352
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
396
353
  const result = await handler.handleToolCall(event, makeCtx());
@@ -400,11 +357,12 @@ describe("external_directory policy state — ask", () => {
400
357
  it("emits user_approved decision when user approves", async () => {
401
358
  const { handler, events } = makeHandler({
402
359
  session: {
403
- checkPermission: makeCheckPermission("ask"),
360
+ checkPermission: makeExtDirCheck("ask"),
404
361
  prompt: vi
405
362
  .fn()
406
363
  .mockResolvedValue({ approved: true, state: "approved" }),
407
364
  },
365
+ tools: ALL_TOOLS,
408
366
  });
409
367
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
410
368
  await handler.handleToolCall(event, makeCtx());
@@ -422,9 +380,10 @@ describe("external_directory policy state — ask", () => {
422
380
  it("blocks when user denies", async () => {
423
381
  const { handler } = makeHandler({
424
382
  session: {
425
- checkPermission: makeCheckPermission("ask"),
383
+ checkPermission: makeExtDirCheck("ask"),
426
384
  prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
427
385
  },
386
+ tools: ALL_TOOLS,
428
387
  });
429
388
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
430
389
  const result = await handler.handleToolCall(event, makeCtx());
@@ -434,9 +393,10 @@ describe("external_directory policy state — ask", () => {
434
393
  it("emits user_denied decision when user denies", async () => {
435
394
  const { handler, events } = makeHandler({
436
395
  session: {
437
- checkPermission: makeCheckPermission("ask"),
396
+ checkPermission: makeExtDirCheck("ask"),
438
397
  prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
439
398
  },
399
+ tools: ALL_TOOLS,
440
400
  });
441
401
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
442
402
  await handler.handleToolCall(event, makeCtx());
@@ -454,13 +414,14 @@ describe("external_directory policy state — ask", () => {
454
414
  it("block reason includes denialReason when user provides one", async () => {
455
415
  const { handler } = makeHandler({
456
416
  session: {
457
- checkPermission: makeCheckPermission("ask"),
417
+ checkPermission: makeExtDirCheck("ask"),
458
418
  prompt: vi.fn().mockResolvedValue({
459
419
  approved: false,
460
420
  state: "denied",
461
421
  denialReason: "not needed",
462
422
  }),
463
423
  },
424
+ tools: ALL_TOOLS,
464
425
  });
465
426
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
466
427
  const result = await handler.handleToolCall(event, makeCtx());
@@ -471,9 +432,10 @@ describe("external_directory policy state — ask", () => {
471
432
  it("blocks with confirmation_unavailable when no UI is available", async () => {
472
433
  const { handler } = makeHandler({
473
434
  session: {
474
- checkPermission: makeCheckPermission("ask"),
435
+ checkPermission: makeExtDirCheck("ask"),
475
436
  canPrompt: vi.fn().mockReturnValue(false),
476
437
  },
438
+ tools: ALL_TOOLS,
477
439
  });
478
440
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
479
441
  const result = await handler.handleToolCall(
@@ -487,9 +449,10 @@ describe("external_directory policy state — ask", () => {
487
449
  it("writes review-log entry with confirmation_unavailable when no UI", async () => {
488
450
  const { handler, session } = makeHandler({
489
451
  session: {
490
- checkPermission: makeCheckPermission("ask"),
452
+ checkPermission: makeExtDirCheck("ask"),
491
453
  canPrompt: vi.fn().mockReturnValue(false),
492
454
  },
455
+ tools: ALL_TOOLS,
493
456
  });
494
457
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
495
458
  await handler.handleToolCall(event, makeCtx({ hasUI: false }));
@@ -507,9 +470,10 @@ describe("external_directory policy state — ask", () => {
507
470
  it("emits confirmation_unavailable decision when no UI", async () => {
508
471
  const { handler, events } = makeHandler({
509
472
  session: {
510
- checkPermission: makeCheckPermission("ask"),
473
+ checkPermission: makeExtDirCheck("ask"),
511
474
  canPrompt: vi.fn().mockReturnValue(false),
512
475
  },
476
+ tools: ALL_TOOLS,
513
477
  });
514
478
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
515
479
  await handler.handleToolCall(event, makeCtx({ hasUI: false }));
@@ -563,6 +527,7 @@ describe("external_directory per-agent override", () => {
563
527
  checkPermission: agentAwareCheck,
564
528
  resolveAgentName: vi.fn().mockReturnValue("special-agent"),
565
529
  },
530
+ tools: ALL_TOOLS,
566
531
  });
567
532
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
568
533
  const result1 = await handler1.handleToolCall(event, makeCtx());
@@ -582,6 +547,7 @@ describe("external_directory per-agent override", () => {
582
547
  checkPermission: agentAwareCheck,
583
548
  resolveAgentName: vi.fn().mockReturnValue(null),
584
549
  },
550
+ tools: ALL_TOOLS,
585
551
  });
586
552
  const result2 = await handler2.handleToolCall(event, makeCtx());
587
553
  expect(result2).toMatchObject({ block: true });
@@ -593,7 +559,8 @@ describe("external_directory per-agent override", () => {
593
559
  describe("external_directory decision event fields", () => {
594
560
  it("decision event value is the external path", async () => {
595
561
  const { handler, events } = makeHandler({
596
- session: { checkPermission: makeCheckPermission("deny") },
562
+ session: { checkPermission: makeExtDirCheck("deny") },
563
+ tools: ALL_TOOLS,
597
564
  });
598
565
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
599
566
  await handler.handleToolCall(event, makeCtx());
@@ -608,9 +575,10 @@ describe("external_directory decision event fields", () => {
608
575
  it("decision event includes agentName when present", async () => {
609
576
  const { handler, events } = makeHandler({
610
577
  session: {
611
- checkPermission: makeCheckPermission("allow"),
578
+ checkPermission: makeExtDirCheck("allow"),
612
579
  resolveAgentName: vi.fn().mockReturnValue("my-agent"),
613
580
  },
581
+ tools: ALL_TOOLS,
614
582
  });
615
583
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
616
584
  await handler.handleToolCall(event, makeCtx());
@@ -625,7 +593,8 @@ describe("external_directory decision event fields", () => {
625
593
 
626
594
  it("decision event agentName is null when no agent", async () => {
627
595
  const { handler, events } = makeHandler({
628
- session: { checkPermission: makeCheckPermission("allow") },
596
+ session: { checkPermission: makeExtDirCheck("allow") },
597
+ tools: ALL_TOOLS,
629
598
  });
630
599
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
631
600
  await handler.handleToolCall(event, makeCtx());