@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.
- package/CHANGELOG.md +26 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +8 -10
- package/src/access-intent/access-path.ts +39 -16
- package/src/access-intent/bash/bash-path-resolver.ts +531 -0
- package/src/access-intent/bash/program.ts +21 -14
- package/src/access-intent/bash/token-classification.ts +1 -1
- package/src/canonicalize-path.ts +11 -5
- package/src/forwarded-permissions/permission-forwarder.ts +11 -1
- package/src/forwarding-manager.ts +7 -1
- package/src/handlers/before-agent-start.ts +1 -0
- package/src/handlers/gates/bash-path-extractor.ts +7 -5
- package/src/handlers/gates/bash-path.ts +13 -13
- package/src/handlers/gates/external-directory.ts +14 -16
- package/src/handlers/gates/path.ts +12 -7
- package/src/handlers/gates/skill-read.ts +7 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +21 -4
- package/src/handlers/gates/tool.ts +4 -2
- package/src/index.ts +12 -1
- package/src/input-normalizer.ts +9 -4
- package/src/path-normalizer.ts +70 -0
- package/src/path-utils.ts +39 -28
- package/src/permission-manager.ts +21 -7
- package/src/permission-session.ts +35 -2
- package/src/prompting-gateway.ts +3 -0
- package/src/rule.ts +8 -6
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/src/subagent-context.ts +17 -9
- package/test/access-intent/access-path.test.ts +124 -15
- package/test/access-intent/bash/program.test.ts +178 -80
- package/test/bash-external-directory.test.ts +15 -1
- package/test/canonicalize-path.test.ts +34 -8
- package/test/forwarding-manager.test.ts +7 -1
- package/test/handlers/external-directory-symlink-acceptance.test.ts +26 -4
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
- package/test/handlers/gates/bash-external-directory.test.ts +5 -2
- package/test/handlers/gates/bash-path.test.ts +14 -9
- package/test/handlers/gates/external-directory-policy.test.ts +28 -18
- package/test/handlers/gates/external-directory.test.ts +9 -1
- package/test/handlers/gates/path.test.ts +90 -20
- package/test/handlers/gates/skill-read.test.ts +22 -13
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
- package/test/handlers/gates/tool.test.ts +13 -1
- package/test/helpers/gate-fixtures.ts +11 -2
- package/test/helpers/session-fixtures.ts +3 -0
- package/test/input-normalizer.test.ts +104 -39
- package/test/path-normalizer.test.ts +123 -0
- package/test/path-utils.test.ts +130 -51
- package/test/permission-forwarder.test.ts +1 -0
- package/test/permission-manager-unified.test.ts +40 -0
- package/test/permission-resolver.test.ts +14 -32
- package/test/permission-session.test.ts +41 -0
- package/test/pi-infrastructure-read.test.ts +27 -4
- package/test/prompting-gateway.test.ts +1 -0
- package/test/rule.test.ts +88 -42
- package/test/session-rules.test.ts +21 -4
- package/test/skill-prompt-sanitizer.test.ts +69 -14
- package/test/subagent-context.test.ts +77 -31
- package/test/synthesize.test.ts +13 -11
- 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(
|
|
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(
|
|
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
|
|
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: "
|
|
192
|
+
kind: "access-path",
|
|
160
193
|
surface: "path",
|
|
161
|
-
|
|
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: "
|
|
244
|
+
kind: "access-path",
|
|
193
245
|
surface: "path",
|
|
194
|
-
|
|
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: "
|
|
307
|
+
kind: "access-path",
|
|
250
308
|
surface: "path",
|
|
251
|
-
|
|
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: "
|
|
328
|
+
kind: "access-path",
|
|
267
329
|
surface: "path",
|
|
268
|
-
|
|
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: "
|
|
349
|
+
kind: "access-path",
|
|
284
350
|
surface: "path",
|
|
285
|
-
|
|
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(
|
|
45
|
-
|
|
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(
|
|
65
|
-
|
|
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(), () => [
|
|
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
|
-
|
|
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,
|