@gotgenes/pi-permission-system 10.3.0 → 10.4.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 (35) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +1 -1
  3. package/src/config-modal.ts +10 -8
  4. package/src/config-store.ts +13 -34
  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/runner.ts +1 -1
  9. package/src/index.ts +68 -51
  10. package/src/permission-event-rpc.ts +19 -15
  11. package/src/permission-prompter.ts +4 -3
  12. package/src/permission-session.ts +10 -67
  13. package/src/permissions-service.ts +3 -5
  14. package/src/prompting-gateway.ts +104 -0
  15. package/src/session-logger.ts +63 -12
  16. package/test/composition-root.test.ts +85 -1
  17. package/test/config-modal.test.ts +13 -7
  18. package/test/config-store.test.ts +23 -49
  19. package/test/forwarded-permissions/io.test.ts +23 -26
  20. package/test/handlers/external-directory-integration.test.ts +45 -32
  21. package/test/handlers/external-directory-session-dedup.test.ts +36 -46
  22. package/test/handlers/gates/runner.test.ts +10 -16
  23. package/test/handlers/input-events.test.ts +19 -4
  24. package/test/handlers/input.test.ts +29 -13
  25. package/test/handlers/tool-call-events.test.ts +23 -5
  26. package/test/helpers/gate-fixtures.ts +6 -6
  27. package/test/helpers/handler-fixtures.ts +24 -39
  28. package/test/permission-event-rpc.test.ts +30 -28
  29. package/test/permission-forwarder.test.ts +6 -5
  30. package/test/permission-prompter.test.ts +28 -28
  31. package/test/permission-session.test.ts +40 -112
  32. package/test/prompting-gateway.test.ts +230 -0
  33. package/test/session-logger.test.ts +151 -64
  34. package/src/runtime.ts +0 -147
  35. package/test/runtime.test.ts +0 -303
