@gotgenes/pi-permission-system 16.2.1 → 17.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 (61) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/src/access-intent/access-intent.ts +8 -10
  5. package/src/access-intent/access-path.ts +39 -16
  6. package/src/access-intent/bash/bash-path-resolver.ts +531 -0
  7. package/src/access-intent/bash/program.ts +21 -14
  8. package/src/access-intent/bash/token-classification.ts +1 -1
  9. package/src/canonicalize-path.ts +11 -5
  10. package/src/forwarded-permissions/permission-forwarder.ts +11 -1
  11. package/src/forwarding-manager.ts +7 -1
  12. package/src/handlers/before-agent-start.ts +1 -0
  13. package/src/handlers/gates/bash-path-extractor.ts +7 -5
  14. package/src/handlers/gates/bash-path.ts +13 -13
  15. package/src/handlers/gates/external-directory.ts +14 -16
  16. package/src/handlers/gates/path.ts +12 -7
  17. package/src/handlers/gates/skill-read.ts +7 -1
  18. package/src/handlers/gates/tool-call-gate-pipeline.ts +21 -4
  19. package/src/handlers/gates/tool.ts +4 -2
  20. package/src/index.ts +12 -1
  21. package/src/input-normalizer.ts +9 -4
  22. package/src/path-normalizer.ts +70 -0
  23. package/src/path-utils.ts +39 -28
  24. package/src/permission-manager.ts +21 -7
  25. package/src/permission-session.ts +35 -2
  26. package/src/prompting-gateway.ts +3 -0
  27. package/src/rule.ts +8 -6
  28. package/src/skill-prompt-sanitizer.ts +15 -4
  29. package/src/subagent-context.ts +17 -9
  30. package/test/access-intent/access-path.test.ts +124 -15
  31. package/test/access-intent/bash/program.test.ts +178 -80
  32. package/test/bash-external-directory.test.ts +15 -1
  33. package/test/canonicalize-path.test.ts +34 -8
  34. package/test/forwarding-manager.test.ts +7 -1
  35. package/test/handlers/external-directory-symlink-acceptance.test.ts +26 -4
  36. package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
  37. package/test/handlers/gates/bash-external-directory.test.ts +5 -2
  38. package/test/handlers/gates/bash-path.test.ts +14 -9
  39. package/test/handlers/gates/external-directory-policy.test.ts +28 -18
  40. package/test/handlers/gates/external-directory.test.ts +9 -1
  41. package/test/handlers/gates/path.test.ts +90 -20
  42. package/test/handlers/gates/skill-read.test.ts +22 -13
  43. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
  44. package/test/handlers/gates/tool.test.ts +13 -1
  45. package/test/helpers/gate-fixtures.ts +11 -2
  46. package/test/helpers/session-fixtures.ts +3 -0
  47. package/test/input-normalizer.test.ts +104 -39
  48. package/test/path-normalizer.test.ts +123 -0
  49. package/test/path-utils.test.ts +130 -51
  50. package/test/permission-forwarder.test.ts +1 -0
  51. package/test/permission-manager-unified.test.ts +40 -0
  52. package/test/permission-resolver.test.ts +14 -32
  53. package/test/permission-session.test.ts +41 -0
  54. package/test/pi-infrastructure-read.test.ts +27 -4
  55. package/test/prompting-gateway.test.ts +1 -0
  56. package/test/rule.test.ts +88 -42
  57. package/test/session-rules.test.ts +21 -4
  58. package/test/skill-prompt-sanitizer.test.ts +69 -14
  59. package/test/subagent-context.test.ts +77 -31
  60. package/test/synthesize.test.ts +13 -11
  61. package/src/access-intent/bash/cwd-projection.ts +0 -498
@@ -1,9 +1,21 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
+ // Mock node:fs so realpathSync (used by canonicalizePath) is controllable.
4
+ // Default implementation is identity — lexical tests are unaffected.
5
+ const realpathSync = vi.hoisted(() =>
6
+ vi.fn<(path: string) => string>((p) => p),
7
+ );
8
+ vi.mock("node:fs", () => ({
9
+ realpathSync,
10
+ default: { realpathSync },
11
+ }));
12
+
13
+ import { AccessPath } from "#src/access-intent/access-path";
3
14
  import type { GateDescriptor } from "#src/handlers/gates/descriptor";
