@komarspn/pi-permission-system 16.0.2

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 (203) hide show
  1. package/CHANGELOG.md +2234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +158 -0
  4. package/config/config.example.json +39 -0
  5. package/package.json +82 -0
  6. package/schemas/permissions.schema.json +158 -0
  7. package/src/active-agent.ts +72 -0
  8. package/src/async-cache.ts +21 -0
  9. package/src/bash-arity.ts +210 -0
  10. package/src/builtin-tool-input-formatters.ts +82 -0
  11. package/src/canonicalize-path.ts +30 -0
  12. package/src/common.ts +121 -0
  13. package/src/config-loader.ts +432 -0
  14. package/src/config-modal.ts +259 -0
  15. package/src/config-paths.ts +47 -0
  16. package/src/config-reporter.ts +34 -0
  17. package/src/config-store.ts +222 -0
  18. package/src/decision-audit.ts +75 -0
  19. package/src/decision-reporter.ts +41 -0
  20. package/src/denial-messages.ts +232 -0
  21. package/src/expand-home.ts +28 -0
  22. package/src/extension-config.ts +79 -0
  23. package/src/extension-paths.ts +66 -0
  24. package/src/forwarded-permissions/io.ts +404 -0
  25. package/src/forwarded-permissions/permission-forwarder.ts +580 -0
  26. package/src/forwarding-manager.ts +74 -0
  27. package/src/gate-prompter.ts +12 -0
  28. package/src/handlers/before-agent-start.ts +94 -0
  29. package/src/handlers/gates/bash-command.ts +75 -0
  30. package/src/handlers/gates/bash-external-directory.ts +127 -0
  31. package/src/handlers/gates/bash-path-extractor.ts +15 -0
  32. package/src/handlers/gates/bash-path.ts +152 -0
  33. package/src/handlers/gates/bash-program.ts +1143 -0
  34. package/src/handlers/gates/bash-token-classification.ts +105 -0
  35. package/src/handlers/gates/candidate-check.ts +32 -0
  36. package/src/handlers/gates/descriptor.ts +81 -0
  37. package/src/handlers/gates/external-directory-messages.ts +20 -0
  38. package/src/handlers/gates/external-directory.ts +133 -0
  39. package/src/handlers/gates/helpers.ts +76 -0
  40. package/src/handlers/gates/path.ts +91 -0
  41. package/src/handlers/gates/runner.ts +186 -0
  42. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  43. package/src/handlers/gates/skill-input.ts +46 -0
  44. package/src/handlers/gates/skill-read.ts +87 -0
  45. package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
  46. package/src/handlers/gates/tool.ts +102 -0
  47. package/src/handlers/gates/types.ts +13 -0
  48. package/src/handlers/index.ts +3 -0
  49. package/src/handlers/lifecycle.ts +95 -0
  50. package/src/handlers/permission-gate-handler.ts +190 -0
  51. package/src/handlers/tool-call-boundary.ts +91 -0
  52. package/src/index.ts +225 -0
  53. package/src/input-normalizer.ts +157 -0
  54. package/src/logging.ts +113 -0
  55. package/src/mcp-targets.ts +170 -0
  56. package/src/node-modules-discovery.ts +76 -0
  57. package/src/normalize.ts +43 -0
  58. package/src/path-utils.ts +355 -0
  59. package/src/pattern-suggest.ts +132 -0
  60. package/src/permission-dialog.ts +138 -0
  61. package/src/permission-event-rpc.ts +223 -0
  62. package/src/permission-events.ts +266 -0
  63. package/src/permission-forwarding.ts +188 -0
  64. package/src/permission-gate.ts +94 -0
  65. package/src/permission-manager.ts +392 -0
  66. package/src/permission-merge.ts +32 -0
  67. package/src/permission-prompter.ts +142 -0
  68. package/src/permission-prompts.ts +93 -0
  69. package/src/permission-resolver.ts +109 -0
  70. package/src/permission-session.ts +189 -0
  71. package/src/permission-ui-prompt.ts +127 -0
  72. package/src/permissions-service.ts +63 -0
  73. package/src/persistent-approval-recorder.ts +139 -0
  74. package/src/policy-loader.ts +350 -0
  75. package/src/prompting-gateway.ts +104 -0
  76. package/src/rule.ts +188 -0
  77. package/src/scope-merge.ts +72 -0
  78. package/src/service-lifecycle.ts +49 -0
  79. package/src/service.ts +163 -0
  80. package/src/session-approval-recorder.ts +6 -0
  81. package/src/session-approval.ts +43 -0
  82. package/src/session-logger.ts +91 -0
  83. package/src/session-rules.ts +79 -0
  84. package/src/skill-prompt-sanitizer.ts +292 -0
  85. package/src/status.ts +35 -0
  86. package/src/subagent-context.ts +104 -0
  87. package/src/subagent-lifecycle-events.ts +72 -0
  88. package/src/subagent-registry.ts +105 -0
  89. package/src/synthesize.ts +92 -0
  90. package/src/system-prompt-sanitizer.ts +274 -0
  91. package/src/tool-access-extractor-registry.ts +68 -0
  92. package/src/tool-input-formatter-registry.ts +67 -0
  93. package/src/tool-input-preview.ts +34 -0
  94. package/src/tool-input-prompt-formatters.ts +63 -0
  95. package/src/tool-preview-formatter.ts +207 -0
  96. package/src/tool-registry.ts +148 -0
  97. package/src/types.ts +64 -0
  98. package/src/wildcard-matcher.ts +120 -0
  99. package/src/yolo-mode.ts +30 -0
  100. package/test/active-agent.test.ts +155 -0
  101. package/test/async-cache.test.ts +48 -0
  102. package/test/bash-arity.test.ts +144 -0
  103. package/test/bash-external-directory.test.ts +956 -0
  104. package/test/builtin-tool-input-formatters.test.ts +109 -0
  105. package/test/canonicalize-path.test.ts +93 -0
  106. package/test/common.test.ts +287 -0
  107. package/test/composition-root.test.ts +603 -0
  108. package/test/config-loader.test.ts +740 -0
  109. package/test/config-modal.test.ts +320 -0
  110. package/test/config-paths.test.ts +83 -0
  111. package/test/config-pipeline.test.ts +90 -0
  112. package/test/config-reporter.test.ts +147 -0
  113. package/test/config-store.test.ts +466 -0
  114. package/test/decision-audit.test.ts +72 -0
  115. package/test/decision-reporter.test.ts +112 -0
  116. package/test/denial-messages.test.ts +656 -0
  117. package/test/detect-permissive-bash-fallback.test.ts +56 -0
  118. package/test/expand-home.test.ts +93 -0
  119. package/test/extension-config.test.ts +129 -0
  120. package/test/extension-paths.test.ts +108 -0
  121. package/test/forwarded-permissions/io.test.ts +251 -0
  122. package/test/forwarding-manager.test.ts +194 -0
  123. package/test/handlers/before-agent-start.test.ts +317 -0
  124. package/test/handlers/external-directory-integration.test.ts +623 -0
  125. package/test/handlers/external-directory-session-dedup.test.ts +430 -0
  126. package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
  127. package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
  128. package/test/handlers/gates/bash-command.test.ts +191 -0
  129. package/test/handlers/gates/bash-external-directory.test.ts +269 -0
  130. package/test/handlers/gates/bash-path.test.ts +337 -0
  131. package/test/handlers/gates/bash-program.test.ts +410 -0
  132. package/test/handlers/gates/bash-token-classification.test.ts +241 -0
  133. package/test/handlers/gates/candidate-check.test.ts +52 -0
  134. package/test/handlers/gates/external-directory-messages.test.ts +61 -0
  135. package/test/handlers/gates/external-directory.test.ts +259 -0
  136. package/test/handlers/gates/helpers.test.ts +177 -0
  137. package/test/handlers/gates/path.test.ts +294 -0
  138. package/test/handlers/gates/runner.test.ts +447 -0
  139. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  140. package/test/handlers/gates/skill-input.test.ts +131 -0
  141. package/test/handlers/gates/skill-read.test.ts +158 -0
  142. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
  143. package/test/handlers/gates/tool.test.ts +223 -0
  144. package/test/handlers/input-events.test.ts +168 -0
  145. package/test/handlers/input.test.ts +199 -0
  146. package/test/handlers/lifecycle.test.ts +221 -0
  147. package/test/handlers/tool-call-boundary.test.ts +145 -0
  148. package/test/handlers/tool-call-events.test.ts +277 -0
  149. package/test/handlers/tool-call.test.ts +395 -0
  150. package/test/handlers/validate-requested-tool.test.ts +92 -0
  151. package/test/helpers/gate-fixtures.ts +323 -0
  152. package/test/helpers/handler-fixtures.ts +335 -0
  153. package/test/helpers/make-fake-pi.ts +100 -0
  154. package/test/helpers/manager-harness.ts +112 -0
  155. package/test/helpers/session-fixtures.ts +204 -0
  156. package/test/input-normalizer.test.ts +367 -0
  157. package/test/logging.test.ts +51 -0
  158. package/test/mcp-targets.test.ts +233 -0
  159. package/test/node-modules-discovery.test.ts +97 -0
  160. package/test/normalize.test.ts +247 -0
  161. package/test/path-utils.test.ts +650 -0
  162. package/test/pattern-suggest.test.ts +248 -0
  163. package/test/permission-dialog.test.ts +241 -0
  164. package/test/permission-event-rpc.test.ts +541 -0
  165. package/test/permission-events.test.ts +402 -0
  166. package/test/permission-forwarder.test.ts +369 -0
  167. package/test/permission-forwarding.test.ts +315 -0
  168. package/test/permission-gate.test.ts +305 -0
  169. package/test/permission-manager-unified.test.ts +3368 -0
  170. package/test/permission-merge.test.ts +61 -0
  171. package/test/permission-prompter.test.ts +518 -0
  172. package/test/permission-prompts.test.ts +363 -0
  173. package/test/permission-resolver.test.ts +265 -0
  174. package/test/permission-session.test.ts +363 -0
  175. package/test/permission-ui-prompt.test.ts +146 -0
  176. package/test/permissions-service.test.ts +177 -0
  177. package/test/persistent-approval-recorder.test.ts +133 -0
  178. package/test/pi-infrastructure-read.test.ts +369 -0
  179. package/test/policy-loader.test.ts +561 -0
  180. package/test/prompting-gateway.test.ts +230 -0
  181. package/test/rule.test.ts +604 -0
  182. package/test/scope-merge.test.ts +116 -0
  183. package/test/service-lifecycle.test.ts +163 -0
  184. package/test/service.test.ts +308 -0
  185. package/test/session-approval.test.ts +75 -0
  186. package/test/session-logger.test.ts +200 -0
  187. package/test/session-rules.test.ts +304 -0
  188. package/test/session-start.test.ts +112 -0
  189. package/test/skill-prompt-sanitizer.test.ts +374 -0
  190. package/test/status.test.ts +10 -0
  191. package/test/subagent-context.test.ts +326 -0
  192. package/test/subagent-lifecycle-events.test.ts +132 -0
  193. package/test/subagent-registry.test.ts +145 -0
  194. package/test/synthesize.test.ts +300 -0
  195. package/test/system-prompt-sanitizer.test.ts +382 -0
  196. package/test/tool-access-extractor-registry.test.ts +77 -0
  197. package/test/tool-input-formatter-registry.test.ts +75 -0
  198. package/test/tool-input-preview.test.ts +129 -0
  199. package/test/tool-input-prompt-formatters.test.ts +115 -0
  200. package/test/tool-preview-formatter.test.ts +458 -0
  201. package/test/tool-registry.test.ts +197 -0
  202. package/test/wildcard-matcher.test.ts +424 -0
  203. package/test/yolo-mode.test.ts +188 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Shared gate-level test fixtures for gate descriptor and runner tests.
