@gotgenes/pi-permission-system 5.3.3 → 5.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.
@@ -205,63 +205,6 @@ describe("handleToolCall", () => {
205
205
  );
206
206
  expect(result).toMatchObject({ block: true });
207
207
  });
208
-
209
- it("blocks when tool ask has no UI available", async () => {
210
- const deps = makeDeps({
211
- runtime: makeRuntime({
212
- permissionManager: {
213
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
214
- } as unknown as ExtensionRuntime["permissionManager"],
215
- }),
216
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
217
- });
218
- const result = await handleToolCall(
219
- deps,
220
- makeToolCallEvent("read"),
221
- makeCtx(),
222
- );
223
- expect(result).toMatchObject({ block: true });
224
- });
225
-
226
- it("allows when user approves the ask prompt", async () => {
227
- const deps = makeDeps({
228
- runtime: makeRuntime({
229
- permissionManager: {
230
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
231
- } as unknown as ExtensionRuntime["permissionManager"],
232
- }),
233
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
234
- promptPermission: vi
235
- .fn()
236
- .mockResolvedValue({ approved: true, state: "approved" }),
237
- });
238
- const result = await handleToolCall(
239
- deps,
240
- makeToolCallEvent("read"),
241
- makeCtx(),
242
- );
243
- expect(result).toEqual({});
244
- });
245
-
246
- it("blocks when user denies the ask prompt", async () => {
247
- const deps = makeDeps({
248
- runtime: makeRuntime({
249
- permissionManager: {
250
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
251
- } as unknown as ExtensionRuntime["permissionManager"],
252
- }),
253
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
254
- promptPermission: vi
255
- .fn()
256
- .mockResolvedValue({ approved: false, state: "denied" }),
257
- });
258
- const result = await handleToolCall(
259
- deps,
260
- makeToolCallEvent("read"),
261
- makeCtx(),
262
- );
263
- expect(result).toMatchObject({ block: true });
264
- });
265
208
  });
266
209
 
267
210
  // ── skill-read gate ────────────────────────────────────────────────────────
@@ -337,211 +280,6 @@ describe("handleToolCall — external-directory gate", () => {
337
280
  const result = await handleToolCall(deps, event, makeCtx());
338
281
  expect(result).toMatchObject({ block: true });
339
282
  });
