@gotgenes/pi-permission-system 10.3.1 → 10.5.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 +24 -0
- package/package.json +1 -1
- package/src/config-modal.ts +10 -8
- package/src/config-store.ts +6 -11
- 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/bash-command.ts +2 -2
- package/src/handlers/gates/bash-external-directory.ts +2 -2
- package/src/handlers/gates/bash-path.ts +2 -2
- package/src/handlers/gates/path.ts +2 -2
- package/src/handlers/gates/runner.ts +3 -3
- package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
- package/src/index.ts +27 -41
- package/src/permission-event-rpc.ts +19 -15
- package/src/permission-prompter.ts +4 -3
- package/src/permission-resolver.ts +69 -2
- package/src/permission-session.ts +7 -83
- package/src/prompting-gateway.ts +104 -0
- package/src/session-logger.ts +17 -3
- package/test/config-modal.test.ts +13 -7
- package/test/config-store.test.ts +7 -9
- 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 +47 -57
- package/test/handlers/gates/bash-external-directory.test.ts +2 -2
- package/test/handlers/gates/bash-path.test.ts +2 -2
- package/test/handlers/gates/runner.test.ts +10 -16
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
- 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 +11 -15
- package/test/helpers/handler-fixtures.ts +31 -50
- 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-resolver.test.ts +194 -0
- package/test/permission-session.test.ts +27 -180
- package/test/prompting-gateway.test.ts +230 -0
|
@@ -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,7 @@ 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
|
-
*
|
|
138
|
-
* as closures that read `session` at call time, so overriding `checkPermission`,
|
|
139
|
-
* `canPrompt`, or `prompt` automatically steers them without extra guards.
|
|
127
|
+
* Prompting is not part of this mock — pass `prompter` to `makeHandler`.
|
|
140
128
|
*/
|
|
141
129
|
export function makeSession(
|
|
142
130
|
overrides: Partial<MockGateHandlerSession> = {},
|
|
@@ -177,35 +165,6 @@ export function makeSession(
|
|
|
177
165
|
vi
|
|
178
166
|
.fn<MockGateHandlerSession["getToolPreviewLimits"]>()
|
|
179
167
|
.mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
|
|
180
|
-
canPrompt:
|
|
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.
|
|
189
|
-
resolve:
|
|
190
|
-
overrides.resolve ??
|
|
191
|
-
vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
|
|
192
|
-
session.checkPermission(
|
|
193
|
-
surface,
|
|
194
|
-
input,
|
|
195
|
-
agentName,
|
|
196
|
-
session.getSessionRuleset(),
|
|
197
|
-
),
|
|
198
|
-
),
|
|
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
168
|
};
|
|
210
169
|
return session;
|
|
211
170
|
}
|
|
@@ -296,10 +255,15 @@ export function makeBashCommandCheck(opts: {
|
|
|
296
255
|
* Constructs a PermissionGateHandler with mocked collaborators.
|
|
297
256
|
*
|
|
298
257
|
* Returns all collaborators so each test file can destructure only what
|
|
299
|
-
* it needs — handler, events, session, and
|
|
258
|
+
* it needs — handler, events, session, toolRegistry, and prompter are all available.
|
|
259
|
+
*
|
|
260
|
+
* The default prompter approves all requests. Pass `prompter` explicitly to
|
|
261
|
+
* steer canConfirm/prompt behavior for the test.
|
|
300
262
|
*/
|
|
301
263
|
export function makeHandler(overrides?: {
|
|
302
264
|
session?: Partial<MockGateHandlerSession>;
|
|
265
|
+
/** Override the GatePrompter passed to GateRunner. Defaults to an allow-all stub. */
|
|
266
|
+
prompter?: GatePrompter;
|
|
303
267
|
toolRegistry?: Partial<ToolRegistry>;
|
|
304
268
|
/** Sugar: builds the `getAll` mock from a list of tool names. */
|
|
305
269
|
tools?: string[];
|
|
@@ -314,10 +278,27 @@ export function makeHandler(overrides?: {
|
|
|
314
278
|
.mockReturnValue(overrides.tools.map((name) => ({ name }))),
|
|
315
279
|
})
|
|
316
280
|
: makeToolRegistry(overrides?.toolRegistry);
|
|
317
|
-
|
|
281
|
+
// Resolver delegates to session's checkPermission + getSessionRuleset —
|
|
282
|
+
// overriding session.checkPermission steers resolve automatically.
|
|
283
|
+
const resolver = {
|
|
284
|
+
resolve: (surface: string, input: unknown, agentName?: string) =>
|
|
285
|
+
session.checkPermission(
|
|
286
|
+
surface,
|
|
287
|
+
input,
|
|
288
|
+
agentName,
|
|
289
|
+
session.getSessionRuleset(),
|
|
290
|
+
),
|
|
291
|
+
};
|
|
292
|
+
const pipeline = new ToolCallGatePipeline(resolver, session);
|
|
318
293
|
const skillInputPipeline = new SkillInputGatePipeline(session);
|
|
319
294
|
const reporter = new GateDecisionReporter(session.logger, events);
|
|
320
|
-
const
|
|
295
|
+
const prompter: GatePrompter = overrides?.prompter ?? {
|
|
296
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
297
|
+
prompt: vi
|
|
298
|
+
.fn<GatePrompter["prompt"]>()
|
|
299
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
300
|
+
};
|
|
301
|
+
const runner = new GateRunner(resolver, session, prompter, reporter);
|
|
321
302
|
const handler = new PermissionGateHandler(
|
|
322
303
|
session,
|
|
323
304
|
toolRegistry,
|
|
@@ -325,7 +306,7 @@ export function makeHandler(overrides?: {
|
|
|
325
306
|
skillInputPipeline,
|
|
326
307
|
runner,
|
|
327
308
|
);
|
|
328
|
-
return { handler, events, session, toolRegistry };
|
|
309
|
+
return { handler, events, session, toolRegistry, prompter };
|
|
329
310
|
}
|
|
330
311
|
|
|
331
312
|
/** 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);
|
|
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
6
|
-
|
|
6
|
+
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
7
7
|
import {
|
|
8
8
|
PermissionForwarder,
|
|
9
9
|
type PermissionForwarderDeps,
|
|
@@ -18,12 +18,11 @@ function makeDeps(
|
|
|
18
18
|
return {
|
|
19
19
|
forwardingDir: "/tmp/forwarding",
|
|
20
20
|
subagentSessionsDir: "/tmp/subagents",
|
|
21
|
-
logger: {
|
|
22
|
-
writeReviewLog: vi.fn(),
|
|
21
|
+
logger: { review: vi.fn(), debug: vi.fn() },
|
|
23
22
|
requestPermissionDecisionFromUi: vi
|
|
24
23
|
.fn()
|
|
25
24
|
.mockResolvedValue({ approved: true, state: "approved" as const }),
|
|
26
|
-
|
|
25
|
+
config: { current: () => ({ ...DEFAULT_EXTENSION_CONFIG }) },
|
|
27
26
|
...overrides,
|
|
28
27
|
};
|
|
29
28
|
}
|
|
@@ -271,7 +270,9 @@ describe("processInbox", () => {
|
|
|
271
270
|
forwardingDir,
|
|
272
271
|
events,
|
|
273
272
|
requestPermissionDecisionFromUi,
|
|
274
|
-
|
|
273
|
+
config: {
|
|
274
|
+
current: () => ({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: true }),
|
|
275
|
+
},
|
|
275
276
|
}),
|
|
276
277
|
);
|
|
277
278
|
|
|
@@ -50,7 +50,7 @@ function makeDeps(
|
|
|
50
50
|
): PermissionPrompterDeps {
|
|
51
51
|
return {
|
|
52
52
|
config: makeConfigReader(),
|
|
53
|
-
|
|
53
|
+
logger: { review: vi.fn() },
|
|
54
54
|
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
55
55
|
forwarder: { requestApproval: mockRequestApproval },
|
|
56
56
|
...overrides,
|
|
@@ -97,32 +97,32 @@ describe("PermissionPrompter", () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it("logs permission_request.auto_approved in yolo mode", async () => {
|
|
100
|
-
const
|
|
100
|
+
const logger = { review: vi.fn() };
|
|
101
101
|
const deps = makeDeps({
|
|
102
102
|
config: makeConfigReader({ yoloMode: true }),
|
|
103
|
-
|
|
103
|
+
logger,
|
|
104
104
|
});
|
|
105
105
|
const prompter = new PermissionPrompter(deps);
|
|
106
106
|
|
|
107
107
|
await prompter.prompt(makeCtx(false), makeDetails());
|
|
108
108
|
|
|
109
|
-
expect(
|
|
109
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
110
110
|
"permission_request.auto_approved",
|
|
111
111
|
expect.objectContaining({ requestId: "req-123" }),
|
|
112
112
|
);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it("does not log permission_request.waiting in yolo mode", async () => {
|
|
116
|
-
const
|
|
116
|
+
const logger = { review: vi.fn() };
|
|
117
117
|
const deps = makeDeps({
|
|
118
118
|
config: makeConfigReader({ yoloMode: true }),
|
|
119
|
-
|
|
119
|
+
logger,
|
|
120
120
|
});
|
|
121
121
|
const prompter = new PermissionPrompter(deps);
|
|
122
122
|
|
|
123
123
|
await prompter.prompt(makeCtx(false), makeDetails());
|
|
124
124
|
|
|
125
|
-
expect(
|
|
125
|
+
expect(logger.review).not.toHaveBeenCalledWith(
|
|
126
126
|
"permission_request.waiting",
|
|
127
127
|
expect.anything(),
|
|
128
128
|
);
|
|
@@ -144,18 +144,18 @@ describe("PermissionPrompter", () => {
|
|
|
144
144
|
|
|
145
145
|
describe("non-yolo path (UI present)", () => {
|
|
146
146
|
it("logs permission_request.waiting before calling confirmPermission", async () => {
|
|
147
|
-
const
|
|
147
|
+
const logger = { review: vi.fn() };
|
|
148
148
|
const approved: PermissionPromptDecision = {
|
|
149
149
|
approved: true,
|
|
150
150
|
state: "approved",
|
|
151
151
|
};
|
|
152
152
|
mockRequestApproval.mockResolvedValue(approved);
|
|
153
|
-
const deps = makeDeps({
|
|
153
|
+
const deps = makeDeps({ logger });
|
|
154
154
|
const prompter = new PermissionPrompter(deps);
|
|
155
155
|
|
|
156
156
|
await prompter.prompt(makeCtx(true), makeDetails());
|
|
157
157
|
|
|
158
|
-
const calls =
|
|
158
|
+
const calls = logger.review.mock.calls.map((c) => c[0] as string);
|
|
159
159
|
expect(
|
|
160
160
|
calls.indexOf("permission_request.waiting"),
|
|
161
161
|
).toBeGreaterThanOrEqual(0);
|
|
@@ -249,17 +249,17 @@ describe("PermissionPrompter", () => {
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
it("logs permission_request.approved when confirmPermission returns approved", async () => {
|
|
252
|
-
const
|
|
252
|
+
const logger = { review: vi.fn() };
|
|
253
253
|
mockRequestApproval.mockResolvedValue({
|
|
254
254
|
approved: true,
|
|
255
255
|
state: "approved",
|
|
256
256
|
});
|
|
257
|
-
const deps = makeDeps({
|
|
257
|
+
const deps = makeDeps({ logger });
|
|
258
258
|
const prompter = new PermissionPrompter(deps);
|
|
259
259
|
|
|
260
260
|
await prompter.prompt(makeCtx(true), makeDetails());
|
|
261
261
|
|
|
262
|
-
expect(
|
|
262
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
263
263
|
"permission_request.approved",
|
|
264
264
|
expect.objectContaining({
|
|
265
265
|
requestId: "req-123",
|
|
@@ -269,17 +269,17 @@ describe("PermissionPrompter", () => {
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
it("logs permission_request.denied when confirmPermission returns denied", async () => {
|
|
272
|
-
const
|
|
272
|
+
const logger = { review: vi.fn() };
|
|
273
273
|
mockRequestApproval.mockResolvedValue({
|
|
274
274
|
approved: false,
|
|
275
275
|
state: "denied",
|
|
276
276
|
});
|
|
277
|
-
const deps = makeDeps({
|
|
277
|
+
const deps = makeDeps({ logger });
|
|
278
278
|
const prompter = new PermissionPrompter(deps);
|
|
279
279
|
|
|
280
280
|
await prompter.prompt(makeCtx(true), makeDetails());
|
|
281
281
|
|
|
282
|
-
expect(
|
|
282
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
283
283
|
"permission_request.denied",
|
|
284
284
|
expect.objectContaining({
|
|
285
285
|
requestId: "req-123",
|
|
@@ -289,18 +289,18 @@ describe("PermissionPrompter", () => {
|
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
it("logs permission_request.denied with denialReason when present", async () => {
|
|
292
|
-
const
|
|
292
|
+
const logger = { review: vi.fn() };
|
|
293
293
|
mockRequestApproval.mockResolvedValue({
|
|
294
294
|
approved: false,
|
|
295
295
|
state: "denied_with_reason",
|
|
296
296
|
denialReason: "too sensitive",
|
|
297
297
|
});
|
|
298
|
-
const deps = makeDeps({
|
|
298
|
+
const deps = makeDeps({ logger });
|
|
299
299
|
const prompter = new PermissionPrompter(deps);
|
|
300
300
|
|
|
301
301
|
await prompter.prompt(makeCtx(true), makeDetails());
|
|
302
302
|
|
|
303
|
-
expect(
|
|
303
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
304
304
|
"permission_request.denied",
|
|
305
305
|
expect.objectContaining({
|
|
306
306
|
denialReason: "too sensitive",
|
|
@@ -406,12 +406,12 @@ describe("PermissionPrompter", () => {
|
|
|
406
406
|
|
|
407
407
|
describe("review log fields", () => {
|
|
408
408
|
it("includes all standard fields in the waiting log entry", async () => {
|
|
409
|
-
const
|
|
409
|
+
const logger = { review: vi.fn() };
|
|
410
410
|
mockRequestApproval.mockResolvedValue({
|
|
411
411
|
approved: true,
|
|
412
412
|
state: "approved",
|
|
413
413
|
});
|
|
414
|
-
const deps = makeDeps({
|
|
414
|
+
const deps = makeDeps({ logger });
|
|
415
415
|
const prompter = new PermissionPrompter(deps);
|
|
416
416
|
const details = makeDetails({
|
|
417
417
|
toolCallId: "tc-1",
|
|
@@ -424,7 +424,7 @@ describe("PermissionPrompter", () => {
|
|
|
424
424
|
|
|
425
425
|
await prompter.prompt(makeCtx(true), details);
|
|
426
426
|
|
|
427
|
-
expect(
|
|
427
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
428
428
|
"permission_request.waiting",
|
|
429
429
|
expect.objectContaining({
|
|
430
430
|
requestId: "req-123",
|
|
@@ -443,17 +443,17 @@ describe("PermissionPrompter", () => {
|
|
|
443
443
|
});
|
|
444
444
|
|
|
445
445
|
it("uses null for optional fields not present in details", async () => {
|
|
446
|
-
const
|
|
446
|
+
const logger = { review: vi.fn() };
|
|
447
447
|
mockRequestApproval.mockResolvedValue({
|
|
448
448
|
approved: true,
|
|
449
449
|
state: "approved",
|
|
450
450
|
});
|
|
451
|
-
const deps = makeDeps({
|
|
451
|
+
const deps = makeDeps({ logger });
|
|
452
452
|
const prompter = new PermissionPrompter(deps);
|
|
453
453
|
|
|
454
454
|
await prompter.prompt(makeCtx(true), makeDetails());
|
|
455
455
|
|
|
456
|
-
expect(
|
|
456
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
457
457
|
"permission_request.waiting",
|
|
458
458
|
expect.objectContaining({
|
|
459
459
|
toolCallId: null,
|
|
@@ -499,17 +499,17 @@ describe("PermissionPrompter", () => {
|
|
|
499
499
|
});
|
|
500
500
|
|
|
501
501
|
it("logs the outcome when confirmPermission resolves via forwarding", async () => {
|
|
502
|
-
const
|
|
502
|
+
const logger = { review: vi.fn() };
|
|
503
503
|
mockRequestApproval.mockResolvedValue({
|
|
504
504
|
approved: true,
|
|
505
505
|
state: "approved",
|
|
506
506
|
});
|
|
507
|
-
const deps = makeDeps({
|
|
507
|
+
const deps = makeDeps({ logger });
|
|
508
508
|
const prompter = new PermissionPrompter(deps);
|
|
509
509
|
|
|
510
510
|
await prompter.prompt(makeCtx(false), makeDetails());
|
|
511
511
|
|
|
512
|
-
expect(
|
|
512
|
+
expect(logger.review).toHaveBeenCalledWith(
|
|
513
513
|
"permission_request.approved",
|
|
514
514
|
expect.objectContaining({ requestId: "req-123" }),
|
|
515
515
|
);
|