@gotgenes/pi-permission-system 17.0.0 → 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 (58) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +1 -1
  3. package/src/access-intent/access-path.ts +5 -5
  4. package/src/access-intent/bash/bash-path-resolver.ts +531 -0
  5. package/src/access-intent/bash/program.ts +21 -14
  6. package/src/access-intent/bash/token-classification.ts +1 -1
  7. package/src/canonicalize-path.ts +11 -5
  8. package/src/forwarded-permissions/permission-forwarder.ts +11 -1
  9. package/src/forwarding-manager.ts +7 -1
  10. package/src/handlers/before-agent-start.ts +1 -0
  11. package/src/handlers/gates/bash-path-extractor.ts +7 -5
  12. package/src/handlers/gates/external-directory.ts +14 -15
  13. package/src/handlers/gates/path.ts +3 -2
  14. package/src/handlers/gates/skill-read.ts +7 -1
  15. package/src/handlers/gates/tool-call-gate-pipeline.ts +21 -4
  16. package/src/handlers/gates/tool.ts +4 -2
  17. package/src/index.ts +12 -1
  18. package/src/input-normalizer.ts +9 -4
  19. package/src/path-normalizer.ts +70 -0
  20. package/src/path-utils.ts +39 -28
  21. package/src/permission-manager.ts +21 -7
  22. package/src/permission-session.ts +35 -2
  23. package/src/prompting-gateway.ts +3 -0
  24. package/src/rule.ts +8 -6
  25. package/src/skill-prompt-sanitizer.ts +15 -4
  26. package/src/subagent-context.ts +17 -9
  27. package/test/access-intent/access-path.test.ts +73 -24
  28. package/test/access-intent/bash/program.test.ts +131 -65
  29. package/test/bash-external-directory.test.ts +15 -1
  30. package/test/canonicalize-path.test.ts +34 -8
  31. package/test/forwarding-manager.test.ts +7 -1
  32. package/test/handlers/external-directory-symlink-acceptance.test.ts +26 -4
  33. package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
  34. package/test/handlers/gates/bash-external-directory.test.ts +5 -1
  35. package/test/handlers/gates/bash-path.test.ts +6 -1
  36. package/test/handlers/gates/external-directory-policy.test.ts +26 -8
  37. package/test/handlers/gates/external-directory.test.ts +9 -1
  38. package/test/handlers/gates/path.test.ts +53 -14
  39. package/test/handlers/gates/skill-read.test.ts +22 -13
  40. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
  41. package/test/handlers/gates/tool.test.ts +13 -1
  42. package/test/helpers/gate-fixtures.ts +10 -0
  43. package/test/helpers/session-fixtures.ts +3 -0
  44. package/test/input-normalizer.test.ts +104 -39
  45. package/test/path-normalizer.test.ts +123 -0
  46. package/test/path-utils.test.ts +130 -51
  47. package/test/permission-forwarder.test.ts +1 -0
  48. package/test/permission-manager-unified.test.ts +40 -0
  49. package/test/permission-resolver.test.ts +12 -3
  50. package/test/permission-session.test.ts +41 -0
  51. package/test/pi-infrastructure-read.test.ts +27 -4
  52. package/test/prompting-gateway.test.ts +1 -0
  53. package/test/rule.test.ts +88 -42
  54. package/test/session-rules.test.ts +21 -4
  55. package/test/skill-prompt-sanitizer.test.ts +69 -14
  56. package/test/subagent-context.test.ts +77 -31
  57. package/test/synthesize.test.ts +13 -11
  58. package/src/access-intent/bash/cwd-projection.ts +0 -500
@@ -30,7 +30,11 @@ function makeForwarder() {
30
30
  }
31
31
 
32
32
  function makeManager() {
33
- return new ForwardingManager("/agent/subagent-sessions", makeForwarder());
33
+ return new ForwardingManager(
34
+ "/agent/subagent-sessions",
35
+ makeForwarder(),
36
+ "linux",
37
+ );
34
38
  }
35
39
 
36
40
  // ── Tests ─────────────────────────────────────────────────────────────────
