@gotgenes/pi-permission-system 16.1.0 → 16.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 (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +1 -1
  3. package/src/access-intent/access-intent.ts +57 -0
  4. package/src/handlers/gates/bash-command.ts +18 -3
  5. package/src/handlers/gates/bash-path.ts +6 -4
  6. package/src/handlers/gates/external-directory-policy.ts +9 -8
  7. package/src/handlers/gates/path.ts +6 -5
  8. package/src/handlers/gates/runner.ts +6 -5
  9. package/src/handlers/gates/tool-call-gate-pipeline.ts +6 -5
  10. package/src/permission-event-rpc.ts +4 -6
  11. package/src/permission-manager.ts +39 -55
  12. package/src/permission-resolver.ts +45 -57
  13. package/src/permissions-service.ts +2 -4
  14. package/src/skill-prompt-sanitizer.ts +2 -2
  15. package/test/handlers/before-agent-start.test.ts +6 -9
  16. package/test/handlers/external-directory-session-dedup.test.ts +16 -40
  17. package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -3
  18. package/test/handlers/gates/bash-command.test.ts +26 -23
  19. package/test/handlers/gates/bash-external-directory.test.ts +36 -32
  20. package/test/handlers/gates/bash-path.test.ts +12 -8
  21. package/test/handlers/gates/external-directory-policy.test.ts +34 -22
  22. package/test/handlers/gates/external-directory.test.ts +11 -5
  23. package/test/handlers/gates/path.test.ts +30 -25
  24. package/test/handlers/gates/runner.test.ts +6 -1
  25. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -2
  26. package/test/handlers/input.test.ts +1 -1
  27. package/test/helpers/gate-fixtures.ts +12 -24
  28. package/test/helpers/handler-fixtures.ts +21 -15
  29. package/test/helpers/session-fixtures.ts +3 -18
  30. package/test/permission-event-rpc.test.ts +24 -20
  31. package/test/permission-manager-unified.test.ts +445 -209
  32. package/test/permission-resolver.test.ts +112 -89
  33. package/test/permissions-service.test.ts +10 -7
  34. package/test/skill-prompt-sanitizer.test.ts +30 -7
@@ -41,7 +41,7 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
41
41
  * Build a fully wired PermissionGateHandler for external-directory dedup
42
42
  * tests.
43
43
  *
44
- * `permissionManager.checkPermission` is configured so that:
44
+ * `permissionManager.check` is configured so that:
45
45
  * - `external_directory` surface returns "ask" on first call
46
46
  * - On subsequent calls it checks the shared `sessionRules` store; if a
47
47
  * matching rule was recorded by the runner, it returns "allow" with
@@ -56,14 +56,16 @@ function makeDeduplicatingHandler(prompter?: GatePrompter): {
56
56
  makeRealSession();
57
57
  const { resolver } = makeRealResolver(permissionManager, sessionRules);
58
58
 
59
- // Configure checkPermission to simulate config-level "ask" for external_directory
60
- // but return "allow/session" when a session rule has been recorded.
61
- vi.mocked(permissionManager.checkPermission).mockImplementation(
62
- (surface, input, _agentName, rules): PermissionCheckResult => {
63
- if (surface === "external_directory") {
64
- const record = (input ?? {}) as Record<string, unknown>;
65
- const pathValue = typeof record.path === "string" ? record.path : null;
59
+ // Unified check(intent) mock: "ask" for external_directory unless a session
60
+ // rule covers it; "allow" for everything else.
61
+ vi.mocked(permissionManager.check).mockImplementation(
62
+ (intent, rules): PermissionCheckResult => {
63
+ const { surface } = intent;
64
+ // Derive a representative path value from whichever intent kind arrived.
65
+ const pathValue =
66
+ intent.kind === "path-values" ? (intent.values[0] ?? null) : null;
66
67
 
68
+ if (surface === "external_directory") {
67
69
  if (pathValue && rules && rules.length > 0) {
68
70
  const match = rules.findLast(
69
71
  (r) =>
@@ -80,7 +82,6 @@ function makeDeduplicatingHandler(prompter?: GatePrompter): {
80
82
  };
81
83
  }
82
84
  }
83
-
84
85
  return {
85
86
  state: "ask",
86
87
  toolName: surface,
@@ -98,19 +99,6 @@ function makeDeduplicatingHandler(prompter?: GatePrompter): {
98
99
  },
99
100
  );
100
101
 
101
- // The external-directory gates resolve through checkPathPolicy (#418); route
102
- // it through the same configured checkPermission so session-approval dedup
103
- // applies to the typed path alias.
104
- vi.mocked(permissionManager.checkPathPolicy).mockImplementation(
105
- (values, agentName, rules, surface = "path") =>
106
- permissionManager.checkPermission(
107
- surface,
108
- { path: values[0] ?? "*" },
109
- agentName,
110
- rules,
111
- ),
112
- );
113
-
114
102
  const events = makeEvents();
115
103
  const reporter = new GateDecisionReporter(logger, events);
116
104
  const resolvedPrompter: GatePrompter = prompter ?? {
@@ -325,13 +313,13 @@ describe("session shutdown clears external-directory approvals", () => {
325
313
  makeRealSession();
326
314
  const { resolver } = makeRealResolver(permissionManager, sessionRules);
327
315
 
328
- // external_directory=ask; session-covered paths return allow/session.
329
- vi.mocked(permissionManager.checkPermission).mockImplementation(
330
- (surface, input, _agentName, rules): PermissionCheckResult => {
316
+ // Unified check(intent): "ask" for external_directory unless session-covered.
317
+ vi.mocked(permissionManager.check).mockImplementation(
318
+ (intent, rules): PermissionCheckResult => {
319
+ const { surface } = intent;
320
+ const pathValue =
321
+ intent.kind === "path-values" ? (intent.values[0] ?? null) : null;
331
322
  if (surface === "external_directory") {
332
- const record = (input ?? {}) as Record<string, unknown>;
333
- const pathValue =
334
- typeof record.path === "string" ? record.path : null;
335
323
  if (pathValue && rules && rules.length > 0) {
336
324
  const match = rules.findLast(
337
325
  (r) =>
@@ -364,18 +352,6 @@ describe("session shutdown clears external-directory approvals", () => {
364
352
  },
365
353
  );
366
354
 
367
- // The external-directory tool gate resolves through checkPathPolicy (#418);
368
- // route it through the same configured checkPermission.
369
- vi.mocked(permissionManager.checkPathPolicy).mockImplementation(
370
- (values, agentName, rules, surface = "path") =>
371
- permissionManager.checkPermission(
372
- surface,
373
- { path: values[0] ?? "*" },
374
- agentName,
375
- rules,
376
- ),
377
- );
378
-
379
355
  const events = makeEvents();
380
356
  const reporter = new GateDecisionReporter(logger, events);
381
357
  const prompter: GatePrompter = {
@@ -32,13 +32,15 @@ function makeKeyedResolver(
32
32
  rules: { match: string; state: PermissionState }[],
33
33
  ): ScopedPermissionResolver {
34
34
  return {
35
- resolve: (_surface: string, input: { command?: string }) => {
36
- const command = input.command ?? "";
35
+ resolve: (intent) => {
36
+ const command =
37
+ intent.kind === "tool"
38
+ ? ((intent.input as { command?: string }).command ?? "")
39
+ : "";
37
40
  const rule = rules.find((r) => command.includes(r.match));
38
41
  const state: PermissionState = rule?.state ?? "allow";
39
42
  return makeCheckResult({ state, source: "bash", command });
40
43
  },
41
- resolvePathPolicy: () => makeCheckResult(),
42
44
  };
43
45
  }
44
46
 
@@ -30,17 +30,18 @@ describe("resolveBashCommandCheck", () => {
30
30
 
31
31
  expect(result.state).toBe("allow");
32
32
  expect(resolver.resolve).toHaveBeenCalledTimes(1);
33
- expect(resolver.resolve).toHaveBeenCalledWith(
34
- "bash",
35
- { command: "npm install pkg" },
36
- undefined,
37
- );
33
+ expect(resolver.resolve).toHaveBeenCalledWith({
34
+ kind: "tool",
35
+ surface: "bash",
36
+ input: { command: "npm install pkg" },
37
+ agentName: undefined,
38
+ });
38
39
  });
39
40
 
40
41
  it("denies the chain when any sub-command is denied, reporting that command's pattern", () => {
41
42
  const resolver = makeResolver();
42
- resolver.resolve.mockImplementation((_surface, input) => {
43
- const command = (input as { command: string }).command;
43
+ resolver.resolve.mockImplementation((intent) => {
44
+ const command = (intent as { input: { command: string } }).input.command;
44
45
  return command.startsWith("npm")
45
46
  ? bashResult("deny", command, "npm *")
46
47
  : bashResult("allow", command, "cd *");
@@ -60,8 +61,8 @@ describe("resolveBashCommandCheck", () => {
60
61
 
61
62
  it("asks when a sub-command asks and none denies", () => {
62
63
  const resolver = makeResolver();
63
- resolver.resolve.mockImplementation((_surface, input) => {
64
- const command = (input as { command: string }).command;
64
+ resolver.resolve.mockImplementation((intent) => {
65
+ const command = (intent as { input: { command: string } }).input.command;
65
66
  return command.startsWith("git")
66
67
  ? bashResult("ask", command, "git *")
67
68
  : bashResult("allow", command, "cd *");
@@ -81,8 +82,8 @@ describe("resolveBashCommandCheck", () => {
81
82
 
82
83
  it("returns the first allow result when every sub-command is allowed", () => {
83
84
  const resolver = makeResolver();
84
- resolver.resolve.mockImplementation((_surface, input) => {
85
- const command = (input as { command: string }).command;
85
+ resolver.resolve.mockImplementation((intent) => {
86
+ const command = (intent as { input: { command: string } }).input.command;
86
87
  return bashResult("allow", command, `${command} *`);
87
88
  });
88
89
 
@@ -109,11 +110,12 @@ describe("resolveBashCommandCheck", () => {
109
110
 
110
111
  expect(result.state).toBe("allow");
111
112
  expect(resolver.resolve).toHaveBeenCalledTimes(1);
112
- expect(resolver.resolve).toHaveBeenCalledWith(
113
- "bash",
114
- { command: "# just a comment" },
115
- undefined,
116
- );
113
+ expect(resolver.resolve).toHaveBeenCalledWith({
114
+ kind: "tool",
115
+ surface: "bash",
116
+ input: { command: "# just a comment" },
117
+ agentName: undefined,
118
+ });
117
119
  });
118
120
 
119
121
  it("falls back to the whole command for an empty/whitespace-only command", () => {
@@ -144,17 +146,18 @@ describe("resolveBashCommandCheck", () => {
144
146
 
145
147
  resolveBashCommandCheck("npm i", [{ text: "npm i" }], "agent-x", resolver);
146
148
 
147
- expect(resolver.resolve).toHaveBeenCalledWith(
148
- "bash",
149
- { command: "npm i" },
150
- "agent-x",
151
- );
149
+ expect(resolver.resolve).toHaveBeenCalledWith({
150
+ kind: "tool",
151
+ surface: "bash",
152
+ input: { command: "npm i" },
153
+ agentName: "agent-x",
154
+ });
152
155
  });
153
156
 
154
157
  it("tags the winning result with the offending command's execution context", () => {
155
158
  const resolver = makeResolver();
156
- resolver.resolve.mockImplementation((_surface, input) => {
157
- const command = (input as { command: string }).command;
159
+ resolver.resolve.mockImplementation((intent) => {
160
+ const command = (intent as { input: { command: string } }).input.command;
158
161
  return command.startsWith("rm")
159
162
  ? bashResult("deny", command, "rm *")
160
163
  : bashResult("allow", command, "echo *");
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
+ import type { AccessIntent } from "#src/access-intent/access-intent";
2
3
  import { BashProgram } from "#src/access-intent/bash/program";
3
4
  import { getNonEmptyString, toRecord } from "#src/common";
4
5
  import { describeBashExternalDirectoryGate } from "#src/handlers/gates/bash-external-directory";
@@ -40,6 +41,13 @@ function makeCheckResult(
40
41
  };
41
42
  }
42
43
 
44
+ /** Extract the policy match values a resolve(intent) call carries. */
45
+ function intentValues(intent: AccessIntent): readonly string[] {
46
+ if (intent.kind === "access-path") return intent.path.matchValues();
47
+ if (intent.kind === "path-values") return intent.values;
48
+ return [];
49
+ }
50
+
43
51
  /**
44
52
  * Mirror the handler's parse-once derivation: parse the bash command into a
45
53
  * shared `BashProgram` and inject it, exactly as `permission-gate-handler.ts`
@@ -76,18 +84,19 @@ describe("describeBashExternalDirectoryGate", () => {
76
84
  expect(result).toBeNull();
77
85
  });
78
86
 
79
- it("resolves each external path on the external_directory surface via resolvePathPolicy (#418)", async () => {
87
+ it("resolves each external path on the external_directory surface via an access-path intent (#418)", async () => {
80
88
  const resolver = makeResolver(makeCheckResult("ask"));
81
89
  await describeGate(
82
90
  makeTcc({ input: { command: "cat /outside/a.ts" } }),
83
91
  resolver,
84
92
  );
85
- expect(resolver.resolvePathPolicy).toHaveBeenCalledWith(
86
- ["/outside/a.ts"],
87
- undefined,
88
- "external_directory",
89
- );
90
- expect(resolver.resolve).not.toHaveBeenCalled();
93
+ const intent = resolver.resolve.mock.calls[0][0];
94
+ expect(intent).toMatchObject({
95
+ kind: "access-path",
96
+ surface: "external_directory",
97
+ agentName: undefined,
98
+ });
99
+ expect(intentValues(intent)).toEqual(["/outside/a.ts"]);
91
100
  });
92
101
 
93
102
  it("returns GateBypass when all external paths are session-covered", async () => {
@@ -122,11 +131,10 @@ describe("describeBashExternalDirectoryGate", () => {
122
131
  // not just session-level allow. This was the bug: source !== "session"
123
132
  // kept config-allowed paths in the uncovered set.
124
133
  const resolver = makeResolver();
125
- resolver.resolvePathPolicy.mockImplementation(
126
- (values: readonly string[]) =>
127
- values.length > 0
128
- ? makeCheckResult("allow", { source: "special" })
129
- : makeCheckResult("ask"),
134
+ resolver.resolve.mockImplementation((intent) =>
135
+ intentValues(intent).length > 0
136
+ ? makeCheckResult("allow", { source: "special" })
137
+ : makeCheckResult("ask"),
130
138
  );
131
139
  const result = await describeGate(makeTcc(), resolver);
132
140
  expect(result).not.toBeNull();
@@ -138,11 +146,10 @@ describe("describeBashExternalDirectoryGate", () => {
138
146
  // silently downgrading a config-level deny to ask. After the fix, the
139
147
  // descriptor's preCheck is derived from the actual path check result.
140
148
  const resolver = makeResolver();
141
- resolver.resolvePathPolicy.mockImplementation(
142
- (values: readonly string[]) =>
143
- values.length > 0
144
- ? makeCheckResult("deny", { source: "special" })
145
- : makeCheckResult("ask"),
149
+ resolver.resolve.mockImplementation((intent) =>
150
+ intentValues(intent).length > 0
151
+ ? makeCheckResult("deny", { source: "special" })
152
+ : makeCheckResult("ask"),
146
153
  );
147
154
  const result = await describeGate(makeTcc(), resolver);
148
155
  expect(isGateDescriptor(result)).toBe(true);
@@ -199,11 +206,10 @@ describe("describeBashExternalDirectoryGate", () => {
199
206
  it("config-allowed path is excluded; remaining ask path produces a descriptor", async () => {
200
207
  // One path config-allowed, one config-ask → descriptor with only the ask path.
201
208
  const resolver = makeResolver();
202
- resolver.resolvePathPolicy.mockImplementation(
203
- (values: readonly string[]) =>
204
- values.includes("/outside/a.ts")
205
- ? makeCheckResult("allow", { source: "special" })
206
- : makeCheckResult("ask"),
209
+ resolver.resolve.mockImplementation((intent) =>
210
+ intentValues(intent).includes("/outside/a.ts")
211
+ ? makeCheckResult("allow", { source: "special" })
212
+ : makeCheckResult("ask"),
207
213
  );
208
214
  const result = await describeGate(
209
215
  makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
@@ -220,11 +226,10 @@ describe("describeBashExternalDirectoryGate", () => {
220
226
  it("config-denied path makes worstCheck deny even when another path is ask", async () => {
221
227
  // One path config-denied, one config-ask → descriptor with preCheck.state === "deny".
222
228
  const resolver = makeResolver();
223
- resolver.resolvePathPolicy.mockImplementation(
224
- (values: readonly string[]) =>
225
- values.includes("/outside/a.ts")
226
- ? makeCheckResult("deny", { source: "special" })
227
- : makeCheckResult("ask"),
229
+ resolver.resolve.mockImplementation((intent) =>
230
+ intentValues(intent).includes("/outside/a.ts")
231
+ ? makeCheckResult("deny", { source: "special" })
232
+ : makeCheckResult("ask"),
228
233
  );
229
234
  const result = await describeGate(
230
235
  makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
@@ -241,11 +246,10 @@ describe("describeBashExternalDirectoryGate", () => {
241
246
 
242
247
  it("only includes uncovered paths when some are session-covered", async () => {
243
248
  const resolver = makeResolver();
244
- resolver.resolvePathPolicy.mockImplementation(
245
- (values: readonly string[]) =>
246
- values.includes("/outside/a.ts")
247
- ? makeCheckResult("allow", { source: "session" })
248
- : makeCheckResult("ask"),
249
+ resolver.resolve.mockImplementation((intent) =>
250
+ intentValues(intent).includes("/outside/a.ts")
251
+ ? makeCheckResult("allow", { source: "session" })
252
+ : makeCheckResult("ask"),
249
253
  );
250
254
  const result = await describeGate(
251
255
  makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
@@ -228,14 +228,16 @@ describe("describeBashPathGate", () => {
228
228
  resolver,
229
229
  )) as GateDescriptor;
230
230
 
231
- expect(resolver.resolvePathPolicy).toHaveBeenCalledWith(
232
- [
231
+ expect(resolver.resolve).toHaveBeenCalledWith({
232
+ kind: "path-values",
233
+ surface: "path",
234
+ values: [
233
235
  "/test/project/nested/src/file.txt",
234
236
  "nested/src/file.txt",
235
237
  "src/file.txt",
236
238
  ],
237
- undefined,
238
- );
239
+ agentName: undefined,
240
+ });
239
241
  // The raw token drives the prompt, denial context, and session approval.
240
242
  expect(result.denialContext).toMatchObject({ pathValue: "src/file.txt" });
241
243
  expect(result.decision.value).toBe("src/file.txt");
@@ -253,10 +255,12 @@ describe("describeBashPathGate", () => {
253
255
  resolver,
254
256
  );
255
257
 
256
- expect(resolver.resolvePathPolicy).toHaveBeenCalledWith(
257
- ["src/foo.ts"],
258
- undefined,
259
- );
258
+ expect(resolver.resolve).toHaveBeenCalledWith({
259
+ kind: "path-values",
260
+ surface: "path",
261
+ values: ["src/foo.ts"],
262
+ agentName: undefined,
263
+ });
260
264
  });
261
265
 
262
266
  it("binds a current-directory token's session approval to the cwd subtree", async () => {
@@ -30,11 +30,12 @@ describe("resolveExternalDirectoryPolicy", () => {
30
30
 
31
31
  const result = resolveExternalDirectoryPolicy(path, resolver, undefined);
32
32
 
33
- expect(resolver.resolvePathPolicy).toHaveBeenCalledWith(
34
- path.matchValues(),
35
- undefined,
36
- "external_directory",
37
- );
33
+ expect(resolver.resolve).toHaveBeenCalledWith({
34
+ kind: "access-path",
35
+ surface: "external_directory",
36
+ path,
37
+ agentName: undefined,
38
+ });
38
39
  expect(result).toEqual(makeCheckResult("ask"));
39
40
  });
40
41
 
@@ -44,11 +45,12 @@ describe("resolveExternalDirectoryPolicy", () => {
44
45
 
45
46
  resolveExternalDirectoryPolicy(path, resolver, "reviewer");
46
47
 
47
- expect(resolver.resolvePathPolicy).toHaveBeenCalledWith(
48
- path.matchValues(),
49
- "reviewer",
50
- "external_directory",
51
- );
48
+ expect(resolver.resolve).toHaveBeenCalledWith({
49
+ kind: "access-path",
50
+ surface: "external_directory",
51
+ path,
52
+ agentName: "reviewer",
53
+ });
52
54
  });
53
55
  });
54
56
 
@@ -74,12 +76,17 @@ describe("selectUncoveredExternalPaths", () => {
74
76
  const allowed = AccessPath.forExternalDirectory("/outside/ok.ts", cwd);
75
77
  const asked = AccessPath.forExternalDirectory("/outside/ask.ts", cwd);
76
78
  const resolver = makeResolver();
77
- resolver.resolvePathPolicy.mockImplementation(
78
- (values: readonly string[]) =>
79
- values.includes("/outside/ok.ts")
80
- ? makeCheckResult("allow")
81
- : makeCheckResult("ask"),
82
- );
79
+ resolver.resolve.mockImplementation((intent) => {
80
+ const values =
81
+ intent.kind === "access-path"
82
+ ? intent.path.matchValues()
83
+ : intent.kind === "path-values"
84
+ ? intent.values
85
+ : [];
86
+ return values.includes("/outside/ok.ts")
87
+ ? makeCheckResult("allow")
88
+ : makeCheckResult("ask");
89
+ });
83
90
 
84
91
  const { uncovered } = selectUncoveredExternalPaths(
85
92
  [allowed, asked],
@@ -94,12 +101,17 @@ describe("selectUncoveredExternalPaths", () => {
94
101
  const asked = AccessPath.forExternalDirectory("/outside/ask.ts", cwd);
95
102
  const denied = AccessPath.forExternalDirectory("/outside/deny.ts", cwd);
96
103
  const resolver = makeResolver();
97
- resolver.resolvePathPolicy.mockImplementation(
98
- (values: readonly string[]) =>
99
- values.includes("/outside/deny.ts")
100
- ? makeCheckResult("deny")
101
- : makeCheckResult("ask"),
102
- );
104
+ resolver.resolve.mockImplementation((intent) => {
105
+ const values =
106
+ intent.kind === "access-path"
107
+ ? intent.path.matchValues()
108
+ : intent.kind === "path-values"
109
+ ? intent.values
110
+ : [];
111
+ return values.includes("/outside/deny.ts")
112
+ ? makeCheckResult("deny")
113
+ : makeCheckResult("ask");
114
+ });
103
115
 
104
116
  const { worstCheck } = selectUncoveredExternalPaths(
105
117
  [asked, denied],
@@ -144,12 +144,18 @@ describe("describeExternalDirectoryGate", () => {
144
144
  undefined,
145
145
  resolver,
146
146
  );
147
- expect(resolver.resolvePathPolicy).toHaveBeenCalledWith(
148
- ["/outside/project/file.ts"],
149
- undefined,
150
- "external_directory",
147
+ expect(resolver.resolve).toHaveBeenCalledWith(
148
+ expect.objectContaining({
149
+ kind: "access-path",
150
+ surface: "external_directory",
151
+ agentName: undefined,
152
+ }),
151
153
  );
152
- expect(resolver.resolve).not.toHaveBeenCalled();
154
+ const intent = resolver.resolve.mock.calls[0][0];
155
+ expect(intent.kind).toBe("access-path");
156
+ if (intent.kind === "access-path") {
157
+ expect(intent.path.matchValues()).toEqual(["/outside/project/file.ts"]);
158
+ }
153
159
  });
154
160
 
155
161
  it("sessionApproval uses deriveApprovalPattern", () => {
@@ -155,11 +155,12 @@ describe("describePathGate", () => {
155
155
  it("resolves the path surface with the file path and agent name", () => {
156
156
  const resolver = makeResolver(makeCheckResult({ state: "allow" }));
157
157
  describePathGate(makeTcc({ agentName: "my-agent" }), resolver);
158
- expect(resolver.resolve).toHaveBeenCalledWith(
159
- "path",
160
- { path: ".env" },
161
- "my-agent",
162
- );
158
+ expect(resolver.resolve).toHaveBeenCalledWith({
159
+ kind: "tool",
160
+ surface: "path",
161
+ input: { path: ".env" },
162
+ agentName: "my-agent",
163
+ });
163
164
  });
164
165
  });
165
166
 
@@ -187,11 +188,12 @@ describe("describePathGate — home-relative paths", () => {
187
188
  toolName: "read",
188
189
  pathValue: "~/.ssh/config",
189
190
  });
190
- expect(resolver.resolve).toHaveBeenCalledWith(
191
- "path",
192
- { path: "~/.ssh/config" },
193
- undefined,
194
- );
191
+ expect(resolver.resolve).toHaveBeenCalledWith({
192
+ kind: "tool",
193
+ surface: "path",
194
+ input: { path: "~/.ssh/config" },
195
+ agentName: undefined,
196
+ });
195
197
  });
196
198
 
197
199
  it("passes raw $HOME/... path to resolver and builds descriptor on deny", () => {
@@ -243,11 +245,12 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
243
245
  resolver,
244
246
  );
245
247
  expect(isGateDescriptor(result)).toBe(true);
246
- expect(resolver.resolve).toHaveBeenCalledWith(
247
- "path",
248
- { path: ".env" },
249
- undefined,
250
- );
248
+ expect(resolver.resolve).toHaveBeenCalledWith({
249
+ kind: "tool",
250
+ surface: "path",
251
+ input: { path: ".env" },
252
+ agentName: undefined,
253
+ });
251
254
  });
252
255
 
253
256
  it("gates an MCP tool via arguments.path", () => {
@@ -259,11 +262,12 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
259
262
  resolver,
260
263
  );
261
264
  expect(isGateDescriptor(result)).toBe(true);
262
- expect(resolver.resolve).toHaveBeenCalledWith(
263
- "path",
264
- { path: ".env" },
265
- undefined,
266
- );
265
+ expect(resolver.resolve).toHaveBeenCalledWith({
266
+ kind: "tool",
267
+ surface: "path",
268
+ input: { path: ".env" },
269
+ agentName: undefined,
270
+ });
267
271
  });
268
272
 
269
273
  it("uses a registered extractor's path for a custom-shaped tool", () => {
@@ -275,11 +279,12 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
275
279
  resolver,
276
280
  extractorLookup("ffgrep", "target"),
277
281
  );
278
- expect(resolver.resolve).toHaveBeenCalledWith(
279
- "path",
280
- { path: "/etc/passwd" },
281
- undefined,
282
- );
282
+ expect(resolver.resolve).toHaveBeenCalledWith({
283
+ kind: "tool",
284
+ surface: "path",
285
+ input: { path: "/etc/passwd" },
286
+ agentName: undefined,
287
+ });
283
288
  });
284
289
 
285
290
  it("returns null for an extension tool without a path", () => {
@@ -215,7 +215,12 @@ describe("GateRunner — descriptor path", () => {
215
215
  const { runner, deps } = makeGateRunner();
216
216
  const result = await runner.run(makeDescriptor(), "test-agent", "tc-1");
217
217
  expect(result).toEqual({ action: "allow" });
218
- expect(deps.resolve).toHaveBeenCalledWith("read", {}, "test-agent");
218
+ expect(deps.resolve).toHaveBeenCalledWith({
219
+ kind: "tool",
220
+ surface: "read",
221
+ input: {},
222
+ agentName: "test-agent",
223
+ });
219
224
  expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
220
225
  expect.objectContaining({
221
226
  agentName: "test-agent",
@@ -198,8 +198,8 @@ describe("ToolCallGatePipeline", () => {
198
198
  // block can only come from the path gate seeing the extracted path.
199
199
  function pathDenyingResolver() {
200
200
  const resolver = makeResolver();
201
- resolver.resolve.mockImplementation((surface) =>
202
- surface === "path"
201
+ resolver.resolve.mockImplementation((intent) =>
202
+ intent.surface === "path"
203
203
  ? makeCheckResult({ state: "deny", matchedPattern: "*" })
204
204
  : makeCheckResult(),
205
205
  );
@@ -67,7 +67,7 @@ describe("handleInput", () => {
67
67
  it("does not check permissions for non-skill input", async () => {
68
68
  const { handler, permissionManager } = makeHandler();
69
69
  await handler.handleInput(makeInputEvent("just a message"), makeCtx());
70
- expect(permissionManager.checkPermission).not.toHaveBeenCalled();
70
+ expect(permissionManager.check).not.toHaveBeenCalled();
71
71
  });
72
72
 
73
73
  it("returns continue when skill is allowed", async () => {