@gotgenes/pi-permission-system 10.3.1 → 10.5.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 (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/src/config-modal.ts +10 -8
  4. package/src/config-store.ts +6 -11
  5. package/src/forwarded-permissions/io.ts +16 -22
  6. package/src/forwarded-permissions/permission-forwarder.ts +16 -19
  7. package/src/gate-prompter.ts +1 -3
  8. package/src/handlers/gates/bash-command.ts +2 -2
  9. package/src/handlers/gates/bash-external-directory.ts +2 -2
  10. package/src/handlers/gates/bash-path.ts +2 -2
  11. package/src/handlers/gates/path.ts +2 -2
  12. package/src/handlers/gates/runner.ts +3 -3
  13. package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
  14. package/src/index.ts +27 -41
  15. package/src/permission-event-rpc.ts +19 -15
  16. package/src/permission-prompter.ts +4 -3
  17. package/src/permission-resolver.ts +69 -2
  18. package/src/permission-session.ts +7 -83
  19. package/src/prompting-gateway.ts +104 -0
  20. package/src/session-logger.ts +17 -3
  21. package/test/config-modal.test.ts +13 -7
  22. package/test/config-store.test.ts +7 -9
  23. package/test/forwarded-permissions/io.test.ts +23 -26
  24. package/test/handlers/external-directory-integration.test.ts +45 -32
  25. package/test/handlers/external-directory-session-dedup.test.ts +47 -57
  26. package/test/handlers/gates/bash-external-directory.test.ts +2 -2
  27. package/test/handlers/gates/bash-path.test.ts +2 -2
  28. package/test/handlers/gates/runner.test.ts +10 -16
  29. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
  30. package/test/handlers/input-events.test.ts +19 -4
  31. package/test/handlers/input.test.ts +29 -13
  32. package/test/handlers/tool-call-events.test.ts +23 -5
  33. package/test/helpers/gate-fixtures.ts +11 -15
  34. package/test/helpers/handler-fixtures.ts +31 -50
  35. package/test/permission-event-rpc.test.ts +30 -28
  36. package/test/permission-forwarder.test.ts +6 -5
  37. package/test/permission-prompter.test.ts +28 -28
  38. package/test/permission-resolver.test.ts +194 -0
  39. package/test/permission-session.test.ts +27 -180
  40. package/test/prompting-gateway.test.ts +230 -0
@@ -21,10 +21,8 @@ import {
21
21
  ToolCallGatePipeline,
22
22
  } from "#src/handlers/gates/tool-call-gate-pipeline";
23
23
  import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
24
- import type { PermissionPromptDecision } from "#src/permission-dialog";
25
24
  import type { PermissionDecisionEvent } from "#src/permission-events";
26
25
  import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
27
- import type { PromptPermissionDetails } from "#src/permission-prompter";
28
26
  import type { Rule } from "#src/rule";
29
27
  import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
30
28
  import type { SessionLogger } from "#src/session-logger";
@@ -35,10 +33,10 @@ import type { PermissionCheckResult, PermissionState } from "#src/types";
35
33
  /**
36
34
  * Precise mock boundary for PermissionGateHandler integration tests.
37
35
  *
38
- * Intersection of every role the handler and its collaborators require,
39
- * plus the context-bound prompting helpers that GatePrompter delegates to.
40
- * Without a cast, TypeScript enforces this at the call sites where the
41
- * mock is passed to GateRunner / ToolCallGatePipeline / PermissionGateHandler.
36
+ * Intersection of every role the handler and its collaborators require.
37
+ * Prompting is not included here it moved to `PromptingGateway` (#339).
38
+ * Pass a `prompter` override to `makeHandler` to steer GateRunner's prompting
39
+ * role; `makeHandler` creates a clean default prompter when none is supplied.
42
40
  *
43
41
  * The 4-arg `checkPermission` overrides the 3-arg version from
44
42
  * GateHandlerSession so the `resolve` delegation can forward session rules.
@@ -46,7 +44,6 @@ import type { PermissionCheckResult, PermissionState } from "#src/types";
46
44
  export type MockGateHandlerSession = ToolCallGateInputs &
47
45
  SkillInputGateInputs &
48
46
  SessionApprovalRecorder &
49
- GatePrompter &
50
47
  GateHandlerSession & {
51
48
  /** Logger source for the reporter the fixture builds. */
52
49
  logger: SessionLogger;
@@ -59,13 +56,6 @@ export type MockGateHandlerSession = ToolCallGateInputs &
59
56
  agentName?: string,
60
57
  rules?: Rule[],
61
58
  ): PermissionCheckResult;
