@gotgenes/pi-permission-system 9.2.0 → 10.1.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 (73) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +12 -11
  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/io.ts +29 -0
  8. package/src/forwarded-permissions/permission-forwarder.ts +549 -0
  9. package/src/forwarding-manager.ts +3 -7
  10. package/src/gate-handler-session.ts +13 -0
  11. package/src/gate-prompter.ts +14 -0
  12. package/src/handlers/before-agent-start.ts +2 -3
  13. package/src/handlers/gates/bash-command.ts +4 -18
  14. package/src/handlers/gates/bash-external-directory.ts +3 -15
  15. package/src/handlers/gates/bash-path.ts +3 -16
  16. package/src/handlers/gates/descriptor.ts +0 -28
  17. package/src/handlers/gates/path.ts +3 -15
  18. package/src/handlers/gates/runner.ts +142 -105
  19. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  20. package/src/handlers/gates/skill-input.ts +44 -0
  21. package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
  22. package/src/handlers/lifecycle.ts +9 -9
  23. package/src/handlers/permission-gate-handler.ts +34 -238
  24. package/src/index.ts +50 -68
  25. package/src/mcp-targets.ts +56 -46
  26. package/src/permission-event-rpc.ts +7 -0
  27. package/src/permission-events.ts +89 -8
  28. package/src/permission-forwarding.ts +23 -0
  29. package/src/permission-prompter.ts +27 -56
  30. package/src/permission-resolver.ts +17 -0
  31. package/src/permission-session.ts +77 -9
  32. package/src/permission-ui-prompt.ts +127 -0
  33. package/src/permissions-service.ts +53 -0
  34. package/src/service-lifecycle.ts +49 -0
  35. package/src/service.ts +17 -0
  36. package/src/session-approval-recorder.ts +6 -0
  37. package/src/session-lifecycle-session.ts +24 -0
  38. package/src/tool-input-preview.ts +0 -62
  39. package/src/tool-input-prompt-formatters.ts +63 -0
  40. package/src/tool-preview-formatter.ts +6 -4
  41. package/test/composition-root.test.ts +5 -0
  42. package/test/decision-reporter.test.ts +112 -0
  43. package/test/denial-messages.test.ts +62 -0
  44. package/test/forwarding-manager.test.ts +26 -44
  45. package/test/handlers/before-agent-start.test.ts +45 -21
  46. package/test/handlers/external-directory-integration.test.ts +86 -22
  47. package/test/handlers/external-directory-session-dedup.test.ts +102 -55
  48. package/test/handlers/gates/bash-command.test.ts +49 -90
  49. package/test/handlers/gates/bash-external-directory.test.ts +54 -95
  50. package/test/handlers/gates/bash-path.test.ts +63 -148
  51. package/test/handlers/gates/path.test.ts +38 -105
  52. package/test/handlers/gates/runner.test.ts +150 -93
  53. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  54. package/test/handlers/gates/skill-input.test.ts +128 -0
  55. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
  56. package/test/handlers/input.test.ts +1 -2
  57. package/test/handlers/lifecycle.test.ts +49 -33
  58. package/test/handlers/tool-call-events.test.ts +1 -1
  59. package/test/helpers/gate-fixtures.ts +147 -16
  60. package/test/helpers/handler-fixtures.ts +143 -27
  61. package/test/mcp-targets.test.ts +55 -0
  62. package/test/permission-event-rpc.test.ts +39 -0
  63. package/test/permission-events.test.ts +78 -10
  64. package/test/permission-forwarder.test.ts +295 -0
  65. package/test/permission-prompter.test.ts +147 -38
  66. package/test/permission-session.test.ts +160 -27
  67. package/test/permission-ui-prompt.test.ts +146 -0
  68. package/test/permissions-service.test.ts +151 -0
  69. package/test/runtime.test.ts +0 -4
  70. package/test/service-lifecycle.test.ts +162 -0
  71. package/test/tool-input-preview.test.ts +0 -111
  72. package/test/tool-input-prompt-formatters.test.ts +115 -0
  73. package/src/forwarded-permissions/polling.ts +0 -379
@@ -19,11 +19,11 @@ import type {
19
19
  } from "#src/handlers/gates/descriptor";
20
20
  import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
21
21
  import type { ToolCallContext } from "#src/handlers/gates/types";
22
- import type { Rule } from "#src/rule";
23
- import type { PermissionCheckResult } from "#src/types";
22
+ import type { PermissionResolver } from "#src/permission-resolver";
24
23
 
