@gotgenes/pi-permission-system 9.2.0 → 10.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 +52 -0
- package/README.md +12 -11
- package/package.json +1 -1
- package/src/agent-prep-session.ts +28 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +11 -0
- package/src/forwarded-permissions/io.ts +29 -0
- package/src/forwarded-permissions/permission-forwarder.ts +549 -0
- package/src/forwarding-manager.ts +3 -7
- package/src/gate-handler-session.ts +13 -0
- package/src/gate-prompter.ts +14 -0
- package/src/handlers/before-agent-start.ts +2 -3
- package/src/handlers/gates/bash-command.ts +4 -18
- package/src/handlers/gates/bash-external-directory.ts +3 -15
- package/src/handlers/gates/bash-path.ts +3 -16
- package/src/handlers/gates/descriptor.ts +0 -28
- package/src/handlers/gates/path.ts +3 -15
- package/src/handlers/gates/runner.ts +142 -105
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +44 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
- package/src/handlers/lifecycle.ts +9 -9
- package/src/handlers/permission-gate-handler.ts +34 -238
- package/src/index.ts +50 -68
- package/src/mcp-targets.ts +56 -46
- package/src/permission-event-rpc.ts +7 -0
- package/src/permission-events.ts +89 -8
- package/src/permission-forwarding.ts +23 -0
- package/src/permission-prompter.ts +27 -56
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +77 -9
- package/src/permission-ui-prompt.ts +127 -0
- package/src/permissions-service.ts +53 -0
- package/src/service-lifecycle.ts +49 -0
- package/src/service.ts +17 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-lifecycle-session.ts +24 -0
- package/src/tool-input-preview.ts +0 -62
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +6 -4
- package/test/composition-root.test.ts +5 -0
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +62 -0
- package/test/forwarding-manager.test.ts +26 -44
- package/test/handlers/before-agent-start.test.ts +45 -21
- package/test/handlers/external-directory-integration.test.ts +86 -22
- package/test/handlers/external-directory-session-dedup.test.ts +102 -55
- package/test/handlers/gates/bash-command.test.ts +49 -90
- package/test/handlers/gates/bash-external-directory.test.ts +54 -95
- package/test/handlers/gates/bash-path.test.ts +63 -148
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +150 -93
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +128 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
- package/test/handlers/input.test.ts +1 -2
- package/test/handlers/lifecycle.test.ts +49 -33
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/helpers/gate-fixtures.ts +147 -16
- package/test/helpers/handler-fixtures.ts +143 -27
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-event-rpc.test.ts +39 -0
- package/test/permission-events.test.ts +78 -10
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-prompter.test.ts +147 -38
- package/test/permission-session.test.ts +160 -27
- package/test/permission-ui-prompt.test.ts +146 -0
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +0 -4
- package/test/service-lifecycle.test.ts +162 -0
- package/test/tool-input-preview.test.ts +0 -111
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/src/forwarded-permissions/polling.ts +0 -379
|
@@ -19,11 +19,11 @@ 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 type {
|
|
23
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
22
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
24
23
|
|
|
25
24
|
import {
|
|
26
25
|
makeGateCheckResult as makeCheckResult,
|
|
26
|
+
makeResolver,
|
|
27
27
|
makeTcc,
|
|
28
28
|
} from "#test/helpers/gate-fixtures";
|
|
29
29
|
|
|
@@ -31,13 +31,6 @@ afterEach(() => {
|
|
|
31
31
|
vi.restoreAllMocks();
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
type CheckPermissionFn = (
|
|
35
|
-
surface: string,
|
|
36
|
-
input: unknown,
|
|
37
|
-
agentName?: string,
|
|
38
|
-
sessionRules?: Rule[],
|
|
39
|
-
) => PermissionCheckResult;
|
|
40
|
-
|
|
41
34
|
/**
|
|
42
35
|
* Mirror the handler's parse-once derivation: parse the bash command into a
|
|
43
36
|
* shared `BashProgram` and inject it, exactly as `permission-gate-handler.ts`
|
|
@@ -45,72 +38,47 @@ type CheckPermissionFn = (
|
|
|
45
38
|
*/
|
|
46
39
|
async function describeGate(
|
|
47
40
|
tcc: ToolCallContext,
|
|
48
|
-
|
|
49
|
-
getSessionRuleset: () => Rule[],
|
|
41
|
+
resolver: PermissionResolver,
|
|
50
42
|
): Promise<GateResult> {
|
|
51
43
|
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
52
44
|
const bashProgram =
|
|
53
45
|
tcc.toolName === "bash" && command
|
|
54
46
|
? await BashProgram.parse(command)
|
|
55
47
|
: null;
|
|
56
|
-
return describeBashPathGate(
|
|
57
|
-
tcc,
|
|
58
|
-
bashProgram,
|
|
59
|
-
checkPermission,
|
|
60
|
-
getSessionRuleset,
|
|
61
|
-
);
|
|
48
|
+
return describeBashPathGate(tcc, bashProgram, resolver);
|
|
62
49
|
}
|
|
63
50
|
|
|
64
51
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
65
52
|
|
|
66
53
|
describe("describeBashPathGate", () => {
|
|
67
54
|
it("returns null for non-bash tools", async () => {
|
|
68
|
-
const checkPermission = vi.fn<CheckPermissionFn>();
|
|
69
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
70
55
|
const result = await describeGate(
|
|
71
56
|
makeTcc({ toolName: "read", input: { path: ".env" } }),
|
|
72
|
-
|
|
73
|
-
getSessionRuleset,
|
|
57
|
+
makeResolver(),
|
|
74
58
|
);
|
|
75
59
|
expect(result).toBeNull();
|
|
76
60
|
});
|
|
77
61
|
|
|
78
62
|
it("returns null when no tokens are extracted", async () => {
|
|
79
|
-
const checkPermission = vi.fn<CheckPermissionFn>();
|
|
80
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
81
63
|
const result = await describeGate(
|
|
82
64
|
makeTcc({ input: { command: "echo hello" } }),
|
|
83
|
-
|
|
84
|
-
getSessionRuleset,
|
|
65
|
+
makeResolver(),
|
|
85
66
|
);
|
|
86
67
|
expect(result).toBeNull();
|
|
87
68
|
});
|
|
88
69
|
|
|
89
70
|
it("returns null when all tokens evaluate to allow", async () => {
|
|
90
|
-
const checkPermission = vi
|
|
91
|
-
.fn<CheckPermissionFn>()
|
|
92
|
-
.mockReturnValue(makeCheckResult({ state: "allow" }));
|
|
93
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
94
71
|
const result = await describeGate(
|
|
95
72
|
makeTcc({ input: { command: "cat .env" } }),
|
|
96
|
-
|
|
97
|
-
getSessionRuleset,
|
|
73
|
+
makeResolver(makeCheckResult({ state: "allow" })),
|
|
98
74
|
);
|
|
99
75
|
expect(result).toBeNull();
|
|
100
76
|
});
|
|
101
77
|
|
|
102
78
|
it("returns GateDescriptor when a token evaluates to deny", async () => {
|
|
103
|
-
const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
|
|
104
|
-
makeCheckResult({
|
|
105
|
-
state: "deny",
|
|
106
|
-
matchedPattern: "*.env",
|
|
107
|
-
}),
|
|
108
|
-
);
|
|
109
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
110
79
|
const result = await describeGate(
|
|
111
80
|
makeTcc({ input: { command: "cat .env" } }),
|
|
112
|
-
|
|
113
|
-
getSessionRuleset,
|
|
81
|
+
makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
|
|
114
82
|
);
|
|
115
83
|
expect(result).not.toBeNull();
|
|
116
84
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -120,14 +88,9 @@ describe("describeBashPathGate", () => {
|
|
|
120
88
|
});
|
|
121
89
|
|
|
122
90
|
it("returns GateDescriptor when a token evaluates to ask", async () => {
|
|
123
|
-
const checkPermission = vi
|
|
124
|
-
.fn<CheckPermissionFn>()
|
|
125
|
-
.mockReturnValue(makeCheckResult({ state: "ask", matchedPattern: "*" }));
|
|
126
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
127
91
|
const result = await describeGate(
|
|
128
92
|
makeTcc({ input: { command: "cat .env" } }),
|
|
129
|
-
|
|
130
|
-
getSessionRuleset,
|
|
93
|
+
makeResolver(makeCheckResult({ state: "ask", matchedPattern: "*" })),
|
|
131
94
|
);
|
|
132
95
|
expect(result).not.toBeNull();
|
|
133
96
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -136,16 +99,9 @@ describe("describeBashPathGate", () => {
|
|
|
136
99
|
});
|
|
137
100
|
|
|
138
101
|
it("descriptor includes triggering token in prompt message", async () => {
|
|
139
|
-
const checkPermission = vi
|
|
140
|
-
.fn<CheckPermissionFn>()
|
|
141
|
-
.mockReturnValue(
|
|
142
|
-
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
143
|
-
);
|
|
144
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
145
102
|
const result = (await describeGate(
|
|
146
103
|
makeTcc({ input: { command: "cat .env" } }),
|
|
147
|
-
|
|
148
|
-
getSessionRuleset,
|
|
104
|
+
makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
|
|
149
105
|
)) as GateDescriptor;
|
|
150
106
|
expect(result.denialContext).toMatchObject({
|
|
151
107
|
kind: "bash_path",
|
|
@@ -156,37 +112,17 @@ describe("describeBashPathGate", () => {
|
|
|
156
112
|
});
|
|
157
113
|
|
|
158
114
|
it("descriptor decision uses surface 'path'", async () => {
|
|
159
|
-
const checkPermission = vi
|
|
160
|
-
.fn<CheckPermissionFn>()
|
|
161
|
-
.mockReturnValue(
|
|
162
|
-
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
163
|
-
);
|
|
164
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
165
115
|
const result = (await describeGate(
|
|
166
116
|
makeTcc({ input: { command: "cat .env" } }),
|
|
167
|
-
|
|
168
|
-
getSessionRuleset,
|
|
117
|
+
makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
|
|
169
118
|
)) as GateDescriptor;
|
|
170
119
|
expect(result.decision.surface).toBe("path");
|
|
171
120
|
});
|
|
172
121
|
|
|
173
122
|
it("returns GateBypass when session rule covers the path", async () => {
|
|
174
|
-
const checkPermission = vi
|
|
175
|
-
.fn<CheckPermissionFn>()
|
|
176
|
-
.mockReturnValue(makeCheckResult({ state: "allow", source: "session" }));
|
|
177
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([
|
|
178
|
-
{
|
|
179
|
-
surface: "path",
|
|
180
|
-
pattern: "*",
|
|
181
|
-
action: "allow",
|
|
182
|
-
layer: "session",
|
|
183
|
-
origin: "session",
|
|
184
|
-
},
|
|
185
|
-
]);
|
|
186
123
|
const result = await describeGate(
|
|
187
124
|
makeTcc({ input: { command: "cat .env" } }),
|
|
188
|
-
|
|
189
|
-
getSessionRuleset,
|
|
125
|
+
makeResolver(makeCheckResult({ state: "allow", source: "session" })),
|
|
190
126
|
);
|
|
191
127
|
expect(result).not.toBeNull();
|
|
192
128
|
expect(isGateBypass(result)).toBe(true);
|
|
@@ -194,31 +130,22 @@ describe("describeBashPathGate", () => {
|
|
|
194
130
|
});
|
|
195
131
|
|
|
196
132
|
it("returns null when command is missing", async () => {
|
|
197
|
-
const
|
|
198
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
199
|
-
const result = await describeGate(
|
|
200
|
-
makeTcc({ input: {} }),
|
|
201
|
-
checkPermission,
|
|
202
|
-
getSessionRuleset,
|
|
203
|
-
);
|
|
133
|
+
const result = await describeGate(makeTcc({ input: {} }), makeResolver());
|
|
204
134
|
expect(result).toBeNull();
|
|
205
135
|
});
|
|
206
136
|
|
|
207
137
|
it("evaluates most restrictive across multiple tokens", async () => {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
138
|
+
const resolver = makeResolver();
|
|
139
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
140
|
+
const record = input as Record<string, unknown>;
|
|
141
|
+
if (record.path === "src/foo.ts") {
|
|
142
|
+
return makeCheckResult({ state: "allow" });
|
|
143
|
+
}
|
|
144
|
+
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
145
|
+
});
|
|
218
146
|
const result = await describeGate(
|
|
219
147
|
makeTcc({ input: { command: "cat src/foo.ts .env" } }),
|
|
220
|
-
|
|
221
|
-
getSessionRuleset,
|
|
148
|
+
resolver,
|
|
222
149
|
);
|
|
223
150
|
expect(result).not.toBeNull();
|
|
224
151
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -226,20 +153,17 @@ describe("describeBashPathGate", () => {
|
|
|
226
153
|
});
|
|
227
154
|
|
|
228
155
|
it("deny wins in multi-token: cp .env README.md", async () => {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
});
|
|
238
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
156
|
+
const resolver = makeResolver();
|
|
157
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
158
|
+
const record = input as Record<string, unknown>;
|
|
159
|
+
if (record.path === ".env") {
|
|
160
|
+
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
161
|
+
}
|
|
162
|
+
return makeCheckResult({ state: "allow" });
|
|
163
|
+
});
|
|
239
164
|
const result = await describeGate(
|
|
240
165
|
makeTcc({ input: { command: "cp .env README.md" } }),
|
|
241
|
-
|
|
242
|
-
getSessionRuleset,
|
|
166
|
+
resolver,
|
|
243
167
|
);
|
|
244
168
|
expect(result).not.toBeNull();
|
|
245
169
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -249,20 +173,17 @@ describe("describeBashPathGate", () => {
|
|
|
249
173
|
});
|
|
250
174
|
|
|
251
175
|
it("extracts redirect target: echo test > .env triggers deny", async () => {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
176
|
+
const resolver = makeResolver();
|
|
177
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
178
|
+
const record = input as Record<string, unknown>;
|
|
179
|
+
if (record.path === ".env") {
|
|
180
|
+
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
181
|
+
}
|
|
182
|
+
return makeCheckResult({ state: "allow" });
|
|
183
|
+
});
|
|
262
184
|
const result = await describeGate(
|
|
263
185
|
makeTcc({ input: { command: "echo test > .env" } }),
|
|
264
|
-
|
|
265
|
-
getSessionRuleset,
|
|
186
|
+
resolver,
|
|
266
187
|
);
|
|
267
188
|
expect(result).not.toBeNull();
|
|
268
189
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -270,47 +191,41 @@ describe("describeBashPathGate", () => {
|
|
|
270
191
|
});
|
|
271
192
|
|
|
272
193
|
it("returns null when all tokens match only the universal default", async () => {
|
|
273
|
-
const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
|
|
274
|
-
makeCheckResult({
|
|
275
|
-
state: "ask",
|
|
276
|
-
matchedPattern: undefined,
|
|
277
|
-
source: "special",
|
|
278
|
-
origin: "builtin",
|
|
279
|
-
}),
|
|
280
|
-
);
|
|
281
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
282
194
|
const result = await describeGate(
|
|
283
195
|
makeTcc({ input: { command: "cat .env" } }),
|
|
284
|
-
|
|
285
|
-
|
|
196
|
+
makeResolver(
|
|
197
|
+
makeCheckResult({
|
|
198
|
+
state: "ask",
|
|
199
|
+
matchedPattern: undefined,
|
|
200
|
+
source: "special",
|
|
201
|
+
origin: "builtin",
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
286
204
|
);
|
|
287
205
|
expect(result).toBeNull();
|
|
288
206
|
});
|
|
289
207
|
|
|
290
208
|
it("ignores tokens matching universal default but fires for explicit rule matches", async () => {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (record.path === ".env") {
|
|
296
|
-
return makeCheckResult({
|
|
297
|
-
state: "deny",
|
|
298
|
-
matchedPattern: "*.env",
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
// Other tokens match only the universal default
|
|
209
|
+
const resolver = makeResolver();
|
|
210
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
211
|
+
const record = input as Record<string, unknown>;
|
|
212
|
+
if (record.path === ".env") {
|
|
302
213
|
return makeCheckResult({
|
|
303
|
-
state: "
|
|
304
|
-
matchedPattern:
|
|
305
|
-
source: "special",
|
|
306
|
-
origin: "builtin",
|
|
214
|
+
state: "deny",
|
|
215
|
+
matchedPattern: "*.env",
|
|
307
216
|
});
|
|
217
|
+
}
|
|
218
|
+
// Other tokens match only the universal default
|
|
219
|
+
return makeCheckResult({
|
|
220
|
+
state: "ask",
|
|
221
|
+
matchedPattern: undefined,
|
|
222
|
+
source: "special",
|
|
223
|
+
origin: "builtin",
|
|
308
224
|
});
|
|
309
|
-
|
|
225
|
+
});
|
|
310
226
|
const result = await describeGate(
|
|
311
227
|
makeTcc({ input: { command: "cat src/foo.ts .env" } }),
|
|
312
|
-
|
|
313
|
-
getSessionRuleset,
|
|
228
|
+
resolver,
|
|
314
229
|
);
|
|
315
230
|
expect(result).not.toBeNull();
|
|
316
231
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import type { GateDescriptor } from "#src/handlers/gates/descriptor";
|
|
4
4
|
import { isGateDescriptor } from "#src/handlers/gates/descriptor";
|
|
5
5
|
import { describePathGate } from "#src/handlers/gates/path";
|
|
6
6
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
7
|
-
import type { Rule } from "#src/rule";
|
|
8
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
9
7
|
|
|
10
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
makeGateCheckResult as makeCheckResult,
|
|
10
|
+
makeResolver,
|
|
11
|
+
} from "#test/helpers/gate-fixtures";
|
|
11
12
|
|
|
12
13
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
13
14
|
|
|
@@ -23,53 +24,36 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
type CheckPermissionFn = (
|
|
27
|
-
surface: string,
|
|
28
|
-
input: unknown,
|
|
29
|
-
agentName?: string,
|
|
30
|
-
sessionRules?: Rule[],
|
|
31
|
-
) => PermissionCheckResult;
|
|
32
|
-
|
|
33
27
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
34
28
|
|
|
35
29
|
describe("describePathGate", () => {
|
|
36
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
37
|
-
|
|
38
30
|
it("returns null for non-path-bearing tools", () => {
|
|
39
|
-
const
|
|
31
|
+
const resolver = makeResolver();
|
|
40
32
|
const result = describePathGate(
|
|
41
33
|
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
42
|
-
|
|
43
|
-
getSessionRuleset,
|
|
34
|
+
resolver,
|
|
44
35
|
);
|
|
45
36
|
expect(result).toBeNull();
|
|
46
|
-
expect(
|
|
37
|
+
expect(resolver.resolve).not.toHaveBeenCalled();
|
|
47
38
|
});
|
|
48
39
|
|
|
49
40
|
it("returns null when tool has no extractable path", () => {
|
|
50
|
-
const
|
|
41
|
+
const resolver = makeResolver();
|
|
51
42
|
const result = describePathGate(
|
|
52
43
|
makeTcc({ toolName: "read", input: {} }),
|
|
53
|
-
|
|
54
|
-
getSessionRuleset,
|
|
44
|
+
resolver,
|
|
55
45
|
);
|
|
56
46
|
expect(result).toBeNull();
|
|
57
47
|
});
|
|
58
48
|
|
|
59
49
|
it("returns null when path check result is allow", () => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
.mockReturnValue(makeCheckResult({ state: "allow" }));
|
|
63
|
-
const result = describePathGate(
|
|
64
|
-
makeTcc(),
|
|
65
|
-
checkPermission,
|
|
66
|
-
getSessionRuleset,
|
|
67
|
-
);
|
|
50
|
+
const resolver = makeResolver(makeCheckResult({ state: "allow" }));
|
|
51
|
+
const result = describePathGate(makeTcc(), resolver);
|
|
68
52
|
expect(result).toBeNull();
|
|
69
53
|
});
|
|
70
54
|
|
|
71
55
|
it("returns null when matchedPattern is undefined (universal default)", () => {
|
|
72
|
-
const
|
|
56
|
+
const resolver = makeResolver(
|
|
73
57
|
makeCheckResult({
|
|
74
58
|
state: "ask",
|
|
75
59
|
matchedPattern: undefined,
|
|
@@ -77,16 +61,12 @@ describe("describePathGate", () => {
|
|
|
77
61
|
origin: "builtin",
|
|
78
62
|
}),
|
|
79
63
|
);
|
|
80
|
-
const result = describePathGate(
|
|
81
|
-
makeTcc(),
|
|
82
|
-
checkPermission,
|
|
83
|
-
getSessionRuleset,
|
|
84
|
-
);
|
|
64
|
+
const result = describePathGate(makeTcc(), resolver);
|
|
85
65
|
expect(result).toBeNull();
|
|
86
66
|
});
|
|
87
67
|
|
|
88
68
|
it("returns GateDescriptor when matchedPattern is defined (explicit path rule)", () => {
|
|
89
|
-
const
|
|
69
|
+
const resolver = makeResolver(
|
|
90
70
|
makeCheckResult({
|
|
91
71
|
state: "ask",
|
|
92
72
|
matchedPattern: "*.env",
|
|
@@ -94,27 +74,16 @@ describe("describePathGate", () => {
|
|
|
94
74
|
origin: "global",
|
|
95
75
|
}),
|
|
96
76
|
);
|
|
97
|
-
const result = describePathGate(
|
|
98
|
-
makeTcc(),
|
|
99
|
-
checkPermission,
|
|
100
|
-
getSessionRuleset,
|
|
101
|
-
);
|
|
77
|
+
const result = describePathGate(makeTcc(), resolver);
|
|
102
78
|
expect(result).not.toBeNull();
|
|
103
79
|
expect(isGateDescriptor(result)).toBe(true);
|
|
104
80
|
});
|
|
105
81
|
|
|
106
82
|
it("returns GateDescriptor when path check result is deny", () => {
|
|
107
|
-
const
|
|
108
|
-
makeCheckResult({
|
|
109
|
-
state: "deny",
|
|
110
|
-
matchedPattern: "*.env",
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
const result = describePathGate(
|
|
114
|
-
makeTcc(),
|
|
115
|
-
checkPermission,
|
|
116
|
-
getSessionRuleset,
|
|
83
|
+
const resolver = makeResolver(
|
|
84
|
+
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
117
85
|
);
|
|
86
|
+
const result = describePathGate(makeTcc(), resolver);
|
|
118
87
|
expect(result).not.toBeNull();
|
|
119
88
|
expect(isGateDescriptor(result)).toBe(true);
|
|
120
89
|
const desc = result as GateDescriptor;
|
|
@@ -123,17 +92,10 @@ describe("describePathGate", () => {
|
|
|
123
92
|
});
|
|
124
93
|
|
|
125
94
|
it("returns GateDescriptor when path check result is ask", () => {
|
|
126
|
-
const
|
|
127
|
-
makeCheckResult({
|
|
128
|
-
state: "ask",
|
|
129
|
-
matchedPattern: "*.env",
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
const result = describePathGate(
|
|
133
|
-
makeTcc(),
|
|
134
|
-
checkPermission,
|
|
135
|
-
getSessionRuleset,
|
|
95
|
+
const resolver = makeResolver(
|
|
96
|
+
makeCheckResult({ state: "ask", matchedPattern: "*.env" }),
|
|
136
97
|
);
|
|
98
|
+
const result = describePathGate(makeTcc(), resolver);
|
|
137
99
|
expect(result).not.toBeNull();
|
|
138
100
|
expect(isGateDescriptor(result)).toBe(true);
|
|
139
101
|
const desc = result as GateDescriptor;
|
|
@@ -142,13 +104,12 @@ describe("describePathGate", () => {
|
|
|
142
104
|
});
|
|
143
105
|
|
|
144
106
|
it("descriptor has correct session approval surface and pattern", () => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
107
|
+
const resolver = makeResolver(
|
|
108
|
+
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
109
|
+
);
|
|
148
110
|
const result = describePathGate(
|
|
149
111
|
makeTcc({ input: { path: "/test/project/src/.env" } }),
|
|
150
|
-
|
|
151
|
-
getSessionRuleset,
|
|
112
|
+
resolver,
|
|
152
113
|
) as GateDescriptor;
|
|
153
114
|
expect(result.sessionApproval).toBeDefined();
|
|
154
115
|
expect(result.sessionApproval?.surface).toBe("path");
|
|
@@ -156,16 +117,10 @@ describe("describePathGate", () => {
|
|
|
156
117
|
});
|
|
157
118
|
|
|
158
119
|
it("descriptor denialContext references the file path and tool name", () => {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
164
|
-
const result = describePathGate(
|
|
165
|
-
makeTcc(),
|
|
166
|
-
checkPermission,
|
|
167
|
-
getSessionRuleset,
|
|
168
|
-
) as GateDescriptor;
|
|
120
|
+
const resolver = makeResolver(
|
|
121
|
+
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
122
|
+
);
|
|
123
|
+
const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
|
|
169
124
|
expect(result.denialContext).toEqual({
|
|
170
125
|
kind: "path",
|
|
171
126
|
toolName: "read",
|
|
@@ -175,43 +130,21 @@ describe("describePathGate", () => {
|
|
|
175
130
|
});
|
|
176
131
|
|
|
177
132
|
it("descriptor decision uses surface 'path' and the file path as value", () => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
const result = describePathGate(
|
|
184
|
-
makeTcc(),
|
|
185
|
-
checkPermission,
|
|
186
|
-
getSessionRuleset,
|
|
187
|
-
) as GateDescriptor;
|
|
133
|
+
const resolver = makeResolver(
|
|
134
|
+
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
135
|
+
);
|
|
136
|
+
const result = describePathGate(makeTcc(), resolver) as GateDescriptor;
|
|
188
137
|
expect(result.decision.surface).toBe("path");
|
|
189
138
|
expect(result.decision.value).toBe(".env");
|
|
190
139
|
});
|
|
191
140
|
|
|
192
|
-
it("
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
pattern: "/project/*",
|
|
197
|
-
action: "allow",
|
|
198
|
-
origin: "session",
|
|
199
|
-
},
|
|
200
|
-
];
|
|
201
|
-
const getSession = vi.fn<() => Rule[]>().mockReturnValue(sessionRules);
|
|
202
|
-
const checkPermission = vi
|
|
203
|
-
.fn<CheckPermissionFn>()
|
|
204
|
-
.mockReturnValue(makeCheckResult({ state: "allow" }));
|
|
205
|
-
describePathGate(
|
|
206
|
-
makeTcc({ agentName: "my-agent" }),
|
|
207
|
-
checkPermission,
|
|
208
|
-
getSession,
|
|
209
|
-
);
|
|
210
|
-
expect(checkPermission).toHaveBeenCalledWith(
|
|
141
|
+
it("resolves the path surface with the file path and agent name", () => {
|
|
142
|
+
const resolver = makeResolver(makeCheckResult({ state: "allow" }));
|
|
143
|
+
describePathGate(makeTcc({ agentName: "my-agent" }), resolver);
|
|
144
|
+
expect(resolver.resolve).toHaveBeenCalledWith(
|
|
211
145
|
"path",
|
|
212
146
|
{ path: ".env" },
|
|
213
147
|
"my-agent",
|
|
214
|
-
sessionRules,
|
|
215
148
|
);
|
|
216
149
|
});
|
|
217
150
|
});
|