@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
|
@@ -2,20 +2,23 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import type { DenialContext } from "#src/denial-messages";
|
|
4
4
|
import { EXTENSION_TAG } from "#src/denial-messages";
|
|
5
|
-
import type {
|
|
6
|
-
import { runGateCheck } from "#src/handlers/gates/runner";
|
|
5
|
+
import type { GateBypass } from "#src/handlers/gates/descriptor";
|
|
7
6
|
import { SessionApproval } from "#src/session-approval";
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
makeDenialDescriptor,
|
|
9
|
+
makeDescriptor,
|
|
10
|
+
makeGateRunner,
|
|
11
|
+
} from "#test/helpers/gate-fixtures";
|
|
9
12
|
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
10
13
|
|
|
11
|
-
// ──
|
|
14
|
+
// ── GateRunner — descriptor path ───────────────────────────────────────────
|
|
12
15
|
|
|
13
|
-
describe("
|
|
16
|
+
describe("GateRunner — descriptor path", () => {
|
|
14
17
|
it("returns allow and emits policy_allow when policy is allow", async () => {
|
|
15
|
-
const deps =
|
|
16
|
-
const result = await
|
|
18
|
+
const { runner, deps } = makeGateRunner();
|
|
19
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
17
20
|
expect(result).toEqual({ action: "allow" });
|
|
18
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
21
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
19
22
|
expect.objectContaining({
|
|
20
23
|
surface: "read",
|
|
21
24
|
value: "read",
|
|
@@ -26,36 +29,31 @@ describe("runGateCheck", () => {
|
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
it("returns block and emits policy_deny when policy is deny", async () => {
|
|
29
|
-
const deps =
|
|
30
|
-
|
|
31
|
-
.fn()
|
|
32
|
-
.mockReturnValue(
|
|
33
|
-
makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
34
|
-
),
|
|
32
|
+
const { runner, deps } = makeGateRunner({
|
|
33
|
+
resolveResult: makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
35
34
|
});
|
|
36
|
-
const result = await
|
|
35
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
37
36
|
expect(result).toMatchObject({ action: "block" });
|
|
38
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
37
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
39
38
|
expect.objectContaining({
|
|
40
39
|
result: "deny",
|
|
41
40
|
resolution: "policy_deny",
|
|
42
41
|
}),
|
|
43
42
|
);
|
|
44
|
-
expect(deps.writeReviewLog).toHaveBeenCalledWith(
|
|
43
|
+
expect(deps.reporter.writeReviewLog).toHaveBeenCalledWith(
|
|
45
44
|
"permission_request.blocked",
|
|
46
45
|
expect.objectContaining({ resolution: "policy_denied" }),
|
|
47
46
|
);
|
|
48
47
|
});
|
|
49
48
|
|
|
50
49
|
it("returns allow and emits session_approved on session hit", async () => {
|
|
51
|
-
const deps =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
),
|
|
50
|
+
const { runner, deps } = makeGateRunner({
|
|
51
|
+
resolveResult: makeCheckResult({
|
|
52
|
+
source: "session",
|
|
53
|
+
matchedPattern: "git *",
|
|
54
|
+
}),
|
|
57
55
|
});
|
|
58
|
-
const result = await
|
|
56
|
+
const result = await runner.run(
|
|
59
57
|
makeDescriptor({
|
|
60
58
|
surface: "bash",
|
|
61
59
|
input: { command: "git status" },
|
|
@@ -63,17 +61,16 @@ describe("runGateCheck", () => {
|
|
|
63
61
|
}),
|
|
64
62
|
null,
|
|
65
63
|
"tc-1",
|
|
66
|
-
deps,
|
|
67
64
|
);
|
|
68
65
|
expect(result).toEqual({ action: "allow" });
|
|
69
|
-
expect(deps.writeReviewLog).toHaveBeenCalledWith(
|
|
66
|
+
expect(deps.reporter.writeReviewLog).toHaveBeenCalledWith(
|
|
70
67
|
"permission_request.session_approved",
|
|
71
68
|
expect.objectContaining({
|
|
72
69
|
resolution: "session_approved",
|
|
73
70
|
sessionApprovalPattern: "git *",
|
|
74
71
|
}),
|
|
75
72
|
);
|
|
76
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
73
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
77
74
|
expect.objectContaining({
|
|
78
75
|
resolution: "session_approved",
|
|
79
76
|
matchedPattern: "git *",
|
|
@@ -82,19 +79,15 @@ describe("runGateCheck", () => {
|
|
|
82
79
|
});
|
|
83
80
|
|
|
84
81
|
it("returns allow and emits user_approved when ask + user approves", async () => {
|
|
85
|
-
const deps =
|
|
86
|
-
|
|
87
|
-
.fn()
|
|
88
|
-
.mockReturnValue(
|
|
89
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
90
|
-
),
|
|
82
|
+
const { runner, deps } = makeGateRunner({
|
|
83
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
91
84
|
promptPermission: vi
|
|
92
85
|
.fn()
|
|
93
86
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
94
87
|
});
|
|
95
|
-
const result = await
|
|
88
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
96
89
|
expect(result).toEqual({ action: "allow" });
|
|
97
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
90
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
98
91
|
expect.objectContaining({
|
|
99
92
|
result: "allow",
|
|
100
93
|
resolution: "user_approved",
|
|
@@ -103,12 +96,8 @@ describe("runGateCheck", () => {
|
|
|
103
96
|
});
|
|
104
97
|
|
|
105
98
|
it("returns allow, emits user_approved_for_session, and records session rule on approved_for_session", async () => {
|
|
106
|
-
const deps =
|
|
107
|
-
|
|
108
|
-
.fn()
|
|
109
|
-
.mockReturnValue(
|
|
110
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
111
|
-
),
|
|
99
|
+
const { runner, deps } = makeGateRunner({
|
|
100
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
112
101
|
promptPermission: vi
|
|
113
102
|
.fn()
|
|
114
103
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
@@ -116,9 +105,9 @@ describe("runGateCheck", () => {
|
|
|
116
105
|
const descriptor = makeDescriptor({
|
|
117
106
|
sessionApproval: SessionApproval.single("read", "*"),
|
|
118
107
|
});
|
|
119
|
-
const result = await
|
|
108
|
+
const result = await runner.run(descriptor, null, "tc-1");
|
|
120
109
|
expect(result).toEqual({ action: "allow" });
|
|
121
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
110
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
122
111
|
expect.objectContaining({
|
|
123
112
|
resolution: "user_approved_for_session",
|
|
124
113
|
}),
|
|
@@ -129,12 +118,8 @@ describe("runGateCheck", () => {
|
|
|
129
118
|
});
|
|
130
119
|
|
|
131
120
|
it("calls recordSessionApproval once with the full SessionApproval when sessionApproval has multiple patterns", async () => {
|
|
132
|
-
const deps =
|
|
133
|
-
|
|
134
|
-
.fn()
|
|
135
|
-
.mockReturnValue(
|
|
136
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
137
|
-
),
|
|
121
|
+
const { runner, deps } = makeGateRunner({
|
|
122
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
138
123
|
promptPermission: vi
|
|
139
124
|
.fn()
|
|
140
125
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
@@ -144,26 +129,22 @@ describe("runGateCheck", () => {
|
|
|
144
129
|
"/outside/b/*",
|
|
145
130
|
]);
|
|
146
131
|
const descriptor = makeDescriptor({ sessionApproval: approval });
|
|
147
|
-
const result = await
|
|
132
|
+
const result = await runner.run(descriptor, null, "tc-1");
|
|
148
133
|
expect(result).toEqual({ action: "allow" });
|
|
149
134
|
expect(deps.recordSessionApproval).toHaveBeenCalledTimes(1);
|
|
150
135
|
expect(deps.recordSessionApproval).toHaveBeenCalledWith(approval);
|
|
151
136
|
});
|
|
152
137
|
|
|
153
138
|
it("returns block and emits user_denied when ask + user denies", async () => {
|
|
154
|
-
const deps =
|
|
155
|
-
|
|
156
|
-
.fn()
|
|
157
|
-
.mockReturnValue(
|
|
158
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
159
|
-
),
|
|
139
|
+
const { runner, deps } = makeGateRunner({
|
|
140
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
160
141
|
promptPermission: vi
|
|
161
142
|
.fn()
|
|
162
143
|
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
163
144
|
});
|
|
164
|
-
const result = await
|
|
145
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
165
146
|
expect(result).toMatchObject({ action: "block" });
|
|
166
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
147
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
167
148
|
expect.objectContaining({
|
|
168
149
|
result: "deny",
|
|
169
150
|
resolution: "user_denied",
|
|
@@ -172,17 +153,13 @@ describe("runGateCheck", () => {
|
|
|
172
153
|
});
|
|
173
154
|
|
|
174
155
|
it("returns block and emits confirmation_unavailable when ask + no UI", async () => {
|
|
175
|
-
const deps =
|
|
176
|
-
|
|
177
|
-
.fn()
|
|
178
|
-
.mockReturnValue(
|
|
179
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
180
|
-
),
|
|
156
|
+
const { runner, deps } = makeGateRunner({
|
|
157
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
181
158
|
canConfirm: vi.fn().mockReturnValue(false),
|
|
182
159
|
});
|
|
183
|
-
const result = await
|
|
160
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
184
161
|
expect(result).toMatchObject({ action: "block" });
|
|
185
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
162
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
186
163
|
expect.objectContaining({
|
|
187
164
|
result: "deny",
|
|
188
165
|
resolution: "confirmation_unavailable",
|
|
@@ -191,73 +168,59 @@ describe("runGateCheck", () => {
|
|
|
191
168
|
});
|
|
192
169
|
|
|
193
170
|
it("emits auto_approved resolution when decision has autoApproved flag", async () => {
|
|
194
|
-
const deps =
|
|
195
|
-
|
|
196
|
-
.fn()
|
|
197
|
-
.mockReturnValue(
|
|
198
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
199
|
-
),
|
|
171
|
+
const { runner, deps } = makeGateRunner({
|
|
172
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
200
173
|
promptPermission: vi.fn().mockResolvedValue({
|
|
201
174
|
approved: true,
|
|
202
175
|
state: "approved",
|
|
203
176
|
autoApproved: true,
|
|
204
177
|
}),
|
|
205
178
|
});
|
|
206
|
-
const result = await
|
|
179
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
207
180
|
expect(result).toEqual({ action: "allow" });
|
|
208
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
181
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
209
182
|
expect.objectContaining({
|
|
210
183
|
resolution: "auto_approved",
|
|
211
184
|
}),
|
|
212
185
|
);
|
|
213
186
|
});
|
|
214
187
|
|
|
215
|
-
it("uses preResolved.state instead of calling
|
|
216
|
-
const deps =
|
|
188
|
+
it("uses preResolved.state instead of calling resolve", async () => {
|
|
189
|
+
const { runner, deps } = makeGateRunner();
|
|
217
190
|
const descriptor = makeDescriptor({
|
|
218
191
|
preResolved: { state: "deny" },
|
|
219
192
|
});
|
|
220
|
-
const result = await
|
|
193
|
+
const result = await runner.run(descriptor, null, "tc-1");
|
|
221
194
|
expect(result).toMatchObject({ action: "block" });
|
|
222
|
-
expect(deps.
|
|
223
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
195
|
+
expect(deps.resolve).not.toHaveBeenCalled();
|
|
196
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
224
197
|
expect.objectContaining({
|
|
225
198
|
resolution: "policy_deny",
|
|
226
199
|
}),
|
|
227
200
|
);
|
|
228
201
|
});
|
|
229
202
|
|
|
230
|
-
it("uses preResolved.state allow without calling
|
|
231
|
-
const deps =
|
|
203
|
+
it("uses preResolved.state allow without calling resolve", async () => {
|
|
204
|
+
const { runner, deps } = makeGateRunner();
|
|
232
205
|
const descriptor = makeDescriptor({
|
|
233
206
|
preResolved: { state: "allow" },
|
|
234
207
|
});
|
|
235
|
-
const result = await
|
|
208
|
+
const result = await runner.run(descriptor, null, "tc-1");
|
|
236
209
|
expect(result).toEqual({ action: "allow" });
|
|
237
|
-
expect(deps.
|
|
238
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
210
|
+
expect(deps.resolve).not.toHaveBeenCalled();
|
|
211
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
239
212
|
expect.objectContaining({
|
|
240
213
|
resolution: "policy_allow",
|
|
241
214
|
}),
|
|
242
215
|
);
|
|
243
216
|
});
|
|
244
217
|
|
|
245
|
-
it("passes agentName to
|
|
246
|
-
const deps =
|
|
247
|
-
const result = await
|
|
248
|
-
makeDescriptor(),
|
|
249
|
-
"test-agent",
|
|
250
|
-
"tc-1",
|
|
251
|
-
deps,
|
|
252
|
-
);
|
|
218
|
+
it("passes agentName to resolve and decision event", async () => {
|
|
219
|
+
const { runner, deps } = makeGateRunner();
|
|
220
|
+
const result = await runner.run(makeDescriptor(), "test-agent", "tc-1");
|
|
253
221
|
expect(result).toEqual({ action: "allow" });
|
|
254
|
-
expect(deps.
|
|
255
|
-
|
|
256
|
-
{},
|
|
257
|
-
"test-agent",
|
|
258
|
-
[],
|
|
259
|
-
);
|
|
260
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
222
|
+
expect(deps.resolve).toHaveBeenCalledWith("read", {}, "test-agent");
|
|
223
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
261
224
|
expect.objectContaining({
|
|
262
225
|
agentName: "test-agent",
|
|
263
226
|
}),
|
|
@@ -265,36 +228,28 @@ describe("runGateCheck", () => {
|
|
|
265
228
|
});
|
|
266
229
|
|
|
267
230
|
it("passes requestId from toolCallId to promptPermission", async () => {
|
|
268
|
-
const deps =
|
|
269
|
-
|
|
270
|
-
.fn()
|
|
271
|
-
.mockReturnValue(
|
|
272
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
273
|
-
),
|
|
231
|
+
const { runner, deps } = makeGateRunner({
|
|
232
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
274
233
|
});
|
|
275
|
-
await
|
|
234
|
+
await runner.run(makeDescriptor(), null, "tc-42");
|
|
276
235
|
expect(deps.promptPermission).toHaveBeenCalledWith(
|
|
277
236
|
expect.objectContaining({ requestId: "tc-42" }),
|
|
278
237
|
);
|
|
279
238
|
});
|
|
280
239
|
|
|
281
240
|
it("does not call recordSessionApproval when user approves once (no sessionApproval)", async () => {
|
|
282
|
-
const deps =
|
|
283
|
-
|
|
284
|
-
.fn()
|
|
285
|
-
.mockReturnValue(
|
|
286
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
287
|
-
),
|
|
241
|
+
const { runner, deps } = makeGateRunner({
|
|
242
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
288
243
|
promptPermission: vi
|
|
289
244
|
.fn()
|
|
290
245
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
291
246
|
});
|
|
292
|
-
await
|
|
247
|
+
await runner.run(makeDescriptor(), null, "tc-1");
|
|
293
248
|
expect(deps.recordSessionApproval).not.toHaveBeenCalled();
|
|
294
249
|
});
|
|
295
250
|
|
|
296
|
-
it("uses preCheck result directly instead of calling
|
|
297
|
-
const deps =
|
|
251
|
+
it("uses preCheck result directly instead of calling resolve", async () => {
|
|
252
|
+
const { runner, deps } = makeGateRunner();
|
|
298
253
|
const descriptor = makeDescriptor({
|
|
299
254
|
preCheck: makeCheckResult({
|
|
300
255
|
state: "deny",
|
|
@@ -302,10 +257,10 @@ describe("runGateCheck", () => {
|
|
|
302
257
|
matchedPattern: "rm *",
|
|
303
258
|
}),
|
|
304
259
|
});
|
|
305
|
-
const result = await
|
|
260
|
+
const result = await runner.run(descriptor, null, "tc-1");
|
|
306
261
|
expect(result).toMatchObject({ action: "block" });
|
|
307
|
-
expect(deps.
|
|
308
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
262
|
+
expect(deps.resolve).not.toHaveBeenCalled();
|
|
263
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(
|
|
309
264
|
expect.objectContaining({
|
|
310
265
|
resolution: "policy_deny",
|
|
311
266
|
origin: "global",
|
|
@@ -315,68 +270,31 @@ describe("runGateCheck", () => {
|
|
|
315
270
|
});
|
|
316
271
|
|
|
317
272
|
it("does not call recordSessionApproval when user approves for session but no sessionApproval on descriptor", async () => {
|
|
318
|
-
const deps =
|
|
319
|
-
|
|
320
|
-
.fn()
|
|
321
|
-
.mockReturnValue(
|
|
322
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
323
|
-
),
|
|
273
|
+
const { runner, deps } = makeGateRunner({
|
|
274
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
324
275
|
promptPermission: vi
|
|
325
276
|
.fn()
|
|
326
277
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
327
278
|
});
|
|
328
279
|
// No sessionApproval on descriptor
|
|
329
|
-
await
|
|
280
|
+
await runner.run(makeDescriptor(), null, "tc-1");
|
|
330
281
|
expect(deps.recordSessionApproval).not.toHaveBeenCalled();
|
|
331
282
|
});
|
|
332
283
|
|
|
333
284
|
describe("denialContext formatting", () => {
|
|
334
|
-
function makeDenialContextDescriptor(
|
|
335
|
-
denialContext: DenialContext,
|
|
336
|
-
overrides: Partial<GateDescriptor> = {},
|
|
337
|
-
): GateDescriptor {
|
|
338
|
-
return {
|
|
339
|
-
surface: "write",
|
|
340
|
-
input: {},
|
|
341
|
-
denialContext,
|
|
342
|
-
promptDetails: {
|
|
343
|
-
source: "tool_call",
|
|
344
|
-
agentName: null,
|
|
345
|
-
message: "Allow tool 'write'?",
|
|
346
|
-
toolCallId: "tc-1",
|
|
347
|
-
toolName: "write",
|
|
348
|
-
},
|
|
349
|
-
logContext: {
|
|
350
|
-
source: "tool_call",
|
|
351
|
-
toolCallId: "tc-1",
|
|
352
|
-
toolName: "write",
|
|
353
|
-
},
|
|
354
|
-
decision: {
|
|
355
|
-
surface: "write",
|
|
356
|
-
value: "write",
|
|
357
|
-
},
|
|
358
|
-
...overrides,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
285
|
it("uses denialContext to format denyReason with extension tag", async () => {
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
.fn()
|
|
366
|
-
.mockReturnValue(
|
|
367
|
-
makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
368
|
-
),
|
|
286
|
+
const { runner } = makeGateRunner({
|
|
287
|
+
resolveResult: makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
369
288
|
});
|
|
370
289
|
const ctx: DenialContext = {
|
|
371
290
|
kind: "tool",
|
|
372
291
|
check: makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
373
292
|
agentName: "test-agent",
|
|
374
293
|
};
|
|
375
|
-
const result = await
|
|
376
|
-
|
|
294
|
+
const result = await runner.run(
|
|
295
|
+
makeDenialDescriptor(ctx),
|
|
377
296
|
"test-agent",
|
|
378
297
|
"tc-1",
|
|
379
|
-
deps,
|
|
380
298
|
);
|
|
381
299
|
expect(result.action).toBe("block");
|
|
382
300
|
if (result.action === "block") {
|
|
@@ -386,24 +304,15 @@ describe("runGateCheck", () => {
|
|
|
386
304
|
});
|
|
387
305
|
|
|
388
306
|
it("uses denialContext to format unavailableReason with extension tag", async () => {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
.fn()
|
|
392
|
-
.mockReturnValue(
|
|
393
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
394
|
-
),
|
|
307
|
+
const { runner } = makeGateRunner({
|
|
308
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
395
309
|
canConfirm: vi.fn().mockReturnValue(false),
|
|
396
310
|
});
|
|
397
311
|
const ctx: DenialContext = {
|
|
398
312
|
kind: "tool",
|
|
399
313
|
check: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
400
314
|
};
|
|
401
|
-
const result = await
|
|
402
|
-
makeDenialContextDescriptor(ctx),
|
|
403
|
-
null,
|
|
404
|
-
"tc-1",
|
|
405
|
-
deps,
|
|
406
|
-
);
|
|
315
|
+
const result = await runner.run(makeDenialDescriptor(ctx), null, "tc-1");
|
|
407
316
|
expect(result.action).toBe("block");
|
|
408
317
|
if (result.action === "block") {
|
|
409
318
|
expect(result.reason).toContain(EXTENSION_TAG);
|
|
@@ -412,12 +321,8 @@ describe("runGateCheck", () => {
|
|
|
412
321
|
});
|
|
413
322
|
|
|
414
323
|
it("uses denialContext to format userDeniedReason with extension tag", async () => {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
.fn()
|
|
418
|
-
.mockReturnValue(
|
|
419
|
-
makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
420
|
-
),
|
|
324
|
+
const { runner } = makeGateRunner({
|
|
325
|
+
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
421
326
|
promptPermission: vi.fn().mockResolvedValue({
|
|
422
327
|
approved: false,
|
|
423
328
|
state: "denied",
|
|
@@ -428,12 +333,7 @@ describe("runGateCheck", () => {
|
|
|
428
333
|
kind: "tool",
|
|
429
334
|
check: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
430
335
|
};
|
|
431
|
-
const result = await
|
|
432
|
-
makeDenialContextDescriptor(ctx),
|
|
433
|
-
null,
|
|
434
|
-
"tc-1",
|
|
435
|
-
deps,
|
|
436
|
-
);
|
|
336
|
+
const result = await runner.run(makeDenialDescriptor(ctx), null, "tc-1");
|
|
437
337
|
expect(result.action).toBe("block");
|
|
438
338
|
if (result.action === "block") {
|
|
439
339
|
expect(result.reason).toContain(EXTENSION_TAG);
|
|
@@ -442,3 +342,68 @@ describe("runGateCheck", () => {
|
|
|
442
342
|
});
|
|
443
343
|
});
|
|
444
344
|
});
|
|
345
|
+
|
|
346
|
+
// ── GateRunner.run — null and bypass dispatch ──────────────────────────────
|
|
347
|
+
|
|
348
|
+
describe("GateRunner.run — null and bypass dispatch", () => {
|
|
349
|
+
it("returns allow for a null gate", async () => {
|
|
350
|
+
const { runner, deps } = makeGateRunner();
|
|
351
|
+
const result = await runner.run(null, null, "tc-1");
|
|
352
|
+
expect(result).toEqual({ action: "allow" });
|
|
353
|
+
expect(deps.reporter.writeReviewLog).not.toHaveBeenCalled();
|
|
354
|
+
expect(deps.reporter.emitDecision).not.toHaveBeenCalled();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("returns allow for a bypass with no log or decision", async () => {
|
|
358
|
+
const { runner, deps } = makeGateRunner();
|
|
359
|
+
const bypass: GateBypass = { action: "allow" };
|
|
360
|
+
const result = await runner.run(bypass, null, "tc-1");
|
|
361
|
+
expect(result).toEqual({ action: "allow" });
|
|
362
|
+
expect(deps.reporter.writeReviewLog).not.toHaveBeenCalled();
|
|
363
|
+
expect(deps.reporter.emitDecision).not.toHaveBeenCalled();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("fires writeReviewLog for a bypass with a log entry", async () => {
|
|
367
|
+
const { runner, deps } = makeGateRunner();
|
|
368
|
+
const bypass: GateBypass = {
|
|
369
|
+
action: "allow",
|
|
370
|
+
log: { event: "infra.bypass", details: { path: "/x" } },
|
|
371
|
+
};
|
|
372
|
+
await runner.run(bypass, null, "tc-1");
|
|
373
|
+
expect(deps.reporter.writeReviewLog).toHaveBeenCalledWith("infra.bypass", {
|
|
374
|
+
path: "/x",
|
|
375
|
+
});
|
|
376
|
+
expect(deps.reporter.emitDecision).not.toHaveBeenCalled();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("fires emitDecision for a bypass with a decision", async () => {
|
|
380
|
+
const { runner, deps } = makeGateRunner();
|
|
381
|
+
const decision = {
|
|
382
|
+
surface: "path",
|
|
383
|
+
value: "/x",
|
|
384
|
+
result: "allow" as const,
|
|
385
|
+
resolution: "policy_allow" as const,
|
|
386
|
+
origin: null,
|
|
387
|
+
agentName: null,
|
|
388
|
+
matchedPattern: null,
|
|
389
|
+
};
|
|
390
|
+
const bypass: GateBypass = { action: "allow", decision };
|
|
391
|
+
await runner.run(bypass, null, "tc-1");
|
|
392
|
+
expect(deps.reporter.emitDecision).toHaveBeenCalledWith(decision);
|
|
393
|
+
expect(deps.reporter.writeReviewLog).not.toHaveBeenCalled();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("routes a descriptor to the gate check logic and returns allow", async () => {
|
|
397
|
+
const { runner } = makeGateRunner();
|
|
398
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
399
|
+
expect(result).toEqual({ action: "allow" });
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("routes a descriptor to the gate check logic and returns block", async () => {
|
|
403
|
+
const { runner } = makeGateRunner({
|
|
404
|
+
resolveResult: makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
405
|
+
});
|
|
406
|
+
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
407
|
+
expect(result).toMatchObject({ action: "block" });
|
|
408
|
+
});
|
|
409
|
+
});
|