@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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +5 -5
- 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/external-directory.ts +14 -15
- package/src/handlers/gates/path.ts +3 -2
- 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 +73 -24
- package/test/access-intent/bash/program.test.ts +131 -65
- 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 -1
- package/test/handlers/gates/bash-path.test.ts +6 -1
- package/test/handlers/gates/external-directory-policy.test.ts +26 -8
- package/test/handlers/gates/external-directory.test.ts +9 -1
- package/test/handlers/gates/path.test.ts +53 -14
- 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 +10 -0
- 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 +12 -3
- 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 -500
|
@@ -30,7 +30,11 @@ function makeForwarder() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function makeManager() {
|
|
33
|
-
return new ForwardingManager(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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", {
|
|
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", {
|
|
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", {
|
|
77
|
-
|
|
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", {
|
|
98
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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(
|
|
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
|
|