3
+ */
4
+ import { vi } from "vitest";
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";
12
+ import type { ToolCallContext } from "#src/handlers/gates/types";
13
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
14
+ import type { PersistentApprovalRecorder } from "#src/persistent-approval-recorder";
15
+ import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
16
+ import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
17
+ import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
18
+ import type { PermissionCheckResult } from "#src/types";
19
+
20
+ import { makeCheckResult } from "#test/helpers/handler-fixtures";
21
+
22
+ /**
23
+ * Permission resolver mock with an optional default check result.
24
+ *
25
+ * Returns a plain object whose `resolve` is a `vi.fn` so callers retain full
26
+ * mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
27
+ */
28
+ export function makeResolver(defaultCheck?: PermissionCheckResult) {
29
+ const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
30
+ const resolvePathPolicy =
31
+ vi.fn<ScopedPermissionResolver["resolvePathPolicy"]>();
32
+ if (defaultCheck) {
33
+ resolve.mockReturnValue(defaultCheck);
34
+ resolvePathPolicy.mockReturnValue(defaultCheck);
35
+ }
36
+ return { resolve, resolvePathPolicy };
37
+ }
38
+
39
+ /**
40
+ * Gate descriptor factory with runner-test defaults.
41
+ *
42
+ * Uses deny as the default `denialContext` check result so tests that
43
+ * verify block paths don't need to override the surface check.
44
+ */
45
+ export function makeDescriptor(
46
+ overrides: Partial<GateDescriptor> = {},
47
+ ): GateDescriptor {
48
+ return {
49
+ surface: "read",
50
+ input: {},
51
+ denialContext: {
52
+ kind: "tool",
53
+ check: makeCheckResult({ state: "deny", matchedPattern: "*" }),
54
+ },
55
+ promptDetails: {
56
+ source: "tool_call",
57
+ agentName: null,
58
+ message: "Allow tool 'read'?",
59
+ toolCallId: "tc-1",
60
+ toolName: "read",
61
+ },
62
+ logContext: {
63
+ source: "tool_call",
64
+ toolCallId: "tc-1",
65
+ toolName: "read",
66
+ },
67
+ decision: {
68
+ surface: "read",
69
+ value: "read",
70
+ },
71
+ ...overrides,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Reporter mock with independently inspectable vi.fn() stubs.
77
+ */
78
+ export function makeReporter(
79
+ overrides: Partial<DecisionReporter> = {},
80
+ ): DecisionReporter {
81
+ return {
82
+ writeReviewLog: vi.fn(),
83
+ emitDecision: vi.fn(),
84
+ ...overrides,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Gate runner factory for `GateRunner` unit tests.
90
+ *
91
+ * Builds one `GateRunner` from four role mocks and returns `{ runner, deps }`
92
+ * so tests can both invoke `runner.run(...)` and assert on the individual
93
+ * mock call records (`deps.reporter.*`, `deps.resolve`, etc.).
94
+ */
95
+ export function makeGateRunner(
96
+ overrides: {
97
+ resolveResult?: PermissionCheckResult;
98
+ resolve?: ScopedPermissionResolver["resolve"];
99
+ resolvePathPolicy?: ScopedPermissionResolver["resolvePathPolicy"];
100
+ recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
101
+ recordPersistentApproval?: PersistentApprovalRecorder["recordApproval"];
102
+ canConfirm?: GatePrompter["canConfirm"];
103
+ prompt?: GatePrompter["prompt"];
104
+ reporter?: Partial<DecisionReporter>;
105
+ } = {},
106
+ ) {
107
+ const reporter = makeReporter(overrides.reporter);
108
+ const resolve =
109
+ overrides.resolve ??
110
+ vi
111
+ .fn<ScopedPermissionResolver["resolve"]>()
112
+ .mockReturnValue(
113
+ overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
114
+ );
115
+ const resolvePathPolicy =
116
+ overrides.resolvePathPolicy ??
117
+ vi
118
+ .fn<ScopedPermissionResolver["resolvePathPolicy"]>()
119
+ .mockReturnValue(
120
+ overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
121
+ );
122
+ const recordSessionApproval =
123
+ overrides.recordSessionApproval ??
124
+ (vi.fn() as SessionApprovalRecorder["recordSessionApproval"]);
125
+ const recordPersistentApproval =
126
+ overrides.recordPersistentApproval ??
127
+ (vi.fn() as PersistentApprovalRecorder["recordApproval"]);
128
+ const canConfirm =
129
+ overrides.canConfirm ??
130
+ (vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
131
+ const prompt =
132
+ overrides.prompt ??
133
+ vi
134
+ .fn<GatePrompter["prompt"]>()
135
+ .mockResolvedValue({ approved: true, state: "approved" });
136
+ const runner = new GateRunner(
137
+ { resolve, resolvePathPolicy },
138
+ { recordSessionApproval },
139
+ { canConfirm, prompt },
140
+ reporter,
141
+ { recordApproval: recordPersistentApproval } as PersistentApprovalRecorder,
142
+ );
143
+ return {
144
+ runner,
145
+ deps: {
146
+ resolve,
147
+ resolvePathPolicy,
148
+ recordSessionApproval,
149
+ recordPersistentApproval,
150
+ canConfirm,
151
+ prompt,
152
+ reporter,
153
+ },
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Gate descriptor variant with write-surface defaults and a caller-supplied
159
+ * denialContext.
160
+ *
161
+ * Use instead of `makeDescriptor` when the test exercises denial-message
162
+ * formatting — the write surface and its matching promptDetails/logContext
163
+ * keep the message helpers' field access consistent.
164
+ */
165
+ export function makeDenialDescriptor(
166
+ denialContext: DenialContext,
167
+ overrides: Partial<GateDescriptor> = {},
168
+ ): GateDescriptor {
169
+ return {
170
+ surface: "write",
171
+ input: {},
172
+ denialContext,
173
+ promptDetails: {
174
+ source: "tool_call",
175
+ agentName: null,
176
+ message: "Allow tool 'write'?",
177
+ toolCallId: "tc-1",
178
+ toolName: "write",
179
+ },
180
+ logContext: {
181
+ source: "tool_call",
182
+ toolCallId: "tc-1",
183
+ toolName: "write",
184
+ },
185
+ decision: {
186
+ surface: "write",
187
+ value: "write",
188
+ },
189
+ ...overrides,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Tool-call context factory with bash defaults.
195
+ *
196
+ * path.test.ts uses different defaults (toolName "read", path input) and
197
+ * keeps a local wrapper; bash-path.test.ts uses this factory directly.
198
+ */
199
+ export function makeTcc(
200
+ overrides: Partial<ToolCallContext> = {},
201
+ ): ToolCallContext {
202
+ return {
203
+ toolName: "bash",
204
+ agentName: null,
205
+ input: { command: "cat .env" },
206
+ toolCallId: "tc-1",
207
+ cwd: "/test/project",
208
+ ...overrides,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Resolver whose `resolve` dispatches on `input.path`, falling back to a
214
+ * default result for any path not in the map.
215
+ *
216
+ * Use when a test needs different results for different path tokens without
217
+ * writing a full `mockImplementation` block.
218
+ *
219
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
220
+ * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
221
+ */
222
+ export function makePathDispatchResolver(
223
+ byPath: Record<string, PermissionCheckResult>,
224
+ defaultResult: PermissionCheckResult,
225
+ ) {
226
+ const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
227
+ resolve.mockImplementation((_surface, input) => {
228
+ const path = (input as Record<string, unknown>).path;
229
+ if (typeof path === "string" && path in byPath) {
230
+ return byPath[path];
231
+ }
232
+ return defaultResult;
233
+ });
234
+ const resolvePathPolicy =
235
+ vi.fn<ScopedPermissionResolver["resolvePathPolicy"]>();
236
+ resolvePathPolicy.mockImplementation((values) => {
237
+ for (const value of values) {
238
+ if (value in byPath) return byPath[value];
239
+ }
240
+ return defaultResult;
241
+ });
242
+ return { resolve, resolvePathPolicy };
243
+ }
244
+
245
+ /**
246
+ * Path-surface check result factory.
247
+ *
248
+ * Shared between bash-path.test.ts and path.test.ts; both use
249
+ * toolName "path", source "special", origin "global" as defaults.
250
+ */
251
+ export function makeGateCheckResult(
252
+ overrides: Partial<PermissionCheckResult> = {},
253
+ ): PermissionCheckResult {
254
+ return {
255
+ toolName: "path",
256
+ state: "allow",
257
+ source: "special",
258
+ origin: "global",
259
+ ...overrides,
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Mock of `ToolCallGateInputs` for `ToolCallGatePipeline` unit tests.
265
+ *
266
+ * Each method is a `vi.fn()` stub so callers retain full mock access
267
+ * (`mock.calls`, `mockReturnValue`, etc.) on the returned object.
268
+ * Pass `overrides` to replace individual stubs without rebuilding the whole
269
+ * mock from scratch.
270
+ */
271
+ export function makeGateInputs(
272
+ overrides: {
273
+ getActiveSkillEntries?: () => SkillPromptEntry[];
274
+ getInfrastructureReadDirs?: () => string[];
275
+ getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
276
+ } = {},
277
+ ): ToolCallGateInputs {
278
+ return {
279
+ getActiveSkillEntries:
280
+ overrides.getActiveSkillEntries ??
281
+ vi.fn<() => SkillPromptEntry[]>(() => []),
282
+ getInfrastructureReadDirs:
283
+ overrides.getInfrastructureReadDirs ?? vi.fn<() => string[]>(() => []),
284
+ getToolPreviewLimits:
285
+ overrides.getToolPreviewLimits ??
286
+ vi.fn<() => ToolPreviewFormatterOptions>(() => ({
287
+ toolInputPreviewMaxLength: 500,
288
+ toolTextSummaryMaxLength: 100,
289
+ toolInputLogPreviewMaxLength: 200,
290
+ })),
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Mock of `SkillInputGateInputs` for `SkillInputGatePipeline` unit tests.
296
+ *
297
+ * Returns a plain object with a `checkPermission` `vi.fn()` stub so callers
298
+ * retain full mock access (`mockReturnValue`, `mock.calls`, etc.).
299
+ */
300
+ export function makeSkillInputInputs(
301
+ overrides: { checkPermission?: SkillInputGateInputs["checkPermission"] } = {},
302
+ ): SkillInputGateInputs {
303
+ return {
304
+ checkPermission:
305
+ overrides.checkPermission ??
306
+ vi
307
+ .fn<SkillInputGateInputs["checkPermission"]>()
308
+ .mockReturnValue(makeCheckResult()),
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Mock `GateNotifier` for `SkillInputGatePipeline` unit tests.
314
+ *
315
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
316
+ * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.) — annotating with
317
+ * `GateNotifier` would erase `Mock<...>` methods from the inferred type.
318
+ */
319
+ export function makeNotifier() {
320
+ return {
321
+ warn: vi.fn<(message: string) => void>(),
322
+ };
323
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Shared handler-level test fixtures for PermissionGateHandler tests.
3
+ *
4
+ * `makeHandler` builds a real PermissionSession + PermissionResolver and wires
5
+ * them into the handler and pipelines exactly as `index.ts` does.
6
+ * Call-site overrides for permission results flow through
7
+ * `permissionManager.checkPermission`; session state overrides are applied
8
+ * via vi.spyOn on the real session instance.
9
+ */
10
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import { vi } from "vitest";
12
+
13
+ import { GateDecisionReporter } from "#src/decision-reporter";
14
+ import type { GatePrompter } from "#src/gate-prompter";
15
+ import { GateRunner } from "#src/handlers/gates/runner";
16
+ import {
17
+ type SkillInputGateInputs,
18
+ SkillInputGatePipeline,
19
+ } from "#src/handlers/gates/skill-input-gate-pipeline";
20
+ import {
21
+ type ToolCallGateInputs,
22
+ ToolCallGatePipeline,
23
+ } from "#src/handlers/gates/tool-call-gate-pipeline";
24
+ import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
25
+ import type { PermissionDecisionEvent } from "#src/permission-events";
26
+ import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
27
+ import type { Rule } from "#src/rule";
28
+ import { SessionRules } from "#src/session-rules";
29
+ import type { ToolRegistry } from "#src/tool-registry";
30
+ import type { PermissionCheckResult, PermissionState } from "#src/types";
31
+ import {
32
+ makeRealResolver,
33
+ makeRealSession,
34
+ } from "#test/helpers/session-fixtures";
35
+
36
+ // ── MockGateHandlerSession ────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Mock type for gate-pipeline inputs (ToolCallGateInputs + SkillInputGateInputs).
40
+ *
41
+ * Used by `makeSurfaceCheck`, `makeBashCommandCheck`, and the `session`
42
+ * override bag in `makeHandler`. The `GateHandlerSession` role (activate +
43
+ * resolveAgentName) is now satisfied by the real `PermissionSession`; this
44
+ * type covers only the pipeline input surface.
45
+ *
46
+ * The 4-arg `checkPermission` is a superset of `SkillInputGateInputs` —
47
+ * it routes through `permissionManager.checkPermission` in production.
48
+ */
49
+ export type MockGateHandlerSession = ToolCallGateInputs &
50
+ SkillInputGateInputs & {
51
+ /** 4-arg form so surface-check mocks can receive optional rules. */
52
+ checkPermission(
53
+ surface: string,
54
+ input: unknown,
55
+ agentName?: string,
56
+ rules?: Rule[],
57
+ ): PermissionCheckResult;
58
+ };
59
+
60
+ // ── Small utility factories ───────────────────────────────────────────────
61
+
62
+ export function makeEvents() {
63
+ return {
64
+ emit: vi.fn(),
65
+ on: vi.fn().mockReturnValue(() => undefined),
66
+ };
67
+ }
68
+
69
+ export function makeCtx(
70
+ overrides: Partial<ExtensionContext> = {},
71
+ ): ExtensionContext {
72
+ return {
73
+ cwd: "/test/project",
74
+ hasUI: true,
75
+ ui: {
76
+ setStatus: vi.fn(),
77
+ notify: vi.fn(),
78
+ select: vi.fn(),
79
+ input: vi.fn(),
80
+ },
81
+ sessionManager: {
82
+ getEntries: vi.fn().mockReturnValue([]),
83
+ getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
84
+ addEntry: vi.fn(),
85
+ },
86
+ ...overrides,
87
+ } as unknown as ExtensionContext;
88
+ }
89
+
90
+ export function makeToolCallEvent(
91
+ toolName: string,
92
+ extraFields: Record<string, unknown> = {},
93
+ ) {
94
+ return {
95
+ type: "tool_call",
96
+ toolCallId: "tc-1",
97
+ name: toolName,
98
+ input: {},
99
+ ...extraFields,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Neutral-default check-result builder.
105
+ *
106
+ * Pass exactly the fields the original fixture hard-coded so divergent
107
+ * defaults across test files are preserved at their call sites.
108
+ */
109
+ export function makeCheckResult(
110
+ overrides: Partial<PermissionCheckResult> = {},
111
+ ): PermissionCheckResult {
112
+ return {
113
+ state: "allow",
114
+ toolName: "read",
115
+ source: "tool",
116
+ origin: "builtin",
117
+ ...overrides,
118
+ };
119
+ }
120
+
121
+ export function makeToolRegistry(
122
+ overrides: Partial<ToolRegistry> = {},
123
+ ): ToolRegistry {
124
+ return {
125
+ getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
126
+ getActive: vi.fn().mockReturnValue(["read", "bash"]),
127
+ setActive: vi.fn(),
128
+ ...overrides,
129
+ };
130
+ }
131
+
132
+ // ── Surface-check factories ────────────────────────────────────────────────
133
+
134
+ /**
135
+ * Surface-dispatching `checkPermission` mock.
136
+ *
137
+ * Returns the matching per-surface result or `defaultResult`.
138
+ * Pass the returned function as `session.checkPermission` in a `makeHandler`
139
+ * override bag — it is applied to `permissionManager.checkPermission`.
140
+ *
141
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
142
+ * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
143
+ */
144
+ export function makeSurfaceCheck(
145
+ bySurface: Record<
146
+ string,
147
+ Partial<PermissionCheckResult> & { state: PermissionState }
148
+ >,
149
+ defaultResult: Partial<PermissionCheckResult> & { state: PermissionState } = {
150
+ state: "allow",
151
+ },
152
+ ) {
153
+ return vi
154
+ .fn<MockGateHandlerSession["checkPermission"]>()
155
+ .mockImplementation((surface): PermissionCheckResult => {
156
+ const base = bySurface[surface] ?? defaultResult;
157
+ return {
158
+ toolName: surface,
159
+ source: "tool",
160
+ origin: "builtin",
161
+ ...base,
162
+ };
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Bash-surface `checkPermission` mock that dispatches on a command regex.
168
+ *
169
+ * Pass the returned function as `session.checkPermission` in a `makeHandler`
170
+ * override bag — it is applied to `permissionManager.checkPermission`.
171
+ *
172
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
173
+ * mock access.
174
+ */
175
+ export function makeBashCommandCheck(opts: {
176
+ deny: RegExp;
177
+ denyMatched: string;
178
+ allowMatched?: string;
179
+ }) {
180
+ return vi
181
+ .fn<MockGateHandlerSession["checkPermission"]>()
182
+ .mockImplementation((surface, input): PermissionCheckResult => {
183
+ if (surface === "bash") {
184
+ const command = (input as { command?: string }).command ?? "";
185
+ return opts.deny.test(command)
186
+ ? makeCheckResult({
187
+ state: "deny",
188
+ source: "bash",
189
+ command,
190
+ matchedPattern: opts.denyMatched,
191
+ })
192
+ : makeCheckResult({
193
+ state: "allow",
194
+ source: "bash",
195
+ command,
196
+ matchedPattern: opts.allowMatched,
197
+ });
198
+ }
199
+ return makeCheckResult({ state: "allow" });
200
+ });
201
+ }
202
+
203
+ // ── makeHandler ────────────────────────────────────────────────────────────
204
+
205
+ /**
206
+ * Constructs a PermissionGateHandler wired with real collaborators.
207
+ *
208
+ * The `session` override bag maps to the real collaborators:
209
+ * - `checkPermission` → applied to `permissionManager.checkPermission`
210
+ * - `getActiveSkillEntries`, `getInfrastructureReadDirs`, `getToolPreviewLimits`
211
+ * → applied as vi.spyOn overrides on the real session
212
+ * - `resolveAgentName` → applied as a vi.spyOn override on the real session
213
+ *
214
+ * Returns `{ handler, events, session, toolRegistry, prompter, recorder,
215
+ * permissionManager, forwarding }` so each test file can destructure only
216
+ * what it needs.
217
+ * `session.activate` is not a mock — use `forwarding.start` to assert it
218
+ * was called.
219
+ */
220
+ export function makeHandler(overrides?: {
221
+ session?: Partial<MockGateHandlerSession> & {
222
+ resolveAgentName?: (
223
+ ctx: ExtensionContext,
224
+ systemPrompt?: string,
225
+ ) => string | null;
226
+ };
227
+ /** Override the GatePrompter passed to GateRunner. Defaults to an allow-all stub. */
228
+ prompter?: GatePrompter;
229
+ toolRegistry?: Partial<ToolRegistry>;
230
+ /** Sugar: builds the `getAll` mock from a list of tool names. */
231
+ tools?: string[];
232
+ }) {
233
+ const { session, permissionManager, sessionRules, forwarding, logger } =
234
+ makeRealSession();
235
+ const { resolver } = makeRealResolver(permissionManager, sessionRules);
236
+
237
+ // Apply session override bag to the real collaborators.
238
+ const so = overrides?.session;
239
+ const surfaceCheck = so?.checkPermission;
240
+ if (surfaceCheck) {
241
+ vi.mocked(permissionManager.checkPermission).mockImplementation(
242
+ surfaceCheck,
243
+ );
244
+ // The bash path and external-directory gates resolve through
245
+ // checkPathPolicy; route it through the same surface dispatcher (threading
246
+ // the real surface) so `path` / `external_directory` overrides apply to
247
+ // bash tokens and tool paths alike (#418).
248
+ vi.mocked(permissionManager.checkPathPolicy).mockImplementation(
249
+ (values, agentName, sessionRules, surface = "path") =>
250
+ surfaceCheck(
251
+ surface,
252
+ { path: values[0] ?? "*" },
253
+ agentName,
254
+ sessionRules,
255
+ ),
256
+ );
257
+ }
258
+ if (so?.getActiveSkillEntries) {
259
+ vi.spyOn(session, "getActiveSkillEntries").mockImplementation(
260
+ so.getActiveSkillEntries,
261
+ );
262
+ }
263
+ if (so?.getInfrastructureReadDirs) {
264
+ vi.spyOn(session, "getInfrastructureReadDirs").mockImplementation(
265
+ so.getInfrastructureReadDirs,
266
+ );
267
+ }
268
+ if (so?.getToolPreviewLimits) {
269
+ vi.spyOn(session, "getToolPreviewLimits").mockImplementation(
270
+ so.getToolPreviewLimits,
271
+ );
272
+ }
273
+ if (so?.resolveAgentName) {
274
+ vi.spyOn(session, "resolveAgentName").mockImplementation(
275
+ so.resolveAgentName,
276
+ );
277
+ }
278
+
279
+ const events = makeEvents();
280
+ const toolRegistry =
281
+ overrides?.tools !== undefined
282
+ ? makeToolRegistry({
283
+ getAll: vi
284
+ .fn()
285
+ .mockReturnValue(overrides.tools.map((name) => ({ name }))),
286
+ })
287
+ : makeToolRegistry(overrides?.toolRegistry);
288
+
289
+ const recorder = new SessionRules();
290
+ const pipeline = new ToolCallGatePipeline(resolver, session);
291
+ const skillInputPipeline = new SkillInputGatePipeline(resolver);
292
+ const reporter = new GateDecisionReporter(logger, events);
293
+ const prompter: GatePrompter = overrides?.prompter ?? {
294
+ canConfirm: vi.fn().mockReturnValue(true),
295
+ prompt: vi
296
+ .fn<GatePrompter["prompt"]>()
297
+ .mockResolvedValue({ approved: true, state: "approved" }),
298
+ };
299
+ const runner = new GateRunner(
300
+ resolver,
301
+ recorder,
302
+ prompter,
303
+ reporter,
304
+ { recordApproval: vi.fn() } as never,
305
+ );
306
+ const handler = new PermissionGateHandler(
307
+ session,
308
+ toolRegistry,
309
+ pipeline,
310
+ skillInputPipeline,
311
+ runner,
312
+ );
313
+ return {
314
+ handler,
315
+ events,
316
+ session,
317
+ logger,
318
+ toolRegistry,
319
+ prompter,
320
+ recorder,
321
+ permissionManager,
322
+ forwarding,
323
+ };
324
+ }
325
+
326
+ // ── Decision-event helper ─────────────────────────────────────────────────
327
+
328
+ /** Extract all permissions:decision payloads from the events.emit mock. */
329
+ export function getDecisionEvents(
330
+ events: ReturnType<typeof makeEvents>,
331
+ ): PermissionDecisionEvent[] {
332
+ return events.emit.mock.calls
333
+ .filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
334
+ .map(([, payload]) => payload as PermissionDecisionEvent);
335
+ }