340
-
341
- it("allows when session has an existing approval for the external path", async () => {
342
- const deps = makeDeps({
343
- runtime: makeRuntime({
344
- sessionRules: {
345
- approve: vi.fn(),
346
- getRuleset: vi.fn().mockReturnValue([
347
- {
348
- surface: "external_directory",
349
- pattern: "/outside/project/*",
350
- action: "allow",
351
- },
352
- ]),
353
- clear: vi.fn(),
354
- } as unknown as ExtensionRuntime["sessionRules"],
355
- }),
356
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
357
- });
358
- const event = {
359
- type: "tool_call",
360
- toolCallId: "tc-session",
361
- name: "read",
362
- input: { path: "/outside/project/file.ts" },
363
- };
364
- const result = await handleToolCall(deps, event, makeCtx());
365
- expect(result).toEqual({});
366
- });
367
-
368
- it("approves session when user selects approved_for_session", async () => {
369
- const sessionRules = {
370
- approve: vi.fn(),
371
- getRuleset: vi.fn().mockReturnValue([]),
372
- clear: vi.fn(),
373
- } as unknown as ExtensionRuntime["sessionRules"];
374
- const deps = makeDeps({
375
- runtime: makeRuntime({
376
- permissionManager: {
377
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
378
- } as unknown as ExtensionRuntime["permissionManager"],
379
- sessionRules,
380
- }),
381
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
382
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
383
- promptPermission: vi
384
- .fn()
385
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
386
- });
387
- const event = {
388
- type: "tool_call",
389
- toolCallId: "tc-sess-approve",
390
- name: "read",
391
- input: { path: "/outside/project/file.ts" },
392
- };
393
- await handleToolCall(deps, event, makeCtx());
394
- expect(sessionRules.approve).toHaveBeenCalledWith(
395
- "external_directory",
396
- expect.any(String),
397
- );
398
- });
399
- });
400
-
401
- // ── Pi infrastructure read bypass ───────────────────────────────────────────
402
-
403
- describe("handleToolCall — Pi infrastructure read bypass", () => {
404
- const infraPath = "/test/agent/git/some-package/SKILL.md";
405
-
406
- it("skips external-directory gate for read tool targeting an infra dir", async () => {
407
- const deps = makeDeps({
408
- runtime: makeRuntime({
409
- permissionManager: {
410
- checkPermission: vi
411
- .fn()
412
- .mockReturnValue(makePermissionResult("allow")),
413
- } as unknown as ExtensionRuntime["permissionManager"],
414
- }),
415
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
416
- });
417
- const event = {
418
- type: "tool_call",
419
- toolCallId: "tc-infra-read",
420
- name: "read",
421
- input: { path: infraPath },
422
- };
423
- const result = await handleToolCall(deps, event, makeCtx());
424
- expect(result).toEqual({});
425
- // external_directory permission check must NOT have been called.
426
- const checkPermission = deps.runtime.permissionManager
427
- .checkPermission as ReturnType<typeof vi.fn>;
428
- const calls = checkPermission.mock.calls as Array<[string, ...unknown[]]>;
429
- const extDirCalls = calls.filter(
430
- ([surface]) => surface === "external_directory",
431
- );
432
- expect(extDirCalls).toHaveLength(0);
433
- });
434
-
435
- it("does NOT skip gate for write tool targeting an infra dir", async () => {
436
- const deps = makeDeps({
437
- runtime: makeRuntime({
438
- permissionManager: {
439
- checkPermission: vi
440
- .fn()
441
- .mockReturnValue(makePermissionResult("deny")),
442
- } as unknown as ExtensionRuntime["permissionManager"],
443
- }),
444
- getAllTools: vi.fn().mockReturnValue([{ name: "write" }]),
445
- });
446
- const event = {
447
- type: "tool_call",
448
- toolCallId: "tc-infra-write",
449
- name: "write",
450
- input: { path: infraPath },
451
- };
452
- const result = await handleToolCall(deps, event, makeCtx());
453
- expect(result).toMatchObject({ block: true });
454
- const checkPermission = deps.runtime.permissionManager
455
- .checkPermission as ReturnType<typeof vi.fn>;
456
- const calls = checkPermission.mock.calls as Array<[string, ...unknown[]]>;
457
- const extDirCalls = calls.filter(
458
- ([surface]) => surface === "external_directory",
459
- );
460
- expect(extDirCalls.length).toBeGreaterThan(0);
461
- });
462
-
463
- it("does NOT skip gate for read tool targeting a non-infra external path", async () => {
464
- const deps = makeDeps({
465
- runtime: makeRuntime({
466
- permissionManager: {
467
- checkPermission: vi
468
- .fn()
469
- .mockReturnValue(makePermissionResult("deny")),
470
- } as unknown as ExtensionRuntime["permissionManager"],
471
- }),
472
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
473
- });
474
- const event = {
475
- type: "tool_call",
476
- toolCallId: "tc-non-infra",
477
- name: "read",
478
- input: { path: "/etc/passwd" },
479
- };
480
- const result = await handleToolCall(deps, event, makeCtx());
481
- expect(result).toMatchObject({ block: true });
482
- const checkPermission = deps.runtime.permissionManager
483
- .checkPermission as ReturnType<typeof vi.fn>;
484
- const calls = checkPermission.mock.calls as Array<[string, ...unknown[]]>;
485
- const extDirCalls = calls.filter(
486
- ([surface]) => surface === "external_directory",
487
- );
488
- expect(extDirCalls.length).toBeGreaterThan(0);
489
- });
490
-
491
- it("writes a review log entry when bypassing the gate", async () => {
492
- const writeReviewLog = vi.fn();
493
- const deps = makeDeps({
494
- runtime: makeRuntime({ writeReviewLog }),
495
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
496
- });
497
- const event = {
498
- type: "tool_call",
499
- toolCallId: "tc-infra-log",
500
- name: "read",
501
- input: { path: infraPath },
502
- };
503
- await handleToolCall(deps, event, makeCtx());
504
- expect(writeReviewLog).toHaveBeenCalledWith(
505
- "permission_request.infrastructure_auto_allowed",
506
- expect.objectContaining({ toolName: "read", path: infraPath }),
507
- );
508
- });
509
-
510
- it("respects config piInfrastructureReadPaths for bypass", async () => {
511
- const customInfraPath = "/custom/infra/packages/SKILL.md";
512
- const deps = makeDeps({
513
- runtime: makeRuntime({
514
- piInfrastructureDirs: [],
515
- config: {
516
- debugLog: false,
517
- permissionReviewLog: true,
518
- yoloMode: false,
519
- piInfrastructureReadPaths: ["/custom/infra/packages"],
520
- },
521
- permissionManager: {
522
- checkPermission: vi
523
- .fn()
524
- .mockReturnValue(makePermissionResult("allow")),
525
- } as unknown as ExtensionRuntime["permissionManager"],
526
- }),
527
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
528
- });
529
- const event = {
530
- type: "tool_call",
531
- toolCallId: "tc-config-infra",
532
- name: "read",
533
- input: { path: customInfraPath },
534
- };
535
- const result = await handleToolCall(deps, event, makeCtx());
536
- expect(result).toEqual({});
537
- const checkPermission = deps.runtime.permissionManager
538
- .checkPermission as ReturnType<typeof vi.fn>;
539
- const calls = checkPermission.mock.calls as Array<[string, ...unknown[]]>;
540
- const extDirCalls = calls.filter(
541
- ([surface]) => surface === "external_directory",
542
- );
543
- expect(extDirCalls).toHaveLength(0);
544
- });
545
283
  });
