@gotgenes/pi-permission-system 10.0.0 → 10.2.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 +33 -0
- package/README.md +1 -1
- 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/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 +53 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +83 -27
- package/src/permissions-service.ts +53 -0
- package/src/runtime.ts +1 -37
- package/src/service-lifecycle.ts +49 -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/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 +83 -114
- 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 +54 -157
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +151 -186
- 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/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +212 -17
- package/test/helpers/handler-fixtures.ts +226 -29
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-forwarding.test.ts +0 -282
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +211 -105
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +2 -86
- 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 -411
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { getNonEmptyString, toRecord } from "#src/common";
|
|
3
3
|
import { describeBashExternalDirectoryGate } from "#src/handlers/gates/bash-external-directory";
|
|
4
4
|
import { BashProgram } from "#src/handlers/gates/bash-program";
|
|
@@ -9,8 +9,11 @@ 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 type { PermissionResolver } from "#src/permission-resolver";
|
|
12
13
|
import type { PermissionCheckResult } from "#src/types";
|
|
13
14
|
|
|
15
|
+
import { makeResolver } from "#test/helpers/gate-fixtures";
|
|
16
|
+
|
|
14
17
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
15
18
|
|
|
16
19
|
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
@@ -44,20 +47,14 @@ function makeCheckResult(
|
|
|
44
47
|
*/
|
|
45
48
|
async function describeGate(
|
|
46
49
|
tcc: ToolCallContext,
|
|
47
|
-
|
|
48
|
-
getSessionRuleset: Parameters<typeof describeBashExternalDirectoryGate>[3],
|
|
50
|
+
resolver: PermissionResolver,
|
|
49
51
|
): Promise<GateResult> {
|
|
50
52
|
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
51
53
|
const bashProgram =
|
|
52
54
|
tcc.toolName === "bash" && command
|
|
53
55
|
? await BashProgram.parse(command)
|
|
54
56
|
: null;
|
|
55
|
-
return describeBashExternalDirectoryGate(
|
|
56
|
-
tcc,
|
|
57
|
-
bashProgram,
|
|
58
|
-
checkPermission,
|
|
59
|
-
getSessionRuleset,
|
|
60
|
-
);
|
|
57
|
+
return describeBashExternalDirectoryGate(tcc, bashProgram, resolver);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
@@ -66,8 +63,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
66
63
|
it("returns null when tool is not bash", async () => {
|
|
67
64
|
const result = await describeGate(
|
|
68
65
|
makeTcc({ toolName: "read" }),
|
|
69
|
-
|
|
70
|
-
vi.fn().mockReturnValue([]),
|
|
66
|
+
makeResolver(makeCheckResult("ask")),
|
|
71
67
|
);
|
|
72
68
|
expect(result).toBeNull();
|
|
73
69
|
});
|
|
@@ -75,8 +71,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
75
71
|
it("returns null when no CWD", async () => {
|
|
76
72
|
const result = await describeGate(
|
|
77
73
|
makeTcc({ cwd: undefined }),
|
|
78
|
-
|
|
79
|
-
vi.fn().mockReturnValue([]),
|
|
74
|
+
makeResolver(makeCheckResult("ask")),
|
|
80
75
|
);
|
|
81
76
|
expect(result).toBeNull();
|
|
82
77
|
});
|
|
@@ -84,21 +79,16 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
84
79
|
it("returns null when command has no external paths", async () => {
|
|
85
80
|
const result = await describeGate(
|
|
86
81
|
makeTcc({ input: { command: "ls -la" } }),
|
|
87
|
-
|
|
88
|
-
vi.fn().mockReturnValue([]),
|
|
82
|
+
makeResolver(makeCheckResult("ask")),
|
|
89
83
|
);
|
|
90
84
|
expect(result).toBeNull();
|
|
91
85
|
});
|
|
92
86
|
|
|
93
87
|
it("returns GateBypass when all external paths are session-covered", async () => {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
.mockReturnValue(makeCheckResult("allow", { source: "session" }));
|
|
97
|
-
const result = await describeGate(
|
|
98
|
-
makeTcc(),
|
|
99
|
-
checkPermission,
|
|
100
|
-
vi.fn().mockReturnValue([]),
|
|
88
|
+
const resolver = makeResolver(
|
|
89
|
+
makeCheckResult("allow", { source: "session" }),
|
|
101
90
|
);
|
|
91
|
+
const result = await describeGate(makeTcc(), resolver);
|
|
102
92
|
expect(result).not.toBeNull();
|
|
103
93
|
expect(isGateBypass(result)).toBe(true);
|
|
104
94
|
const bypass = result as GateBypass;
|
|
@@ -110,11 +100,9 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
110
100
|
});
|
|
111
101
|
|
|
112
102
|
it("returns GateDescriptor with multi-pattern sessionApproval for uncovered paths", async () => {
|
|
113
|
-
const checkPermission = vi.fn().mockReturnValue(makeCheckResult("ask"));
|
|
114
103
|
const result = await describeGate(
|
|
115
104
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
116
|
-
|
|
117
|
-
vi.fn().mockReturnValue([]),
|
|
105
|
+
makeResolver(makeCheckResult("ask")),
|
|
118
106
|
);
|
|
119
107
|
expect(isGateDescriptor(result)).toBe(true);
|
|
120
108
|
const desc = result as GateDescriptor;
|
|
@@ -127,20 +115,13 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
127
115
|
// Config-level allow (source: "special") should suppress the prompt,
|
|
128
116
|
// not just session-level allow. This was the bug: source !== "session"
|
|
129
117
|
// kept config-allowed paths in the uncovered set.
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
.
|
|
133
|
-
(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
const result = await describeGate(
|
|
140
|
-
makeTcc(),
|
|
141
|
-
checkPermission,
|
|
142
|
-
vi.fn().mockReturnValue([]),
|
|
143
|
-
);
|
|
118
|
+
const resolver = makeResolver();
|
|
119
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
120
|
+
if ((input as Record<string, unknown>).path)
|
|
121
|
+
return makeCheckResult("allow", { source: "special" });
|
|
122
|
+
return makeCheckResult("ask");
|
|
123
|
+
});
|
|
124
|
+
const result = await describeGate(makeTcc(), resolver);
|
|
144
125
|
expect(result).not.toBeNull();
|
|
145
126
|
expect(isGateBypass(result)).toBe(true);
|
|
146
127
|
});
|
|
@@ -149,20 +130,14 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
149
130
|
// The path-less extCheck used to always return the "*" catch-all (ask),
|
|
150
131
|
// silently downgrading a config-level deny to ask. After the fix, the
|
|
151
132
|
// descriptor's preCheck is derived from the actual path check result.
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
const result = await describeGate(
|
|
162
|
-
makeTcc(),
|
|
163
|
-
checkPermission,
|
|
164
|
-
vi.fn().mockReturnValue([]),
|
|
165
|
-
);
|
|
133
|
+
const resolver = makeResolver();
|
|
134
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
135
|
+
if ((input as Record<string, unknown>).path)
|
|
136
|
+
return makeCheckResult("deny", { source: "special" });
|
|
137
|
+
// Path-less catch-all returns ask — should NOT be used as preCheck.
|
|
138
|
+
return makeCheckResult("ask");
|
|
139
|
+
});
|
|
140
|
+
const result = await describeGate(makeTcc(), resolver);
|
|
166
141
|
expect(isGateDescriptor(result)).toBe(true);
|
|
167
142
|
const desc = result as GateDescriptor;
|
|
168
143
|
expect(desc.preCheck?.state).toBe("deny");
|
|
@@ -171,8 +146,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
171
146
|
it("descriptor surface is 'external_directory'", async () => {
|
|
172
147
|
const result = await describeGate(
|
|
173
148
|
makeTcc(),
|
|
174
|
-
|
|
175
|
-
vi.fn().mockReturnValue([]),
|
|
149
|
+
makeResolver(makeCheckResult("ask")),
|
|
176
150
|
);
|
|
177
151
|
const desc = result as GateDescriptor;
|
|
178
152
|
expect(desc.surface).toBe("external_directory");
|
|
@@ -181,8 +155,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
181
155
|
it("descriptor decision surface is 'external_directory'", async () => {
|
|
182
156
|
const result = await describeGate(
|
|
183
157
|
makeTcc(),
|
|
184
|
-
|
|
185
|
-
vi.fn().mockReturnValue([]),
|
|
158
|
+
makeResolver(makeCheckResult("ask")),
|
|
186
159
|
);
|
|
187
160
|
const desc = result as GateDescriptor;
|
|
188
161
|
expect(desc.decision.surface).toBe("external_directory");
|
|
@@ -191,8 +164,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
191
164
|
it("denialContext contains the command and external paths", async () => {
|
|
192
165
|
const result = await describeGate(
|
|
193
166
|
makeTcc({ input: { command: "cat /outside/file.ts" } }),
|
|
194
|
-
|
|
195
|
-
vi.fn().mockReturnValue([]),
|
|
167
|
+
makeResolver(makeCheckResult("ask")),
|
|
196
168
|
);
|
|
197
169
|
const desc = result as GateDescriptor;
|
|
198
170
|
expect(desc.denialContext).toMatchObject({
|
|
@@ -205,8 +177,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
205
177
|
it("promptDetails includes command and tool_call source", async () => {
|
|
206
178
|
const result = await describeGate(
|
|
207
179
|
makeTcc({ agentName: "agent-1", toolCallId: "tc-5" }),
|
|
208
|
-
|
|
209
|
-
vi.fn().mockReturnValue([]),
|
|
180
|
+
makeResolver(makeCheckResult("ask")),
|
|
210
181
|
);
|
|
211
182
|
const desc = result as GateDescriptor;
|
|
212
183
|
expect(desc.promptDetails).toMatchObject({
|
|
@@ -220,19 +191,15 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
220
191
|
|
|
221
192
|
it("config-allowed path is excluded; remaining ask path produces a descriptor", async () => {
|
|
222
193
|
// One path config-allowed, one config-ask → descriptor with only the ask path.
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
.
|
|
226
|
-
(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return makeCheckResult("ask");
|
|
230
|
-
},
|
|
231
|
-
);
|
|
194
|
+
const resolver = makeResolver();
|
|
195
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
196
|
+
if ((input as Record<string, unknown>).path === "/outside/a.ts")
|
|
197
|
+
return makeCheckResult("allow", { source: "special" });
|
|
198
|
+
return makeCheckResult("ask");
|
|
199
|
+
});
|
|
232
200
|
const result = await describeGate(
|
|
233
201
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
234
|
-
|
|
235
|
-
vi.fn().mockReturnValue([]),
|
|
202
|
+
resolver,
|
|
236
203
|
);
|
|
237
204
|
expect(isGateDescriptor(result)).toBe(true);
|
|
238
205
|
const desc = result as GateDescriptor;
|
|
@@ -244,19 +211,15 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
244
211
|
|
|
245
212
|
it("config-denied path makes worstCheck deny even when another path is ask", async () => {
|
|
246
213
|
// One path config-denied, one config-ask → descriptor with preCheck.state === "deny".
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return makeCheckResult("ask");
|
|
254
|
-
},
|
|
255
|
-
);
|
|
214
|
+
const resolver = makeResolver();
|
|
215
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
216
|
+
if ((input as Record<string, unknown>).path === "/outside/a.ts")
|
|
217
|
+
return makeCheckResult("deny", { source: "special" });
|
|
218
|
+
return makeCheckResult("ask");
|
|
219
|
+
});
|
|
256
220
|
const result = await describeGate(
|
|
257
221
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
258
|
-
|
|
259
|
-
vi.fn().mockReturnValue([]),
|
|
222
|
+
resolver,
|
|
260
223
|
);
|
|
261
224
|
expect(isGateDescriptor(result)).toBe(true);
|
|
262
225
|
const desc = result as GateDescriptor;
|
|
@@ -268,20 +231,16 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
268
231
|
});
|
|
269
232
|
|
|
270
233
|
it("only includes uncovered paths when some are session-covered", async () => {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
.
|
|
274
|
-
(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return makeCheckResult("ask");
|
|
279
|
-
},
|
|
280
|
-
);
|
|
234
|
+
const resolver = makeResolver();
|
|
235
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
236
|
+
if ((input as Record<string, unknown>).path === "/outside/a.ts") {
|
|
237
|
+
return makeCheckResult("allow", { source: "session" });
|
|
238
|
+
}
|
|
239
|
+
return makeCheckResult("ask");
|
|
240
|
+
});
|
|
281
241
|
const result = await describeGate(
|
|
282
242
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
283
|
-
|
|
284
|
-
vi.fn().mockReturnValue([]),
|
|
243
|
+
resolver,
|
|
285
244
|
);
|
|
286
245
|
expect(isGateDescriptor(result)).toBe(true);
|
|
287
246
|
const desc = result as GateDescriptor;
|
|
@@ -19,11 +19,12 @@ 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
|
+
makePathDispatchResolver,
|
|
27
|
+
makeResolver,
|
|
27
28
|
makeTcc,
|
|
28
29
|
} from "#test/helpers/gate-fixtures";
|
|
29
30
|
|
|
@@ -31,13 +32,6 @@ afterEach(() => {
|
|
|
31
32
|
vi.restoreAllMocks();
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
type CheckPermissionFn = (
|
|
35
|
-
surface: string,
|
|
36
|
-
input: unknown,
|
|
37
|
-
agentName?: string,
|
|
38
|
-
sessionRules?: Rule[],
|
|
39
|
-
) => PermissionCheckResult;
|
|
40
|
-
|
|
41
35
|
/**
|
|
42
36
|
* Mirror the handler's parse-once derivation: parse the bash command into a
|
|
43
37
|
* shared `BashProgram` and inject it, exactly as `permission-gate-handler.ts`
|
|
@@ -45,72 +39,47 @@ type CheckPermissionFn = (
|
|
|
45
39
|
*/
|
|
46
40
|
async function describeGate(
|
|
47
41
|
tcc: ToolCallContext,
|
|
48
|
-
|
|
49
|
-
getSessionRuleset: () => Rule[],
|
|
42
|
+
resolver: PermissionResolver,
|
|
50
43
|
): Promise<GateResult> {
|
|
51
44
|
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
52
45
|
const bashProgram =
|
|
53
46
|
tcc.toolName === "bash" && command
|
|
54
47
|
? await BashProgram.parse(command)
|
|
55
48
|
: null;
|
|
56
|
-
return describeBashPathGate(
|
|
57
|
-
tcc,
|
|
58
|
-
bashProgram,
|
|
59
|
-
checkPermission,
|
|
60
|
-
getSessionRuleset,
|
|
61
|
-
);
|
|
49
|
+
return describeBashPathGate(tcc, bashProgram, resolver);
|
|
62
50
|
}
|
|
63
51
|
|
|
64
52
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
65
53
|
|
|
66
54
|
describe("describeBashPathGate", () => {
|
|
67
55
|
it("returns null for non-bash tools", async () => {
|
|
68
|
-
const checkPermission = vi.fn<CheckPermissionFn>();
|
|
69
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
70
56
|
const result = await describeGate(
|
|
71
57
|
makeTcc({ toolName: "read", input: { path: ".env" } }),
|
|
72
|
-
|
|
73
|
-
getSessionRuleset,
|
|
58
|
+
makeResolver(),
|
|
74
59
|
);
|
|
75
60
|
expect(result).toBeNull();
|
|
76
61
|
});
|
|
77
62
|
|
|
78
63
|
it("returns null when no tokens are extracted", async () => {
|
|
79
|
-
const checkPermission = vi.fn<CheckPermissionFn>();
|
|
80
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
81
64
|
const result = await describeGate(
|
|
82
65
|
makeTcc({ input: { command: "echo hello" } }),
|
|
83
|
-
|
|
84
|
-
getSessionRuleset,
|
|
66
|
+
makeResolver(),
|
|
85
67
|
);
|
|
86
68
|
expect(result).toBeNull();
|
|
87
69
|
});
|
|
88
70
|
|
|
89
71
|
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
72
|
const result = await describeGate(
|
|
95
|
-
makeTcc(
|
|
96
|
-
|
|
97
|
-
getSessionRuleset,
|
|
73
|
+
makeTcc(),
|
|
74
|
+
makeResolver(makeCheckResult({ state: "allow" })),
|
|
98
75
|
);
|
|
99
76
|
expect(result).toBeNull();
|
|
100
77
|
});
|
|
101
78
|
|
|
102
79
|
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
80
|
const result = await describeGate(
|
|
111
|
-
makeTcc(
|
|
112
|
-
|
|
113
|
-
getSessionRuleset,
|
|
81
|
+
makeTcc(),
|
|
82
|
+
makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
|
|
114
83
|
);
|
|
115
84
|
expect(result).not.toBeNull();
|
|
116
85
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -120,14 +89,9 @@ describe("describeBashPathGate", () => {
|
|
|
120
89
|
});
|
|
121
90
|
|
|
122
91
|
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
92
|
const result = await describeGate(
|
|
128
|
-
makeTcc(
|
|
129
|
-
|
|
130
|
-
getSessionRuleset,
|
|
93
|
+
makeTcc(),
|
|
94
|
+
makeResolver(makeCheckResult({ state: "ask", matchedPattern: "*" })),
|
|
131
95
|
);
|
|
132
96
|
expect(result).not.toBeNull();
|
|
133
97
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -136,16 +100,9 @@ describe("describeBashPathGate", () => {
|
|
|
136
100
|
});
|
|
137
101
|
|
|
138
102
|
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
103
|
const result = (await describeGate(
|
|
146
|
-
makeTcc(
|
|
147
|
-
|
|
148
|
-
getSessionRuleset,
|
|
104
|
+
makeTcc(),
|
|
105
|
+
makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
|
|
149
106
|
)) as GateDescriptor;
|
|
150
107
|
expect(result.denialContext).toMatchObject({
|
|
151
108
|
kind: "bash_path",
|
|
@@ -156,37 +113,17 @@ describe("describeBashPathGate", () => {
|
|
|
156
113
|
});
|
|
157
114
|
|
|
158
115
|
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
116
|
const result = (await describeGate(
|
|
166
|
-
makeTcc(
|
|
167
|
-
|
|
168
|
-
getSessionRuleset,
|
|
117
|
+
makeTcc(),
|
|
118
|
+
makeResolver(makeCheckResult({ state: "deny", matchedPattern: "*.env" })),
|
|
169
119
|
)) as GateDescriptor;
|
|
170
120
|
expect(result.decision.surface).toBe("path");
|
|
171
121
|
});
|
|
172
122
|
|
|
173
123
|
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
124
|
const result = await describeGate(
|
|
187
|
-
makeTcc(
|
|
188
|
-
|
|
189
|
-
getSessionRuleset,
|
|
125
|
+
makeTcc(),
|
|
126
|
+
makeResolver(makeCheckResult({ state: "allow", source: "session" })),
|
|
190
127
|
);
|
|
191
128
|
expect(result).not.toBeNull();
|
|
192
129
|
expect(isGateBypass(result)).toBe(true);
|
|
@@ -194,31 +131,18 @@ describe("describeBashPathGate", () => {
|
|
|
194
131
|
});
|
|
195
132
|
|
|
196
133
|
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
|
-
);
|
|
134
|
+
const result = await describeGate(makeTcc({ input: {} }), makeResolver());
|
|
204
135
|
expect(result).toBeNull();
|
|
205
136
|
});
|
|
206
137
|
|
|
207
138
|
it("evaluates most restrictive across multiple tokens", async () => {
|
|
208
|
-
const
|
|
209
|
-
.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (record.path === "src/foo.ts") {
|
|
213
|
-
return makeCheckResult({ state: "allow" });
|
|
214
|
-
}
|
|
215
|
-
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
216
|
-
});
|
|
217
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
139
|
+
const resolver = makePathDispatchResolver(
|
|
140
|
+
{ "src/foo.ts": makeCheckResult({ state: "allow" }) },
|
|
141
|
+
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
142
|
+
);
|
|
218
143
|
const result = await describeGate(
|
|
219
144
|
makeTcc({ input: { command: "cat src/foo.ts .env" } }),
|
|
220
|
-
|
|
221
|
-
getSessionRuleset,
|
|
145
|
+
resolver,
|
|
222
146
|
);
|
|
223
147
|
expect(result).not.toBeNull();
|
|
224
148
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -226,20 +150,13 @@ describe("describeBashPathGate", () => {
|
|
|
226
150
|
});
|
|
227
151
|
|
|
228
152
|
it("deny wins in multi-token: cp .env README.md", async () => {
|
|
229
|
-
const
|
|
230
|
-
.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (record.path === ".env") {
|
|
234
|
-
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
235
|
-
}
|
|
236
|
-
return makeCheckResult({ state: "allow" });
|
|
237
|
-
});
|
|
238
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
153
|
+
const resolver = makePathDispatchResolver(
|
|
154
|
+
{ ".env": makeCheckResult({ state: "deny", matchedPattern: "*.env" }) },
|
|
155
|
+
makeCheckResult({ state: "allow" }),
|
|
156
|
+
);
|
|
239
157
|
const result = await describeGate(
|
|
240
158
|
makeTcc({ input: { command: "cp .env README.md" } }),
|
|
241
|
-
|
|
242
|
-
getSessionRuleset,
|
|
159
|
+
resolver,
|
|
243
160
|
);
|
|
244
161
|
expect(result).not.toBeNull();
|
|
245
162
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -249,20 +166,13 @@ describe("describeBashPathGate", () => {
|
|
|
249
166
|
});
|
|
250
167
|
|
|
251
168
|
it("extracts redirect target: echo test > .env triggers deny", async () => {
|
|
252
|
-
const
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (record.path === ".env") {
|
|
257
|
-
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
258
|
-
}
|
|
259
|
-
return makeCheckResult({ state: "allow" });
|
|
260
|
-
});
|
|
261
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
169
|
+
const resolver = makePathDispatchResolver(
|
|
170
|
+
{ ".env": makeCheckResult({ state: "deny", matchedPattern: "*.env" }) },
|
|
171
|
+
makeCheckResult({ state: "allow" }),
|
|
172
|
+
);
|
|
262
173
|
const result = await describeGate(
|
|
263
174
|
makeTcc({ input: { command: "echo test > .env" } }),
|
|
264
|
-
|
|
265
|
-
getSessionRuleset,
|
|
175
|
+
resolver,
|
|
266
176
|
);
|
|
267
177
|
expect(result).not.toBeNull();
|
|
268
178
|
expect(isGateDescriptor(result)).toBe(true);
|
|
@@ -270,7 +180,24 @@ describe("describeBashPathGate", () => {
|
|
|
270
180
|
});
|
|
271
181
|
|
|
272
182
|
it("returns null when all tokens match only the universal default", async () => {
|
|
273
|
-
const
|
|
183
|
+
const result = await describeGate(
|
|
184
|
+
makeTcc(),
|
|
185
|
+
makeResolver(
|
|
186
|
+
makeCheckResult({
|
|
187
|
+
state: "ask",
|
|
188
|
+
matchedPattern: undefined,
|
|
189
|
+
source: "special",
|
|
190
|
+
origin: "builtin",
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
expect(result).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("ignores tokens matching universal default but fires for explicit rule matches", async () => {
|
|
198
|
+
const resolver = makePathDispatchResolver(
|
|
199
|
+
{ ".env": makeCheckResult({ state: "deny", matchedPattern: "*.env" }) },
|
|
200
|
+
// Other tokens match only the universal default (no matchedPattern)
|
|
274
201
|
makeCheckResult({
|
|
275
202
|
state: "ask",
|
|
276
203
|
matchedPattern: undefined,
|
|
@@ -278,39 +205,9 @@ describe("describeBashPathGate", () => {
|
|
|
278
205
|
origin: "builtin",
|
|
279
206
|
}),
|
|
280
207
|
);
|
|
281
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
282
|
-
const result = await describeGate(
|
|
283
|
-
makeTcc({ input: { command: "cat .env" } }),
|
|
284
|
-
checkPermission,
|
|
285
|
-
getSessionRuleset,
|
|
286
|
-
);
|
|
287
|
-
expect(result).toBeNull();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("ignores tokens matching universal default but fires for explicit rule matches", async () => {
|
|
291
|
-
const checkPermission = vi
|
|
292
|
-
.fn<CheckPermissionFn>()
|
|
293
|
-
.mockImplementation((_surface, input) => {
|
|
294
|
-
const record = input as Record<string, unknown>;
|
|
295
|
-
if (record.path === ".env") {
|
|
296
|
-
return makeCheckResult({
|
|
297
|
-
state: "deny",
|
|
298
|
-
matchedPattern: "*.env",
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
// Other tokens match only the universal default
|
|
302
|
-
return makeCheckResult({
|
|
303
|
-
state: "ask",
|
|
304
|
-
matchedPattern: undefined,
|
|
305
|
-
source: "special",
|
|
306
|
-
origin: "builtin",
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
310
208
|
const result = await describeGate(
|
|
311
209
|
makeTcc({ input: { command: "cat src/foo.ts .env" } }),
|
|
312
|
-
|
|
313
|
-
getSessionRuleset,
|
|
210
|
+
resolver,
|
|
314
211
|
);
|
|
315
212
|
expect(result).not.toBeNull();
|
|
316
213
|
expect(isGateDescriptor(result)).toBe(true);
|