25
24
  import {
26
25
  makeGateCheckResult as makeCheckResult,
26
+ makeResolver,
27
27
  makeTcc,
28
28
  } from "#test/helpers/gate-fixtures";
29
29
 
@@ -31,13 +31,6 @@ afterEach(() => {
31
31
  vi.restoreAllMocks();
32
32
  });
33
33
 
34
- type CheckPermissionFn = (
35
- surface: string,
36
- input: unknown,
37
- agentName?: string,
38
- sessionRules?: Rule[],
39
- ) => PermissionCheckResult;
40
-
41
34
  /**
42
35
  * Mirror the handler's parse-once derivation: parse the bash command into a
43
36
  * shared `BashProgram` and inject it, exactly as `permission-gate-handler.ts`
@@ -45,72 +38,47 @@ type CheckPermissionFn = (
45
38
  */
46
39
  async function describeGate(
47
40
  tcc: ToolCallContext,
48
- checkPermission: CheckPermissionFn,
49
- getSessionRuleset: () => Rule[],
41
+ resolver: PermissionResolver,
50
42
  ): Promise<GateResult> {
51
43
  const command = getNonEmptyString(toRecord(tcc.input).command);
52
44
  const bashProgram =
53
45
  tcc.toolName === "bash" && command
54
46
  ? await BashProgram.parse(command)
55
47
  : null;
56
- return describeBashPathGate(
57
- tcc,
58
- bashProgram,
59
- checkPermission,
60
- getSessionRuleset,
61
- );
48
+ return describeBashPathGate(tcc, bashProgram, resolver);
62
49
  }
63
50
 
64
51
  // ── tests ──────────────────────────────────────────────────────────────────
65
52
 
