@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
@@ -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",
@@ -40,9 +40,10 @@ describe("ToolCallGatePipeline", () => {
40
40
 
41
41
  describe("evaluate — non-bash tool", () => {
42
42
  it("returns allow when all gates pass", async () => {
43
+ const resolver = makeResolver(makeCheckResult());
43
44
  const inputs = makeGateInputs();
44
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
45
- const pipeline = new ToolCallGatePipeline(inputs);
45
+ const { runner } = makeGateRunner();
46
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
46
47
 
47
48
  const result = await pipeline.evaluate(
48
49
  makeTcc({ toolName: "read", input: {} }),
@@ -53,12 +54,12 @@ describe("ToolCallGatePipeline", () => {
53
54
  });
54
55
 
55
56
  it("returns block when the tool gate denies", async () => {
56
- const { resolve } = makeResolver(
57
+ const resolver = makeResolver(
57
58
  makeCheckResult({ state: "deny", matchedPattern: "*" }),
58
59
  );
59
- const inputs = makeGateInputs({ resolve });
60
- const { runner } = makeGateRunner({ resolve });
61
- const pipeline = new ToolCallGatePipeline(inputs);
60
+ const inputs = makeGateInputs();
61
+ const { runner } = makeGateRunner();
62
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
62
63
 
63
64
  const result = await pipeline.evaluate(
64
65
  makeTcc({ toolName: "read", input: {} }),
@@ -69,13 +70,14 @@ describe("ToolCallGatePipeline", () => {
69
70
  });
70
71
 
71
72
  it("short-circuits after the first blocking gate without evaluating later ones", async () => {
73
+ const resolver = makeResolver(makeCheckResult());
72
74
  const inputs = makeGateInputs();
73
75
  const { runner } = makeGateRunner();
74
76
  const runSpy = vi
75
77
  .spyOn(runner, "run")
76
78
  .mockResolvedValue({ action: "block", reason: "first gate blocked" });
77
79
 
78
- const pipeline = new ToolCallGatePipeline(inputs);
80
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
79
81
  const result = await pipeline.evaluate(
80
82
  makeTcc({ toolName: "read", input: {} }),
81
83
  runner,
@@ -92,9 +94,10 @@ describe("ToolCallGatePipeline", () => {
92
94
  toolTextSummaryMaxLength: 100,
93
95
  toolInputLogPreviewMaxLength: 200,
94
96
  }));
97
+ const resolver = makeResolver(makeCheckResult());
95
98
  const inputs = makeGateInputs({ getToolPreviewLimits });
96
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
97
- const pipeline = new ToolCallGatePipeline(inputs);
99
+ const { runner } = makeGateRunner();
100
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
98
101
 
99
102
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
100
103
 
@@ -103,9 +106,10 @@ describe("ToolCallGatePipeline", () => {
103
106
 
104
107
  it("calls getInfrastructureReadDirs() during evaluate", async () => {
105
108
  const getInfrastructureReadDirs = vi.fn<() => string[]>(() => []);
109
+ const resolver = makeResolver(makeCheckResult());
106
110
  const inputs = makeGateInputs({ getInfrastructureReadDirs });
107
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
108
- const pipeline = new ToolCallGatePipeline(inputs);
111
+ const { runner } = makeGateRunner();
112
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
109
113
 
110
114
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
111
115
 
@@ -114,9 +118,10 @@ describe("ToolCallGatePipeline", () => {
114
118
 
115
119
  it("calls getActiveSkillEntries() during evaluate", async () => {
116
120
  const getActiveSkillEntries = vi.fn<() => []>(() => []);
121
+ const resolver = makeResolver(makeCheckResult());
117
122
  const inputs = makeGateInputs({ getActiveSkillEntries });
118
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
119
- const pipeline = new ToolCallGatePipeline(inputs);
123
+ const { runner } = makeGateRunner();
124
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
120
125
 
121
126
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
122
127
 
@@ -124,9 +129,10 @@ describe("ToolCallGatePipeline", () => {
124
129
  });
125
130
 
126
131
  it("does not call BashProgram.parse for non-bash tools", async () => {
132
+ const resolver = makeResolver(makeCheckResult());
127
133
  const inputs = makeGateInputs();
128
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
129
- const pipeline = new ToolCallGatePipeline(inputs);
134
+ const { runner } = makeGateRunner();
135
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
130
136
 
131
137
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
132
138
 
@@ -138,9 +144,10 @@ describe("ToolCallGatePipeline", () => {
138
144
 
139
145
  describe("evaluate — bash tool", () => {
140
146
  it("returns allow when the bash command is permitted", async () => {
147
+ const resolver = makeResolver(makeCheckResult());
141
148
  const inputs = makeGateInputs();
142
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
143
- const pipeline = new ToolCallGatePipeline(inputs);
149
+ const { runner } = makeGateRunner();
150
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
144
151
 
145
152
  const result = await pipeline.evaluate(
146
153
  makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
@@ -151,9 +158,10 @@ describe("ToolCallGatePipeline", () => {
151
158
  });
152
159
 
153
160
  it("parses BashProgram exactly once per evaluate for bash tools with a command", async () => {
161
+ const resolver = makeResolver(makeCheckResult());
154
162
  const inputs = makeGateInputs();
155
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
156
- const pipeline = new ToolCallGatePipeline(inputs);
163
+ const { runner } = makeGateRunner();
164
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
157
165
 
158
166
  await pipeline.evaluate(
159
167
  makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
@@ -165,9 +173,10 @@ describe("ToolCallGatePipeline", () => {
165
173
  });
166
174
 
167
175
  it("does not parse BashProgram when the bash command is empty", async () => {
176
+ const resolver = makeResolver(makeCheckResult());
168
177
  const inputs = makeGateInputs();
169
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
170
- const pipeline = new ToolCallGatePipeline(inputs);
178
+ const { runner } = makeGateRunner();
179
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
171
180
 
172
181
  await pipeline.evaluate(
173
182
  makeTcc({ toolName: "bash", input: { command: "" } }),
@@ -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,
@@ -10,7 +10,7 @@ import { GateRunner } from "#src/handlers/gates/runner";
10
10
  import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
11
11
  import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
12
12
  import type { ToolCallContext } from "#src/handlers/gates/types";
13
- import type { PermissionResolver } from "#src/permission-resolver";
13
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
14
14
  import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
15
15
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
16
16
  import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
@@ -25,7 +25,7 @@ import { makeCheckResult } from "#test/helpers/handler-fixtures";
25
25
  * mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
26
26
  */
27
27
  export function makeResolver(defaultCheck?: PermissionCheckResult) {
28
- const resolve = vi.fn<PermissionResolver["resolve"]>();
28
+ const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
29
29
  if (defaultCheck) {
30
30
  resolve.mockReturnValue(defaultCheck);
31
31
  }
@@ -91,10 +91,10 @@ export function makeReporter(
91
91
  export function makeGateRunner(
92
92
  overrides: {
93
93
  resolveResult?: PermissionCheckResult;
94
- resolve?: PermissionResolver["resolve"];
94
+ resolve?: ScopedPermissionResolver["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
  ) {
@@ -102,7 +102,7 @@ export function makeGateRunner(
102
102
  const resolve =
103
103
  overrides.resolve ??
104
104
  vi
105
- .fn<PermissionResolver["resolve"]>()
105
+ .fn<ScopedPermissionResolver["resolve"]>()
106
106
  .mockReturnValue(
107
107
  overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
108
108
  );
@@ -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
  };
@@ -204,7 +204,7 @@ export function makePathDispatchResolver(
204
204
  byPath: Record<string, PermissionCheckResult>,
205
205
  defaultResult: PermissionCheckResult,
206
206
  ) {
207
- const resolve = vi.fn<PermissionResolver["resolve"]>();
207
+ const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
208
208
  resolve.mockImplementation((_surface, input) => {
209
209
  const path = (input as Record<string, unknown>).path;
210
210
  if (typeof path === "string" && path in byPath) {
@@ -243,16 +243,12 @@ export function makeGateCheckResult(
243
243
  */
244
244
  export function makeGateInputs(
245
245
  overrides: {
246
- resolve?: PermissionResolver["resolve"];
247
246
  getActiveSkillEntries?: () => SkillPromptEntry[];
248
247
  getInfrastructureReadDirs?: () => string[];
249
248
  getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
250
249
  } = {},
251
250
  ): ToolCallGateInputs {
252
251
  return {
253
- resolve:
254
- overrides.resolve ??
255
- vi.fn<PermissionResolver["resolve"]>().mockReturnValue(makeCheckResult()),
256
252
  getActiveSkillEntries:
257
253
  overrides.getActiveSkillEntries ??
258
254
  vi.fn<() => SkillPromptEntry[]>(() => []),