@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
@@ -3,9 +3,11 @@ import { describe, expect, it, vi } from "vitest";
3
3
  import { getEventInput } from "#src/handlers/permission-gate-handler";
4
4
 
5
5
  import {
6
+ makeBashCommandCheck,
6
7
  makeCheckResult,
7
8
  makeCtx,
8
9
  makeHandler,
10
+ makeSurfaceCheck,
9
11
  makeToolCallEvent,
10
12
  } from "#test/helpers/handler-fixtures";
11
13
 
@@ -65,11 +67,7 @@ describe("handleToolCall", () => {
65
67
  });
66
68
 
67
69
  it("blocks when tool is not registered", async () => {
68
- const { handler } = makeHandler({
69
- toolRegistry: {
70
- getAll: vi.fn().mockReturnValue([{ name: "read" }]),
71
- },
72
- });
70
+ const { handler } = makeHandler({ tools: ["read"] });
73
71
  const result = await handler.handleToolCall(
74
72
  makeToolCallEvent("unknown-tool"),
75
73
  makeCtx(),
@@ -170,16 +168,11 @@ describe("handleToolCall — external-directory gate", () => {
170
168
  .fn()
171
169
  .mockReturnValue(makeCheckResult({ state: "deny" })),
172
170
  },
173
- toolRegistry: {
174
- getAll: vi.fn().mockReturnValue([{ name: "read" }]),
175
- },
171
+ tools: ["read"],
176
172
  });
177
- const event = {
178
- type: "tool_call",
179
- toolCallId: "tc-ext",
180
- name: "read",
173
+ const event = makeToolCallEvent("read", {
181
174
  input: { path: "/outside/project/file.ts" },
182
- };
175
+ });
183
176
  const result = await handler.handleToolCall(event, makeCtx());
184
177
  expect(result).toMatchObject({ block: true });
185
178
  });
@@ -195,16 +188,11 @@ describe("handleToolCall — bash external-directory gate", () => {
195
188
  .fn()
196
189
  .mockReturnValue(makeCheckResult({ state: "deny" })),
197
190
  },
198
- toolRegistry: {
199
- getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
200
- },
191
+ tools: ["bash"],
201
192
  });
202
- const event = {
203
- type: "tool_call",
204
- toolCallId: "tc-bash-ext",
205
- name: "bash",
193
+ const event = makeToolCallEvent("bash", {
206
194
  input: { command: "cat /outside/project/file.ts" },
207
- };
195
+ });
208
196
  const result = await handler.handleToolCall(event, makeCtx());
209
197
  expect(result).toMatchObject({ block: true });
210
198
  });