@@ -81,9 +81,7 @@ describe("GateRunner — descriptor path", () => {
81
81
  it("returns allow and emits user_approved when ask + user approves", async () => {
82
82
  const { runner, deps } = makeGateRunner({
83
83
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
84
- promptPermission: vi
85
- .fn()
86
- .mockResolvedValue({ approved: true, state: "approved" }),
84
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
87
85
  });
88
86
  const result = await runner.run(makeDescriptor(), null, "tc-1");
89
87
  expect(result).toEqual({ action: "allow" });
@@ -98,7 +96,7 @@ describe("GateRunner — descriptor path", () => {
98
96
  it("returns allow, emits user_approved_for_session, and records session rule on approved_for_session", async () => {
99
97
  const { runner, deps } = makeGateRunner({
100
98
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
101
- promptPermission: vi
99
+ prompt: vi
102
100
  .fn()
103
101
  .mockResolvedValue({ approved: true, state: "approved_for_session" }),
104
102
  });
@@ -120,7 +118,7 @@ describe("GateRunner — descriptor path", () => {
120
118
  it("calls recordSessionApproval once with the full SessionApproval when sessionApproval has multiple patterns", async () => {
121
119
  const { runner, deps } = makeGateRunner({
122
120
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
123
- promptPermission: vi
121
+ prompt: vi
124
122
  .fn()
125
123
  .mockResolvedValue({ approved: true, state: "approved_for_session" }),
126
124
  });
@@ -138,9 +136,7 @@ describe("GateRunner — descriptor path", () => {
138
136
  it("returns block and emits user_denied when ask + user denies", async () => {
139
137
  const { runner, deps } = makeGateRunner({
140
138
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
141
- promptPermission: vi
142
- .fn()
143
- .mockResolvedValue({ approved: false, state: "denied" }),
139
+ prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
144
140
  });
145
141
  const result = await runner.run(makeDescriptor(), null, "tc-1");
146
142
  expect(result).toMatchObject({ action: "block" });
@@ -170,7 +166,7 @@ describe("GateRunner — descriptor path", () => {
170
166
  it("emits auto_approved resolution when decision has autoApproved flag", async () => {
171
167
  const { runner, deps } = makeGateRunner({
172
168
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
173
- promptPermission: vi.fn().mockResolvedValue({
169
+ prompt: vi.fn().mockResolvedValue({
174
170
  approved: true,
175
171
  state: "approved",
176
172
  autoApproved: true,
@@ -227,12 +223,12 @@ describe("GateRunner — descriptor path", () => {
227
223
  );
228
224
  });
229
225
 
230
- it("passes requestId from toolCallId to promptPermission", async () => {
226
+ it("passes requestId from toolCallId to prompt", async () => {
231
227
  const { runner, deps } = makeGateRunner({
232
228
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
233
229
  });
234
230
  await runner.run(makeDescriptor(), null, "tc-42");
235
- expect(deps.promptPermission).toHaveBeenCalledWith(
231
+ expect(deps.prompt).toHaveBeenCalledWith(
236
232
  expect.objectContaining({ requestId: "tc-42" }),
237
233
  );
238
234
  });
@@ -240,9 +236,7 @@ describe("GateRunner — descriptor path", () => {
240
236
  it("does not call recordSessionApproval when user approves once (no sessionApproval)", async () => {
241
237
  const { runner, deps } = makeGateRunner({
242
238
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
243
- promptPermission: vi
244
- .fn()
245
- .mockResolvedValue({ approved: true, state: "approved" }),
239
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
246
240
  });
247
241
  await runner.run(makeDescriptor(), null, "tc-1");
248
242
  expect(deps.recordSessionApproval).not.toHaveBeenCalled();
@@ -272,7 +266,7 @@ describe("GateRunner — descriptor path", () => {
272
266
  it("does not call recordSessionApproval when user approves for session but no sessionApproval on descriptor", async () => {
273
267
  const { runner, deps } = makeGateRunner({
274
268
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
275
- promptPermission: vi
269
+ prompt: vi
276
270
  .fn()
277
271
  .mockResolvedValue({ approved: true, state: "approved_for_session" }),
278
272
  });
@@ -323,7 +317,7 @@ describe("GateRunner — descriptor path", () => {
323
317
  it("uses denialContext to format userDeniedReason with extension tag", async () => {
324
318
  const { runner } = makeGateRunner({
325
319
  resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
326
- promptPermission: vi.fn().mockResolvedValue({
320
+ prompt: vi.fn().mockResolvedValue({
327
321
  approved: false,
328
322
  state: "denied",
329
323
  denialReason: "too risky",
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { describe, expect, it, vi } from "vitest";
5
5
 
6
+ import type { GatePrompter } from "#src/gate-prompter";
6
7
  import {
7
8
  getDecisionEvents,
8
9
  makeCheckResult,
@@ -70,8 +71,11 @@ describe("handleInput decision events — skill gate", () => {
70
71
  const { handler, events } = makeHandler({
71
72
  session: {
72
73
  checkPermission: makeSkillCheckPermission("ask"),
74
+ },
75
+ prompter: {
76
+ canConfirm: vi.fn().mockReturnValue(true),
73
77
  prompt: vi
74
- .fn()
78
+ .fn<GatePrompter["prompt"]>()
75
79
  .mockResolvedValue({ approved: true, state: "approved" }),
76
80
  },
77
81
  });
@@ -91,7 +95,12 @@ describe("handleInput decision events — skill gate", () => {
91
95
  const { handler, events } = makeHandler({
92
96
  session: {
93
97
  checkPermission: makeSkillCheckPermission("ask"),
94
- prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
98
+ },
99
+ prompter: {
100
+ canConfirm: vi.fn().mockReturnValue(true),
101
+ prompt: vi
102
+ .fn<GatePrompter["prompt"]>()
103
+ .mockResolvedValue({ approved: false, state: "denied" }),
95
104
  },
96
105
  });
97
106
  await handler.handleInput({ text: "/skill:explorer" }, makeCtx());
@@ -110,7 +119,10 @@ describe("handleInput decision events — skill gate", () => {
110
119
  const { handler, events } = makeHandler({
111
120
  session: {
112
121
  checkPermission: makeSkillCheckPermission("ask"),
113
- canPrompt: vi.fn().mockReturnValue(false),
122
+ },
123
+ prompter: {
124
+ canConfirm: vi.fn().mockReturnValue(false),
125
+ prompt: vi.fn<GatePrompter["prompt"]>(),
114
126
  },
115
127
  });
116
128
  await handler.handleInput(
@@ -132,7 +144,10 @@ describe("handleInput decision events — skill gate", () => {
132
144
  const { handler, events } = makeHandler({
133
145
  session: {
134
146
  checkPermission: makeSkillCheckPermission("ask"),
135
- prompt: vi.fn().mockResolvedValue({
147
+ },
148
+ prompter: {
149
+ canConfirm: vi.fn().mockReturnValue(true),
150
+ prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
136
151
  approved: true,
137
152
  state: "approved",
138
153
  autoApproved: true,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
-
2
+ import type { GatePrompter } from "#src/gate-prompter";
3
3
  import { extractSkillNameFromInput } from "#src/handlers/permission-gate-handler";
4
4
 
5
5
  import { makeCtx, makeHandler } from "#test/helpers/handler-fixtures";
@@ -120,7 +120,10 @@ describe("handleInput", () => {
120
120
  const { handler } = makeHandler({
121
121
  session: {
122
122
  checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
123
- canPrompt: vi.fn().mockReturnValue(false),
123
+ },
124
+ prompter: {
125
+ canConfirm: vi.fn().mockReturnValue(false),
126
+ prompt: vi.fn<GatePrompter["prompt"]>(),
124
127
  },
125
128
  });
126
129
  const result = await handler.handleInput(
@@ -131,12 +134,16 @@ describe("handleInput", () => {
131
134
  });
132
135
 
133
136
  it("prompts and returns continue when skill ask is approved", async () => {
134
- const { handler, session } = makeHandler({
137
+ const approvePrompt = vi
138
+ .fn<GatePrompter["prompt"]>()
139
+ .mockResolvedValue({ approved: true, state: "approved" });
140
+ const { handler, prompter } = makeHandler({
135
141
  session: {
136
142
  checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
137
- prompt: vi
138
- .fn()
139
- .mockResolvedValue({ approved: true, state: "approved" }),
143
+ },
144
+ prompter: {
145
+ canConfirm: vi.fn().mockReturnValue(true),
146
+ prompt: approvePrompt,
140
147
  },
141
148
  });
142
149
  const result = await handler.handleInput(
@@ -144,14 +151,19 @@ describe("handleInput", () => {
144
151
  makeCtx(),
145
152
  );
146
153
  expect(result).toEqual({ action: "continue" });
147
- expect(session.prompt).toHaveBeenCalledOnce();
154
+ expect(prompter.prompt).toHaveBeenCalledOnce();
148
155
  });
149
156
 
150
157
  it("returns handled when skill ask is denied by user", async () => {
151
158
  const { handler } = makeHandler({
152
159
  session: {
153
160
  checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
154
- prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
161
+ },
162
+ prompter: {
163
+ canConfirm: vi.fn().mockReturnValue(true),
164
+ prompt: vi
165
+ .fn<GatePrompter["prompt"]>()
166
+ .mockResolvedValue({ approved: false, state: "denied" }),
155
167
  },
156
168
  });
157
169
  const result = await handler.handleInput(
@@ -162,17 +174,21 @@ describe("handleInput", () => {
162
174
  });
163
175
 
164
176
  it("passes agentName in the prompt permission request", async () => {
165
- const { handler, session } = makeHandler({
177
+ const approvePrompt = vi
178
+ .fn<GatePrompter["prompt"]>()
179
+ .mockResolvedValue({ approved: true, state: "approved" });
180
+ const { handler, prompter } = makeHandler({
166
181
  session: {
167
182
  checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
168
183
  resolveAgentName: vi.fn().mockReturnValue("code-agent"),
169
- prompt: vi
170
- .fn()
171
- .mockResolvedValue({ approved: true, state: "approved" }),
184
+ },
185
+ prompter: {
186
+ canConfirm: vi.fn().mockReturnValue(true),
187
+ prompt: approvePrompt,
172
188
  },
173
189
  });
174
190
  await handler.handleInput(makeInputEvent("/skill:librarian"), makeCtx());
175
- expect(session.promptPermission).toHaveBeenCalledWith(
191
+ expect(prompter.prompt).toHaveBeenCalledWith(
176
192
  expect.objectContaining({
177
193
  agentName: "code-agent",
178
194
  skillName: "librarian",
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { describe, expect, it, vi } from "vitest";
6
6
 
7
+ import type { GatePrompter } from "#src/gate-prompter";
7
8
  import {
8
9
  getDecisionEvents,
9
10
  makeCheckResult,
@@ -110,8 +111,11 @@ describe("handleToolCall decision events — user_approved", () => {
110
111
  checkPermission: vi
111
112
  .fn()
112
113
  .mockReturnValue(makeCheckResult({ state: "ask" })),
114
+ },
115
+ prompter: {
116
+ canConfirm: vi.fn().mockReturnValue(true),
113
117
  prompt: vi
114
- .fn()
118
+ .fn<GatePrompter["prompt"]>()
115
119
  .mockResolvedValue({ approved: true, state: "approved" }),
116
120
  },
117
121
  });
@@ -132,7 +136,10 @@ describe("handleToolCall decision events — user_approved", () => {
132
136
  checkPermission: vi
133
137
  .fn()
134
138
  .mockReturnValue(makeCheckResult({ state: "ask" })),
135
- prompt: vi.fn().mockResolvedValue({
139
+ },
140
+ prompter: {
141
+ canConfirm: vi.fn().mockReturnValue(true),
142
+ prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
136
143
  approved: true,
137
144
  state: "approved_for_session",
138
145
  }),
@@ -159,7 +166,12 @@ describe("handleToolCall decision events — user_denied", () => {
159
166
  checkPermission: vi
160
167
  .fn()
161
168
  .mockReturnValue(makeCheckResult({ state: "ask" })),
162
- prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
169
+ },
170
+ prompter: {
171
+ canConfirm: vi.fn().mockReturnValue(true),
172
+ prompt: vi
173
+ .fn<GatePrompter["prompt"]>()
174
+ .mockResolvedValue({ approved: false, state: "denied" }),
163
175
  },
164
176
  });
165
177
 
@@ -183,7 +195,10 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
183
195
  checkPermission: vi
184
196
  .fn()
185
197
  .mockReturnValue(makeCheckResult({ state: "ask" })),
186
- canPrompt: vi.fn().mockReturnValue(false),
198
+ },
199
+ prompter: {
200
+ canConfirm: vi.fn().mockReturnValue(false),
201
+ prompt: vi.fn<GatePrompter["prompt"]>(),
187
202
  },
188
203
  });
189
204
 
@@ -239,7 +254,10 @@ describe("handleToolCall decision events — auto_approved", () => {
239
254
  checkPermission: vi
240
255
  .fn()
241
256
  .mockReturnValue(makeCheckResult({ state: "ask" })),
242
- prompt: vi.fn().mockResolvedValue({
257
+ },
258
+ prompter: {
259
+ canConfirm: vi.fn().mockReturnValue(true),
260
+ prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
243
261
  approved: true,
244
262
  state: "approved",
245
263
  autoApproved: true,
@@ -94,7 +94,7 @@ export function makeGateRunner(
94
94
  resolve?: PermissionResolver["resolve"];
95
95
  recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
96
96
  canConfirm?: GatePrompter["canConfirm"];
97
- promptPermission?: GatePrompter["promptPermission"];
97
+ prompt?: GatePrompter["prompt"];
98
98
  reporter?: Partial<DecisionReporter>;
99
99
  } = {},
100
100
  ) {
@@ -112,15 +112,15 @@ export function makeGateRunner(
112
112
  const canConfirm =
113
113
  overrides.canConfirm ??
114
114
  (vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
115
- const promptPermission =
116
- overrides.promptPermission ??
115
+ const prompt =
116
+ overrides.prompt ??
117
117
  vi
118
- .fn<GatePrompter["promptPermission"]>()
118
+ .fn<GatePrompter["prompt"]>()
119
119
  .mockResolvedValue({ approved: true, state: "approved" });
120
120
  const runner = new GateRunner(
121
121
  { resolve },
122
122
  { recordSessionApproval },
123
- { canConfirm, promptPermission },
123
+ { canConfirm, prompt },
124
124
  reporter,
125
125
  );
126
126
  return {
@@ -129,7 +129,7 @@ export function makeGateRunner(
129
129
  resolve,
130
130
  recordSessionApproval,
131
131
  canConfirm,
132
- promptPermission,
132
+ prompt,
133
133
  reporter,
134
134
  },
135
135
  };
@@ -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,11 @@ 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
+ * The `resolve` delegation is inlined as a closure that reads `session` at
128
+ * call time, so overriding `checkPermission` or `getSessionRuleset`
129
+ * automatically steers it without extra guards.
130
+ *
131
+ * Prompting is not part of this mock — pass `prompter` to `makeHandler`.
140
132
  */
141
133
  export function makeSession(
142
134
  overrides: Partial<MockGateHandlerSession> = {},
@@ -177,15 +169,7 @@ export function makeSession(
177
169
  vi
178
170
  .fn<MockGateHandlerSession["getToolPreviewLimits"]>()
179
171
  .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.
172
+ // Resolve delegation — closure reads `session` at call time so overrides win.
189
173
  resolve:
190
174
  overrides.resolve ??
191
175
  vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
@@ -196,16 +180,6 @@ export function makeSession(
196
180
  session.getSessionRuleset(),
197
181
  ),
198
182
  ),
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
183
  };
210
184
  return session;
211
185
  }
@@ -296,10 +270,15 @@ export function makeBashCommandCheck(opts: {
296
270
  * Constructs a PermissionGateHandler with mocked collaborators.
297
271
  *
298
272
  * Returns all collaborators so each test file can destructure only what
299
- * it needs — handler, events, session, and toolRegistry are all available.
273
+ * it needs — handler, events, session, toolRegistry, and prompter are all available.
274
+ *
275
+ * The default prompter approves all requests. Pass `prompter` explicitly to
276
+ * steer canConfirm/prompt behavior for the test.
300
277
  */
301
278
  export function makeHandler(overrides?: {
302
279
  session?: Partial<MockGateHandlerSession>;
280
+ /** Override the GatePrompter passed to GateRunner. Defaults to an allow-all stub. */
281
+ prompter?: GatePrompter;
303
282
  toolRegistry?: Partial<ToolRegistry>;
304
283
  /** Sugar: builds the `getAll` mock from a list of tool names. */
305
284
  tools?: string[];
@@ -317,7 +296,13 @@ export function makeHandler(overrides?: {
317
296
  const pipeline = new ToolCallGatePipeline(session);
318
297
  const skillInputPipeline = new SkillInputGatePipeline(session);
319
298
  const reporter = new GateDecisionReporter(session.logger, events);
320
- const runner = new GateRunner(session, session, session, reporter);
299
+ const prompter: GatePrompter = overrides?.prompter ?? {
300
+ canConfirm: vi.fn().mockReturnValue(true),
301
+ prompt: vi
302
+ .fn<GatePrompter["prompt"]>()
303
+ .mockResolvedValue({ approved: true, state: "approved" }),
304
+ };
305
+ const runner = new GateRunner(session, session, prompter, reporter);
321
306
  const handler = new PermissionGateHandler(
322
307
  session,
323
308
  toolRegistry,
@@ -325,7 +310,7 @@ export function makeHandler(overrides?: {
325
310
  skillInputPipeline,
326
311
  runner,
327
312
  );
328
- return { handler, events, session, toolRegistry };
313
+ return { handler, events, session, toolRegistry, prompter };
329
314
  }
330
315
 
331
316
  /** 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);