4
15
  import { isGateDescriptor } from "#src/handlers/gates/descriptor";
5
16
  import { describePathGate } from "#src/handlers/gates/path";
6
17
  import type { ToolCallContext } from "#src/handlers/gates/types";
18
+ import { PathNormalizer } from "#src/path-normalizer";
7
19
 
8
20
  import {
9
21
  makeGateCheckResult as makeCheckResult,
@@ -24,14 +36,24 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
24
36
  };
25
37
  }
26
38
 
39
+ // The gate reads the path normalizer (platform + cwd baked in) from the
40
+ // session; here it is bound to the makeTcc default cwd.
41
+ const normalizer = new PathNormalizer(process.platform, "/test/project");
42
+
27
43
  // ── tests ──────────────────────────────────────────────────────────────────
28
44
 
29
45
  describe("describePathGate", () => {
46
+ beforeEach(() => {
47
+ realpathSync.mockReset();
48
+ realpathSync.mockImplementation((p: string) => p);
49
+ });
50
+
30
51
  it("returns null for non-path-bearing tools", () => {
31
52
  const resolver = makeResolver();
32
53
  const result = describePathGate(
33
54
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
34
55
  resolver,
56
+ normalizer,
35
57
  );
36
58
  expect(result).toBeNull();
37
59
  expect(resolver.resolve).not.toHaveBeenCalled();
@@ -42,13 +64,14 @@ describe("describePathGate", () => {
42
64
  const result = describePathGate(
43
65
  makeTcc({ toolName: "read", input: {} }),
44
66
  resolver,
67
+ normalizer,
45
68
  );
46
69
  expect(result).toBeNull();
47
70
  });
48
71
 
49
72
  it("returns null when path check result is allow", () => {
50
73
  const resolver = makeResolver(makeCheckResult({ state: "allow" }));
51
- const result = describePathGate(makeTcc(), resolver);
74
+ const result = describePathGate(makeTcc(), resolver, normalizer);
52
75
  expect(result).toBeNull();
53
76
  });
54
77
 
@@ -61,7 +84,7 @@ describe("describePathGate", () => {
61
84
  origin: "builtin",
62
85
  }),
63
86
  );
64
- const result = describePathGate(makeTcc(), resolver);
87
+ const result = describePathGate(makeTcc(), resolver, normalizer);
65
88
  expect(result).toBeNull();
66
89
  });
67
90
 
@@ -74,7 +97,7 @@ describe("describePathGate", () => {
74
97
  origin: "global",
75
98
  }),
76
99
  );
77
- const result = describePathGate(makeTcc(), resolver);
100
+ const result = describePathGate(makeTcc(), resolver, normalizer);
78
101
  expect(result).not.toBeNull();
79
102
  expect(isGateDescriptor(result)).toBe(true);
80
103
  });
