@gotgenes/pi-permission-system 10.0.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 +26 -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 +49 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +77 -9
- package/src/permissions-service.ts +53 -0
- 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 +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-forwarder.test.ts +295 -0
- package/test/permission-forwarding.test.ts +0 -282
- package/test/permission-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +160 -27
- 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 -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,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);
|