@gotgenes/pi-permission-system 10.3.0 → 10.4.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 +19 -0
- package/package.json +1 -1
- package/src/config-modal.ts +10 -8
- package/src/config-store.ts +13 -34
- package/src/forwarded-permissions/io.ts +16 -22
- package/src/forwarded-permissions/permission-forwarder.ts +16 -19
- package/src/gate-prompter.ts +1 -3
- package/src/handlers/gates/runner.ts +1 -1
- package/src/index.ts +68 -51
- package/src/permission-event-rpc.ts +19 -15
- package/src/permission-prompter.ts +4 -3
- package/src/permission-session.ts +10 -67
- package/src/permissions-service.ts +3 -5
- package/src/prompting-gateway.ts +104 -0
- package/src/session-logger.ts +63 -12
- package/test/composition-root.test.ts +85 -1
- package/test/config-modal.test.ts +13 -7
- package/test/config-store.test.ts +23 -49
- package/test/forwarded-permissions/io.test.ts +23 -26
- package/test/handlers/external-directory-integration.test.ts +45 -32
- package/test/handlers/external-directory-session-dedup.test.ts +36 -46
- package/test/handlers/gates/runner.test.ts +10 -16
- package/test/handlers/input-events.test.ts +19 -4
- package/test/handlers/input.test.ts +29 -13
- package/test/handlers/tool-call-events.test.ts +23 -5
- package/test/helpers/gate-fixtures.ts +6 -6
- package/test/helpers/handler-fixtures.ts +24 -39
- package/test/permission-event-rpc.test.ts +30 -28
- package/test/permission-forwarder.test.ts +6 -5
- package/test/permission-prompter.test.ts +28 -28
- package/test/permission-session.test.ts +40 -112
- package/test/prompting-gateway.test.ts +230 -0
- package/test/session-logger.test.ts +151 -64
- package/src/runtime.ts +0 -147
- package/test/runtime.test.ts +0 -303
|
@@ -81,9 +81,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
81
81
|
it("returns allow and emits user_approved when ask + user approves", async () => {
|
|
82
82
|
const { runner, deps } = makeGateRunner({
|
|
83
83
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
84
|
-
|
|
85
|
-
.fn()
|
|
86
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
84
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
87
85
|
});
|
|
88
86
|
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
89
87
|
expect(result).toEqual({ action: "allow" });
|
|
@@ -98,7 +96,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
98
96
|
it("returns allow, emits user_approved_for_session, and records session rule on approved_for_session", async () => {
|
|
99
97
|
const { runner, deps } = makeGateRunner({
|
|
100
98
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
101
|
-
|
|
99
|
+
prompt: vi
|
|
102
100
|
.fn()
|
|
103
101
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
104
102
|
});
|
|
@@ -120,7 +118,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
120
118
|
it("calls recordSessionApproval once with the full SessionApproval when sessionApproval has multiple patterns", async () => {
|
|
121
119
|
const { runner, deps } = makeGateRunner({
|
|
122
120
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
123
|
-
|
|
121
|
+
prompt: vi
|
|
124
122
|
.fn()
|
|
125
123
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
126
124
|
});
|
|
@@ -138,9 +136,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
138
136
|
it("returns block and emits user_denied when ask + user denies", async () => {
|
|
139
137
|
const { runner, deps } = makeGateRunner({
|
|
140
138
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
141
|
-
|
|
142
|
-
.fn()
|
|
143
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
139
|
+
prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
|
|
144
140
|
});
|
|
145
141
|
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
146
142
|
expect(result).toMatchObject({ action: "block" });
|
|
@@ -170,7 +166,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
170
166
|
it("emits auto_approved resolution when decision has autoApproved flag", async () => {
|
|
171
167
|
const { runner, deps } = makeGateRunner({
|
|
172
168
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
173
|
-
|
|
169
|
+
prompt: vi.fn().mockResolvedValue({
|
|
174
170
|
approved: true,
|
|
175
171
|
state: "approved",
|
|
176
172
|
autoApproved: true,
|
|
@@ -227,12 +223,12 @@ describe("GateRunner — descriptor path", () => {
|
|
|
227
223
|
);
|
|
228
224
|
});
|
|
229
225
|
|
|
230
|
-
it("passes requestId from toolCallId to
|
|
226
|
+
it("passes requestId from toolCallId to prompt", async () => {
|
|
231
227
|
const { runner, deps } = makeGateRunner({
|
|
232
228
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
233
229
|
});
|
|
234
230
|
await runner.run(makeDescriptor(), null, "tc-42");
|
|
235
|
-
expect(deps.
|
|
231
|
+
expect(deps.prompt).toHaveBeenCalledWith(
|
|
236
232
|
expect.objectContaining({ requestId: "tc-42" }),
|
|
237
233
|
);
|
|
238
234
|
});
|
|
@@ -240,9 +236,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
240
236
|
it("does not call recordSessionApproval when user approves once (no sessionApproval)", async () => {
|
|
241
237
|
const { runner, deps } = makeGateRunner({
|
|
242
238
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
243
|
-
|
|
244
|
-
.fn()
|
|
245
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
239
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
246
240
|
});
|
|
247
241
|
await runner.run(makeDescriptor(), null, "tc-1");
|
|
248
242
|
expect(deps.recordSessionApproval).not.toHaveBeenCalled();
|
|
@@ -272,7 +266,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
272
266
|
it("does not call recordSessionApproval when user approves for session but no sessionApproval on descriptor", async () => {
|
|
273
267
|
const { runner, deps } = makeGateRunner({
|
|
274
268
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
275
|
-
|
|
269
|
+
prompt: vi
|
|
276
270
|
.fn()
|
|
277
271
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
278
272
|
});
|
|
@@ -323,7 +317,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
323
317
|
it("uses denialContext to format userDeniedReason with extension tag", async () => {
|
|
324
318
|
const { runner } = makeGateRunner({
|
|
325
319
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
326
|
-
|
|
320
|
+
prompt: vi.fn().mockResolvedValue({
|
|
327
321
|
approved: false,
|
|
328
322
|
state: "denied",
|
|
329
323
|
denialReason: "too risky",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
6
7
|
import {
|
|
7
8
|
getDecisionEvents,
|
|
8
9
|
makeCheckResult,
|
|
@@ -70,8 +71,11 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
70
71
|
const { handler, events } = makeHandler({
|
|
71
72
|
session: {
|
|
72
73
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
74
|
+
},
|
|
75
|
+
prompter: {
|
|
76
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
73
77
|
prompt: vi
|
|
74
|
-
.fn()
|
|
78
|
+
.fn<GatePrompter["prompt"]>()
|
|
75
79
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
76
80
|
},
|
|
77
81
|
});
|
|
@@ -91,7 +95,12 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
91
95
|
const { handler, events } = makeHandler({
|
|
92
96
|
session: {
|
|
93
97
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
94
|
-
|
|
98
|
+
},
|
|
99
|
+
prompter: {
|
|
100
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
101
|
+
prompt: vi
|
|
102
|
+
.fn<GatePrompter["prompt"]>()
|
|
103
|
+
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
95
104
|
},
|
|
96
105
|
});
|
|
97
106
|
await handler.handleInput({ text: "/skill:explorer" }, makeCtx());
|
|
@@ -110,7 +119,10 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
110
119
|
const { handler, events } = makeHandler({
|
|
111
120
|
session: {
|
|
112
121
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
113
|
-
|
|
122
|
+
},
|
|
123
|
+
prompter: {
|
|
124
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
125
|
+
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
114
126
|
},
|
|
115
127
|
});
|
|
116
128
|
await handler.handleInput(
|
|
@@ -132,7 +144,10 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
132
144
|
const { handler, events } = makeHandler({
|
|
133
145
|
session: {
|
|
134
146
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
135
|
-
|
|
147
|
+
},
|
|
148
|
+
prompter: {
|
|
149
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
150
|
+
prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
|
|
136
151
|
approved: true,
|
|
137
152
|
state: "approved",
|
|
138
153
|
autoApproved: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
2
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
3
3
|
import { extractSkillNameFromInput } from "#src/handlers/permission-gate-handler";
|
|
4
4
|
|
|
5
5
|
import { makeCtx, makeHandler } from "#test/helpers/handler-fixtures";
|
|
@@ -120,7 +120,10 @@ describe("handleInput", () => {
|
|
|
120
120
|
const { handler } = makeHandler({
|
|
121
121
|
session: {
|
|
122
122
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
123
|
-
|
|
123
|
+
},
|
|
124
|
+
prompter: {
|
|
125
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
126
|
+
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
124
127
|
},
|
|
125
128
|
});
|
|
126
129
|
const result = await handler.handleInput(
|
|
@@ -131,12 +134,16 @@ describe("handleInput", () => {
|
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
it("prompts and returns continue when skill ask is approved", async () => {
|
|
134
|
-
const
|
|
137
|
+
const approvePrompt = vi
|
|
138
|
+
.fn<GatePrompter["prompt"]>()
|
|
139
|
+
.mockResolvedValue({ approved: true, state: "approved" });
|
|
140
|
+
const { handler, prompter } = makeHandler({
|
|
135
141
|
session: {
|
|
136
142
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
},
|
|
144
|
+
prompter: {
|
|
145
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
146
|
+
prompt: approvePrompt,
|
|
140
147
|
},
|
|
141
148
|
});
|
|
142
149
|
const result = await handler.handleInput(
|
|
@@ -144,14 +151,19 @@ describe("handleInput", () => {
|
|
|
144
151
|
makeCtx(),
|
|
145
152
|
);
|
|
146
153
|
expect(result).toEqual({ action: "continue" });
|
|
147
|
-
expect(
|
|
154
|
+
expect(prompter.prompt).toHaveBeenCalledOnce();
|
|
148
155
|
});
|
|
149
156
|
|
|
150
157
|
it("returns handled when skill ask is denied by user", async () => {
|
|
151
158
|
const { handler } = makeHandler({
|
|
152
159
|
session: {
|
|
153
160
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
154
|
-
|
|
161
|
+
},
|
|
162
|
+
prompter: {
|
|
163
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
164
|
+
prompt: vi
|
|
165
|
+
.fn<GatePrompter["prompt"]>()
|
|
166
|
+
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
155
167
|
},
|
|
156
168
|
});
|
|
157
169
|
const result = await handler.handleInput(
|
|
@@ -162,17 +174,21 @@ describe("handleInput", () => {
|
|
|
162
174
|
});
|
|
163
175
|
|
|
164
176
|
it("passes agentName in the prompt permission request", async () => {
|
|
165
|
-
const
|
|
177
|
+
const approvePrompt = vi
|
|
178
|
+
.fn<GatePrompter["prompt"]>()
|
|
179
|
+
.mockResolvedValue({ approved: true, state: "approved" });
|
|
180
|
+
const { handler, prompter } = makeHandler({
|
|
166
181
|
session: {
|
|
167
182
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
168
183
|
resolveAgentName: vi.fn().mockReturnValue("code-agent"),
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
},
|
|
185
|
+
prompter: {
|
|
186
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
187
|
+
prompt: approvePrompt,
|
|
172
188
|
},
|
|
173
189
|
});
|
|
174
190
|
await handler.handleInput(makeInputEvent("/skill:librarian"), makeCtx());
|
|
175
|
-
expect(
|
|
191
|
+
expect(prompter.prompt).toHaveBeenCalledWith(
|
|
176
192
|
expect.objectContaining({
|
|
177
193
|
agentName: "code-agent",
|
|
178
194
|
skillName: "librarian",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
7
8
|
import {
|
|
8
9
|
getDecisionEvents,
|
|
9
10
|
makeCheckResult,
|
|
@@ -110,8 +111,11 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
110
111
|
checkPermission: vi
|
|
111
112
|
.fn()
|
|
112
113
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
114
|
+
},
|
|
115
|
+
prompter: {
|
|
116
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
113
117
|
prompt: vi
|
|
114
|
-
.fn()
|
|
118
|
+
.fn<GatePrompter["prompt"]>()
|
|
115
119
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
116
120
|
},
|
|
117
121
|
});
|
|
@@ -132,7 +136,10 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
132
136
|
checkPermission: vi
|
|
133
137
|
.fn()
|
|
134
138
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
135
|
-
|
|
139
|
+
},
|
|
140
|
+
prompter: {
|
|
141
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
142
|
+
prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
|
|
136
143
|
approved: true,
|
|
137
144
|
state: "approved_for_session",
|
|
138
145
|
}),
|
|
@@ -159,7 +166,12 @@ describe("handleToolCall decision events — user_denied", () => {
|
|
|
159
166
|
checkPermission: vi
|
|
160
167
|
.fn()
|
|
161
168
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
162
|
-
|
|
169
|
+
},
|
|
170
|
+
prompter: {
|
|
171
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
172
|
+
prompt: vi
|
|
173
|
+
.fn<GatePrompter["prompt"]>()
|
|
174
|
+
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
163
175
|
},
|
|
164
176
|
});
|
|
165
177
|
|
|
@@ -183,7 +195,10 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
|
|
|
183
195
|
checkPermission: vi
|
|
184
196
|
.fn()
|
|
185
197
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
186
|
-
|
|
198
|
+
},
|
|
199
|
+
prompter: {
|
|
200
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
201
|
+
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
187
202
|
},
|
|
188
203
|
});
|
|
189
204
|
|
|
@@ -239,7 +254,10 @@ describe("handleToolCall decision events — auto_approved", () => {
|
|
|
239
254
|
checkPermission: vi
|
|
240
255
|
.fn()
|
|
241
256
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
242
|
-
|
|
257
|
+
},
|
|
258
|
+
prompter: {
|
|
259
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
260
|
+
prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
|
|
243
261
|
approved: true,
|
|
244
262
|
state: "approved",
|
|
245
263
|
autoApproved: true,
|
|
@@ -94,7 +94,7 @@ export function makeGateRunner(
|
|
|
94
94
|
resolve?: PermissionResolver["resolve"];
|
|
95
95
|
recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
|
|
96
96
|
canConfirm?: GatePrompter["canConfirm"];
|
|
97
|
-
|
|
97
|
+
prompt?: GatePrompter["prompt"];
|
|
98
98
|
reporter?: Partial<DecisionReporter>;
|
|
99
99
|
} = {},
|
|
100
100
|
) {
|
|
@@ -112,15 +112,15 @@ export function makeGateRunner(
|
|
|
112
112
|
const canConfirm =
|
|
113
113
|
overrides.canConfirm ??
|
|
114
114
|
(vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
|
|
115
|
-
const
|
|
116
|
-
overrides.
|
|
115
|
+
const prompt =
|
|
116
|
+
overrides.prompt ??
|
|
117
117
|
vi
|
|
118
|
-
.fn<GatePrompter["
|
|
118
|
+
.fn<GatePrompter["prompt"]>()
|
|
119
119
|
.mockResolvedValue({ approved: true, state: "approved" });
|
|
120
120
|
const runner = new GateRunner(
|
|
121
121
|
{ resolve },
|
|
122
122
|
{ recordSessionApproval },
|
|
123
|
-
{ canConfirm,
|
|
123
|
+
{ canConfirm, prompt },
|
|
124
124
|
reporter,
|
|
125
125
|
);
|
|
126
126
|
return {
|
|
@@ -129,7 +129,7 @@ export function makeGateRunner(
|
|
|
129
129
|
resolve,
|
|
130
130
|
recordSessionApproval,
|
|
131
131
|
canConfirm,
|
|
132
|
-
|
|
132
|
+
prompt,
|
|
133
133
|
reporter,
|
|
134
134
|
},
|
|
135
135
|
};
|
|
@@ -21,10 +21,8 @@ import {
|
|
|
21
21
|
ToolCallGatePipeline,
|
|
22
22
|
} from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
23
23
|
import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
|
|
24
|
-
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
25
24
|
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
26
25
|
import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
|
|
27
|
-
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
28
26
|
import type { Rule } from "#src/rule";
|
|
29
27
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
30
28
|
import type { SessionLogger } from "#src/session-logger";
|
|
@@ -35,10 +33,10 @@ import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
|
35
33
|
/**
|
|
36
34
|
* Precise mock boundary for PermissionGateHandler integration tests.
|
|
37
35
|
*
|
|
38
|
-
* Intersection of every role the handler and its collaborators require
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
36
|
+
* Intersection of every role the handler and its collaborators require.
|
|
37
|
+
* Prompting is not included here — it moved to `PromptingGateway` (#339).
|
|
38
|
+
* Pass a `prompter` override to `makeHandler` to steer GateRunner's prompting
|
|
39
|
+
* role; `makeHandler` creates a clean default prompter when none is supplied.
|
|
42
40
|
*
|
|
43
41
|
* The 4-arg `checkPermission` overrides the 3-arg version from
|
|
44
42
|
* GateHandlerSession so the `resolve` delegation can forward session rules.
|
|
@@ -46,7 +44,6 @@ import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
|
46
44
|
export type MockGateHandlerSession = ToolCallGateInputs &
|
|
47
45
|
SkillInputGateInputs &
|
|
48
46
|
SessionApprovalRecorder &
|
|
49
|
-
GatePrompter &
|
|
50
47
|
GateHandlerSession & {
|
|
51
48
|
/** Logger source for the reporter the fixture builds. */
|
|
52
49
|
logger: SessionLogger;
|
|
@@ -59,13 +56,6 @@ export type MockGateHandlerSession = ToolCallGateInputs &
|
|
|
59
56
|
agentName?: string,
|
|
60
57
|
rules?: Rule[],
|
|
61
58
|
): PermissionCheckResult;
|
|
62
|
-
/** Context-bound canPrompt — overriding this steers canConfirm. */
|
|
63
|
-
canPrompt(ctx: ExtensionContext): boolean;
|
|
64
|
-
/** Context-bound prompt — overriding this steers promptPermission. */
|
|
65
|
-
prompt(
|
|
66
|
-
ctx: ExtensionContext,
|
|
67
|
-
details: PromptPermissionDetails,
|
|
68
|
-
): Promise<PermissionPromptDecision>;
|
|
69
59
|
};
|
|
70
60
|
|
|
71
61
|
export function makeEvents() {
|
|
@@ -134,9 +124,11 @@ export function makeCheckResult(
|
|
|
134
124
|
* field against `MockGateHandlerSession` individually — a missing field fails
|
|
135
125
|
* `pnpm run check` instead of failing silently at runtime.
|
|
136
126
|
*
|
|
137
|
-
* The `resolve
|
|
138
|
-
*
|
|
139
|
-
*
|
|
127
|
+
* The `resolve` delegation is inlined as a closure that reads `session` at
|
|
128
|
+
* call time, so overriding `checkPermission` or `getSessionRuleset`
|
|
129
|
+
* automatically steers it without extra guards.
|
|
130
|
+
*
|
|
131
|
+
* Prompting is not part of this mock — pass `prompter` to `makeHandler`.
|
|
140
132
|
*/
|
|
141
133
|
export function makeSession(
|
|
142
134
|
overrides: Partial<MockGateHandlerSession> = {},
|
|
@@ -177,15 +169,7 @@ export function makeSession(
|
|
|
177
169
|
vi
|
|
178
170
|
.fn<MockGateHandlerSession["getToolPreviewLimits"]>()
|
|
179
171
|
.mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
|
|
180
|
-
|
|
181
|
-
overrides.canPrompt ??
|
|
182
|
-
vi.fn<MockGateHandlerSession["canPrompt"]>().mockReturnValue(true),
|
|
183
|
-
prompt:
|
|
184
|
-
overrides.prompt ??
|
|
185
|
-
vi
|
|
186
|
-
.fn<MockGateHandlerSession["prompt"]>()
|
|
187
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
188
|
-
// Delegations — closures read `session` at call time so overrides win.
|
|
172
|
+
// Resolve delegation — closure reads `session` at call time so overrides win.
|
|
189
173
|
resolve:
|
|
190
174
|
overrides.resolve ??
|
|
191
175
|
vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
|
|
@@ -196,16 +180,6 @@ export function makeSession(
|
|
|
196
180
|
session.getSessionRuleset(),
|
|
197
181
|
),
|
|
198
182
|
),
|
|
199
|
-
canConfirm:
|
|
200
|
-
overrides.canConfirm ??
|
|
201
|
-
vi.fn<MockGateHandlerSession["canConfirm"]>(() =>
|
|
202
|
-
session.canPrompt(undefined as unknown as ExtensionContext),
|
|
203
|
-
),
|
|
204
|
-
promptPermission:
|
|
205
|
-
overrides.promptPermission ??
|
|
206
|
-
vi.fn<MockGateHandlerSession["promptPermission"]>((details) =>
|
|
207
|
-
session.prompt(undefined as unknown as ExtensionContext, details),
|
|
208
|
-
),
|
|
209
183
|
};
|
|
210
184
|
return session;
|
|
211
185
|
}
|
|
@@ -296,10 +270,15 @@ export function makeBashCommandCheck(opts: {
|
|
|
296
270
|
* Constructs a PermissionGateHandler with mocked collaborators.
|
|
297
271
|
*
|
|
298
272
|
* Returns all collaborators so each test file can destructure only what
|
|
299
|
-
* it needs — handler, events, session, and
|
|
273
|
+
* it needs — handler, events, session, toolRegistry, and prompter are all available.
|
|
274
|
+
*
|
|
275
|
+
* The default prompter approves all requests. Pass `prompter` explicitly to
|
|
276
|
+
* steer canConfirm/prompt behavior for the test.
|
|
300
277
|
*/
|
|
301
278
|
export function makeHandler(overrides?: {
|
|
302
279
|
session?: Partial<MockGateHandlerSession>;
|
|
280
|
+
/** Override the GatePrompter passed to GateRunner. Defaults to an allow-all stub. */
|
|
281
|
+
prompter?: GatePrompter;
|
|
303
282
|
toolRegistry?: Partial<ToolRegistry>;
|
|
304
283
|
/** Sugar: builds the `getAll` mock from a list of tool names. */
|
|
305
284
|
tools?: string[];
|
|
@@ -317,7 +296,13 @@ export function makeHandler(overrides?: {
|
|
|
317
296
|
const pipeline = new ToolCallGatePipeline(session);
|
|
318
297
|
const skillInputPipeline = new SkillInputGatePipeline(session);
|
|
319
298
|
const reporter = new GateDecisionReporter(session.logger, events);
|
|
320
|
-
const
|
|
299
|
+
const prompter: GatePrompter = overrides?.prompter ?? {
|
|
300
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
301
|
+
prompt: vi
|
|
302
|
+
.fn<GatePrompter["prompt"]>()
|
|
303
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
304
|
+
};
|
|
305
|
+
const runner = new GateRunner(session, session, prompter, reporter);
|
|
321
306
|
const handler = new PermissionGateHandler(
|
|
322
307
|
session,
|
|
323
308
|
toolRegistry,
|
|
@@ -325,7 +310,7 @@ export function makeHandler(overrides?: {
|
|
|
325
310
|
skillInputPipeline,
|
|
326
311
|
runner,
|
|
327
312
|
);
|
|
328
|
-
return { handler, events, session, toolRegistry };
|
|
313
|
+
return { handler, events, session, toolRegistry, prompter };
|
|
329
314
|
}
|
|
330
315
|
|
|
331
316
|
/** Extract all permissions:decision payloads from the events.emit mock. */
|
|
@@ -36,13 +36,13 @@ function makeDeps(
|
|
|
36
36
|
overrides: Partial<PermissionRpcDeps> = {},
|
|
37
37
|
): PermissionRpcDeps {
|
|
38
38
|
return {
|
|
39
|
-
|
|
39
|
+
permissionManager: {
|
|
40
40
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getRuntimeContext: vi.fn().mockReturnValue(null),
|
|
41
|
+
},
|
|
42
|
+
sessionRules: { getRuleset: vi.fn().mockReturnValue([]) },
|
|
43
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(null) },
|
|
44
44
|
requestPermissionDecisionFromUi: vi.fn(),
|
|
45
|
-
|
|
45
|
+
logger: { review: vi.fn() },
|
|
46
46
|
...overrides,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
@@ -73,9 +73,9 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
73
73
|
it("replies allow for an allowed surface/value", async () => {
|
|
74
74
|
const bus = createEventBus();
|
|
75
75
|
const deps = makeDeps({
|
|
76
|
-
|
|
76
|
+
permissionManager: {
|
|
77
77
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
78
|
-
}
|
|
78
|
+
},
|
|
79
79
|
});
|
|
80
80
|
registerPermissionRpcHandlers(bus, deps);
|
|
81
81
|
|
|
@@ -100,14 +100,14 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
100
100
|
it("replies deny for a denied surface/value", async () => {
|
|
101
101
|
const bus = createEventBus();
|
|
102
102
|
const deps = makeDeps({
|
|
103
|
-
|
|
103
|
+
permissionManager: {
|
|
104
104
|
checkPermission: vi.fn().mockReturnValue(
|
|
105
105
|
makeCheckResult("deny", {
|
|
106
106
|
origin: "project",
|
|
107
107
|
matchedPattern: "rm *",
|
|
108
108
|
}),
|
|
109
109
|
),
|
|
110
|
-
}
|
|
110
|
+
},
|
|
111
111
|
});
|
|
112
112
|
registerPermissionRpcHandlers(bus, deps);
|
|
113
113
|
|
|
@@ -131,13 +131,13 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
131
131
|
it("replies ask for an ask surface/value", async () => {
|
|
132
132
|
const bus = createEventBus();
|
|
133
133
|
const deps = makeDeps({
|
|
134
|
-
|
|
134
|
+
permissionManager: {
|
|
135
135
|
checkPermission: vi
|
|
136
136
|
.fn()
|
|
137
137
|
.mockReturnValue(
|
|
138
138
|
makeCheckResult("ask", { matchedPattern: undefined }),
|
|
139
139
|
),
|
|
140
|
-
}
|
|
140
|
+
},
|
|
141
141
|
});
|
|
142
142
|
registerPermissionRpcHandlers(bus, deps);
|
|
143
143
|
|
|
@@ -161,7 +161,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
161
161
|
const checkPermission = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
162
162
|
const bus = createEventBus();
|
|
163
163
|
const deps = makeDeps({
|
|
164
|
-
|
|
164
|
+
permissionManager: { checkPermission },
|
|
165
165
|
});
|
|
166
166
|
registerPermissionRpcHandlers(bus, deps);
|
|
167
167
|
|
|
@@ -197,8 +197,8 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
197
197
|
const checkPermission = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
198
198
|
const bus = createEventBus();
|
|
199
199
|
const deps = makeDeps({
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
permissionManager: { checkPermission },
|
|
201
|
+
sessionRules: { getRuleset: vi.fn().mockReturnValue(sessionRules) },
|
|
202
202
|
});
|
|
203
203
|
registerPermissionRpcHandlers(bus, deps);
|
|
204
204
|
|
|
@@ -246,7 +246,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
246
246
|
const checkPermission = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
247
247
|
const bus = createEventBus();
|
|
248
248
|
const deps = makeDeps({
|
|
249
|
-
|
|
249
|
+
permissionManager: { checkPermission },
|
|
250
250
|
});
|
|
251
251
|
const handles = registerPermissionRpcHandlers(bus, deps);
|
|
252
252
|
handles.unsubCheck();
|
|
@@ -290,7 +290,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
290
290
|
const ctx = makeCtxWithUi();
|
|
291
291
|
const approvedDecision = { approved: true, state: "approved" as const };
|
|
292
292
|
const deps = makeDeps({
|
|
293
|
-
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
293
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
|
|
294
294
|
requestPermissionDecisionFromUi: vi
|
|
295
295
|
.fn()
|
|
296
296
|
.mockResolvedValue(approvedDecision),
|
|
@@ -325,7 +325,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
325
325
|
.fn()
|
|
326
326
|
.mockResolvedValue({ approved: true, state: "approved" as const });
|
|
327
327
|
const deps = makeDeps({
|
|
328
|
-
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
328
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
|
|
329
329
|
requestPermissionDecisionFromUi: requestUi,
|
|
330
330
|
});
|
|
331
331
|
registerPermissionRpcHandlers(bus, deps);
|
|
@@ -363,7 +363,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
363
363
|
.fn()
|
|
364
364
|
.mockResolvedValue({ approved: true, state: "approved" as const });
|
|
365
365
|
const deps = makeDeps({
|
|
366
|
-
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
366
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
|
|
367
367
|
requestPermissionDecisionFromUi: requestUi,
|
|
368
368
|
});
|
|
369
369
|
registerPermissionRpcHandlers(bus, deps);
|
|
@@ -399,7 +399,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
399
399
|
denialReason: "Too risky",
|
|
400
400
|
};
|
|
401
401
|
const deps = makeDeps({
|
|
402
|
-
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
402
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
|
|
403
403
|
requestPermissionDecisionFromUi: vi
|
|
404
404
|
.fn()
|
|
405
405
|
.mockResolvedValue(deniedDecision),
|
|
@@ -430,7 +430,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
430
430
|
it("replies with no_ui error when context has no UI", async () => {
|
|
431
431
|
const bus = createEventBus();
|
|
432
432
|
const deps = makeDeps({
|
|
433
|
-
getRuntimeContext: vi.fn().mockReturnValue(null),
|
|
433
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(null) },
|
|
434
434
|
});
|
|
435
435
|
registerPermissionRpcHandlers(bus, deps);
|
|
436
436
|
|
|
@@ -453,9 +453,11 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
453
453
|
it("replies with no_ui error when context hasUI is false", async () => {
|
|
454
454
|
const bus = createEventBus();
|
|
455
455
|
const deps = makeDeps({
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
456
|
+
session: {
|
|
457
|
+
getRuntimeContext: vi
|
|
458
|
+
.fn()
|
|
459
|
+
.mockReturnValue({ hasUI: false, ui: makeUi() }),
|
|
460
|
+
},
|
|
459
461
|
});
|
|
460
462
|
registerPermissionRpcHandlers(bus, deps);
|
|
461
463
|
|
|
@@ -478,13 +480,13 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
478
480
|
it("writes to the review log after a prompt decision", async () => {
|
|
479
481
|
const bus = createEventBus();
|
|
480
482
|
const ctx = makeCtxWithUi();
|
|
481
|
-
const
|
|
483
|
+
const logger = { review: vi.fn() };
|
|
482
484
|
const deps = makeDeps({
|
|
483
|
-
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
485
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
|
|
484
486
|
requestPermissionDecisionFromUi: vi
|
|
485
487
|
.fn()
|
|
486
488
|
.mockResolvedValue({ approved: true, state: "approved" as const }),
|
|
487
|
-
|
|
489
|
+
logger,
|
|
488
490
|
});
|
|
489
491
|
registerPermissionRpcHandlers(bus, deps);
|
|
490
492
|
|
|
@@ -501,7 +503,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
501
503
|
});
|
|
502
504
|
await replyPromise;
|
|
503
505
|
|
|
504
|
-
expect(
|
|
506
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
505
507
|
"permission_request.rpc_prompt",
|
|
506
508
|
expect.objectContaining({
|
|
507
509
|
requestId: "req-log",
|
|
@@ -520,7 +522,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
520
522
|
const bus = createEventBus();
|
|
521
523
|
const ctx = makeCtxWithUi();
|
|
522
524
|
const deps = makeDeps({
|
|
523
|
-
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
525
|
+
session: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
|
|
524
526
|
requestPermissionDecisionFromUi: requestUi,
|
|
525
527
|
});
|
|
526
528
|
const handles = registerPermissionRpcHandlers(bus, deps);
|