@@ -83,7 +106,7 @@ describe("describePathGate", () => {
83
106
  const resolver = makeResolver(
84
107
  makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
85
108
  );
86
- const result = describePathGate(makeTcc(), resolver);
109
+ const result = describePathGate(makeTcc(), resolver, normalizer);
87
110
  expect(result).not.toBeNull();
88
111
  expect(isGateDescriptor(result)).toBe(true);
89
112
  const desc = result as GateDescriptor;
@@ -95,7 +118,7 @@ describe("describePathGate", () => {
95
118
  const resolver = makeResolver(
96
119
  makeCheckResult({ state: "ask", matchedPattern: "*.env" }),
97
120
  );
98
- const result = describePathGate(makeTcc(), resolver);
121
+ const result = describePathGate(makeTcc(), resolver, normalizer);
99
122
  expect(result).not.toBeNull();
100
123
  expect(isGateDescriptor(result)).toBe(true);
101
124
  const desc = result as GateDescriptor;
@@ -110,6 +133,7 @@ describe("describePathGate", () => {
110
133
  const result = describePathGate(
111
134
  makeTcc({ input: { path: "/test/project/src/.env" } }),
112
135
  resolver,
136
+ normalizer,
113
137
  ) as GateDescriptor;
114
138
  expect(result.sessionApproval).toBeDefined();
115
139
  expect(result.sessionApproval?.surface).toBe("path");
@@ -123,6 +147,7 @@ describe("describePathGate", () => {
123
147
  const result = describePathGate(
124
148
  makeTcc({ input: { path: "index.html" }, cwd: "/test/project" }),
125
149
  resolver,
150
+ normalizer,
126
151
  ) as GateDescriptor;
127
152
  expect(result.sessionApproval?.surface).toBe("path");
128
153
  expect(result.sessionApproval?.representativePattern).toBe(
@@ -134,7 +159,11 @@ describe("describePathGate", () => {
134
159
  const resolver = makeResolver(
135
160
  makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
136
161
  );
137
- const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
162
+ const result = describePathGate(
163
+ makeTcc(),
164
+ resolver,
165
+ normalizer,
166
+ ) as GateDescriptor;
138
167
  expect(result.denialContext).toEqual({
139
168
  kind: "path",
140
169
  toolName: "read",
@@ -147,21 +176,43 @@ describe("describePathGate", () => {
147
176
  const resolver = makeResolver(
148
177
  makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
149
178
  );
150
- const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
179
+ const result = describePathGate(
180
+ makeTcc(),
181
+ resolver,
182
+ normalizer,
183
+ ) as GateDescriptor;
151
184
  expect(result.decision.surface).toBe("path");
152
185
  expect(result.decision.value).toBe(".env");
153
186
  });
154
187
 
155
- it("resolves the path surface with the file path and agent name", () => {
188
+ it("resolves the path surface with an access-path intent and agent name", () => {
156
189
  const resolver = makeResolver(makeCheckResult({ state: "allow" }));
157
- describePathGate(makeTcc({ agentName: "my-agent" }), resolver);
190
+ describePathGate(makeTcc({ agentName: "my-agent" }), resolver, normalizer);
158
191
  expect(resolver.resolve).toHaveBeenCalledWith({
159
- kind: "tool",
192
+ kind: "access-path",
160
193
  surface: "path",
161
- input: { path: ".env" },
194
+ path: AccessPath.forPath(".env", {
195
+ cwd: "/test/project",
196
+ platform: "linux",
197
+ }),
162
198
  agentName: "my-agent",
163
199
  });
164
200
  });
201
+
202
+ it("emits an access-path whose matchValues include the symlink-resolved form (#486)", () => {
203
+ // /test/project/.env is a symlink to /vault/secret.env.
204
+ realpathSync.mockImplementation((p: string) =>
205
+ p === "/test/project/.env" ? "/vault/secret.env" : p,
206
+ );
207
+ const resolver = makeResolver(makeCheckResult({ state: "allow" }));
208
+ describePathGate(makeTcc(), resolver, normalizer);
209
+
210
+ const intent = resolver.resolve.mock.lastCall?.[0];
211
+ expect(intent?.kind).toBe("access-path");
212
+ expect(intent?.kind === "access-path" && intent.path.matchValues()).toEqual(
213
+ ["/test/project/.env", ".env", "/vault/secret.env"],
214
+ );
215
+ });
165
216
  });
166
217
 
167
218
  // Home-relative path characterization (#350) ──────────────────────────────
@@ -178,6 +229,7 @@ describe("describePathGate — home-relative paths", () => {
178
229
  const result = describePathGate(
179
230
  makeTcc({ input: { path: "~/.ssh/config" } }),
180
231
  resolver,
232
+ normalizer,
181
233
  ) as GateDescriptor;
182
234
 
183
235
  expect(isGateDescriptor(result)).toBe(true);
@@ -189,9 +241,12 @@ describe("describePathGate — home-relative paths", () => {
189
241
  pathValue: "~/.ssh/config",
190
242
  });
191
243
  expect(resolver.resolve).toHaveBeenCalledWith({
192
- kind: "tool",
244
+ kind: "access-path",
193
245
  surface: "path",
194
- input: { path: "~/.ssh/config" },
246
+ path: AccessPath.forPath("~/.ssh/config", {
247
+ cwd: "/test/project",
248
+ platform: "linux",
249
+ }),
195
250
  agentName: undefined,
196
251
  });
197
252
  });
@@ -203,6 +258,7 @@ describe("describePathGate — home-relative paths", () => {
203
258
  const result = describePathGate(
204
259
  makeTcc({ input: { path: "$HOME/.ssh/config" } }),
205
260
  resolver,
261
+ normalizer,
206
262
  ) as GateDescriptor;
207
263
 
208
264
  expect(isGateDescriptor(result)).toBe(true);
@@ -218,6 +274,7 @@ describe("describePathGate — home-relative paths", () => {
218
274
  const result = describePathGate(
219
275
  makeTcc({ input: { path: "~/.ssh/config" } }),
220
276
  resolver,
277
+ normalizer,
221
278
  );
222
279
  expect(result).toBeNull();
223
280
  });
@@ -243,12 +300,16 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
243
300
  const result = describePathGate(
244
301
  makeTcc({ toolName: "my-ext", input: { path: ".env" } }),
245
302
  resolver,
303
+ normalizer,
246
304
  );
247
305
  expect(isGateDescriptor(result)).toBe(true);
248
306
  expect(resolver.resolve).toHaveBeenCalledWith({
249
- kind: "tool",
307
+ kind: "access-path",
250
308
  surface: "path",
251
- input: { path: ".env" },
309
+ path: AccessPath.forPath(".env", {
310
+ cwd: "/test/project",
311
+ platform: "linux",
312
+ }),
252
313
  agentName: undefined,
253
314
  });
254
315
  });
@@ -260,12 +321,16 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
260
321
  const result = describePathGate(
261
322
  makeTcc({ toolName: "mcp", input: { arguments: { path: ".env" } } }),
262
323
  resolver,
324
+ normalizer,
263
325
  );
264
326
  expect(isGateDescriptor(result)).toBe(true);
265
327
  expect(resolver.resolve).toHaveBeenCalledWith({
266
- kind: "tool",
328
+ kind: "access-path",
267
329
  surface: "path",
268
- input: { path: ".env" },
330
+ path: AccessPath.forPath(".env", {
331
+ cwd: "/test/project",
332
+ platform: "linux",
333
+ }),
269
334
  agentName: undefined,
270
335
  });
271
336
  });
@@ -277,12 +342,16 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
277
342
  describePathGate(
278
343
  makeTcc({ toolName: "ffgrep", input: { target: "/etc/passwd" } }),
279
344
  resolver,
345
+ normalizer,
280
346
  extractorLookup("ffgrep", "target"),
281
347
  );
282
348
  expect(resolver.resolve).toHaveBeenCalledWith({
283
- kind: "tool",
349
+ kind: "access-path",
284
350
  surface: "path",
285
- input: { path: "/etc/passwd" },
351
+ path: AccessPath.forPath("/etc/passwd", {
352
+ cwd: "/test/project",
353
+ platform: "linux",
354
+ }),
286
355
  agentName: undefined,
287
356
  });
288
357
  });
@@ -292,6 +361,7 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
292
361
  const result = describePathGate(
293
362
  makeTcc({ toolName: "my-ext", input: { other: true } }),
294
363
  resolver,
364
+ normalizer,
295
365
  );
296
366
  expect(result).toBeNull();
297
367
  expect(resolver.resolve).not.toHaveBeenCalled();
@@ -41,34 +41,39 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
41
41
 
42
42
  describe("describeSkillReadGate", () => {
43
43
  it("returns null when tool is not read", () => {
44
- const result = describeSkillReadGate(makeTcc({ toolName: "write" }), () => [
45
- makeSkillEntry(),
46
- ]);
44
+ const result = describeSkillReadGate(
45
+ makeTcc({ toolName: "write" }),
46
+ "linux",
47
+ () => [makeSkillEntry()],
48
+ );
47
49
  expect(result).toBeNull();
48
50
  });
49
51
 
50
52
  it("returns null when no active skill entries", () => {
51
- const result = describeSkillReadGate(makeTcc(), () => []);
53
+ const result = describeSkillReadGate(makeTcc(), "linux", () => []);
52
54
  expect(result).toBeNull();
53
55
  });
54
56
 
55
57
  it("returns null when read path does not match any skill", () => {
56
58
  const result = describeSkillReadGate(
57
59
  makeTcc({ input: { path: "/test/project/src/index.ts" } }),
60
+ "linux",
58
61
  () => [makeSkillEntry()],
59
62
  );
60
63
  expect(result).toBeNull();
61
64
  });
62
65
 
63
66
  it("returns null when input has no path", () => {
64
- const result = describeSkillReadGate(makeTcc({ input: {} }), () => [
65
- makeSkillEntry(),
66
- ]);
67
+ const result = describeSkillReadGate(
68
+ makeTcc({ input: {} }),
69
+ "linux",
70
+ () => [makeSkillEntry()],
71
+ );
67
72
  expect(result).toBeNull();
68
73
  });
69
74
 
70
75
  it("returns GateDescriptor with preResolved.state matching skill entry state (ask)", () => {
71
- const result = describeSkillReadGate(makeTcc(), () => [
76
+ const result = describeSkillReadGate(makeTcc(), "linux", () => [
72
77
  makeSkillEntry({ state: "ask" }),
73
78
  ]);
74
79
  expect(result).not.toBeNull();
@@ -77,7 +82,7 @@ describe("describeSkillReadGate", () => {
77
82
  });
78
83
 
79
84
  it("returns GateDescriptor with preResolved.state matching skill entry state (allow)", () => {
80
- const result = describeSkillReadGate(makeTcc(), () => [
85
+ const result = describeSkillReadGate(makeTcc(), "linux", () => [
81
86
  makeSkillEntry({ state: "allow" }),
82
87
  ]);
83
88
  expect(result).not.toBeNull();
@@ -86,7 +91,7 @@ describe("describeSkillReadGate", () => {
86
91
  });
87
92
 
88
93
  it("returns GateDescriptor with preResolved.state matching skill entry state (deny)", () => {
89
- const result = describeSkillReadGate(makeTcc(), () => [
94
+ const result = describeSkillReadGate(makeTcc(), "linux", () => [
90
95
  makeSkillEntry({ state: "deny" }),
91
96
  ]);
92
97
  expect(result).not.toBeNull();
@@ -95,7 +100,7 @@ describe("describeSkillReadGate", () => {
95
100
  });
96
101
 
97
102
  it("decision surface is 'skill' and decision value is the skill name", () => {
98
- const result = describeSkillReadGate(makeTcc(), () => [
103
+ const result = describeSkillReadGate(makeTcc(), "linux", () => [
99
104
  makeSkillEntry({ name: "my-skill" }),
100
105
  ])!;
101
106
  expect(result.decision.surface).toBe("skill");
@@ -103,7 +108,7 @@ describe("describeSkillReadGate", () => {
103
108
  });
104
109
 
105
110
  it("denialContext contains the skill name and read path", () => {
106
- const result = describeSkillReadGate(makeTcc(), () => [
111
+ const result = describeSkillReadGate(makeTcc(), "linux", () => [
107
112
  makeSkillEntry({ name: "librarian" }),
108
113
  ])!;
109
114
  expect(result.denialContext).toEqual({
@@ -117,6 +122,7 @@ describe("describeSkillReadGate", () => {
117
122
  it("promptDetails includes skill_read source and skillName", () => {
118
123
  const result = describeSkillReadGate(
119
124
  makeTcc({ agentName: "test-agent", toolCallId: "tc-42" }),
125
+ "linux",
120
126
  () => [makeSkillEntry({ name: "my-skill" })],
121
127
  )!;
122
128
  expect(result.promptDetails).toMatchObject({
@@ -132,6 +138,7 @@ describe("describeSkillReadGate", () => {
132
138
  it("logContext includes skill_read source and skillName", () => {
133
139
  const result = describeSkillReadGate(
134
140
  makeTcc({ agentName: "agent-1" }),
141
+ "linux",
135
142
  () => [makeSkillEntry({ name: "librarian" })],
136
143
  )!;
137
144
  expect(result.logContext).toMatchObject({
@@ -142,7 +149,9 @@ describe("describeSkillReadGate", () => {
142
149
  });
143
150
 
144
151
  it("surface is 'skill' on the descriptor", () => {
145
- const result = describeSkillReadGate(makeTcc(), () => [makeSkillEntry()])!;
152
+ const result = describeSkillReadGate(makeTcc(), "linux", () => [
153
+ makeSkillEntry(),
154
+ ])!;
146
155
  expect(result.surface).toBe("skill");
147
156
  });
148
157
  });
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type { AccessPath } from "#src/access-intent/access-path";
4
4
  import { ToolCallGatePipeline } from "#src/handlers/gates/tool-call-gate-pipeline";
5
+ import { PathNormalizer } from "#src/path-normalizer";
5
6
 
6
7
  import {
7
8
  makeGateInputs,
@@ -172,7 +173,7 @@ describe("ToolCallGatePipeline", () => {
172
173
  expect(mockBashProgramParse).toHaveBeenCalledTimes(1);
173
174
  expect(mockBashProgramParse).toHaveBeenCalledWith(
174
175
  "echo hello",
175
- "/test/project",
176
+ expect.any(PathNormalizer),
176
177
  );
177
178
  });
178
179
 
@@ -53,6 +53,7 @@ describe("describeToolGate", () => {
53
53
  makeTcc({ toolName: "read" }),
54
54
  makeCheckResult("ask"),
55
55
  makeFormatter(),
56
+ "linux",
56
57
  );
57
58
  expect(desc.surface).toBe("read");
58
59
  expect(desc.decision.surface).toBe("read");
@@ -63,6 +64,7 @@ describe("describeToolGate", () => {
63
64
  makeTcc({ toolName: "write" }),
64
65
  makeCheckResult("ask"),
65
66
  makeFormatter(),
67
+ "linux",
66
68
  );
67
69
  expect(desc.decision.value).toBe("write");
68
70
  });
@@ -76,6 +78,7 @@ describe("describeToolGate", () => {
76
78
  makeTcc({ toolName: "bash", input: { command: "git status" } }),
77
79
  check,
78
80
  makeFormatter(),
81
+ "linux",
79
82
  );
80
83
  expect(desc.surface).toBe("bash");
81
84
  expect(desc.decision.surface).toBe("bash");
@@ -91,6 +94,7 @@ describe("describeToolGate", () => {
91
94
  makeTcc({ toolName: "mcp", input: { tool: "server:tool" } }),
92
95
  check,
93
96
  makeFormatter(),
97
+ "linux",
94
98
  );
95
99
  expect(desc.surface).toBe("mcp");
96
100
  expect(desc.decision.surface).toBe("mcp");
@@ -99,7 +103,7 @@ describe("describeToolGate", () => {
99
103
 
100
104
  it("populates denialContext with kind 'tool' and check result", () => {
101
105
  const check = makeCheckResult("deny", { toolName: "read" });
102
- const desc = describeToolGate(makeTcc(), check, makeFormatter());
106
+ const desc = describeToolGate(makeTcc(), check, makeFormatter(), "linux");
103
107
  expect(desc.denialContext).toEqual({
104
108
  kind: "tool",
105
109
  check,
@@ -114,6 +118,7 @@ describe("describeToolGate", () => {
114
118
  makeTcc({ agentName: "my-agent" }),
115
119
  check,
116
120
  makeFormatter(),
121
+ "linux",
117
122
  );
118
123
  expect(desc.denialContext.agentName).toBe("my-agent");
119
124
  });
@@ -124,6 +129,7 @@ describe("describeToolGate", () => {
124
129
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
125
130
  check,
126
131
  makeFormatter(),
132
+ "linux",
127
133
  );
128
134
  expect(desc.denialContext).toMatchObject({
129
135
  kind: "tool",
@@ -140,6 +146,7 @@ describe("describeToolGate", () => {
140
146
  makeTcc({ toolName: "bash", input: { command: "git status" } }),
141
147
  check,
142
148
  makeFormatter(),
149
+ "linux",
143
150
  );
144
151
  expect(desc.sessionApproval).toBeDefined();
145
152
  expect(desc.sessionApproval?.surface).toBe("bash");
@@ -156,6 +163,7 @@ describe("describeToolGate", () => {
156
163
  }),
157
164
  check,
158
165
  makeFormatter(),
166
+ "linux",
159
167
  );
160
168
  expect(desc.sessionApproval?.surface).toBe("edit");
161
169
  expect(desc.sessionApproval?.representativePattern).toBe("/test/project/*");
@@ -174,6 +182,7 @@ describe("describeToolGate", () => {
174
182
  }),
175
183
  check,
176
184
  makeFormatter(),
185
+ "linux",
177
186
  );
178
187
  expect(desc.sessionApproval?.representativePattern).toBe(
179
188
  "/test/project/src/*",
@@ -186,6 +195,7 @@ describe("describeToolGate", () => {
186
195
  makeTcc({ toolName: "read", agentName: "my-agent", toolCallId: "tc-42" }),
187
196
  check,
188
197
  makeFormatter(),
198
+ "linux",
189
199
  );
190
200
  expect(desc.promptDetails).toMatchObject({
191
201
  source: "tool_call",
@@ -203,6 +213,7 @@ describe("describeToolGate", () => {
203
213
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
204
214
  check,
205
215
  makeFormatter(),
216
+ "linux",
206
217
  );
207
218
  expect(desc.logContext).toMatchObject({
208
219
  source: "tool_call",
@@ -216,6 +227,7 @@ describe("describeToolGate", () => {
216
227
  makeTcc({ toolName: "edit", input: { path: "/a.ts" } }),
217
228
  makeCheckResult("ask", { toolName: "edit" }),
218
229
  makeFormatter(),
230
+ "linux",
219
231
  );
220
232
  expect(desc.surface).toBe("edit");
221
233
  expect(desc.input).toEqual({ path: "/a.ts" });
@@ -10,6 +10,7 @@ import { GateRunner } from "#src/handlers/gates/runner";
10
10
  import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
11
11
  import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
12
12
  import type { ToolCallContext } from "#src/handlers/gates/types";
13
+ import { PathNormalizer } from "#src/path-normalizer";
13
14
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
14
15
  import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
15
16
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
@@ -213,8 +214,7 @@ export function makePathDispatchResolver(
213
214
  }
214
215
  return defaultResult;
215
216
  }
216
- const values =
217
- intent.kind === "access-path" ? intent.path.matchValues() : intent.values;
217
+ const values = intent.path.matchValues();
218
218
  for (const value of values) {
219
219
  if (value in byPath) return byPath[value];
220
220
  }
@@ -254,6 +254,8 @@ export function makeGateInputs(
254
254
  getActiveSkillEntries?: () => SkillPromptEntry[];
255
255
  getInfrastructureReadDirs?: () => string[];
256
256
  getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
257
+ getPathNormalizer?: () => PathNormalizer;
258
+ getPlatform?: () => NodeJS.Platform;
257
259
  } = {},
258
260
  ): ToolCallGateInputs {
259
261
  return {
@@ -269,6 +271,13 @@ export function makeGateInputs(
269
271
  toolTextSummaryMaxLength: 100,
270
272
  toolInputLogPreviewMaxLength: 200,
271
273
  })),
274
+ getPathNormalizer:
275
+ overrides.getPathNormalizer ??
276
+ vi.fn<() => PathNormalizer>(
277
+ () => new PathNormalizer(process.platform, "/test/cwd"),
278
+ ),
279
+ getPlatform:
280
+ overrides.getPlatform ?? vi.fn<() => NodeJS.Platform>(() => "linux"),
272
281
  };
273
282
  }
274
283
 
@@ -126,6 +126,7 @@ export function makeRealSession(overrides?: {
126
126
  sessionRules?: SessionRules;
127
127
  configStore?: SessionConfigStore;
128
128
  gateway?: PromptingGatewayLifecycle;
129
+ platform?: NodeJS.Platform;
129
130
  }): {
130
131
  session: PermissionSession;
131
132
  paths: ExtensionPaths;
@@ -146,6 +147,7 @@ export function makeRealSession(overrides?: {
146
147
  const sessionRules = overrides?.sessionRules ?? new SessionRules();
147
148
  const configStore = overrides?.configStore ?? makeConfigStore();
148
149
  const gateway = overrides?.gateway ?? makeGateway();
150
+ const platform = overrides?.platform ?? process.platform;
149
151
  const session = new PermissionSession(
150
152
  paths,
151
153
  forwarding,
@@ -153,6 +155,7 @@ export function makeRealSession(overrides?: {
153
155
  sessionRules,
154
156
  configStore,
155
157
  gateway,
158
+ platform,
156
159
  );
157
160
  return {
158
161
  session,