@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,17 +1,10 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
// ──
|
|
3
|
+
// ── Injected mock ───────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
mockConfirmPermission: vi.fn(),
|
|
7
|
-
}));
|
|
5
|
+
const mockRequestApproval = vi.fn();
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
confirmPermission: mockConfirmPermission,
|
|
11
|
-
processForwardedPermissionRequests: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
// ── Imports (after mocks) ───────────────────────────────────────────────────
|
|
7
|
+
// ── Imports ─────────────────────────────────────────────────────────────────
|
|
15
8
|
|
|
16
9
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
10
|
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
@@ -51,12 +44,8 @@ function makeDeps(
|
|
|
51
44
|
return {
|
|
52
45
|
getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: false }),
|
|
53
46
|
writeReviewLog: vi.fn(),
|
|
54
|
-
subagentSessionsDir: "/sessions/subagents",
|
|
55
|
-
forwardingDir: "/sessions/permission-forwarding",
|
|
56
47
|
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
57
|
-
|
|
58
|
-
.fn()
|
|
59
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
48
|
+
forwarder: { requestApproval: mockRequestApproval },
|
|
60
49
|
...overrides,
|
|
61
50
|
};
|
|
62
51
|
}
|
|
@@ -65,7 +54,11 @@ function makeDeps(
|
|
|
65
54
|
|
|
66
55
|
describe("PermissionPrompter", () => {
|
|
67
56
|
beforeEach(() => {
|
|
68
|
-
|
|
57
|
+
mockRequestApproval.mockReset();
|
|
58
|
+
mockRequestApproval.mockResolvedValue({
|
|
59
|
+
approved: true,
|
|
60
|
+
state: "approved",
|
|
61
|
+
});
|
|
69
62
|
});
|
|
70
63
|
|
|
71
64
|
// ── Yolo-mode auto-approve ───────────────────────────────────────────────
|
|
@@ -89,7 +82,7 @@ describe("PermissionPrompter", () => {
|
|
|
89
82
|
state: "approved",
|
|
90
83
|
autoApproved: true,
|
|
91
84
|
});
|
|
92
|
-
expect(
|
|
85
|
+
expect(mockRequestApproval).not.toHaveBeenCalled();
|
|
93
86
|
expect(events.emit).not.toHaveBeenCalledWith(
|
|
94
87
|
"permissions:ui_prompt",
|
|
95
88
|
expect.anything(),
|
|
@@ -136,7 +129,7 @@ describe("PermissionPrompter", () => {
|
|
|
136
129
|
|
|
137
130
|
await prompter.prompt(makeCtx(true), makeDetails());
|
|
138
131
|
|
|
139
|
-
expect(
|
|
132
|
+
expect(mockRequestApproval).not.toHaveBeenCalled();
|
|
140
133
|
});
|
|
141
134
|
});
|
|
142
135
|
|
|
@@ -149,7 +142,7 @@ describe("PermissionPrompter", () => {
|
|
|
149
142
|
approved: true,
|
|
150
143
|
state: "approved",
|
|
151
144
|
};
|
|
152
|
-
|
|
145
|
+
mockRequestApproval.mockResolvedValue(approved);
|
|
153
146
|
const deps = makeDeps({ writeReviewLog });
|
|
154
147
|
const prompter = new PermissionPrompter(deps);
|
|
155
148
|
|
|
@@ -169,7 +162,7 @@ describe("PermissionPrompter", () => {
|
|
|
169
162
|
emit: vi.fn(),
|
|
170
163
|
on: vi.fn().mockReturnValue(() => undefined),
|
|
171
164
|
};
|
|
172
|
-
|
|
165
|
+
mockRequestApproval.mockResolvedValue({
|
|
173
166
|
approved: true,
|
|
174
167
|
state: "approved",
|
|
175
168
|
});
|
|
@@ -201,7 +194,7 @@ describe("PermissionPrompter", () => {
|
|
|
201
194
|
emit: vi.fn(),
|
|
202
195
|
on: vi.fn().mockReturnValue(() => undefined),
|
|
203
196
|
};
|
|
204
|
-
|
|
197
|
+
mockRequestApproval.mockResolvedValue({
|
|
205
198
|
approved: true,
|
|
206
199
|
state: "approved",
|
|
207
200
|
});
|
|
@@ -233,7 +226,7 @@ describe("PermissionPrompter", () => {
|
|
|
233
226
|
emit: vi.fn(),
|
|
234
227
|
on: vi.fn().mockReturnValue(() => undefined),
|
|
235
228
|
};
|
|
236
|
-
|
|
229
|
+
mockRequestApproval.mockResolvedValue({
|
|
237
230
|
approved: true,
|
|
238
231
|
state: "approved",
|
|
239
232
|
});
|
|
@@ -250,7 +243,7 @@ describe("PermissionPrompter", () => {
|
|
|
250
243
|
|
|
251
244
|
it("logs permission_request.approved when confirmPermission returns approved", async () => {
|
|
252
245
|
const writeReviewLog = vi.fn();
|
|
253
|
-
|
|
246
|
+
mockRequestApproval.mockResolvedValue({
|
|
254
247
|
approved: true,
|
|
255
248
|
state: "approved",
|
|
256
249
|
});
|
|
@@ -270,7 +263,7 @@ describe("PermissionPrompter", () => {
|
|
|
270
263
|
|
|
271
264
|
it("logs permission_request.denied when confirmPermission returns denied", async () => {
|
|
272
265
|
const writeReviewLog = vi.fn();
|
|
273
|
-
|
|
266
|
+
mockRequestApproval.mockResolvedValue({
|
|
274
267
|
approved: false,
|
|
275
268
|
state: "denied",
|
|
276
269
|
});
|
|
@@ -290,7 +283,7 @@ describe("PermissionPrompter", () => {
|
|
|
290
283
|
|
|
291
284
|
it("logs permission_request.denied with denialReason when present", async () => {
|
|
292
285
|
const writeReviewLog = vi.fn();
|
|
293
|
-
|
|
286
|
+
mockRequestApproval.mockResolvedValue({
|
|
294
287
|
approved: false,
|
|
295
288
|
state: "denied_with_reason",
|
|
296
289
|
denialReason: "too sensitive",
|
|
@@ -314,7 +307,7 @@ describe("PermissionPrompter", () => {
|
|
|
314
307
|
state: "denied_with_reason",
|
|
315
308
|
denialReason: "sensitive",
|
|
316
309
|
};
|
|
317
|
-
|
|
310
|
+
mockRequestApproval.mockResolvedValue(decision);
|
|
318
311
|
const deps = makeDeps();
|
|
319
312
|
const prompter = new PermissionPrompter(deps);
|
|
320
313
|
|
|
@@ -324,7 +317,7 @@ describe("PermissionPrompter", () => {
|
|
|
324
317
|
});
|
|
325
318
|
|
|
326
319
|
it("passes sessionLabel option to confirmPermission when present", async () => {
|
|
327
|
-
|
|
320
|
+
mockRequestApproval.mockResolvedValue({
|
|
328
321
|
approved: true,
|
|
329
322
|
state: "approved",
|
|
330
323
|
});
|
|
@@ -334,17 +327,16 @@ describe("PermissionPrompter", () => {
|
|
|
334
327
|
|
|
335
328
|
await prompter.prompt(makeCtx(true), details);
|
|
336
329
|
|
|
337
|
-
expect(
|
|
330
|
+
expect(mockRequestApproval).toHaveBeenCalledWith(
|
|
338
331
|
expect.anything(),
|
|
339
332
|
expect.any(String),
|
|
340
|
-
expect.anything(),
|
|
341
333
|
{ sessionLabel: "Yes, for 'read' tool" },
|
|
342
334
|
{ source: "tool_call", surface: "read", value: "read" },
|
|
343
335
|
);
|
|
344
336
|
});
|
|
345
337
|
|
|
346
338
|
it("passes the display fields (source/surface/value) to confirmPermission", async () => {
|
|
347
|
-
|
|
339
|
+
mockRequestApproval.mockResolvedValue({
|
|
348
340
|
approved: true,
|
|
349
341
|
state: "approved",
|
|
350
342
|
});
|
|
@@ -357,17 +349,16 @@ describe("PermissionPrompter", () => {
|
|
|
357
349
|
|
|
358
350
|
await prompter.prompt(makeCtx(false), details);
|
|
359
351
|
|
|
360
|
-
expect(
|
|
352
|
+
expect(mockRequestApproval).toHaveBeenCalledWith(
|
|
361
353
|
expect.anything(),
|
|
362
354
|
expect.any(String),
|
|
363
|
-
expect.anything(),
|
|
364
355
|
undefined,
|
|
365
356
|
{ source: "tool_call", surface: "bash", value: "git push" },
|
|
366
357
|
);
|
|
367
358
|
});
|
|
368
359
|
|
|
369
360
|
it("passes undefined options to confirmPermission when sessionLabel is absent", async () => {
|
|
370
|
-
|
|
361
|
+
mockRequestApproval.mockResolvedValue({
|
|
371
362
|
approved: true,
|
|
372
363
|
state: "approved",
|
|
373
364
|
});
|
|
@@ -376,17 +367,16 @@ describe("PermissionPrompter", () => {
|
|
|
376
367
|
|
|
377
368
|
await prompter.prompt(makeCtx(true), makeDetails());
|
|
378
369
|
|
|
379
|
-
expect(
|
|
370
|
+
expect(mockRequestApproval).toHaveBeenCalledWith(
|
|
380
371
|
expect.anything(),
|
|
381
372
|
expect.any(String),
|
|
382
|
-
expect.anything(),
|
|
383
373
|
undefined,
|
|
384
374
|
{ source: "tool_call", surface: "read", value: "read" },
|
|
385
375
|
);
|
|
386
376
|
});
|
|
387
377
|
|
|
388
378
|
it("passes the message from details to confirmPermission", async () => {
|
|
389
|
-
|
|
379
|
+
mockRequestApproval.mockResolvedValue({
|
|
390
380
|
approved: true,
|
|
391
381
|
state: "approved",
|
|
392
382
|
});
|
|
@@ -396,10 +386,9 @@ describe("PermissionPrompter", () => {
|
|
|
396
386
|
|
|
397
387
|
await prompter.prompt(makeCtx(true), details);
|
|
398
388
|
|
|
399
|
-
expect(
|
|
389
|
+
expect(mockRequestApproval).toHaveBeenCalledWith(
|
|
400
390
|
expect.anything(),
|
|
401
391
|
"Allow bash: git status?",
|
|
402
|
-
expect.anything(),
|
|
403
392
|
undefined,
|
|
404
393
|
{ source: "tool_call", surface: "read", value: "read" },
|
|
405
394
|
);
|
|
@@ -411,7 +400,7 @@ describe("PermissionPrompter", () => {
|
|
|
411
400
|
describe("review log fields", () => {
|
|
412
401
|
it("includes all standard fields in the waiting log entry", async () => {
|
|
413
402
|
const writeReviewLog = vi.fn();
|
|
414
|
-
|
|
403
|
+
mockRequestApproval.mockResolvedValue({
|
|
415
404
|
approved: true,
|
|
416
405
|
state: "approved",
|
|
417
406
|
});
|
|
@@ -448,7 +437,7 @@ describe("PermissionPrompter", () => {
|
|
|
448
437
|
|
|
449
438
|
it("uses null for optional fields not present in details", async () => {
|
|
450
439
|
const writeReviewLog = vi.fn();
|
|
451
|
-
|
|
440
|
+
mockRequestApproval.mockResolvedValue({
|
|
452
441
|
approved: true,
|
|
453
442
|
state: "approved",
|
|
454
443
|
});
|
|
@@ -479,13 +468,13 @@ describe("PermissionPrompter", () => {
|
|
|
479
468
|
approved: true,
|
|
480
469
|
state: "approved",
|
|
481
470
|
};
|
|
482
|
-
|
|
471
|
+
mockRequestApproval.mockResolvedValue(forwarded);
|
|
483
472
|
const deps = makeDeps();
|
|
484
473
|
const prompter = new PermissionPrompter(deps);
|
|
485
474
|
|
|
486
475
|
await prompter.prompt(makeCtx(false), makeDetails());
|
|
487
476
|
|
|
488
|
-
expect(
|
|
477
|
+
expect(mockRequestApproval).toHaveBeenCalled();
|
|
489
478
|
});
|
|
490
479
|
|
|
491
480
|
it("returns the decision from confirmPermission in the subagent path", async () => {
|
|
@@ -493,7 +482,7 @@ describe("PermissionPrompter", () => {
|
|
|
493
482
|
approved: false,
|
|
494
483
|
state: "denied",
|
|
495
484
|
};
|
|
496
|
-
|
|
485
|
+
mockRequestApproval.mockResolvedValue(forwarded);
|
|
497
486
|
const deps = makeDeps();
|
|
498
487
|
const prompter = new PermissionPrompter(deps);
|
|
499
488
|
|
|
@@ -504,7 +493,7 @@ describe("PermissionPrompter", () => {
|
|
|
504
493
|
|
|
505
494
|
it("logs the outcome when confirmPermission resolves via forwarding", async () => {
|
|
506
495
|
const writeReviewLog = vi.fn();
|
|
507
|
-
|
|
496
|
+
mockRequestApproval.mockResolvedValue({
|
|
508
497
|
approved: true,
|
|
509
498
|
state: "approved",
|
|
510
499
|
});
|
|
@@ -217,6 +217,79 @@ describe("PermissionSession", () => {
|
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
describe("resolve", () => {
|
|
221
|
+
it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
|
|
222
|
+
const pm = makePermissionManager();
|
|
223
|
+
mockCreatePermissionManagerForCwd.mockReturnValue(pm);
|
|
224
|
+
const { session } = createSession();
|
|
225
|
+
|
|
226
|
+
session.resolve("bash", { command: "ls" }, "agent-x");
|
|
227
|
+
|
|
228
|
+
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
229
|
+
"bash",
|
|
230
|
+
{ command: "ls" },
|
|
231
|
+
"agent-x",
|
|
232
|
+
[],
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("defaults agentName to undefined when omitted", () => {
|
|
237
|
+
const pm = makePermissionManager();
|
|
238
|
+
mockCreatePermissionManagerForCwd.mockReturnValue(pm);
|
|
239
|
+
const { session } = createSession();
|
|
240
|
+
|
|
241
|
+
session.resolve("read", { path: ".env" });
|
|
242
|
+
|
|
243
|
+
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
244
|
+
"read",
|
|
245
|
+
{ path: ".env" },
|
|
246
|
+
undefined,
|
|
247
|
+
[],
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("applies a recorded session approval on the next resolve", () => {
|
|
252
|
+
const pm = makePermissionManager();
|
|
253
|
+
mockCreatePermissionManagerForCwd.mockReturnValue(pm);
|
|
254
|
+
const { session } = createSession();
|
|
255
|
+
|
|
256
|
+
session.recordSessionApproval(SessionApproval.single("bash", "git *"));
|
|
257
|
+
session.resolve("bash", { command: "git status" });
|
|
258
|
+
|
|
259
|
+
const sessionRules = vi.mocked(pm.checkPermission).mock.calls[0][3];
|
|
260
|
+
expect(sessionRules).toHaveLength(1);
|
|
261
|
+
expect(sessionRules?.[0]).toMatchObject({
|
|
262
|
+
surface: "bash",
|
|
263
|
+
pattern: "git *",
|
|
264
|
+
action: "allow",
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("returns the PermissionManager's check result", () => {
|
|
269
|
+
const pm = makePermissionManager({
|
|
270
|
+
checkPermission: vi.fn().mockReturnValue({
|
|
271
|
+
state: "deny",
|
|
272
|
+
toolName: "bash",
|
|
273
|
+
source: "bash",
|
|
274
|
+
origin: "global",
|
|
275
|
+
matchedPattern: "rm *",
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
mockCreatePermissionManagerForCwd.mockReturnValue(pm);
|
|
279
|
+
const { session } = createSession();
|
|
280
|
+
|
|
281
|
+
const result = session.resolve("bash", { command: "rm -rf /" });
|
|
282
|
+
|
|
283
|
+
expect(result).toEqual({
|
|
284
|
+
state: "deny",
|
|
285
|
+
toolName: "bash",
|
|
286
|
+
source: "bash",
|
|
287
|
+
origin: "global",
|
|
288
|
+
matchedPattern: "rm *",
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
220
293
|
describe("activate and deactivate", () => {
|
|
221
294
|
it("stores the context on activate", () => {
|
|
222
295
|
const { session, forwarding } = createSession();
|
|
@@ -432,26 +505,25 @@ describe("PermissionSession", () => {
|
|
|
432
505
|
});
|
|
433
506
|
|
|
434
507
|
describe("infrastructure paths", () => {
|
|
435
|
-
it("
|
|
436
|
-
const { session } = createSession();
|
|
437
|
-
expect(session.getInfrastructureDirs()).toEqual([
|
|
438
|
-
"/test/agent",
|
|
439
|
-
"/test/agent/git",
|
|
440
|
-
]);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it("getInfrastructureReadPaths returns config paths", () => {
|
|
508
|
+
it("getInfrastructureReadDirs combines piInfrastructureDirs and piInfrastructureReadPaths", () => {
|
|
444
509
|
const runtimeDeps = makeRuntimeDeps();
|
|
445
510
|
(runtimeDeps.getConfig as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
446
511
|
piInfrastructureReadPaths: ["/extra/path"],
|
|
447
512
|
});
|
|
448
513
|
const { session } = createSession({ runtimeDeps });
|
|
449
|
-
expect(session.
|
|
514
|
+
expect(session.getInfrastructureReadDirs()).toEqual([
|
|
515
|
+
"/test/agent",
|
|
516
|
+
"/test/agent/git",
|
|
517
|
+
"/extra/path",
|
|
518
|
+
]);
|
|
450
519
|
});
|
|
451
520
|
|
|
452
|
-
it("
|
|
521
|
+
it("getInfrastructureReadDirs returns only piInfrastructureDirs when config omits the field", () => {
|
|
453
522
|
const { session } = createSession();
|
|
454
|
-
expect(session.
|
|
523
|
+
expect(session.getInfrastructureReadDirs()).toEqual([
|
|
524
|
+
"/test/agent",
|
|
525
|
+
"/test/agent/git",
|
|
526
|
+
]);
|
|
455
527
|
});
|
|
456
528
|
});
|
|
457
529
|
|
|
@@ -478,6 +550,26 @@ describe("PermissionSession", () => {
|
|
|
478
550
|
const { session } = createSession({ runtimeDeps });
|
|
479
551
|
expect(session.config).toBe(fakeConfig);
|
|
480
552
|
});
|
|
553
|
+
|
|
554
|
+
it("getToolPreviewLimits returns resolved preview limits from config", () => {
|
|
555
|
+
const runtimeDeps = makeRuntimeDeps();
|
|
556
|
+
(runtimeDeps.getConfig as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
557
|
+
toolInputPreviewMaxLength: 400,
|
|
558
|
+
toolTextSummaryMaxLength: 120,
|
|
559
|
+
});
|
|
560
|
+
const { session } = createSession({ runtimeDeps });
|
|
561
|
+
const limits = session.getToolPreviewLimits();
|
|
562
|
+
expect(limits.toolInputPreviewMaxLength).toBe(400);
|
|
563
|
+
expect(limits.toolTextSummaryMaxLength).toBe(120);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("getToolPreviewLimits falls back to built-in defaults when config omits fields", () => {
|
|
567
|
+
const { session } = createSession();
|
|
568
|
+
const limits = session.getToolPreviewLimits();
|
|
569
|
+
expect(limits.toolInputPreviewMaxLength).toBeGreaterThan(0);
|
|
570
|
+
expect(limits.toolTextSummaryMaxLength).toBeGreaterThan(0);
|
|
571
|
+
expect(limits.toolInputLogPreviewMaxLength).toBeGreaterThan(0);
|
|
572
|
+
});
|
|
481
573
|
});
|
|
482
574
|
|
|
483
575
|
describe("reload", () => {
|
|
@@ -532,6 +624,62 @@ describe("PermissionSession", () => {
|
|
|
532
624
|
});
|
|
533
625
|
});
|
|
534
626
|
|
|
627
|
+
describe("canConfirm", () => {
|
|
628
|
+
it("returns true when context is active and canPrompt returns true", () => {
|
|
629
|
+
const { session } = createSession();
|
|
630
|
+
session.activate(makeCtx());
|
|
631
|
+
expect(session.canConfirm()).toBe(true);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("returns false when no context is active", () => {
|
|
635
|
+
const { session } = createSession();
|
|
636
|
+
expect(session.canConfirm()).toBe(false);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("returns false when canPrompt returns false", () => {
|
|
640
|
+
const runtimeDeps = makeRuntimeDeps();
|
|
641
|
+
(
|
|
642
|
+
runtimeDeps.canRequestPermissionConfirmation as ReturnType<typeof vi.fn>
|
|
643
|
+
).mockReturnValue(false);
|
|
644
|
+
const { session } = createSession({ runtimeDeps });
|
|
645
|
+
session.activate(makeCtx());
|
|
646
|
+
expect(session.canConfirm()).toBe(false);
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe("promptPermission", () => {
|
|
651
|
+
it("delegates to prompt with stored context", async () => {
|
|
652
|
+
const { session, runtimeDeps } = createSession();
|
|
653
|
+
const ctx = makeCtx();
|
|
654
|
+
session.activate(ctx);
|
|
655
|
+
const details = {
|
|
656
|
+
requestId: "req-1",
|
|
657
|
+
source: "tool_call" as const,
|
|
658
|
+
agentName: null,
|
|
659
|
+
message: "Allow?",
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const result = await session.promptPermission(details);
|
|
663
|
+
|
|
664
|
+
expect(runtimeDeps.promptPermission).toHaveBeenCalledWith(ctx, details);
|
|
665
|
+
expect(result).toEqual({ approved: true, state: "approved" });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("throws when no context is active", async () => {
|
|
669
|
+
const { session } = createSession();
|
|
670
|
+
const details = {
|
|
671
|
+
requestId: "req-1",
|
|
672
|
+
source: "tool_call" as const,
|
|
673
|
+
agentName: null,
|
|
674
|
+
message: "Allow?",
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
await expect(session.promptPermission(details)).rejects.toThrow(
|
|
678
|
+
"promptPermission called before the session was activated",
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
535
683
|
describe("canPrompt", () => {
|
|
536
684
|
it("delegates to runtimeDeps.canRequestPermissionConfirmation", () => {
|
|
537
685
|
const { session, runtimeDeps } = createSession();
|
|
@@ -573,19 +721,4 @@ describe("PermissionSession", () => {
|
|
|
573
721
|
expect(result).toEqual({ approved: true, state: "approved" });
|
|
574
722
|
});
|
|
575
723
|
});
|
|
576
|
-
|
|
577
|
-
describe("createPermissionRequestId", () => {
|
|
578
|
-
it("starts with the given prefix", () => {
|
|
579
|
-
const { session } = createSession();
|
|
580
|
-
const id = session.createPermissionRequestId("skill-input");
|
|
581
|
-
expect(id.startsWith("skill-input-")).toBe(true);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it("generates unique IDs on repeated calls", () => {
|
|
585
|
-
const { session } = createSession();
|
|
586
|
-
const id1 = session.createPermissionRequestId("test");
|
|
587
|
-
const id2 = session.createPermissionRequestId("test");
|
|
588
|
-
expect(id1).not.toBe(id2);
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
724
|
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PermissionManager } from "#src/permission-manager";
|
|
3
|
+
import { LocalPermissionsService } from "#src/permissions-service";
|
|
4
|
+
import type { Ruleset } from "#src/rule";
|
|
5
|
+
import type { SessionRules } from "#src/session-rules";
|
|
6
|
+
import type {
|
|
7
|
+
ToolInputFormatter,
|
|
8
|
+
ToolInputFormatterRegistry,
|
|
9
|
+
} from "#src/tool-input-formatter-registry";
|
|
10
|
+
|
|
11
|
+
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
12
|
+
|
|
13
|
+
// ── input-normalizer stub ──────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const mockBuildInputForSurface = vi.hoisted(() =>
|
|
16
|
+
vi.fn<(surface: string, value?: string) => unknown>(),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
vi.mock("#src/input-normalizer", () => ({
|
|
20
|
+
buildInputForSurface: mockBuildInputForSurface,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function makePermissionManager(): PermissionManager {
|
|
26
|
+
return {
|
|
27
|
+
checkPermission: vi
|
|
28
|
+
.fn<PermissionManager["checkPermission"]>()
|
|
29
|
+
.mockReturnValue(makeCheckResult()),
|
|
30
|
+
getToolPermission: vi
|
|
31
|
+
.fn<PermissionManager["getToolPermission"]>()
|
|
32
|
+
.mockReturnValue("allow"),
|
|
33
|
+
} as unknown as PermissionManager;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeSessionRules(rules: Ruleset = []): SessionRules {
|
|
37
|
+
return {
|
|
38
|
+
getRuleset: vi.fn<SessionRules["getRuleset"]>().mockReturnValue(rules),
|
|
39
|
+
} as unknown as SessionRules;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeFormatterRegistry(): ToolInputFormatterRegistry {
|
|
43
|
+
return {
|
|
44
|
+
register: vi
|
|
45
|
+
.fn<ToolInputFormatterRegistry["register"]>()
|
|
46
|
+
.mockReturnValue(vi.fn()),
|
|
47
|
+
} as unknown as ToolInputFormatterRegistry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeService(overrides?: {
|
|
51
|
+
permissionManager?: PermissionManager;
|
|
52
|
+
sessionRules?: SessionRules;
|
|
53
|
+
formatterRegistry?: ToolInputFormatterRegistry;
|
|
54
|
+
}) {
|
|
55
|
+
const permissionManager =
|
|
56
|
+
overrides?.permissionManager ?? makePermissionManager();
|
|
57
|
+
const sessionRules = overrides?.sessionRules ?? makeSessionRules();
|
|
58
|
+
const formatterRegistry =
|
|
59
|
+
overrides?.formatterRegistry ?? makeFormatterRegistry();
|
|
60
|
+
const service = new LocalPermissionsService(
|
|
61
|
+
permissionManager,
|
|
62
|
+
sessionRules,
|
|
63
|
+
formatterRegistry,
|
|
64
|
+
);
|
|
65
|
+
return { service, permissionManager, sessionRules, formatterRegistry };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── tests ──────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockBuildInputForSurface.mockReset();
|
|
72
|
+
mockBuildInputForSurface.mockReturnValue({ type: "tool-input" });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("checkPermission", () => {
|
|
76
|
+
it("builds the surface input from surface and value", () => {
|
|
77
|
+
const { service } = makeService();
|
|
78
|
+
service.checkPermission("bash", "echo hi");
|
|
79
|
+
expect(mockBuildInputForSurface).toHaveBeenCalledWith("bash", "echo hi");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("builds the surface input with undefined value when value is omitted", () => {
|
|
83
|
+
const { service } = makeService();
|
|
84
|
+
service.checkPermission("read");
|
|
85
|
+
expect(mockBuildInputForSurface).toHaveBeenCalledWith("read", undefined);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("calls permissionManager.checkPermission with surface, built input, agentName, and current ruleset", () => {
|
|
89
|
+
const ruleset: Ruleset = [
|
|
90
|
+
{ surface: "bash", pattern: "*", action: "allow", origin: "global" },
|
|
91
|
+
];
|
|
92
|
+
const builtInput = { type: "bash-input" };
|
|
93
|
+
mockBuildInputForSurface.mockReturnValue(builtInput);
|
|
94
|
+
const { service, permissionManager, sessionRules } = makeService({
|
|
95
|
+
sessionRules: makeSessionRules(ruleset),
|
|
96
|
+
});
|
|
97
|
+
service.checkPermission("bash", "echo hi", "my-agent");
|
|
98
|
+
expect(permissionManager.checkPermission).toHaveBeenCalledWith(
|
|
99
|
+
"bash",
|
|
100
|
+
builtInput,
|
|
101
|
+
"my-agent",
|
|
102
|
+
ruleset,
|
|
103
|
+
);
|
|
104
|
+
void sessionRules; // used indirectly
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns the result from permissionManager.checkPermission", () => {
|
|
108
|
+
const expected = makeCheckResult({ state: "deny", toolName: "bash" });
|
|
109
|
+
const { service, permissionManager } = makeService();
|
|
110
|
+
vi.mocked(permissionManager.checkPermission).mockReturnValue(expected);
|
|
111
|
+
const result = service.checkPermission("bash", "rm -rf /");
|
|
112
|
+
expect(result).toBe(expected);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("getToolPermission", () => {
|
|
117
|
+
it("delegates to permissionManager.getToolPermission", () => {
|
|
118
|
+
const { service, permissionManager } = makeService();
|
|
119
|
+
vi.mocked(permissionManager.getToolPermission).mockReturnValue("deny");
|
|
120
|
+
const result = service.getToolPermission("write", "my-agent");
|
|
121
|
+
expect(permissionManager.getToolPermission).toHaveBeenCalledWith(
|
|
122
|
+
"write",
|
|
123
|
+
"my-agent",
|
|
124
|
+
);
|
|
125
|
+
expect(result).toBe("deny");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("omits agentName when not provided", () => {
|
|
129
|
+
const { service, permissionManager } = makeService();
|
|
130
|
+
service.getToolPermission("read");
|
|
131
|
+
expect(permissionManager.getToolPermission).toHaveBeenCalledWith(
|
|
132
|
+
"read",
|
|
133
|
+
undefined,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("registerToolInputFormatter", () => {
|
|
139
|
+
it("delegates to formatterRegistry.register and returns the unsubscribe function", () => {
|
|
140
|
+
const unsub = vi.fn();
|
|
141
|
+
const { service, formatterRegistry } = makeService();
|
|
142
|
+
vi.mocked(formatterRegistry.register).mockReturnValue(unsub);
|
|
143
|
+
const formatter: ToolInputFormatter = vi.fn();
|
|
144
|
+
const result = service.registerToolInputFormatter("my-tool", formatter);
|
|
145
|
+
expect(formatterRegistry.register).toHaveBeenCalledWith(
|
|
146
|
+
"my-tool",
|
|
147
|
+
formatter,
|
|
148
|
+
);
|
|
149
|
+
expect(result).toBe(unsub);
|
|
150
|
+
});
|
|
151
|
+
});
|
package/test/runtime.test.ts
CHANGED
|
@@ -49,10 +49,6 @@ vi.mock("../src/config-reporter", () => ({
|
|
|
49
49
|
buildResolvedConfigLogEntry: mockBuildResolvedConfigLogEntry,
|
|
50
50
|
}));
|
|
51
51
|
|
|
52
|
-
vi.mock("../src/forwarded-permissions/polling", () => ({
|
|
53
|
-
processForwardedPermissionRequests: vi.fn().mockResolvedValue(undefined),
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
52
|
vi.mock("../src/subagent-context", () => ({
|
|
57
53
|
isSubagentExecutionContext: vi.fn().mockReturnValue(false),
|
|
58
54
|
}));
|