62
- /** Context-bound canPrompt — overriding this steers canConfirm. */
63
- canPrompt(ctx: ExtensionContext): boolean;
64
- /** Context-bound prompt — overriding this steers promptPermission. */
65
- prompt(
66
- ctx: ExtensionContext,
67
- details: PromptPermissionDetails,
68
- ): Promise<PermissionPromptDecision>;
69
59
  };
70
60
 
71
61
  export function makeEvents() {
@@ -134,9 +124,7 @@ export function makeCheckResult(
134
124
  * field against `MockGateHandlerSession` individually — a missing field fails
135
125
  * `pnpm run check` instead of failing silently at runtime.
136
126
  *
137
- * The `resolve`, `canConfirm`, and `promptPermission` delegations are inlined
138
- * as closures that read `session` at call time, so overriding `checkPermission`,
139
- * `canPrompt`, or `prompt` automatically steers them without extra guards.
127
+ * Prompting is not part of this mock — pass `prompter` to `makeHandler`.
140
128
  */
141
129
  export function makeSession(
142
130
  overrides: Partial<MockGateHandlerSession> = {},
@@ -177,35 +165,6 @@ export function makeSession(
177
165
  vi
178
166
  .fn<MockGateHandlerSession["getToolPreviewLimits"]>()
179
167
  .mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
180
- canPrompt:
181
- overrides.canPrompt ??
182
- vi.fn<MockGateHandlerSession["canPrompt"]>().mockReturnValue(true),
183
- prompt:
184
- overrides.prompt ??
185
- vi
186
- .fn<MockGateHandlerSession["prompt"]>()
187
- .mockResolvedValue({ approved: true, state: "approved" }),
188
- // Delegations — closures read `session` at call time so overrides win.
189
- resolve:
190
- overrides.resolve ??
191
- vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
192
- session.checkPermission(
193
- surface,
194
- input,
195
- agentName,
196
- session.getSessionRuleset(),
197
- ),
198
- ),
199
- canConfirm:
200
- overrides.canConfirm ??
201
- vi.fn<MockGateHandlerSession["canConfirm"]>(() =>
202
- session.canPrompt(undefined as unknown as ExtensionContext),
203
- ),
204
- promptPermission:
205
- overrides.promptPermission ??
206
- vi.fn<MockGateHandlerSession["promptPermission"]>((details) =>
207
- session.prompt(undefined as unknown as ExtensionContext, details),
208
- ),
209
168
  };
210
169
  return session;
211
170
  }
@@ -296,10 +255,15 @@ export function makeBashCommandCheck(opts: {
296
255
  * Constructs a PermissionGateHandler with mocked collaborators.
297
256
  *
298
257
  * Returns all collaborators so each test file can destructure only what
299
- * it needs — handler, events, session, and toolRegistry are all available.
258
+ * it needs — handler, events, session, toolRegistry, and prompter are all available.
259
+ *
260
+ * The default prompter approves all requests. Pass `prompter` explicitly to
261
+ * steer canConfirm/prompt behavior for the test.
300
262
  */
301
263
  export function makeHandler(overrides?: {
302
264
  session?: Partial<MockGateHandlerSession>;
265
+ /** Override the GatePrompter passed to GateRunner. Defaults to an allow-all stub. */
266
+ prompter?: GatePrompter;
303
267
  toolRegistry?: Partial<ToolRegistry>;
304
268
  /** Sugar: builds the `getAll` mock from a list of tool names. */
305
269
  tools?: string[];
@@ -314,10 +278,27 @@ export function makeHandler(overrides?: {
314
278
  .mockReturnValue(overrides.tools.map((name) => ({ name }))),
315
279
  })
316
280
  : makeToolRegistry(overrides?.toolRegistry);
317
- const pipeline = new ToolCallGatePipeline(session);
281
+ // Resolver delegates to session's checkPermission + getSessionRuleset —
282
+ // overriding session.checkPermission steers resolve automatically.
283
+ const resolver = {
284
+ resolve: (surface: string, input: unknown, agentName?: string) =>
285
+ session.checkPermission(
286
+ surface,
287
+ input,
288
+ agentName,
289
+ session.getSessionRuleset(),
290
+ ),
291
+ };
292
+ const pipeline = new ToolCallGatePipeline(resolver, session);
318
293
  const skillInputPipeline = new SkillInputGatePipeline(session);
319
294
  const reporter = new GateDecisionReporter(session.logger, events);
320
- const runner = new GateRunner(session, session, session, reporter);
295
+ const prompter: GatePrompter = overrides?.prompter ?? {
296
+ canConfirm: vi.fn().mockReturnValue(true),
297
+ prompt: vi
298
+ .fn<GatePrompter["prompt"]>()
299
+ .mockResolvedValue({ approved: true, state: "approved" }),
300
+ };
301
+ const runner = new GateRunner(resolver, session, prompter, reporter);
321
302
  const handler = new PermissionGateHandler(
322
303
  session,
323
304
  toolRegistry,
@@ -325,7 +306,7 @@ export function makeHandler(overrides?: {
325
306
  skillInputPipeline,
326
307
  runner,
327
308
  );
328
- return { handler, events, session, toolRegistry };
309
+ return { handler, events, session, toolRegistry, prompter };
329
310
  }
330
311
 
331
312
  /** Extract all permissions:decision payloads from the events.emit mock. */
@@ -36,13 +36,13 @@ function makeDeps(
36
36
  overrides: Partial<PermissionRpcDeps> = {},
37
37
  ): PermissionRpcDeps {
38
38
  return {
39
- getPermissionManager: vi.fn().mockReturnValue({
39
+ permissionManager: {
40
40
  checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
41
- }),
42
- getSessionRules: vi.fn().mockReturnValue([]),
43
- getRuntimeContext: vi.fn().mockReturnValue(null),
41
+ },
42
+ sessionRules: { getRuleset: vi.fn().mockReturnValue([]) },
43
+ session: { getRuntimeContext: vi.fn().mockReturnValue(null) },
44
44
  requestPermissionDecisionFromUi: vi.fn(),
45
- writeReviewLog: vi.fn(),
45
+ logger: { review: vi.fn() },
46
46
  ...overrides,
47
47
  };
48
48
  }
@@ -73,9 +73,9 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
73
73
  it("replies allow for an allowed surface/value", async () => {
74
74
  const bus = createEventBus();
75
75
  const deps = makeDeps({
76
- getPermissionManager: vi.fn().mockReturnValue({
76
+ permissionManager: {
77
77
  checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
78
- }),
78
+ },
79
79
  });
80
80
  registerPermissionRpcHandlers(bus, deps);
81
81
 
@@ -100,14 +100,14 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
100
100
  it("replies deny for a denied surface/value", async () => {
101
101
  const bus = createEventBus();
102
102
  const deps = makeDeps({
103
- getPermissionManager: vi.fn().mockReturnValue({
103
+ permissionManager: {
104
104
  checkPermission: vi.fn().mockReturnValue(
105
105
  makeCheckResult("deny", {
106
106
  origin: "project",
107
107
  matchedPattern: "rm *",
108
108
  }),
109
109
  ),
110
- }),
110
+ },
111
111
  });
112
112
  registerPermissionRpcHandlers(bus, deps);
113
113
 
@@ -131,13 +131,13 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
131
131
  it("replies ask for an ask surface/value", async () => {
132
132
  const bus = createEventBus();
133
133
  const deps = makeDeps({
134
- getPermissionManager: vi.fn().mockReturnValue({
134
+ permissionManager: {
135
135
  checkPermission: vi
136
136
  .fn()
137
137
  .mockReturnValue(
138
138
  makeCheckResult("ask", { matchedPattern: undefined }),
139
139
  ),
140
- }),
140
+ },
141
141
  });
142
142
  registerPermissionRpcHandlers(bus, deps);
143
143
 
@@ -161,7 +161,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
161
161
  const checkPermission = vi.fn().mockReturnValue(makeCheckResult("allow"));
162
162
  const bus = createEventBus();
163
163
  const deps = makeDeps({
164
- getPermissionManager: vi.fn().mockReturnValue({ checkPermission }),
164
+ permissionManager: { checkPermission },
165
165
  });
166
166
  registerPermissionRpcHandlers(bus, deps);
167
167
 
@@ -197,8 +197,8 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
197
197
  const checkPermission = vi.fn().mockReturnValue(makeCheckResult("allow"));
198
198
  const bus = createEventBus();
199
199
  const deps = makeDeps({
200
- getPermissionManager: vi.fn().mockReturnValue({ checkPermission }),
201
- getSessionRules: vi.fn().mockReturnValue(sessionRules),
200
+ permissionManager: { checkPermission },
201
+ sessionRules: { getRuleset: vi.fn().mockReturnValue(sessionRules) },
202
202
  });
203
203
  registerPermissionRpcHandlers(bus, deps);
204
204
 
@@ -246,7 +246,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
246
246
  const checkPermission = vi.fn().mockReturnValue(makeCheckResult("allow"));
247
247
  const bus = createEventBus();
248
248
  const deps = makeDeps({
249
- getPermissionManager: vi.fn().mockReturnValue({ checkPermission }),
249
+ permissionManager: { checkPermission },
250
250
  });
251
251
  const handles = registerPermissionRpcHandlers(bus, deps);
252
252
  handles.unsubCheck();
@@ -290,7 +290,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
290
290
  const ctx = makeCtxWithUi();
291
291
  const approvedDecision = { approved: true, state: "approved" as const };
292
292
  const deps = makeDeps({
293
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
293
+ session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
294
294
  requestPermissionDecisionFromUi: vi
295
295
  .fn()
296
296
  .mockResolvedValue(approvedDecision),
@@ -325,7 +325,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
325
325
  .fn()
326
326
  .mockResolvedValue({ approved: true, state: "approved" as const });
327
327
  const deps = makeDeps({
328
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
328
+ session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
329
329
  requestPermissionDecisionFromUi: requestUi,
330
330
  });
331
331
  registerPermissionRpcHandlers(bus, deps);
@@ -363,7 +363,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
363
363
  .fn()
364
364
  .mockResolvedValue({ approved: true, state: "approved" as const });
365
365
  const deps = makeDeps({
366
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
366
+ session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
367
367
  requestPermissionDecisionFromUi: requestUi,
368
368
  });
369
369
  registerPermissionRpcHandlers(bus, deps);
@@ -399,7 +399,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
399
399
  denialReason: "Too risky",
400
400
  };
401
401
  const deps = makeDeps({
402
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
402
+ session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
403
403
  requestPermissionDecisionFromUi: vi
404
404
  .fn()
405
405
  .mockResolvedValue(deniedDecision),
@@ -430,7 +430,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
430
430
  it("replies with no_ui error when context has no UI", async () => {
431
431
  const bus = createEventBus();
432
432
  const deps = makeDeps({
433
- getRuntimeContext: vi.fn().mockReturnValue(null),
433
+ session: { getRuntimeContext: vi.fn().mockReturnValue(null) },
434
434
  });
435
435
  registerPermissionRpcHandlers(bus, deps);
436
436
 
@@ -453,9 +453,11 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
453
453
  it("replies with no_ui error when context hasUI is false", async () => {
454
454
  const bus = createEventBus();
455
455
  const deps = makeDeps({
456
- getRuntimeContext: vi
457
- .fn()
458
- .mockReturnValue({ hasUI: false, ui: makeUi() }),
456
+ session: {
457
+ getRuntimeContext: vi
458
+ .fn()
459
+ .mockReturnValue({ hasUI: false, ui: makeUi() }),
460
+ },
459
461
  });
460
462
  registerPermissionRpcHandlers(bus, deps);
461
463
 
@@ -478,13 +480,13 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
478
480
  it("writes to the review log after a prompt decision", async () => {
479
481
  const bus = createEventBus();
480
482
  const ctx = makeCtxWithUi();
481
- const writeReviewLog = vi.fn();
483
+ const logger = { review: vi.fn() };
482
484
  const deps = makeDeps({
483
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
485
+ session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
484
486
  requestPermissionDecisionFromUi: vi
485
487
  .fn()
486
488
  .mockResolvedValue({ approved: true, state: "approved" as const }),
487
- writeReviewLog,
489
+ logger,
488
490
  });
489
491
  registerPermissionRpcHandlers(bus, deps);
490
492
 
@@ -501,7 +503,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
501
503
  });
502
504
  await replyPromise;
503
505
 
504
- expect(writeReviewLog).toHaveBeenCalledWith(
506
+ expect(logger.review).toHaveBeenCalledWith(
505
507
  "permission_request.rpc_prompt",
506
508
  expect.objectContaining({
507
509
  requestId: "req-log",
@@ -520,7 +522,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
520
522
  const bus = createEventBus();
521
523
  const ctx = makeCtxWithUi();
522
524
  const deps = makeDeps({
523
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
525
+ session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
524
526
  requestPermissionDecisionFromUi: requestUi,
525
527
  });
526
528
  const handles = registerPermissionRpcHandlers(bus, deps);
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { afterEach, describe, expect, test, vi } from "vitest";
6
-
6
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
7
7
  import {
8
8
  PermissionForwarder,
9
9
  type PermissionForwarderDeps,
@@ -18,12 +18,11 @@ function makeDeps(
18
18
  return {
19
19
  forwardingDir: "/tmp/forwarding",
20
20
  subagentSessionsDir: "/tmp/subagents",
21
- logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
22
- writeReviewLog: vi.fn(),
21
+ logger: { review: vi.fn(), debug: vi.fn() },
23
22
  requestPermissionDecisionFromUi: vi
24
23
  .fn()
25
24
  .mockResolvedValue({ approved: true, state: "approved" as const }),
26
- shouldAutoApprove: () => false,
25
+ config: { current: () => ({ ...DEFAULT_EXTENSION_CONFIG }) },
27
26
  ...overrides,
28
27
  };
29
28
  }
@@ -271,7 +270,9 @@ describe("processInbox", () => {
271
270
  forwardingDir,
272
271
  events,
273
272
  requestPermissionDecisionFromUi,
274
- shouldAutoApprove: () => true,
273
+ config: {
274
+ current: () => ({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: true }),
275
+ },
275
276
  }),
276
277
  );
277
278
 
@@ -50,7 +50,7 @@ function makeDeps(
50
50
  ): PermissionPrompterDeps {
51
51
  return {
52
52
  config: makeConfigReader(),
53
- writeReviewLog: vi.fn(),
53
+ logger: { review: vi.fn() },
54
54
  events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
55
55
  forwarder: { requestApproval: mockRequestApproval },
56
56
  ...overrides,
@@ -97,32 +97,32 @@ describe("PermissionPrompter", () => {
97
97
  });
98
98
 
99
99
  it("logs permission_request.auto_approved in yolo mode", async () => {
100
- const writeReviewLog = vi.fn();
100
+ const logger = { review: vi.fn() };
101
101
  const deps = makeDeps({
102
102
  config: makeConfigReader({ yoloMode: true }),
103
- writeReviewLog,
103
+ logger,
104
104
  });
105
105
  const prompter = new PermissionPrompter(deps);
106
106
 
107
107
  await prompter.prompt(makeCtx(false), makeDetails());
108
108
 
109
- expect(writeReviewLog).toHaveBeenCalledWith(
109
+ expect(logger.review).toHaveBeenCalledWith(
110
110
  "permission_request.auto_approved",
111
111
  expect.objectContaining({ requestId: "req-123" }),
112
112
  );
113
113
  });
114
114
 
115
115
  it("does not log permission_request.waiting in yolo mode", async () => {
116
- const writeReviewLog = vi.fn();
116
+ const logger = { review: vi.fn() };
117
117
  const deps = makeDeps({
118
118
  config: makeConfigReader({ yoloMode: true }),
119
- writeReviewLog,
119
+ logger,
120
120
  });
121
121
  const prompter = new PermissionPrompter(deps);
122
122
 
123
123
  await prompter.prompt(makeCtx(false), makeDetails());
124
124
 
125
- expect(writeReviewLog).not.toHaveBeenCalledWith(
125
+ expect(logger.review).not.toHaveBeenCalledWith(
126
126
  "permission_request.waiting",
127
127
  expect.anything(),
128
128
  );
@@ -144,18 +144,18 @@ describe("PermissionPrompter", () => {
144
144
 
145
145
  describe("non-yolo path (UI present)", () => {
146
146
  it("logs permission_request.waiting before calling confirmPermission", async () => {
147
- const writeReviewLog = vi.fn();
147
+ const logger = { review: vi.fn() };
148
148
  const approved: PermissionPromptDecision = {
149
149
  approved: true,
150
150
  state: "approved",
151
151
  };
152
152
  mockRequestApproval.mockResolvedValue(approved);
153
- const deps = makeDeps({ writeReviewLog });
153
+ const deps = makeDeps({ logger });
154
154
  const prompter = new PermissionPrompter(deps);
155
155
 
156
156
  await prompter.prompt(makeCtx(true), makeDetails());
157
157
 
158
- const calls = writeReviewLog.mock.calls.map((c) => c[0] as string);
158
+ const calls = logger.review.mock.calls.map((c) => c[0] as string);
159
159
  expect(
160
160
  calls.indexOf("permission_request.waiting"),
161
161
  ).toBeGreaterThanOrEqual(0);
@@ -249,17 +249,17 @@ describe("PermissionPrompter", () => {
249
249
  });
250
250
 
251
251
  it("logs permission_request.approved when confirmPermission returns approved", async () => {
252
- const writeReviewLog = vi.fn();
252
+ const logger = { review: vi.fn() };
253
253
  mockRequestApproval.mockResolvedValue({
254
254
  approved: true,
255
255
  state: "approved",
256
256
  });
257
- const deps = makeDeps({ writeReviewLog });
257
+ const deps = makeDeps({ logger });
258
258
  const prompter = new PermissionPrompter(deps);
259
259
 
260
260
  await prompter.prompt(makeCtx(true), makeDetails());
261
261
 
262
- expect(writeReviewLog).toHaveBeenCalledWith(
262
+ expect(logger.review).toHaveBeenCalledWith(
263
263
  "permission_request.approved",
264
264
  expect.objectContaining({
265
265
  requestId: "req-123",
@@ -269,17 +269,17 @@ describe("PermissionPrompter", () => {
269
269
  });
270
270
 
271
271
  it("logs permission_request.denied when confirmPermission returns denied", async () => {
272
- const writeReviewLog = vi.fn();
272
+ const logger = { review: vi.fn() };
273
273
  mockRequestApproval.mockResolvedValue({
274
274
  approved: false,
275
275
  state: "denied",
276
276
  });
277
- const deps = makeDeps({ writeReviewLog });
277
+ const deps = makeDeps({ logger });
278
278
  const prompter = new PermissionPrompter(deps);
279
279
 
280
280
  await prompter.prompt(makeCtx(true), makeDetails());
281
281
 
282
- expect(writeReviewLog).toHaveBeenCalledWith(
282
+ expect(logger.review).toHaveBeenCalledWith(
283
283
  "permission_request.denied",
284
284
  expect.objectContaining({
285
285
  requestId: "req-123",
@@ -289,18 +289,18 @@ describe("PermissionPrompter", () => {
289
289
  });
290
290
 
291
291
  it("logs permission_request.denied with denialReason when present", async () => {
292
- const writeReviewLog = vi.fn();
292
+ const logger = { review: vi.fn() };
293
293
  mockRequestApproval.mockResolvedValue({
294
294
  approved: false,
295
295
  state: "denied_with_reason",
296
296
  denialReason: "too sensitive",
297
297
  });
298
- const deps = makeDeps({ writeReviewLog });
298
+ const deps = makeDeps({ logger });
299
299
  const prompter = new PermissionPrompter(deps);
300
300
 
301
301
  await prompter.prompt(makeCtx(true), makeDetails());
302
302
 
303
- expect(writeReviewLog).toHaveBeenCalledWith(
303
+ expect(logger.review).toHaveBeenCalledWith(
304
304
  "permission_request.denied",
305
305
  expect.objectContaining({
306
306
  denialReason: "too sensitive",
@@ -406,12 +406,12 @@ describe("PermissionPrompter", () => {
406
406
 
407
407
  describe("review log fields", () => {
408
408
  it("includes all standard fields in the waiting log entry", async () => {
409
- const writeReviewLog = vi.fn();
409
+ const logger = { review: vi.fn() };
410
410
  mockRequestApproval.mockResolvedValue({
411
411
  approved: true,
412
412
  state: "approved",
413
413
  });
414
- const deps = makeDeps({ writeReviewLog });
414
+ const deps = makeDeps({ logger });
415
415
  const prompter = new PermissionPrompter(deps);
416
416
  const details = makeDetails({
417
417
  toolCallId: "tc-1",
@@ -424,7 +424,7 @@ describe("PermissionPrompter", () => {
424
424
 
425
425
  await prompter.prompt(makeCtx(true), details);
426
426
 
427
- expect(writeReviewLog).toHaveBeenCalledWith(
427
+ expect(logger.review).toHaveBeenCalledWith(
428
428
  "permission_request.waiting",
429
429
  expect.objectContaining({
430
430
  requestId: "req-123",
@@ -443,17 +443,17 @@ describe("PermissionPrompter", () => {
443
443
  });
444
444
 
445
445
  it("uses null for optional fields not present in details", async () => {
446
- const writeReviewLog = vi.fn();
446
+ const logger = { review: vi.fn() };
447
447
  mockRequestApproval.mockResolvedValue({
448
448
  approved: true,
449
449
  state: "approved",
450
450
  });
451
- const deps = makeDeps({ writeReviewLog });
451
+ const deps = makeDeps({ logger });
452
452
  const prompter = new PermissionPrompter(deps);
453
453
 
454
454
  await prompter.prompt(makeCtx(true), makeDetails());
455
455
 
456
- expect(writeReviewLog).toHaveBeenCalledWith(
456
+ expect(logger.review).toHaveBeenCalledWith(
457
457
  "permission_request.waiting",
458
458
  expect.objectContaining({
459
459
  toolCallId: null,
@@ -499,17 +499,17 @@ describe("PermissionPrompter", () => {
499
499
  });
500
500
 
501
501
  it("logs the outcome when confirmPermission resolves via forwarding", async () => {
502
- const writeReviewLog = vi.fn();
502
+ const logger = { review: vi.fn() };
503
503
  mockRequestApproval.mockResolvedValue({
504
504
  approved: true,
505
505
  state: "approved",
506
506
  });
507
- const deps = makeDeps({ writeReviewLog });
507
+ const deps = makeDeps({ logger });
508
508
  const prompter = new PermissionPrompter(deps);
509
509
 
510
510
  await prompter.prompt(makeCtx(false), makeDetails());
511
511
 
512
- expect(writeReviewLog).toHaveBeenCalledWith(
512
+ expect(logger.review).toHaveBeenCalledWith(
513
513
  "permission_request.approved",
514
514
  expect.objectContaining({ requestId: "req-123" }),
515
515
  );