@gotgenes/pi-permission-system 8.1.0 → 8.2.1
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 +15 -0
- package/package.json +1 -1
- package/src/config-loader.ts +53 -46
- package/src/handlers/gates/bash-external-directory.ts +2 -4
- package/src/handlers/gates/bash-path-extractor.ts +135 -169
- package/src/handlers/gates/bash-path.ts +2 -4
- package/src/handlers/gates/bash-token-classification.ts +105 -0
- package/src/handlers/gates/descriptor.ts +6 -6
- package/src/handlers/gates/external-directory.ts +2 -4
- package/src/handlers/gates/helpers.ts +30 -1
- package/src/handlers/gates/path.ts +2 -4
- package/src/handlers/gates/runner.ts +29 -56
- package/src/handlers/gates/tool.ts +5 -4
- package/src/handlers/permission-gate-handler.ts +4 -3
- package/src/permission-manager.ts +6 -49
- package/src/permission-session.ts +3 -2
- package/src/scope-merge.ts +72 -0
- package/src/session-approval.ts +43 -0
- package/src/session-rules.ts +13 -0
- package/test/config-loader.test.ts +82 -0
- package/test/handlers/before-agent-start.test.ts +2 -20
- package/test/handlers/external-directory-integration.test.ts +44 -82
- package/test/handlers/external-directory-session-dedup.test.ts +17 -41
- package/test/handlers/gates/bash-external-directory.test.ts +11 -9
- package/test/handlers/gates/bash-path.test.ts +5 -26
- package/test/handlers/gates/bash-token-classification.test.ts +241 -0
- package/test/handlers/gates/external-directory.test.ts +2 -5
- package/test/handlers/gates/helpers.test.ts +81 -0
- package/test/handlers/gates/path.test.ts +5 -14
- package/test/handlers/gates/runner.test.ts +95 -113
- package/test/handlers/gates/tool.test.ts +2 -2
- package/test/handlers/input-events.test.ts +42 -95
- package/test/handlers/input.test.ts +3 -71
- package/test/handlers/lifecycle.test.ts +3 -20
- package/test/handlers/tool-call-events.test.ts +30 -127
- package/test/handlers/tool-call.test.ts +21 -110
- package/test/helpers/gate-fixtures.ts +105 -0
- package/test/helpers/handler-fixtures.ts +141 -0
- package/test/helpers/manager-harness.ts +51 -0
- package/test/permission-session.test.ts +7 -22
- package/test/permission-system.test.ts +4 -40
- package/test/scope-merge.test.ts +116 -0
- package/test/session-approval.test.ts +75 -0
- package/test/session-rules.test.ts +49 -0
|
@@ -15,39 +15,18 @@ import type {
|
|
|
15
15
|
GateDescriptor,
|
|
16
16
|
} from "#src/handlers/gates/descriptor";
|
|
17
17
|
import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
|
|
18
|
-
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
19
18
|
import type { Rule } from "#src/rule";
|
|
20
19
|
import type { PermissionCheckResult } from "#src/types";
|
|
21
20
|
|
|
21
|
+
import {
|
|
22
|
+
makeGateCheckResult as makeCheckResult,
|
|
23
|
+
makeTcc,
|
|
24
|
+
} from "#test/helpers/gate-fixtures";
|
|
25
|
+
|
|
22
26
|
afterEach(() => {
|
|
23
27
|
vi.restoreAllMocks();
|
|
24
28
|
});
|
|
25
29
|
|
|
26
|
-
// ── helpers ────────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
29
|
-
return {
|
|
30
|
-
toolName: "bash",
|
|
31
|
-
agentName: null,
|
|
32
|
-
input: { command: "cat .env" },
|
|
33
|
-
toolCallId: "tc-1",
|
|
34
|
-
cwd: "/test/project",
|
|
35
|
-
...overrides,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makeCheckResult(
|
|
40
|
-
overrides: Partial<PermissionCheckResult> = {},
|
|
41
|
-
): PermissionCheckResult {
|
|
42
|
-
return {
|
|
43
|
-
toolName: "path",
|
|
44
|
-
state: "allow",
|
|
45
|
-
source: "special",
|
|
46
|
-
origin: "global",
|
|
47
|
-
...overrides,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
30
|
type CheckPermissionFn = (
|
|
52
31
|
surface: string,
|
|
53
32
|
input: unknown,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyTokenAsPathCandidate,
|
|
5
|
+
classifyTokenAsRuleCandidate,
|
|
6
|
+
} from "#src/handlers/gates/bash-token-classification";
|
|
7
|
+
|
|
8
|
+
// ── Shared rejection behaviour ─────────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// Both classifiers delegate to the private `rejectNonPathToken` predicate for
|
|
11
|
+
// the seven shared rejection cases tested below. Testing via both exports
|
|
12
|
+
// pins that predicate through each caller.
|
|
13
|
+
|
|
14
|
+
describe("classifyTokenAsPathCandidate", () => {
|
|
15
|
+
describe("shared rejection: rejectNonPathToken", () => {
|
|
16
|
+
test("empty string → null", () => {
|
|
17
|
+
expect(classifyTokenAsPathCandidate("")).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("flag (leading dash) → null", () => {
|
|
21
|
+
expect(classifyTokenAsPathCandidate("-r")).toBeNull();
|
|
22
|
+
expect(classifyTokenAsPathCandidate("--recursive")).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("env assignment (= before any /) → null", () => {
|
|
26
|
+
expect(classifyTokenAsPathCandidate("FOO=/bar")).toBeNull();
|
|
27
|
+
expect(classifyTokenAsPathCandidate("HOME=/home/user")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("env-like token where = comes after / is NOT rejected as assignment", () => {
|
|
31
|
+
// /foo=bar: slashIndex (0) < eqIndex (4) → not an assignment → continues
|
|
32
|
+
// Starts with /, so path candidate accepts it.
|
|
33
|
+
expect(classifyTokenAsPathCandidate("/foo=bar")).toBe("/foo=bar");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("URL → null", () => {
|
|
37
|
+
expect(classifyTokenAsPathCandidate("https://example.com")).toBeNull();
|
|
38
|
+
expect(classifyTokenAsPathCandidate("http://localhost:3000")).toBeNull();
|
|
39
|
+
expect(classifyTokenAsPathCandidate("file:///tmp/foo")).toBeNull();
|
|
40
|
+
expect(
|
|
41
|
+
classifyTokenAsPathCandidate("git+ssh://github.com/a/b"),
|
|
42
|
+
).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("@scope/package → null", () => {
|
|
46
|
+
expect(classifyTokenAsPathCandidate("@foo/bar")).toBeNull();
|
|
47
|
+
expect(classifyTokenAsPathCandidate("@scope/pkg")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("@/ prefix is NOT rejected (it looks like an absolute-rooted scoped path)", () => {
|
|
51
|
+
// @/ passes the @ guard; then for path candidate it doesn't start with /
|
|
52
|
+
// or ~/, and doesn't contain .., so it returns null anyway from the
|
|
53
|
+
// acceptance gate — but the rejection is not due to the @ guard.
|
|
54
|
+
// This test documents that @/ is not rejected by the shared rejection.
|
|
55
|
+
// The path classifier then rejects it for not matching any acceptance shape.
|
|
56
|
+
expect(classifyTokenAsPathCandidate("@/foo/bar")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("bare-slash token → null", () => {
|
|
60
|
+
expect(classifyTokenAsPathCandidate("/")).toBeNull();
|
|
61
|
+
expect(classifyTokenAsPathCandidate("//")).toBeNull();
|
|
62
|
+
expect(classifyTokenAsPathCandidate("///")).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("regex metacharacters → null", () => {
|
|
66
|
+
// REGEX_METACHAR_PATTERN: .*, .+, \|, \(, \), [...], ^/
|
|
67
|
+
expect(classifyTokenAsPathCandidate("foo.*")).toBeNull();
|
|
68
|
+
expect(classifyTokenAsPathCandidate("bar.+")).toBeNull();
|
|
69
|
+
expect(classifyTokenAsPathCandidate("a\\|b")).toBeNull();
|
|
70
|
+
expect(classifyTokenAsPathCandidate("\\(group\\)")).toBeNull();
|
|
71
|
+
expect(classifyTokenAsPathCandidate("[abc]")).toBeNull();
|
|
72
|
+
expect(classifyTokenAsPathCandidate("^/start")).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("path-candidate acceptance gate", () => {
|
|
77
|
+
test("absolute path (starts with /) → returned as-is", () => {
|
|
78
|
+
expect(classifyTokenAsPathCandidate("/etc/hosts")).toBe("/etc/hosts");
|
|
79
|
+
expect(classifyTokenAsPathCandidate("/tmp")).toBe("/tmp");
|
|
80
|
+
expect(classifyTokenAsPathCandidate("/home/user/file.txt")).toBe(
|
|
81
|
+
"/home/user/file.txt",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("home-relative path (starts with ~/) → returned as-is", () => {
|
|
86
|
+
expect(classifyTokenAsPathCandidate("~/Documents")).toBe("~/Documents");
|
|
87
|
+
expect(classifyTokenAsPathCandidate("~/.ssh/config")).toBe(
|
|
88
|
+
"~/.ssh/config",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("parent-traversal (contains ..) → returned as-is", () => {
|
|
93
|
+
expect(classifyTokenAsPathCandidate("../../etc/passwd")).toBe(
|
|
94
|
+
"../../etc/passwd",
|
|
95
|
+
);
|
|
96
|
+
expect(classifyTokenAsPathCandidate("../foo")).toBe("../foo");
|
|
97
|
+
expect(classifyTokenAsPathCandidate("..")).toBe("..");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("plain word with no path shape → null", () => {
|
|
101
|
+
expect(classifyTokenAsPathCandidate("hello")).toBeNull();
|
|
102
|
+
expect(classifyTokenAsPathCandidate("myfile.txt")).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("dot-file (starts with .) → null (strict path gate)", () => {
|
|
106
|
+
// Path candidate does NOT accept dot-files; rule candidate does.
|
|
107
|
+
expect(classifyTokenAsPathCandidate(".env")).toBeNull();
|
|
108
|
+
expect(classifyTokenAsPathCandidate(".gitignore")).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("relative path with / but no leading / or ~/ → null (strict path gate)", () => {
|
|
112
|
+
// Path candidate does NOT accept bare relative paths; rule candidate does.
|
|
113
|
+
expect(classifyTokenAsPathCandidate("src/foo.ts")).toBeNull();
|
|
114
|
+
expect(classifyTokenAsPathCandidate("./build")).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("classifyTokenAsRuleCandidate", () => {
|
|
120
|
+
describe("shared rejection: rejectNonPathToken", () => {
|
|
121
|
+
test("empty string → null", () => {
|
|
122
|
+
expect(classifyTokenAsRuleCandidate("")).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("flag (leading dash) → null", () => {
|
|
126
|
+
expect(classifyTokenAsRuleCandidate("-r")).toBeNull();
|
|
127
|
+
expect(classifyTokenAsRuleCandidate("--recursive")).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("env assignment (= before any /) → null", () => {
|
|
131
|
+
expect(classifyTokenAsRuleCandidate("FOO=/bar")).toBeNull();
|
|
132
|
+
expect(classifyTokenAsRuleCandidate("HOME=/home/user")).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("env-like token where = comes after / is NOT rejected as assignment", () => {
|
|
136
|
+
// /foo=bar: slashIndex (0) < eqIndex (4) → not an assignment → continues.
|
|
137
|
+
// Contains /, so rule candidate accepts it.
|
|
138
|
+
expect(classifyTokenAsRuleCandidate("/foo=bar")).toBe("/foo=bar");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("URL → null", () => {
|
|
142
|
+
expect(classifyTokenAsRuleCandidate("https://example.com")).toBeNull();
|
|
143
|
+
expect(classifyTokenAsRuleCandidate("http://localhost:3000")).toBeNull();
|
|
144
|
+
expect(classifyTokenAsRuleCandidate("file:///tmp/foo")).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("@scope/package → null", () => {
|
|
148
|
+
expect(classifyTokenAsRuleCandidate("@foo/bar")).toBeNull();
|
|
149
|
+
expect(classifyTokenAsRuleCandidate("@scope/pkg")).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("bare-slash token → null", () => {
|
|
153
|
+
expect(classifyTokenAsRuleCandidate("/")).toBeNull();
|
|
154
|
+
expect(classifyTokenAsRuleCandidate("//")).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("regex metacharacters → null", () => {
|
|
158
|
+
expect(classifyTokenAsRuleCandidate("foo.*")).toBeNull();
|
|
159
|
+
expect(classifyTokenAsRuleCandidate("bar.+")).toBeNull();
|
|
160
|
+
expect(classifyTokenAsRuleCandidate("a\\|b")).toBeNull();
|
|
161
|
+
expect(classifyTokenAsRuleCandidate("[abc]")).toBeNull();
|
|
162
|
+
expect(classifyTokenAsRuleCandidate("^/start")).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("rule-candidate acceptance gate (broader than path)", () => {
|
|
167
|
+
test("absolute path (starts with /) → returned as-is", () => {
|
|
168
|
+
expect(classifyTokenAsRuleCandidate("/etc/hosts")).toBe("/etc/hosts");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("home-relative path (starts with ~/) → returned as-is", () => {
|
|
172
|
+
expect(classifyTokenAsRuleCandidate("~/Documents")).toBe("~/Documents");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("parent-traversal (contains ..) → returned as-is", () => {
|
|
176
|
+
expect(classifyTokenAsRuleCandidate("../foo")).toBe("../foo");
|
|
177
|
+
expect(classifyTokenAsRuleCandidate("..")).toBe("..");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("dot-file (starts with .) → returned as-is", () => {
|
|
181
|
+
// Rule candidate accepts dot-files; path candidate does not.
|
|
182
|
+
expect(classifyTokenAsRuleCandidate(".env")).toBe(".env");
|
|
183
|
+
expect(classifyTokenAsRuleCandidate(".gitignore")).toBe(".gitignore");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("current-dir relative (starts with ./) → returned as-is", () => {
|
|
187
|
+
expect(classifyTokenAsRuleCandidate("./src")).toBe("./src");
|
|
188
|
+
expect(classifyTokenAsRuleCandidate("./build/output.js")).toBe(
|
|
189
|
+
"./build/output.js",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("relative path containing / → returned as-is", () => {
|
|
194
|
+
// Rule candidate accepts any token with / (not already rejected).
|
|
195
|
+
expect(classifyTokenAsRuleCandidate("src/foo.ts")).toBe("src/foo.ts");
|
|
196
|
+
expect(classifyTokenAsRuleCandidate("packages/pi-foo/index.ts")).toBe(
|
|
197
|
+
"packages/pi-foo/index.ts",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("plain word with no path shape → null", () => {
|
|
202
|
+
expect(classifyTokenAsRuleCandidate("hello")).toBeNull();
|
|
203
|
+
expect(classifyTokenAsRuleCandidate("myfile.txt")).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("rule-vs-path divergence", () => {
|
|
208
|
+
const dotFiles = [".env", ".gitignore", ".eslintrc"];
|
|
209
|
+
const relPaths = ["src/index.ts", "lib/utils.js", "config/settings.json"];
|
|
210
|
+
|
|
211
|
+
for (const tok of dotFiles) {
|
|
212
|
+
test(`dot-file "${tok}": rule accepts, path rejects`, () => {
|
|
213
|
+
expect(classifyTokenAsRuleCandidate(tok)).toBe(tok);
|
|
214
|
+
expect(classifyTokenAsPathCandidate(tok)).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const tok of relPaths) {
|
|
219
|
+
test(`relative path "${tok}": rule accepts, path rejects`, () => {
|
|
220
|
+
expect(classifyTokenAsRuleCandidate(tok)).toBe(tok);
|
|
221
|
+
expect(classifyTokenAsPathCandidate(tok)).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const sharedAccepted = ["/etc/hosts", "~/docs", "../sibling"];
|
|
226
|
+
for (const tok of sharedAccepted) {
|
|
227
|
+
test(`"${tok}": both classifiers accept`, () => {
|
|
228
|
+
expect(classifyTokenAsRuleCandidate(tok)).toBe(tok);
|
|
229
|
+
expect(classifyTokenAsPathCandidate(tok)).toBe(tok);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const sharedRejected = ["hello", "--flag", "FOO=/bar", "https://x.com"];
|
|
234
|
+
for (const tok of sharedRejected) {
|
|
235
|
+
test(`"${tok}": both classifiers reject`, () => {
|
|
236
|
+
expect(classifyTokenAsRuleCandidate(tok)).toBeNull();
|
|
237
|
+
expect(classifyTokenAsPathCandidate(tok)).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -126,11 +126,8 @@ describe("describeExternalDirectoryGate", () => {
|
|
|
126
126
|
["/test/agent"],
|
|
127
127
|
) as GateDescriptor;
|
|
128
128
|
expect(result.sessionApproval).toBeDefined();
|
|
129
|
-
expect(result.sessionApproval).
|
|
130
|
-
|
|
131
|
-
"external_directory",
|
|
132
|
-
);
|
|
133
|
-
expect(result.sessionApproval).toHaveProperty("pattern");
|
|
129
|
+
expect(result.sessionApproval?.surface).toBe("external_directory");
|
|
130
|
+
expect(result.sessionApproval?.representativePattern).toBeDefined();
|
|
134
131
|
});
|
|
135
132
|
|
|
136
133
|
it("denialContext contains the external path and cwd", () => {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
buildDecisionEvent,
|
|
4
5
|
deriveDecisionValue,
|
|
5
6
|
deriveResolution,
|
|
6
7
|
} from "#src/handlers/gates/helpers";
|
|
8
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
7
9
|
|
|
8
10
|
describe("deriveDecisionValue", () => {
|
|
9
11
|
it("returns command for bash", () => {
|
|
@@ -82,3 +84,82 @@ describe("deriveResolution", () => {
|
|
|
82
84
|
);
|
|
83
85
|
});
|
|
84
86
|
});
|
|
87
|
+
|
|
88
|
+
describe("buildDecisionEvent", () => {
|
|
89
|
+
function makeCheck(
|
|
90
|
+
overrides: Partial<PermissionCheckResult> = {},
|
|
91
|
+
): PermissionCheckResult {
|
|
92
|
+
return {
|
|
93
|
+
state: "allow",
|
|
94
|
+
toolName: "read",
|
|
95
|
+
source: "tool",
|
|
96
|
+
origin: "builtin",
|
|
97
|
+
matchedPattern: "*",
|
|
98
|
+
...overrides,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
it("builds a decision event with all fields populated", () => {
|
|
103
|
+
const event = buildDecisionEvent(
|
|
104
|
+
{ surface: "read", value: "read" },
|
|
105
|
+
makeCheck({ origin: "global", matchedPattern: "read" }),
|
|
106
|
+
"test-agent",
|
|
107
|
+
"allow",
|
|
108
|
+
"policy_allow",
|
|
109
|
+
);
|
|
110
|
+
expect(event).toEqual({
|
|
111
|
+
surface: "read",
|
|
112
|
+
value: "read",
|
|
113
|
+
result: "allow",
|
|
114
|
+
resolution: "policy_allow",
|
|
115
|
+
origin: "global",
|
|
116
|
+
agentName: "test-agent",
|
|
117
|
+
matchedPattern: "read",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("normalises undefined origin to null", () => {
|
|
122
|
+
const event = buildDecisionEvent(
|
|
123
|
+
{ surface: "bash", value: "git status" },
|
|
124
|
+
makeCheck({ origin: undefined }),
|
|
125
|
+
null,
|
|
126
|
+
"allow",
|
|
127
|
+
"user_approved",
|
|
128
|
+
);
|
|
129
|
+
expect(event.origin).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("normalises null agentName to null", () => {
|
|
133
|
+
const event = buildDecisionEvent(
|
|
134
|
+
{ surface: "read", value: "read" },
|
|
135
|
+
makeCheck(),
|
|
136
|
+
null,
|
|
137
|
+
"deny",
|
|
138
|
+
"policy_deny",
|
|
139
|
+
);
|
|
140
|
+
expect(event.agentName).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("normalises undefined matchedPattern to null", () => {
|
|
144
|
+
const event = buildDecisionEvent(
|
|
145
|
+
{ surface: "read", value: "read" },
|
|
146
|
+
makeCheck({ matchedPattern: undefined }),
|
|
147
|
+
null,
|
|
148
|
+
"deny",
|
|
149
|
+
"policy_deny",
|
|
150
|
+
);
|
|
151
|
+
expect(event.matchedPattern).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("passes result and resolution through", () => {
|
|
155
|
+
const event = buildDecisionEvent(
|
|
156
|
+
{ surface: "bash", value: "rm -rf /" },
|
|
157
|
+
makeCheck(),
|
|
158
|
+
null,
|
|
159
|
+
"deny",
|
|
160
|
+
"user_denied",
|
|
161
|
+
);
|
|
162
|
+
expect(event.result).toBe("deny");
|
|
163
|
+
expect(event.resolution).toBe("user_denied");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -7,8 +7,11 @@ import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
|
7
7
|
import type { Rule } from "#src/rule";
|
|
8
8
|
import type { PermissionCheckResult } from "#src/types";
|
|
9
9
|
|
|
10
|
+
import { makeGateCheckResult as makeCheckResult } from "#test/helpers/gate-fixtures";
|
|
11
|
+
|
|
10
12
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
11
13
|
|
|
14
|
+
// path.test.ts uses read-tool defaults; the shared makeTcc uses bash defaults.
|
|
12
15
|
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
13
16
|
return {
|
|
14
17
|
toolName: "read",
|
|
@@ -20,18 +23,6 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
function makeCheckResult(
|
|
24
|
-
overrides: Partial<PermissionCheckResult> = {},
|
|
25
|
-
): PermissionCheckResult {
|
|
26
|
-
return {
|
|
27
|
-
toolName: "path",
|
|
28
|
-
state: "allow",
|
|
29
|
-
source: "special",
|
|
30
|
-
origin: "global",
|
|
31
|
-
...overrides,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
type CheckPermissionFn = (
|
|
36
27
|
surface: string,
|
|
37
28
|
input: unknown,
|
|
@@ -160,8 +151,8 @@ describe("describePathGate", () => {
|
|
|
160
151
|
getSessionRuleset,
|
|
161
152
|
) as GateDescriptor;
|
|
162
153
|
expect(result.sessionApproval).toBeDefined();
|
|
163
|
-
expect(result.sessionApproval).
|
|
164
|
-
expect(result.sessionApproval).
|
|
154
|
+
expect(result.sessionApproval?.surface).toBe("path");
|
|
155
|
+
expect(result.sessionApproval?.representativePattern).toBeDefined();
|
|
165
156
|
});
|
|
166
157
|
|
|
167
158
|
it("descriptor denialContext references the file path and tool name", () => {
|