66
53
  describe("describeBashPathGate", () => {
67
54
  it("returns null for non-bash tools", async () => {
68
- const checkPermission = vi.fn<CheckPermissionFn>();
69
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
70
55
  const result = await describeGate(
71
56
  makeTcc({ toolName: "read", input: { path: ".env" } }),
72
- checkPermission,
73
- getSessionRuleset,
57
+ makeResolver(),
74
58
  );
75
59
  expect(result).toBeNull();
76
60
  });
77
61
 
78
62
  it("returns null when no tokens are extracted", async () => {
79
- const checkPermission = vi.fn<CheckPermissionFn>();
80
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
81
63
  const result = await describeGate(
82
64
  makeTcc({ input: { command: "echo hello" } }),
83
- checkPermission,
84
- getSessionRuleset,
65
+ makeResolver(),
85
66
  );
86
67
  expect(result).toBeNull();
87
68
  });
88
69
 
89
70
  it("returns null when all tokens evaluate to allow", async () => {
90
- const checkPermission = vi
91
- .fn<CheckPermissionFn>()
92
- .mockReturnValue(makeCheckResult({ state: "allow" }));
93
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
94
71
  const result = await describeGate(
95
72
  makeTcc({ input: { command: "cat .env" } }),
96
- checkPermission,
97
- getSessionRuleset,
73
+ makeResolver(makeCheckResult({ state: "allow" })),
98
74
  );
99
75
  expect(result).toBeNull();
100
76
  });
101
77
 
102
78
  it("returns GateDescriptor when a token evaluates to deny", async () => {
103
- const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
104
- makeCheckResult({
105
- state: "deny",
106
- matchedPattern: "*.env",
107
- }),
108
- );
109
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
110
79
  const result = await describeGate(
111
80
  makeTcc({ input: { command: "cat .env" } }),
112
- checkPermission,
113
- getSessionRuleset,
81
+ makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
114
82
  );
115
83
  expect(result).not.toBeNull();
116
84
  expect(isGateDescriptor(result)).toBe(true);
@@ -120,14 +88,9 @@ describe("describeBashPathGate", () => {
120
88
  });
121
89
 
122
90
  it("returns GateDescriptor when a token evaluates to ask", async () => {
123
- const checkPermission = vi
124
- .fn<CheckPermissionFn>()
125
- .mockReturnValue(makeCheckResult({ state: "ask", matchedPattern: "*" }));
126
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
127
91
  const result = await describeGate(
128
92
  makeTcc({ input: { command: "cat .env" } }),
129
- checkPermission,
130
- getSessionRuleset,
93
+ makeResolver(makeCheckResult({ state: "ask", matchedPattern: "*" })),
131
94
  );
132
95
  expect(result).not.toBeNull();
133
96
  expect(isGateDescriptor(result)).toBe(true);
@@ -136,16 +99,9 @@ describe("describeBashPathGate", () => {
136
99
  });
137
100
 
138
101
  it("descriptor includes triggering token in prompt message", async () => {
139
- const checkPermission = vi
140
- .fn<CheckPermissionFn>()
141
- .mockReturnValue(
142
- makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
143
- );
144
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
145
102
  const result = (await describeGate(
146
103
  makeTcc({ input: { command: "cat .env" } }),
147
- checkPermission,
148
- getSessionRuleset,
104
+ makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
149
105
  )) as GateDescriptor;
150
106
  expect(result.denialContext).toMatchObject({
151
107
  kind: "bash_path",
@@ -156,37 +112,17 @@ describe("describeBashPathGate", () => {
156
112
  });
157
113
 
158
114
  it("descriptor decision uses surface 'path'", async () => {
159
- const checkPermission = vi
160
- .fn<CheckPermissionFn>()
161
- .mockReturnValue(
162
- makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
163
- );
164
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
165
115
  const result = (await describeGate(
166
116
  makeTcc({ input: { command: "cat .env" } }),
167
- checkPermission,
168
- getSessionRuleset,
117
+ makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
169
118
  )) as GateDescriptor;
170
119
  expect(result.decision.surface).toBe("path");
171
120
  });
172
121
 
173
122
  it("returns GateBypass when session rule covers the path", async () => {
174
- const checkPermission = vi
175
- .fn<CheckPermissionFn>()
176
- .mockReturnValue(makeCheckResult({ state: "allow", source: "session" }));
177
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([
178
- {
179
- surface: "path",
180
- pattern: "*",
181
- action: "allow",
182
- layer: "session",
183
- origin: "session",
184
- },
185
- ]);
186
123
  const result = await describeGate(
187
124
  makeTcc({ input: { command: "cat .env" } }),
188
- checkPermission,
189
- getSessionRuleset,
125
+ makeResolver(makeCheckResult({ state: "allow", source: "session" })),
190
126
  );
191
127
  expect(result).not.toBeNull();
192
128
  expect(isGateBypass(result)).toBe(true);
@@ -194,31 +130,22 @@ describe("describeBashPathGate", () => {
194
130
  });
195
131
 
196
132
  it("returns null when command is missing", async () => {
197
- const checkPermission = vi.fn<CheckPermissionFn>();
198
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
199
- const result = await describeGate(
200
- makeTcc({ input: {} }),
201
- checkPermission,
202
- getSessionRuleset,
203
- );
133
+ const result = await describeGate(makeTcc({ input: {} }), makeResolver());
204
134
  expect(result).toBeNull();
205
135
  });
206
136
 
207
137
  it("evaluates most restrictive across multiple tokens", async () => {
208
- const checkPermission = vi
209
- .fn<CheckPermissionFn>()
210
- .mockImplementation((_surface, input) => {
211
- const record = input as Record<string, unknown>;
212
- if (record.path === "src/foo.ts") {
213
- return makeCheckResult({ state: "allow" });
214
- }
215
- return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
216
- });
217
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
138
+ const resolver = makeResolver();
139
+ resolver.resolve.mockImplementation((_surface, input) => {
140
+ const record = input as Record<string, unknown>;
141
+ if (record.path === "src/foo.ts") {
142
+ return makeCheckResult({ state: "allow" });
143
+ }
144
+ return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
145
+ });
218
146
  const result = await describeGate(
219
147
  makeTcc({ input: { command: "cat src/foo.ts .env" } }),
220
- checkPermission,
221
- getSessionRuleset,
148
+ resolver,
222
149
  );
223
150
  expect(result).not.toBeNull();
224
151
  expect(isGateDescriptor(result)).toBe(true);
@@ -226,20 +153,17 @@ describe("describeBashPathGate", () => {
226
153
  });
227
154
 
228
155
  it("deny wins in multi-token: cp .env README.md", async () => {
229
- const checkPermission = vi
230
- .fn<CheckPermissionFn>()
231
- .mockImplementation((_surface, input) => {
232
- const record = input as Record<string, unknown>;
233
- if (record.path === ".env") {
234
- return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
235
- }
236
- return makeCheckResult({ state: "allow" });
237
- });
238
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
156
+ const resolver = makeResolver();
157
+ resolver.resolve.mockImplementation((_surface, input) => {
158
+ const record = input as Record<string, unknown>;
159
+ if (record.path === ".env") {
160
+ return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
161
+ }
162
+ return makeCheckResult({ state: "allow" });
163
+ });
239
164
  const result = await describeGate(
240
165
  makeTcc({ input: { command: "cp .env README.md" } }),
241
- checkPermission,
242
- getSessionRuleset,
166
+ resolver,
243
167
  );
244
168
  expect(result).not.toBeNull();
245
169
  expect(isGateDescriptor(result)).toBe(true);
@@ -249,20 +173,17 @@ describe("describeBashPathGate", () => {
249
173
  });
250
174
 
251
175
  it("extracts redirect target: echo test > .env triggers deny", async () => {
252
- const checkPermission = vi
253
- .fn<CheckPermissionFn>()
254
- .mockImplementation((_surface, input) => {
255
- const record = input as Record<string, unknown>;
256
- if (record.path === ".env") {
257
- return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
258
- }
259
- return makeCheckResult({ state: "allow" });
260
- });
261
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
176
+ const resolver = makeResolver();
177
+ resolver.resolve.mockImplementation((_surface, input) => {
178
+ const record = input as Record<string, unknown>;
179
+ if (record.path === ".env") {
180
+ return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
181
+ }
182
+ return makeCheckResult({ state: "allow" });
183
+ });
262
184
  const result = await describeGate(
263
185
  makeTcc({ input: { command: "echo test > .env" } }),
264
- checkPermission,
265
- getSessionRuleset,
186
+ resolver,
266
187
  );
267
188
  expect(result).not.toBeNull();
268
189
  expect(isGateDescriptor(result)).toBe(true);
@@ -270,47 +191,41 @@ describe("describeBashPathGate", () => {
270
191
  });
271
192
 
272
193
  it("returns null when all tokens match only the universal default", async () => {
273
- const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
274
- makeCheckResult({
275
- state: "ask",
276
- matchedPattern: undefined,
277
- source: "special",
278
- origin: "builtin",
279
- }),
280
- );
281
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
282
194
  const result = await describeGate(
283
195
  makeTcc({ input: { command: "cat .env" } }),
284
- checkPermission,
285
- getSessionRuleset,
196
+ makeResolver(
197
+ makeCheckResult({
198
+ state: "ask",
199
+ matchedPattern: undefined,
200
+ source: "special",
201
+ origin: "builtin",
202
+ }),
203
+ ),
286
204
  );
287
205
  expect(result).toBeNull();
288
206
  });
289
207
 
290
208
  it("ignores tokens matching universal default but fires for explicit rule matches", async () => {
291
- const checkPermission = vi
292
- .fn<CheckPermissionFn>()
293
- .mockImplementation((_surface, input) => {
294
- const record = input as Record<string, unknown>;
295
- if (record.path === ".env") {
296
- return makeCheckResult({
297
- state: "deny",
298
- matchedPattern: "*.env",
299
- });
300
- }
301
- // Other tokens match only the universal default
209
+ const resolver = makeResolver();
210
+ resolver.resolve.mockImplementation((_surface, input) => {
211
+ const record = input as Record<string, unknown>;
212
+ if (record.path === ".env") {
302
213
  return makeCheckResult({
303
- state: "ask",
304
- matchedPattern: undefined,
305
- source: "special",
306
- origin: "builtin",
214
+ state: "deny",
215
+ matchedPattern: "*.env",
307
216
  });
217
+ }
218
+ // Other tokens match only the universal default
219
+ return makeCheckResult({
220
+ state: "ask",
221
+ matchedPattern: undefined,
222
+ source: "special",
223
+ origin: "builtin",
308
224
  });
309
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
225
+ });
310
226
  const result = await describeGate(
311
227
  makeTcc({ input: { command: "cat src/foo.ts .env" } }),
312
- checkPermission,
313
- getSessionRuleset,
228
+ resolver,
314
229
  );
315
230
  expect(result).not.toBeNull();
316
231
  expect(isGateDescriptor(result)).toBe(true);
@@ -1,13 +1,14 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import { describe, expect, it } from "vitest";
2
2
 
3
3
  import type { GateDescriptor } from "#src/handlers/gates/descriptor";
4
4
  import { isGateDescriptor } from "#src/handlers/gates/descriptor";
5
5
  import { describePathGate } from "#src/handlers/gates/path";
6
6
  import type { ToolCallContext } from "#src/handlers/gates/types";
7
- import type { Rule } from "#src/rule";
8
- import type { PermissionCheckResult } from "#src/types";
9
7
 
10
- import { makeGateCheckResult as makeCheckResult } from "#test/helpers/gate-fixtures";
8
+ import {
9
+ makeGateCheckResult as makeCheckResult,
10
+ makeResolver,
11
+ } from "#test/helpers/gate-fixtures";
11
12
 
12
13
  // ── helpers ────────────────────────────────────────────────────────────────
13
14
 
@@ -23,53 +24,36 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
23
24
  };
24
25
  }
25
26
 
26
- type CheckPermissionFn = (
27
- surface: string,
28
- input: unknown,
29
- agentName?: string,
30
- sessionRules?: Rule[],
31
- ) => PermissionCheckResult;
32
-
33
27
  // ── tests ──────────────────────────────────────────────────────────────────
34
28
 
35
29
  describe("describePathGate", () => {
36
- const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
37
-
38
30
  it("returns null for non-path-bearing tools", () => {
39
- const checkPermission = vi.fn<CheckPermissionFn>();
31
+ const resolver = makeResolver();
40
32
  const result = describePathGate(
41
33
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
42
- checkPermission,
43
- getSessionRuleset,
34
+ resolver,
44
35
  );
45
36
  expect(result).toBeNull();
46
- expect(checkPermission).not.toHaveBeenCalled();
37
+ expect(resolver.resolve).not.toHaveBeenCalled();
47
38
  });
48
39
 
49
40
  it("returns null when tool has no extractable path", () => {
50
- const checkPermission = vi.fn<CheckPermissionFn>();
41
+ const resolver = makeResolver();
51
42
  const result = describePathGate(
52
43
  makeTcc({ toolName: "read", input: {} }),
53
- checkPermission,
54
- getSessionRuleset,
44
+ resolver,
55
45
  );
56
46
  expect(result).toBeNull();
57
47
  });
58
48
 
59
49
  it("returns null when path check result is allow", () => {
60
- const checkPermission = vi
61
- .fn<CheckPermissionFn>()
62
- .mockReturnValue(makeCheckResult({ state: "allow" }));
63
- const result = describePathGate(
64
- makeTcc(),
65
- checkPermission,
66
- getSessionRuleset,
67
- );
50
+ const resolver = makeResolver(makeCheckResult({ state: "allow" }));
51
+ const result = describePathGate(makeTcc(), resolver);
68
52
  expect(result).toBeNull();
69
53
  });
70
54
 
71
55
  it("returns null when matchedPattern is undefined (universal default)", () => {
72
- const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
56
+ const resolver = makeResolver(
73
57
  makeCheckResult({
74
58
  state: "ask",
75
59
  matchedPattern: undefined,
@@ -77,16 +61,12 @@ describe("describePathGate", () => {
77
61
  origin: "builtin",
78
62
  }),
79
63
  );
80
- const result = describePathGate(
81
- makeTcc(),
82
- checkPermission,
83
- getSessionRuleset,
84
- );
64
+ const result = describePathGate(makeTcc(), resolver);
85
65
  expect(result).toBeNull();
86
66
  });
87
67
 
88
68
  it("returns GateDescriptor when matchedPattern is defined (explicit path rule)", () => {
89
- const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
69
+ const resolver = makeResolver(
90
70
  makeCheckResult({
91
71
  state: "ask",
92
72
  matchedPattern: "*.env",
@@ -94,27 +74,16 @@ describe("describePathGate", () => {
94
74
  origin: "global",
95
75
  }),
96
76
  );
97
- const result = describePathGate(
98
- makeTcc(),
99
- checkPermission,
100
- getSessionRuleset,
101
- );
77
+ const result = describePathGate(makeTcc(), resolver);
102
78
  expect(result).not.toBeNull();
103
79
  expect(isGateDescriptor(result)).toBe(true);
104
80
  });
105
81
 
106
82
  it("returns GateDescriptor when path check result is deny", () => {
107
- const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
108
- makeCheckResult({
109
- state: "deny",
110
- matchedPattern: "*.env",
111
- }),
112
- );
113
- const result = describePathGate(
114
- makeTcc(),
115
- checkPermission,
116
- getSessionRuleset,
83
+ const resolver = makeResolver(
84
+ makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
117
85
  );
86
+ const result = describePathGate(makeTcc(), resolver);
118
87
  expect(result).not.toBeNull();
119
88
  expect(isGateDescriptor(result)).toBe(true);
120
89
  const desc = result as GateDescriptor;
@@ -123,17 +92,10 @@ describe("describePathGate", () => {
123
92
  });
124
93
 
125
94
  it("returns GateDescriptor when path check result is ask", () => {
126
- const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
127
- makeCheckResult({
128
- state: "ask",
129
- matchedPattern: "*.env",
130
- }),
131
- );
132
- const result = describePathGate(
133
- makeTcc(),
134
- checkPermission,
135
- getSessionRuleset,
95
+ const resolver = makeResolver(
96
+ makeCheckResult({ state: "ask", matchedPattern: "*.env" }),
136
97
  );
98
+ const result = describePathGate(makeTcc(), resolver);
137
99
  expect(result).not.toBeNull();
138
100
  expect(isGateDescriptor(result)).toBe(true);
139
101
  const desc = result as GateDescriptor;
@@ -142,13 +104,12 @@ describe("describePathGate", () => {
142
104
  });
143
105
 
144
106
  it("descriptor has correct session approval surface and pattern", () => {
145
- const checkPermission = vi
146
- .fn<CheckPermissionFn>()
147
- .mockReturnValue(makeCheckResult({ state: "ask", matchedPattern: "*" }));
107
+ const resolver = makeResolver(
108
+ makeCheckResult({ state: "ask", matchedPattern: "*" }),
109
+ );
148
110
  const result = describePathGate(
149
111
  makeTcc({ input: { path: "/test/project/src/.env" } }),
150
- checkPermission,
151
- getSessionRuleset,
112
+ resolver,
152
113
  ) as GateDescriptor;
153
114
  expect(result.sessionApproval).toBeDefined();
154
115
  expect(result.sessionApproval?.surface).toBe("path");
@@ -156,16 +117,10 @@ describe("describePathGate", () => {
156
117
  });
157
118
 
158
119
  it("descriptor denialContext references the file path and tool name", () => {
159
- const checkPermission = vi
160
- .fn<CheckPermissionFn>()
161
- .mockReturnValue(
162
- makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
163
- );
164
- const result = describePathGate(
165
- makeTcc(),
166
- checkPermission,
167
- getSessionRuleset,
168
- ) as GateDescriptor;
120
+ const resolver = makeResolver(
121
+ makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
122
+ );
123
+ const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
169
124
  expect(result.denialContext).toEqual({
170
125
  kind: "path",
171
126
  toolName: "read",
@@ -175,43 +130,21 @@ describe("describePathGate", () => {
175
130
  });
176
131
 
177
132
  it("descriptor decision uses surface 'path' and the file path as value", () => {
178
- const checkPermission = vi
179
- .fn<CheckPermissionFn>()
180
- .mockReturnValue(
181
- makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
182
- );
183
- const result = describePathGate(
184
- makeTcc(),
185
- checkPermission,
186
- getSessionRuleset,
187
- ) as GateDescriptor;
133
+ const resolver = makeResolver(
134
+ makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
135
+ );
136
+ const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
188
137
  expect(result.decision.surface).toBe("path");
189
138
  expect(result.decision.value).toBe(".env");
190
139
  });
191
140
 
192
- it("passes agentName and session rules to checkPermission", () => {
193
- const sessionRules: Rule[] = [
194
- {
195
- surface: "path",
196
- pattern: "/project/*",
197
- action: "allow",
198
- origin: "session",
199
- },
200
- ];
201
- const getSession = vi.fn<() => Rule[]>().mockReturnValue(sessionRules);
202
- const checkPermission = vi
203
- .fn<CheckPermissionFn>()
204
- .mockReturnValue(makeCheckResult({ state: "allow" }));
205
- describePathGate(
206
- makeTcc({ agentName: "my-agent" }),
207
- checkPermission,
208
- getSession,
209
- );
210
- expect(checkPermission).toHaveBeenCalledWith(
141
+ it("resolves the path surface with the file path and agent name", () => {
142
+ const resolver = makeResolver(makeCheckResult({ state: "allow" }));
143
+ describePathGate(makeTcc({ agentName: "my-agent" }), resolver);
144
+ expect(resolver.resolve).toHaveBeenCalledWith(
211
145
  "path",
212
146
  { path: ".env" },
213
147
  "my-agent",
214
- sessionRules,
215
148
  );
216
149
  });
217
150
  });