@@ -214,44 +202,24 @@ describe("handleToolCall — bash external-directory gate", () => {
214
202
 
215
203
  describe("handleToolCall — path gate (tools)", () => {
216
204
  it("blocks a read of .env when path surface denies *.env", async () => {
217
- const checkPermission = vi
218
- .fn()
219
- .mockImplementation(
220
- (surface: string, _input: unknown, _agentName?: string) => {
221
- if (surface === "path") {
222
- return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
223
- }
224
- return makeCheckResult();
225
- },
226
- );
227
205
  const { handler } = makeHandler({
228
- session: { checkPermission },
229
- toolRegistry: {
230
- getAll: vi.fn().mockReturnValue([{ name: "read" }]),
206
+ session: {
207
+ checkPermission: makeSurfaceCheck({
208
+ path: { state: "deny", matchedPattern: "*.env" },
209
+ }),
231
210
  },
211
+ tools: ["read"],
232
212
  });
233
- const event = {
234
- type: "tool_call",
235
- toolCallId: "tc-path",
236
- name: "read",
237
- input: { path: ".env" },
238
- };
213
+ const event = makeToolCallEvent("read", { input: { path: ".env" } });
239
214
  const result = await handler.handleToolCall(event, makeCtx());
240
215
  expect(result).toMatchObject({ block: true });
241
216
  });
242
217
 
243
218
  it("allows a read when path surface allows", async () => {
244
- const { handler } = makeHandler({
245
- toolRegistry: {
246
- getAll: vi.fn().mockReturnValue([{ name: "read" }]),
247
- },
248
- });
249
- const event = {
250
- type: "tool_call",
251
- toolCallId: "tc-path-ok",
252
- name: "read",
219
+ const { handler } = makeHandler({ tools: ["read"] });
220
+ const event = makeToolCallEvent("read", {
253
221
  input: { path: "src/index.ts" },
254
- };
222
+ });
255
223
  const result = await handler.handleToolCall(event, makeCtx());
256
224
  expect(result).toEqual({});
257
225
  });
@@ -261,28 +229,15 @@ describe("handleToolCall — path gate (tools)", () => {
261
229
 
262
230
  describe("handleToolCall — bash path gate", () => {
263
231
  it("blocks a bash command accessing .env when path surface denies", async () => {
264
- const checkPermission = vi
265
- .fn()
266
- .mockImplementation(
267
- (surface: string, _input: unknown, _agentName?: string) => {
268
- if (surface === "path") {
269
- return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
270
- }
271
- return makeCheckResult();
272
- },
273
- );
274
232
  const { handler } = makeHandler({
275
- session: { checkPermission },
276
- toolRegistry: {
277
- getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
233
+ session: {
234
+ checkPermission: makeSurfaceCheck({
235
+ path: { state: "deny", matchedPattern: "*.env" },
236
+ }),
278
237
  },
238
+ tools: ["bash"],
279
239
  });
280
- const event = {
281
- type: "tool_call",
282
- toolCallId: "tc-bash-path",
283
- name: "bash",
284
- input: { command: "cat .env" },
285
- };
240
+ const event = makeToolCallEvent("bash", { input: { command: "cat .env" } });
286
241
  const result = await handler.handleToolCall(event, makeCtx());
287
242
  expect(result).toMatchObject({ block: true });
288
243
  });
@@ -292,108 +247,44 @@ describe("handleToolCall — bash path gate", () => {
292
247
 
293
248
  describe("handleToolCall — bash command chain gate", () => {
294
249
  it("blocks a chain when a later sub-command is denied (#301)", async () => {
295
- const checkPermission = vi
296
- .fn()
297
- .mockImplementation((surface: string, input: unknown) => {
298
- if (surface === "bash") {
299
- const command = (input as { command?: string }).command ?? "";
300
- return /^npm\b/.test(command)
301
- ? makeCheckResult({
302
- state: "deny",
303
- source: "bash",
304
- command,
305
- matchedPattern: "npm *",
306
- })
307
- : makeCheckResult({
308
- state: "allow",
309
- source: "bash",
310
- command,
311
- matchedPattern: "echo *",
312
- });
313
- }
314
- return makeCheckResult({ state: "allow" });
315
- });
316
250
  const { handler } = makeHandler({
317
- session: { checkPermission },
318
- toolRegistry: {
319
- getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
251
+ session: {
252
+ checkPermission: makeBashCommandCheck({
253
+ deny: /^npm\b/,
254
+ denyMatched: "npm *",
255
+ allowMatched: "echo *",
256
+ }),
320
257
  },
258
+ tools: ["bash"],
321
259
  });
322
- const event = {
323
- type: "tool_call",
324
- toolCallId: "tc-bash-chain",
325
- name: "bash",
260
+ const event = makeToolCallEvent("bash", {
326
261
  input: { command: "echo start && npm install compromised-package" },
327
- };
262
+ });
328
263
  const result = await handler.handleToolCall(event, makeCtx());
329
264
  expect(result).toMatchObject({ block: true });
330
265
  });
331
266
 
332
267
  it("blocks a command nested inside command substitution (#306)", async () => {
333
- const checkPermission = vi
334
- .fn()
335
- .mockImplementation((surface: string, input: unknown) => {
336
- if (surface === "bash") {
337
- const command = (input as { command?: string }).command ?? "";
338
- return /^rm\b/.test(command)
339
- ? makeCheckResult({
340
- state: "deny",
341
- source: "bash",
342
- command,
343
- matchedPattern: "rm *",
344
- })
345
- : makeCheckResult({
346
- state: "allow",
347
- source: "bash",
348
- command,
349
- matchedPattern: "echo *",
350
- });
351
- }
352
- return makeCheckResult({ state: "allow" });
353
- });
354
268
  const { handler } = makeHandler({
355
- session: { checkPermission },
356
- toolRegistry: {
357
- getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
269
+ session: {
270
+ checkPermission: makeBashCommandCheck({
271
+ deny: /^rm\b/,
272
+ denyMatched: "rm *",
273
+ allowMatched: "echo *",
274
+ }),
358
275
  },
276
+ tools: ["bash"],
359
277
  });
360
- const event = {
361
- type: "tool_call",
362
- toolCallId: "tc-bash-substitution",
363
- name: "bash",
278
+ const event = makeToolCallEvent("bash", {
364
279
  input: { command: "echo $(rm -rf foo)" },
365
- };
280
+ });
366
281
  const result = await handler.handleToolCall(event, makeCtx());
367
282
  expect(result).toMatchObject({ block: true });
368
283
  });
369
284
 
370
285
  it("allows a single non-chained bash command", async () => {
371
- const checkPermission = vi
372
- .fn()
373
- .mockImplementation((surface: string, input: unknown) => {
374
- if (surface === "bash") {
375
- const command = (input as { command?: string }).command ?? "";
376
- return makeCheckResult({
377
- state: "allow",
378
- source: "bash",
379
- command,
380
- matchedPattern: "echo *",
381
- });
382
- }
383
- return makeCheckResult({ state: "allow" });
384
- });
385
- const { handler } = makeHandler({
386
- session: { checkPermission },
387
- toolRegistry: {
388
- getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
389
- },
390
- });
391
- const event = {
392
- type: "tool_call",
393
- toolCallId: "tc-bash-single",
394
- name: "bash",
395
- input: { command: "echo hi" },
396
- };
286
+ const { handler } = makeHandler({ tools: ["bash"] });
287
+ const event = makeToolCallEvent("bash", { input: { command: "echo hi" } });
397
288
  const result = await handler.handleToolCall(event, makeCtx());
398
289
  expect(result).toEqual({});
399
290
  });
@@ -2,16 +2,36 @@
2
2
  * Shared gate-level test fixtures for gate descriptor and runner tests.
3
3
  */
4
4
  import { vi } from "vitest";
5
-
6
- import type {
7
- GateDescriptor,
8
- GateRunnerDeps,
9
- } from "#src/handlers/gates/descriptor";
5
+ import type { DecisionReporter } from "#src/decision-reporter";
6
+ import type { DenialContext } from "#src/denial-messages";
7
+ import type { GatePrompter } from "#src/gate-prompter";
8
+ import type { GateDescriptor } from "#src/handlers/gates/descriptor";
9
+ import { GateRunner } from "#src/handlers/gates/runner";
10
+ import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
11
+ import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
10
12
  import type { ToolCallContext } from "#src/handlers/gates/types";
13
+ import type { PermissionResolver } from "#src/permission-resolver";
14
+ import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
15
+ import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
16
+ import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
11
17
  import type { PermissionCheckResult } from "#src/types";
12
18
 
13
19
  import { makeCheckResult } from "#test/helpers/handler-fixtures";
14
20
 
21
+ /**
22
+ * Permission resolver mock with an optional default check result.
23
+ *
24
+ * Returns a plain object whose `resolve` is a `vi.fn` so callers retain full
25
+ * mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
26
+ */
27
+ export function makeResolver(defaultCheck?: PermissionCheckResult) {
28
+ const resolve = vi.fn<PermissionResolver["resolve"]>();
29
+ if (defaultCheck) {
30
+ resolve.mockReturnValue(defaultCheck);
31
+ }
32
+ return { resolve };
33
+ }
34
+
15
35
  /**
16
36
  * Gate descriptor factory with runner-test defaults.
17
37
  *
@@ -48,21 +68,105 @@ export function makeDescriptor(
48
68
  };
49
69
  }
50
70
 
51
- export function makeRunnerDeps(
52
- overrides: Partial<GateRunnerDeps> = {},
53
- ): GateRunnerDeps {
71
+ /**
72
+ * Reporter mock with independently inspectable vi.fn() stubs.
73
+ */
74
+ export function makeReporter(
75
+ overrides: Partial<DecisionReporter> = {},
76
+ ): DecisionReporter {
54
77
  return {
55
- checkPermission: vi
56
- .fn()
57
- .mockReturnValue(makeCheckResult({ matchedPattern: "*" })),
58
- getSessionRuleset: vi.fn().mockReturnValue([]),
59
- recordSessionApproval: vi.fn(),
60
78
  writeReviewLog: vi.fn(),
61
79
  emitDecision: vi.fn(),
62
- canConfirm: vi.fn().mockReturnValue(true),
63
- promptPermission: vi
64
- .fn()
65
- .mockResolvedValue({ approved: true, state: "approved" }),
80
+ ...overrides,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Gate runner factory for `GateRunner` unit tests.
86
+ *
87
+ * Builds one `GateRunner` from four role mocks and returns `{ runner, deps }`
88
+ * so tests can both invoke `runner.run(...)` and assert on the individual
89
+ * mock call records (`deps.reporter.*`, `deps.resolve`, etc.).
90
+ */
91
+ export function makeGateRunner(
92
+ overrides: {
93
+ resolveResult?: PermissionCheckResult;
94
+ resolve?: PermissionResolver["resolve"];
95
+ recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
96
+ canConfirm?: GatePrompter["canConfirm"];
97
+ promptPermission?: GatePrompter["promptPermission"];
98
+ reporter?: Partial<DecisionReporter>;
99
+ } = {},
100
+ ) {
101
+ const reporter = makeReporter(overrides.reporter);
102
+ const resolve =
103
+ overrides.resolve ??
104
+ vi
105
+ .fn<PermissionResolver["resolve"]>()
106
+ .mockReturnValue(
107
+ overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
108
+ );
109
+ const recordSessionApproval =
110
+ overrides.recordSessionApproval ??
111
+ (vi.fn() as SessionApprovalRecorder["recordSessionApproval"]);
112
+ const canConfirm =
113
+ overrides.canConfirm ??
114
+ (vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
115
+ const promptPermission =
116
+ overrides.promptPermission ??
117
+ vi
118
+ .fn<GatePrompter["promptPermission"]>()
119
+ .mockResolvedValue({ approved: true, state: "approved" });
120
+ const runner = new GateRunner(
121
+ { resolve },
122
+ { recordSessionApproval },
123
+ { canConfirm, promptPermission },
124
+ reporter,
125
+ );
126
+ return {
127
+ runner,
128
+ deps: {
129
+ resolve,
130
+ recordSessionApproval,
131
+ canConfirm,
132
+ promptPermission,
133
+ reporter,
134
+ },
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Gate descriptor variant with write-surface defaults and a caller-supplied
140
+ * denialContext.
141
+ *
142
+ * Use instead of `makeDescriptor` when the test exercises denial-message
143
+ * formatting — the write surface and its matching promptDetails/logContext
144
+ * keep the message helpers' field access consistent.
145
+ */
146
+ export function makeDenialDescriptor(
147
+ denialContext: DenialContext,
148
+ overrides: Partial<GateDescriptor> = {},
149
+ ): GateDescriptor {
150
+ return {
151
+ surface: "write",
152
+ input: {},
153
+ denialContext,
154
+ promptDetails: {
155
+ source: "tool_call",
156
+ agentName: null,
157
+ message: "Allow tool 'write'?",
158
+ toolCallId: "tc-1",
159
+ toolName: "write",
160
+ },
161
+ logContext: {
162
+ source: "tool_call",
163
+ toolCallId: "tc-1",
164
+ toolName: "write",
165
+ },
166
+ decision: {
167
+ surface: "write",
168
+ value: "write",
169
+ },
66
170
  ...overrides,
67
171
  };
68
172
  }
@@ -86,6 +190,31 @@ export function makeTcc(
86
190
  };
87
191
  }
88
192
 
193
+ /**
194
+ * Resolver whose `resolve` dispatches on `input.path`, falling back to a
195
+ * default result for any path not in the map.
196
+ *
197
+ * Use when a test needs different results for different path tokens without
198
+ * writing a full `mockImplementation` block.
199
+ *
200
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
201
+ * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
202
+ */
203
+ export function makePathDispatchResolver(
204
+ byPath: Record<string, PermissionCheckResult>,
205
+ defaultResult: PermissionCheckResult,
206
+ ) {
207
+ const resolve = vi.fn<PermissionResolver["resolve"]>();
208
+ resolve.mockImplementation((_surface, input) => {
209
+ const path = (input as Record<string, unknown>).path;
210
+ if (typeof path === "string" && path in byPath) {
211
+ return byPath[path];
212
+ }
213
+ return defaultResult;
214
+ });
215
+ return { resolve };
216
+ }
217
+
89
218
  /**
90
219
  * Path-surface check result factory.
91
220
  *
@@ -103,3 +232,69 @@ export function makeGateCheckResult(
103
232
  ...overrides,
104
233
  };
105
234
  }
235
+
236
+ /**
237
+ * Mock of `ToolCallGateInputs` for `ToolCallGatePipeline` unit tests.
238
+ *
239
+ * Each method is a `vi.fn()` stub so callers retain full mock access
240
+ * (`mock.calls`, `mockReturnValue`, etc.) on the returned object.
241
+ * Pass `overrides` to replace individual stubs without rebuilding the whole
242
+ * mock from scratch.
243
+ */
244
+ export function makeGateInputs(
245
+ overrides: {
246
+ resolve?: PermissionResolver["resolve"];
247
+ getActiveSkillEntries?: () => SkillPromptEntry[];
248
+ getInfrastructureReadDirs?: () => string[];
249
+ getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
250
+ } = {},
251
+ ): ToolCallGateInputs {
252
+ return {
253
+ resolve:
254
+ overrides.resolve ??
255
+ vi.fn<PermissionResolver["resolve"]>().mockReturnValue(makeCheckResult()),
256
+ getActiveSkillEntries:
257
+ overrides.getActiveSkillEntries ??
258
+ vi.fn<() => SkillPromptEntry[]>(() => []),
259
+ getInfrastructureReadDirs:
260
+ overrides.getInfrastructureReadDirs ?? vi.fn<() => string[]>(() => []),
261
+ getToolPreviewLimits:
262
+ overrides.getToolPreviewLimits ??
263
+ vi.fn<() => ToolPreviewFormatterOptions>(() => ({
264
+ toolInputPreviewMaxLength: 500,
265
+ toolTextSummaryMaxLength: 100,
266
+ toolInputLogPreviewMaxLength: 200,
267
+ })),
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Mock of `SkillInputGateInputs` for `SkillInputGatePipeline` unit tests.
273
+ *
274
+ * Returns a plain object with a `checkPermission` `vi.fn()` stub so callers
275
+ * retain full mock access (`mockReturnValue`, `mock.calls`, etc.).
276
+ */
277
+ export function makeSkillInputInputs(
278
+ overrides: { checkPermission?: SkillInputGateInputs["checkPermission"] } = {},
279
+ ): SkillInputGateInputs {
280
+ return {
281
+ checkPermission:
282
+ overrides.checkPermission ??
283
+ vi
284
+ .fn<SkillInputGateInputs["checkPermission"]>()
285
+ .mockReturnValue(makeCheckResult()),
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Mock `GateNotifier` for `SkillInputGatePipeline` unit tests.
291
+ *
292
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
293
+ * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.) — annotating with
294
+ * `GateNotifier` would erase `Mock<...>` methods from the inferred type.
295
+ */
296
+ export function makeNotifier() {
297
+ return {
298
+ warn: vi.fn<(message: string) => void>(),
299
+ };
300
+ }