@@ -180,6 +184,7 @@ describe("ForwardingManager", () => {
180
184
  const manager = new ForwardingManager(
181
185
  "/custom/subagent-dir",
182
186
  makeForwarder(),
187
+ "linux",
183
188
  );
184
189
  const ctx = makeCtx();
185
190
  manager.start(ctx);
@@ -187,6 +192,7 @@ describe("ForwardingManager", () => {
187
192
  expect(mockIsSubagentExecutionContext).toHaveBeenCalledWith(
188
193
  ctx,
189
194
  "/custom/subagent-dir",
195
+ "linux",
190
196
  undefined,
191
197
  );
192
198
  });
@@ -22,6 +22,7 @@ import {
22
22
  } from "#src/handlers/gates/descriptor";
23
23
  import { describeExternalDirectoryGate } from "#src/handlers/gates/external-directory";
24
24
  import type { ToolCallContext } from "#src/handlers/gates/types";
25
+ import { PathNormalizer } from "#src/path-normalizer";
25
26
  import { PermissionResolver } from "#src/permission-resolver";
26
27
  import { SessionRules } from "#src/session-rules";
27
28
  import type { ScopeConfig } from "#src/types";
@@ -83,7 +84,13 @@ describe("external_directory symlink acceptance (#418)", () => {
83
84
  },
84
85
  });
85
86
  try {
86
- const result = describeExternalDirectoryGate(readTcc(), [], resolver);
87
+ const result = describeExternalDirectoryGate(
88
+ readTcc(),
89
+ [],
90
+ resolver,
91
+ new PathNormalizer(process.platform, cwd),
92
+ "linux",
93
+ );
87
94
  expect(isGateDescriptor(result)).toBe(true);
88
95
  expect((result as GateDescriptor).preCheck?.state).toBe("allow");
89
96
  } finally {
@@ -101,7 +108,13 @@ describe("external_directory symlink acceptance (#418)", () => {
101
108
  },
102
109
  });
103
110
  try {
104
- const result = describeExternalDirectoryGate(readTcc(), [], resolver);
111
+ const result = describeExternalDirectoryGate(
112
+ readTcc(),
113
+ [],
114
+ resolver,
115
+ new PathNormalizer(process.platform, cwd),
116
+ "linux",
117
+ );
105
118
  expect(isGateDescriptor(result)).toBe(true);
106
119
  expect((result as GateDescriptor).preCheck?.state).toBe("allow");
107
120
  } finally {
@@ -114,7 +127,13 @@ describe("external_directory symlink acceptance (#418)", () => {
114
127
  permission: { external_directory: { "*": "ask" } },
115
128
  });
116
129
  try {
117
- const result = describeExternalDirectoryGate(readTcc(), [], resolver);
130
+ const result = describeExternalDirectoryGate(
131
+ readTcc(),
132
+ [],
133
+ resolver,
134
+ new PathNormalizer(process.platform, cwd),
135
+ "linux",
136
+ );
118
137
  expect(isGateDescriptor(result)).toBe(true);
119
138
  expect((result as GateDescriptor).preCheck?.state).toBe("ask");
120
139
  } finally {
@@ -137,7 +156,10 @@ describe("external_directory symlink acceptance (#418)", () => {
137
156
  toolCallId: "tc-2",
138
157
  cwd,
139
158
  };
140
- const program = await BashProgram.parse(command, cwd);
159
+ const program = await BashProgram.parse(
160
+ command,
161
+ new PathNormalizer(process.platform, cwd),
162
+ );
141
163
  const result = describeBashExternalDirectoryGate(tcc, program, resolver);
142
164
  // All external paths are covered by the allow → bypass, no prompt.
143
165
  expect(isGateBypass(result)).toBe(true);
@@ -12,6 +12,7 @@
12
12
  import { describe, expect, it } from "vitest";
13
13
  import { BashProgram } from "#src/access-intent/bash/program";
14
14
  import { resolveBashCommandCheck } from "#src/handlers/gates/bash-command";
15
+ import { PathNormalizer } from "#src/path-normalizer";
15
16
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
16
17
  import type { PermissionState } from "#src/types";
17
18
 
@@ -48,7 +49,10 @@ async function decide(
48
49
  command: string,
49
50
  resolver: ScopedPermissionResolver,
50
51
  ): Promise<PermissionState> {
51
- const program = await BashProgram.parse(command, "/cwd");
52
+ const program = await BashProgram.parse(
53
+ command,
54
+ new PathNormalizer(process.platform, "/cwd"),
55
+ );
52
56
  return resolveBashCommandCheck(
53
57
  command,
54
58
  program.commands(),
@@ -9,6 +9,7 @@ import type {
9
9
  } from "#src/handlers/gates/descriptor";
10
10
  import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
11
11
  import type { ToolCallContext } from "#src/handlers/gates/types";
12
+ import { PathNormalizer } from "#src/path-normalizer";
12
13
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
13
14
  import type { PermissionCheckResult } from "#src/types";
14
15
  import { getNonEmptyString, toRecord } from "#src/value-guards";
@@ -59,7 +60,10 @@ async function describeGate(
59
60
  const command = getNonEmptyString(toRecord(tcc.input).command);
60
61
  const bashProgram =
61
62
  tcc.toolName === "bash" && command
62
- ? await BashProgram.parse(command, tcc.cwd)
63
+ ? await BashProgram.parse(
64
+ command,
65
+ new PathNormalizer(process.platform, tcc.cwd),
66
+ )
63
67
  : null;
64
68
  return describeBashExternalDirectoryGate(tcc, bashProgram, resolver);
65
69
  }
@@ -19,6 +19,7 @@ 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 { PathNormalizer } from "#src/path-normalizer";
22
23
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
23
24
  import { getNonEmptyString, toRecord } from "#src/value-guards";
24
25
 
@@ -45,7 +46,10 @@ async function describeGate(
45
46
  const command = getNonEmptyString(toRecord(tcc.input).command);
46
47
  const bashProgram =
47
48
  tcc.toolName === "bash" && command
48
- ? await BashProgram.parse(command, tcc.cwd)
49
+ ? await BashProgram.parse(
50
+ command,
51
+ new PathNormalizer(process.platform, tcc.cwd),
52
+ )
49
53
  : null;
50
54
  return describeBashPathGate(tcc, bashProgram, resolver);
51
55
  }
@@ -235,6 +239,7 @@ describe("describeBashPathGate", () => {
235
239
  path: AccessPath.forPath("src/file.txt", {
236
240
  cwd: "/test/project",
237
241
  resolveBase: "/test/project/nested",
242
+ platform: "linux",
238
243
  }),
239
244
  agentName: undefined,
240
245
  });
@@ -25,7 +25,10 @@ function makeCheckResult(
25
25
 
26
26
  describe("resolveExternalDirectoryPolicy", () => {
27
27
  it("resolves the path's match aliases on the external_directory surface (#418)", () => {
28
- const path = AccessPath.forPath("/outside/a.ts", { cwd });
28
+ const path = AccessPath.forPath("/outside/a.ts", {
29
+ cwd,
30
+ platform: "linux",
31
+ });
29
32
  const resolver = makeResolver(makeCheckResult("ask"));
30
33
 
31
34
  const result = resolveExternalDirectoryPolicy(path, resolver, undefined);
@@ -40,7 +43,10 @@ describe("resolveExternalDirectoryPolicy", () => {
40
43
  });
41
44
 
42
45
  it("threads the agent name through to the resolver", () => {
43
- const path = AccessPath.forPath("/outside/a.ts", { cwd });
46
+ const path = AccessPath.forPath("/outside/a.ts", {
47
+ cwd,
48
+ platform: "linux",
49
+ });
44
50
  const resolver = makeResolver(makeCheckResult("allow"));
45
51
 
46
52
  resolveExternalDirectoryPolicy(path, resolver, "reviewer");
@@ -57,8 +63,8 @@ describe("resolveExternalDirectoryPolicy", () => {
57
63
  describe("selectUncoveredExternalPaths", () => {
58
64
  it("returns no uncovered paths when every path resolves to allow", () => {
59
65
  const paths = [
60
- AccessPath.forPath("/outside/a.ts", { cwd }),
61
- AccessPath.forPath("/outside/b.ts", { cwd }),
66
+ AccessPath.forPath("/outside/a.ts", { cwd, platform: "linux" }),
67
+ AccessPath.forPath("/outside/b.ts", { cwd, platform: "linux" }),
62
68
  ];
63
69
  const resolver = makeResolver(makeCheckResult("allow"));
64
70
 
@@ -73,8 +79,14 @@ describe("selectUncoveredExternalPaths", () => {
73
79
  });
74
80
 
75
81
  it("collects only paths whose resolved state is not allow", () => {
76
- const allowed = AccessPath.forPath("/outside/ok.ts", { cwd });
77
- const asked = AccessPath.forPath("/outside/ask.ts", { cwd });
82
+ const allowed = AccessPath.forPath("/outside/ok.ts", {
83
+ cwd,
84
+ platform: "linux",
85
+ });
86
+ const asked = AccessPath.forPath("/outside/ask.ts", {
87
+ cwd,
88
+ platform: "linux",
89
+ });
78
90
  const resolver = makeResolver();
79
91
  resolver.resolve.mockImplementation((intent) => {
80
92
  const values =
@@ -94,8 +106,14 @@ describe("selectUncoveredExternalPaths", () => {
94
106
  });
95
107
 
96
108
  it("returns the most restrictive uncovered check as worstCheck (deny > ask)", () => {
97
- const asked = AccessPath.forPath("/outside/ask.ts", { cwd });
98
- const denied = AccessPath.forPath("/outside/deny.ts", { cwd });
109
+ const asked = AccessPath.forPath("/outside/ask.ts", {
110
+ cwd,
111
+ platform: "linux",
112
+ });
113
+ const denied = AccessPath.forPath("/outside/deny.ts", {
114
+ cwd,
115
+ platform: "linux",
116
+ });
99
117
  const resolver = makeResolver();
100
118
  resolver.resolve.mockImplementation((intent) => {
101
119
  const values =
@@ -7,6 +7,7 @@ import type {
7
7
  import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
8
8
  import { describeExternalDirectoryGate } from "#src/handlers/gates/external-directory";
9
9
  import type { ToolCallContext } from "#src/handlers/gates/types";
10
+ import { PathNormalizer } from "#src/path-normalizer";
10
11
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
11
12
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
12
13
  import { makeResolver } from "#test/helpers/gate-fixtures";
@@ -37,7 +38,14 @@ function gateUnderTest(
37
38
  makeCheckResult({ state: "ask", toolName: "external_directory" }),
38
39
  ),
39
40
  ) {
40
- return describeExternalDirectoryGate(tcc, infraDirs, resolver, extractors);
41
+ return describeExternalDirectoryGate(
42
+ tcc,
43
+ infraDirs,
44
+ resolver,
45
+ new PathNormalizer(process.platform, tcc.cwd),
46
+ "linux",
47
+ extractors,
48
+ );
41
49
  }
42
50
 
43
51
  // ── tests ────────────────────��────────────────────────────────────��────────
@@ -15,6 +15,7 @@ import type { GateDescriptor } from "#src/handlers/gates/descriptor";
15
15
  import { isGateDescriptor } from "#src/handlers/gates/descriptor";
16
16
  import { describePathGate } from "#src/handlers/gates/path";
17
17
  import type { ToolCallContext } from "#src/handlers/gates/types";
18
+ import { PathNormalizer } from "#src/path-normalizer";
18
19
 
19
20
  import {
20
21
  makeGateCheckResult as makeCheckResult,
@@ -35,6 +36,10 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
35
36
  };
36
37
  }
37
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
+
38
43
  // ── tests ──────────────────────────────────────────────────────────────────
39
44
 
40
45
  describe("describePathGate", () => {
@@ -48,6 +53,7 @@ describe("describePathGate", () => {
48
53
  const result = describePathGate(
49
54
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
50
55
  resolver,
56
+ normalizer,
51
57
  );
52
58
  expect(result).toBeNull();
53
59
  expect(resolver.resolve).not.toHaveBeenCalled();
@@ -58,13 +64,14 @@ describe("describePathGate", () => {
58
64
  const result = describePathGate(
59
65
  makeTcc({ toolName: "read", input: {} }),
60
66
  resolver,
67
+ normalizer,
61
68
  );
62
69
  expect(result).toBeNull();
63
70
  });
64
71
 
65
72
  it("returns null when path check result is allow", () => {
66
73
  const resolver = makeResolver(makeCheckResult({ state: "allow" }));
67
- const result = describePathGate(makeTcc(), resolver);
74
+ const result = describePathGate(makeTcc(), resolver, normalizer);
68
75
  expect(result).toBeNull();
69
76
  });
70
77
 
@@ -77,7 +84,7 @@ describe("describePathGate", () => {
77
84
  origin: "builtin",
78
85
  }),
79
86
  );
80
- const result = describePathGate(makeTcc(), resolver);
87
+ const result = describePathGate(makeTcc(), resolver, normalizer);
81
88
  expect(result).toBeNull();
82
89
  });
83
90
 
@@ -90,7 +97,7 @@ describe("describePathGate", () => {
90
97
  origin: "global",
91
98
  }),
92
99
  );
93
- const result = describePathGate(makeTcc(), resolver);
100
+ const result = describePathGate(makeTcc(), resolver, normalizer);
94
101
  expect(result).not.toBeNull();
95
102
  expect(isGateDescriptor(result)).toBe(true);
96
103
  });
@@ -99,7 +106,7 @@ describe("describePathGate", () => {
99
106
  const resolver = makeResolver(
100
107
  makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
101
108
  );
102
- const result = describePathGate(makeTcc(), resolver);
109
+ const result = describePathGate(makeTcc(), resolver, normalizer);
103
110
  expect(result).not.toBeNull();
104
111
  expect(isGateDescriptor(result)).toBe(true);
105
112
  const desc = result as GateDescriptor;
@@ -111,7 +118,7 @@ describe("describePathGate", () => {
111
118
  const resolver = makeResolver(
112
119
  makeCheckResult({ state: "ask", matchedPattern: "*.env" }),
113
120
  );
114
- const result = describePathGate(makeTcc(), resolver);
121
+ const result = describePathGate(makeTcc(), resolver, normalizer);
115
122
  expect(result).not.toBeNull();
116
123
  expect(isGateDescriptor(result)).toBe(true);
117
124
  const desc = result as GateDescriptor;
@@ -126,6 +133,7 @@ describe("describePathGate", () => {
126
133
  const result = describePathGate(
127
134
  makeTcc({ input: { path: "/test/project/src/.env" } }),
128
135
  resolver,
136
+ normalizer,
129
137
  ) as GateDescriptor;
130
138
  expect(result.sessionApproval).toBeDefined();
131
139
  expect(result.sessionApproval?.surface).toBe("path");
@@ -139,6 +147,7 @@ describe("describePathGate", () => {
139
147
  const result = describePathGate(
140
148
  makeTcc({ input: { path: "index.html" }, cwd: "/test/project" }),
141
149
  resolver,
150
+ normalizer,
142
151
  ) as GateDescriptor;
143
152
  expect(result.sessionApproval?.surface).toBe("path");
144
153
  expect(result.sessionApproval?.representativePattern).toBe(
@@ -150,7 +159,11 @@ describe("describePathGate", () => {
150
159
  const resolver = makeResolver(
151
160
  makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
152
161
  );
153
- const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
162
+ const result = describePathGate(
163
+ makeTcc(),
164
+ resolver,
165
+ normalizer,
166
+ ) as GateDescriptor;
154
167
  expect(result.denialContext).toEqual({
155
168
  kind: "path",
156
169
  toolName: "read",
@@ -163,18 +176,25 @@ describe("describePathGate", () => {
163
176
  const resolver = makeResolver(
164
177
  makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
165
178
  );
166
- const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
179
+ const result = describePathGate(
180
+ makeTcc(),
181
+ resolver,
182
+ normalizer,
183
+ ) as GateDescriptor;
167
184
  expect(result.decision.surface).toBe("path");
168
185
  expect(result.decision.value).toBe(".env");
169
186
  });
170
187
 
171
188
  it("resolves the path surface with an access-path intent and agent name", () => {
172
189
  const resolver = makeResolver(makeCheckResult({ state: "allow" }));
173
- describePathGate(makeTcc({ agentName: "my-agent" }), resolver);
190
+ describePathGate(makeTcc({ agentName: "my-agent" }), resolver, normalizer);
174
191
  expect(resolver.resolve).toHaveBeenCalledWith({
175
192
  kind: "access-path",
176
193
  surface: "path",
177
- path: AccessPath.forPath(".env", { cwd: "/test/project" }),
194
+ path: AccessPath.forPath(".env", {
195
+ cwd: "/test/project",
196
+ platform: "linux",
197
+ }),
178
198
  agentName: "my-agent",
179
199
  });
180
200
  });
@@ -185,7 +205,7 @@ describe("describePathGate", () => {
185
205
  p === "/test/project/.env" ? "/vault/secret.env" : p,
186
206
  );
187
207
  const resolver = makeResolver(makeCheckResult({ state: "allow" }));
188
- describePathGate(makeTcc(), resolver);
208
+ describePathGate(makeTcc(), resolver, normalizer);
189
209
 
190
210
  const intent = resolver.resolve.mock.lastCall?.[0];
191
211
  expect(intent?.kind).toBe("access-path");
@@ -209,6 +229,7 @@ describe("describePathGate — home-relative paths", () => {
209
229
  const result = describePathGate(
210
230
  makeTcc({ input: { path: "~/.ssh/config" } }),
211
231
  resolver,
232
+ normalizer,
212
233
  ) as GateDescriptor;
213
234
 
214
235
  expect(isGateDescriptor(result)).toBe(true);
@@ -222,7 +243,10 @@ describe("describePathGate — home-relative paths", () => {
222
243
  expect(resolver.resolve).toHaveBeenCalledWith({
223
244
  kind: "access-path",
224
245
  surface: "path",
225
- path: AccessPath.forPath("~/.ssh/config", { cwd: "/test/project" }),
246
+ path: AccessPath.forPath("~/.ssh/config", {
247
+ cwd: "/test/project",
248
+ platform: "linux",
249
+ }),
226
250
  agentName: undefined,
227
251
  });
228
252
  });
@@ -234,6 +258,7 @@ describe("describePathGate — home-relative paths", () => {
234
258
  const result = describePathGate(
235
259
  makeTcc({ input: { path: "$HOME/.ssh/config" } }),
236
260
  resolver,
261
+ normalizer,
237
262
  ) as GateDescriptor;
238
263
 
239
264
  expect(isGateDescriptor(result)).toBe(true);
@@ -249,6 +274,7 @@ describe("describePathGate — home-relative paths", () => {
249
274
  const result = describePathGate(
250
275
  makeTcc({ input: { path: "~/.ssh/config" } }),
251
276
  resolver,
277
+ normalizer,
252
278
  );
253
279
  expect(result).toBeNull();
254
280
  });
@@ -274,12 +300,16 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
274
300
  const result = describePathGate(
275
301
  makeTcc({ toolName: "my-ext", input: { path: ".env" } }),
276
302
  resolver,
303
+ normalizer,
277
304
  );
278
305
  expect(isGateDescriptor(result)).toBe(true);
279
306
  expect(resolver.resolve).toHaveBeenCalledWith({
280
307
  kind: "access-path",
281
308
  surface: "path",
282
- path: AccessPath.forPath(".env", { cwd: "/test/project" }),
309
+ path: AccessPath.forPath(".env", {
310
+ cwd: "/test/project",
311
+ platform: "linux",
312
+ }),
283
313
  agentName: undefined,
284
314
  });
285
315
  });
@@ -291,12 +321,16 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
291
321
  const result = describePathGate(
292
322
  makeTcc({ toolName: "mcp", input: { arguments: { path: ".env" } } }),
293
323
  resolver,
324
+ normalizer,
294
325
  );
295
326
  expect(isGateDescriptor(result)).toBe(true);
296
327
  expect(resolver.resolve).toHaveBeenCalledWith({
297
328
  kind: "access-path",
298
329
  surface: "path",
299
- path: AccessPath.forPath(".env", { cwd: "/test/project" }),
330
+ path: AccessPath.forPath(".env", {
331
+ cwd: "/test/project",
332
+ platform: "linux",
333
+ }),
300
334
  agentName: undefined,
301
335
  });
302
336
  });
@@ -308,12 +342,16 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
308
342
  describePathGate(
309
343
  makeTcc({ toolName: "ffgrep", input: { target: "/etc/passwd" } }),
310
344
  resolver,
345
+ normalizer,
311
346
  extractorLookup("ffgrep", "target"),
312
347
  );
313
348
  expect(resolver.resolve).toHaveBeenCalledWith({
314
349
  kind: "access-path",
315
350
  surface: "path",
316
- path: AccessPath.forPath("/etc/passwd", { cwd: "/test/project" }),
351
+ path: AccessPath.forPath("/etc/passwd", {
352
+ cwd: "/test/project",
353
+ platform: "linux",
354
+ }),
317
355
  agentName: undefined,
318
356
  });
319
357
  });
@@ -323,6 +361,7 @@ describe("describePathGate — extension and MCP tools (#352)", () => {
323
361
  const result = describePathGate(
324
362
  makeTcc({ toolName: "my-ext", input: { other: true } }),
325
363
  resolver,
364
+ normalizer,
326
365
  );
327
366
  expect(result).toBeNull();
328
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