546
284
 
547
285
  // ── bash external-directory gate ──────────────────────────────────────────
@@ -567,246 +305,4 @@ describe("handleToolCall — bash external-directory gate", () => {
567
305
  const result = await handleToolCall(deps, event, makeCtx());
568
306
  expect(result).toMatchObject({ block: true });
569
307
  });
570
-
571
- it("skips bash external gate when all referenced paths are session-approved", async () => {
572
- const deps = makeDeps({
573
- runtime: makeRuntime({
574
- sessionRules: {
575
- approve: vi.fn(),
576
- // /outside/project/* covers /outside/project/file.ts
577
- getRuleset: vi.fn().mockReturnValue([
578
- {
579
- surface: "external_directory",
580
- pattern: "/outside/project/*",
581
- action: "allow",
582
- },
583
- ]),
584
- clear: vi.fn(),
585
- } as unknown as ExtensionRuntime["sessionRules"],
586
- }),
587
- getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
588
- });
589
- const event = {
590
- type: "tool_call",
591
- toolCallId: "tc-bash-sess",
592
- name: "bash",
593
- input: { command: "cat /outside/project/file.ts" },
594
- };
595
- const result = await handleToolCall(deps, event, makeCtx());
596
- expect(result).toEqual({});
597
- });
598
- });
599
-
600
- // ── session approval ───────────────────────────────────────────────
601
-
602
- describe("handleToolCall — session-hit detection (normal gate)", () => {
603
- it("skips gate and logs session_approved when bash check returns source=session", async () => {
604
- const sessionRules = {
605
- approve: vi.fn(),
606
- getRuleset: vi.fn().mockReturnValue([]),
607
- clear: vi.fn(),
608
- } as unknown as ExtensionRuntime["sessionRules"];
609
- const deps = makeDeps({
610
- runtime: makeRuntime({
611
- permissionManager: {
612
- checkPermission: vi.fn().mockReturnValue({
613
- state: "allow",
614
- toolName: "bash",
615
- source: "session",
616
- command: "git status",
617
- matchedPattern: "git *",
618
- }),
619
- } as unknown as ExtensionRuntime["permissionManager"],
620
- sessionRules,
621
- }),
622
- });
623
- const event = makeToolCallEvent("bash", {
624
- input: { command: "git status" },
625
- });
626
- const result = await handleToolCall(deps, event, makeCtx());
627
- expect(result).toEqual({});
628
- expect(deps.runtime.writeReviewLog).toHaveBeenCalledWith(
629
- "permission_request.session_approved",
630
- expect.objectContaining({
631
- resolution: "session_approved",
632
- toolName: "bash",
633
- }),
634
- );
635
- });
636
-
637
- it("skips gate and logs session_approved when mcp check returns source=session", async () => {
638
- const deps = makeDeps({
639
- runtime: makeRuntime({
640
- permissionManager: {
641
- checkPermission: vi.fn().mockReturnValue({
642
- state: "allow",
643
- toolName: "mcp",
644
- source: "session",
645
- target: "exa:search",
646
- matchedPattern: "exa:*",
647
- }),
648
- } as unknown as ExtensionRuntime["permissionManager"],
649
- }),
650
- getAllTools: vi.fn().mockReturnValue([{ name: "mcp" }]),
651
- });
652
- const event = makeToolCallEvent("mcp", { input: { tool: "exa:search" } });
653
- const result = await handleToolCall(deps, event, makeCtx());
654
- expect(result).toEqual({});
655
- expect(deps.runtime.writeReviewLog).toHaveBeenCalledWith(
656
- "permission_request.session_approved",
657
- expect.objectContaining({
658
- resolution: "session_approved",
659
- toolName: "mcp",
660
- }),
661
- );
662
- });
663
-
664
- it("does NOT call sessionRules.approve when source is session (already recorded)", async () => {
665
- const sessionRules = {
666
- approve: vi.fn(),
667
- getRuleset: vi.fn().mockReturnValue([]),
668
- clear: vi.fn(),
669
- } as unknown as ExtensionRuntime["sessionRules"];
670
- const deps = makeDeps({
671
- runtime: makeRuntime({
672
- permissionManager: {
673
- checkPermission: vi.fn().mockReturnValue({
674
- state: "allow",
675
- toolName: "bash",
676
- source: "session",
677
- command: "git status",
678
- matchedPattern: "git *",
679
- }),
680
- } as unknown as ExtensionRuntime["permissionManager"],
681
- sessionRules,
682
- }),
683
- });
684
- const event = makeToolCallEvent("bash", {
685
- input: { command: "git status" },
686
- });
687
- await handleToolCall(deps, event, makeCtx());
688
- expect(sessionRules.approve).not.toHaveBeenCalled();
689
- });
690
- });
691
-
692
- describe("handleToolCall — session recording on approved_for_session", () => {
693
- it("records bash session approval with suggestBashPattern result", async () => {
694
- const sessionRules = {
695
- approve: vi.fn(),
696
- getRuleset: vi.fn().mockReturnValue([]),
697
- clear: vi.fn(),
698
- } as unknown as ExtensionRuntime["sessionRules"];
699
- const deps = makeDeps({
700
- runtime: makeRuntime({
701
- permissionManager: {
702
- checkPermission: vi.fn().mockReturnValue({
703
- state: "ask",
704
- toolName: "bash",
705
- source: "bash",
706
- command: "git status",
707
- }),
708
- } as unknown as ExtensionRuntime["permissionManager"],
709
- sessionRules,
710
- }),
711
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
712
- promptPermission: vi
713
- .fn()
714
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
715
- });
716
- const event = makeToolCallEvent("bash", {
717
- input: { command: "git status" },
718
- });
719
- await handleToolCall(deps, event, makeCtx());
720
- // git arity=2: "git status" prefix covers all 2 tokens → trailing wildcard.
721
- expect(sessionRules.approve).toHaveBeenCalledWith("bash", "git status*");
722
- });
723
-
724
- it("records mcp session approval with suggestMcpPattern result", async () => {
725
- const sessionRules = {
726
- approve: vi.fn(),
727
- getRuleset: vi.fn().mockReturnValue([]),
728
- clear: vi.fn(),
729
- } as unknown as ExtensionRuntime["sessionRules"];
730
- const deps = makeDeps({
731
- runtime: makeRuntime({
732
- permissionManager: {
733
- checkPermission: vi.fn().mockReturnValue({
734
- state: "ask",
735
- toolName: "mcp",
736
- source: "mcp",
737
- target: "exa:search",
738
- }),
739
- } as unknown as ExtensionRuntime["permissionManager"],
740
- sessionRules,
741
- }),
742
- getAllTools: vi.fn().mockReturnValue([{ name: "mcp" }]),
743
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
744
- promptPermission: vi
745
- .fn()
746
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
747
- });
748
- const event = makeToolCallEvent("mcp", { input: { tool: "exa:search" } });
749
- await handleToolCall(deps, event, makeCtx());
750
- expect(sessionRules.approve).toHaveBeenCalledWith("mcp", "exa:*");
751
- });
752
-
753
- it("records tool session approval with * pattern for read surface", async () => {
754
- const sessionRules = {
755
- approve: vi.fn(),
756
- getRuleset: vi.fn().mockReturnValue([]),
757
- clear: vi.fn(),
758
- } as unknown as ExtensionRuntime["sessionRules"];
759
- const deps = makeDeps({
760
- runtime: makeRuntime({
761
- permissionManager: {
762
- checkPermission: vi.fn().mockReturnValue({
763
- state: "ask",
764
- toolName: "read",
765
- source: "tool",
766
- origin: "builtin",
767
- }),
768
- } as unknown as ExtensionRuntime["permissionManager"],
769
- sessionRules,
770
- }),
771
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
772
- promptPermission: vi
773
- .fn()
774
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
775
- });
776
- const event = makeToolCallEvent("read", {
777
- input: { path: "/test/project/foo.ts" },
778
- });
779
- await handleToolCall(deps, event, makeCtx());
780
- expect(sessionRules.approve).toHaveBeenCalledWith("read", "*");
781
- });
782
-
783
- it("does NOT call sessionRules.approve when user approves once (not for session)", async () => {
784
- const sessionRules = {
785
- approve: vi.fn(),
786
- getRuleset: vi.fn().mockReturnValue([]),
787
- clear: vi.fn(),
788
- } as unknown as ExtensionRuntime["sessionRules"];
789
- const deps = makeDeps({
790
- runtime: makeRuntime({
791
- permissionManager: {
792
- checkPermission: vi.fn().mockReturnValue({
793
- state: "ask",
794
- toolName: "bash",
795
- source: "bash",
796
- command: "git status",
797
- }),
798
- } as unknown as ExtensionRuntime["permissionManager"],
799
- sessionRules,
800
- }),
801
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
802
- promptPermission: vi
803
- .fn()
804
- .mockResolvedValue({ approved: true, state: "approved" }),
805
- });
806
- const event = makeToolCallEvent("bash", {
807
- input: { command: "git status" },
808
- });
809
- await handleToolCall(deps, event, makeCtx());
810
- expect(sessionRules.approve).not.toHaveBeenCalled();
811
- });
812
308
  });