@gotgenes/pi-permission-system 3.11.0 → 4.0.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.
@@ -31,10 +31,7 @@ import {
31
31
  SUBAGENT_ENV_HINT_KEYS,
32
32
  SUBAGENT_PARENT_SESSION_ENV_KEY,
33
33
  } from "../src/permission-forwarding";
34
- import {
35
- normalizeRawPermission,
36
- PermissionManager,
37
- } from "../src/permission-manager";
34
+ import { PermissionManager } from "../src/permission-manager";
38
35
  import {
39
36
  findSkillPathMatch,
40
37
  parseAllSkillPromptSections,
@@ -518,20 +515,7 @@ test("Before-agent-start cache dedupes unchanged active-tool exposure and prompt
518
515
 
519
516
  test("Before-agent-start prompt cache invalidates on permission changes while runtime enforcement stays authoritative", () => {
520
517
  const { manager, globalConfigPath, cleanup } = createManager({
521
- defaultPolicy: {
522
- tools: "allow",
523
- bash: "allow",
524
- mcp: "allow",
525
- skills: "allow",
526
- special: "allow",
527
- },
528
- tools: {
529
- write: "deny",
530
- },
531
- bash: {},
532
- mcp: {},
533
- skills: {},
534
- special: {},
518
+ permission: { "*": "allow", write: "deny" },
535
519
  });
536
520
 
537
521
  try {
@@ -551,22 +535,7 @@ test("Before-agent-start prompt cache invalidates on permission changes while ru
551
535
  assert.equal(manager.checkPermission("write", {}, undefined).state, "deny");
552
536
 
553
537
  const updatedConfig = `${JSON.stringify(
554
- {
555
- defaultPolicy: {
556
- tools: "allow",
557
- bash: "allow",
558
- mcp: "allow",
559
- skills: "allow",
560
- special: "allow",
561
- },
562
- tools: {
563
- write: "allow",
564
- },
565
- bash: {},
566
- mcp: {},
567
- skills: {},
568
- special: {},
569
- },
538
+ { permission: { "*": "allow", write: "allow" } },
570
539
  null,
571
540
  2,
572
541
  )}\n`;
@@ -637,10 +606,6 @@ test("Permission-system logger respects debug toggle and keeps review log enable
637
606
  assert.equal(reviewWarning, undefined);
638
607
  assert.equal(existsSync(debugLogPath), false);
639
608
  assert.equal(existsSync(reviewLogPath), true);
640
- assert.match(
641
- readFileSync(reviewLogPath, "utf8"),
642
- /permission_request\.waiting/,
643
- );
644
609
 
645
610
  config.debugLog = true;
646
611
  const enabledDebugWarning = logger.debug("debug.enabled", { sample: true });
@@ -654,16 +619,7 @@ test("Permission-system logger respects debug toggle and keeps review log enable
654
619
 
655
620
  test("PermissionManager canonical built-in permission checking", () => {
656
621
  const { manager, cleanup } = createManager({
657
- defaultPolicy: {
658
- tools: "deny",
659
- bash: "ask",
660
- mcp: "ask",
661
- skills: "ask",
662
- special: "ask",
663
- },
664
- tools: {
665
- read: "allow",
666
- },
622
+ permission: { "*": "deny", read: "allow" },
667
623
  });
668
624
 
669
625
  try {
@@ -679,49 +635,26 @@ test("PermissionManager canonical built-in permission checking", () => {
679
635
  }
680
636
  });
681
637
 
682
- test("Bash patterns stay higher priority than tool-level bash fallback", () => {
683
- const { manager, cleanup } = createManager(
684
- {
685
- defaultPolicy: {
686
- tools: "ask",
687
- bash: "ask",
688
- mcp: "ask",
689
- skills: "ask",
690
- special: "ask",
691
- },
692
- bash: {
693
- "rm -rf *": "deny",
694
- },
695
- },
696
- {
697
- reviewer: `---
698
- name: reviewer
699
- permission:
700
- tools:
701
- bash: allow
702
- ---
703
- `,
638
+ test("Bash specific deny patterns override catch-all within the same config", () => {
639
+ // In the flat format, patterns within a surface map are ordered by insertion.
640
+ // Last-match-wins means specific patterns placed AFTER the catch-all override it.
641
+ const { manager, cleanup } = createManager({
642
+ permission: {
643
+ "*": "ask",
644
+ bash: { "*": "allow", "rm -rf *": "deny" },
704
645
  },
705
- );
646
+ });
706
647
 
707
648
  try {
708
- const denied = manager.checkPermission(
709
- "bash",
710
- { command: "rm -rf build" },
711
- "reviewer",
712
- );
649
+ const denied = manager.checkPermission("bash", { command: "rm -rf build" });
713
650
  assert.equal(denied.state, "deny");
714
651
  assert.equal(denied.source, "bash");
715
652
  assert.equal(denied.matchedPattern, "rm -rf *");
716
653
 
717
- const fallback = manager.checkPermission(
718
- "bash",
719
- { command: "echo hello" },
720
- "reviewer",
721
- );
722
- assert.equal(fallback.state, "allow");
723
- assert.equal(fallback.source, "bash");
724
- assert.equal(fallback.matchedPattern, undefined);
654
+ const allowed = manager.checkPermission("bash", { command: "echo hello" });
655
+ assert.equal(allowed.state, "allow");
656
+ assert.equal(allowed.source, "bash");
657
+ assert.equal(allowed.matchedPattern, "*");
725
658
  } finally {
726
659
  cleanup();
727
660
  }
@@ -729,17 +662,9 @@ permission:
729
662
 
730
663
  test("MCP wildcard matching uses the registered mcp tool", () => {
731
664
  const { manager, cleanup } = createManager({
732
- defaultPolicy: {
733
- tools: "ask",
734
- bash: "ask",
735
- mcp: "ask",
736
- skills: "ask",
737
- special: "ask",
738
- },
739
- mcp: {
740
- "*": "deny",
741
- "research_*": "ask",
742
- "research_query-*": "allow",
665
+ permission: {
666
+ "*": "ask",
667
+ mcp: { "*": "deny", "research_*": "ask", "research_query-*": "allow" },
743
668
  },
744
669
  });
745
670
 
@@ -752,12 +677,12 @@ test("MCP wildcard matching uses the registered mcp tool", () => {
752
677
  assert.equal(queryDocs.matchedPattern, "research_query-*");
753
678
  assert.equal(queryDocs.target, "research_query-docs");
754
679
 
755
- const resolve = manager.checkPermission("mcp", {
680
+ const resolve2 = manager.checkPermission("mcp", {
756
681
  tool: "research:resolve-context",
757
682
  });
758
- assert.equal(resolve.state, "ask");
759
- assert.equal(resolve.matchedPattern, "research_*");
760
- assert.equal(resolve.target, "research_resolve-context");
683
+ assert.equal(resolve2.state, "ask");
684
+ assert.equal(resolve2.matchedPattern, "research_*");
685
+ assert.equal(resolve2.target, "research_resolve-context");
761
686
 
762
687
  const unknown = manager.checkPermission("mcp", { tool: "search:provider" });
763
688
  assert.equal(unknown.state, "deny");
@@ -770,18 +695,10 @@ test("MCP wildcard matching uses the registered mcp tool", () => {
770
695
 
771
696
  test("Arbitrary extension tools use exact-name tool permissions instead of MCP fallback", () => {
772
697
  const { manager, cleanup } = createManager({
773
- defaultPolicy: {
774
- tools: "deny",
775
- bash: "ask",
776
- mcp: "allow",
777
- skills: "ask",
778
- special: "ask",
779
- },
780
- tools: {
781
- third_party_tool: "allow",
782
- },
783
- mcp: {
698
+ permission: {
784
699
  "*": "deny",
700
+ third_party_tool: "allow",
701
+ mcp: { "*": "deny" },
785
702
  },
786
703
  });
787
704
 
@@ -790,6 +707,8 @@ test("Arbitrary extension tools use exact-name tool permissions instead of MCP f
790
707
  assert.equal(allowed.state, "allow");
791
708
  assert.equal(allowed.source, "tool");
792
709
 
710
+ // another_extension_tool has no explicit rule — falls through to the
711
+ // universal default (permission["*"] = "deny") with source "default".
793
712
  const fallback = manager.checkPermission("another_extension_tool", {});
794
713
  assert.equal(fallback.state, "deny");
795
714
  assert.equal(fallback.source, "default");
@@ -800,17 +719,13 @@ test("Arbitrary extension tools use exact-name tool permissions instead of MCP f
800
719
 
801
720
  test("Skill permission matching", () => {
802
721
  const { manager, cleanup } = createManager({
803
- defaultPolicy: {
804
- tools: "ask",
805
- bash: "ask",
806
- mcp: "ask",
807
- skills: "ask",
808
- special: "ask",
809
- },
810
- skills: {
722
+ permission: {
811
723
  "*": "ask",
812
- "web-*": "deny",
813
- "requesting-code-review": "allow",
724
+ skill: {
725
+ "*": "ask",
726
+ "web-*": "deny",
727
+ "requesting-code-review": "allow",
728
+ },
814
729
  },
815
730
  });
816
731
 
@@ -841,16 +756,9 @@ test("Skill permission matching", () => {
841
756
  test("MCP proxy tool infers server-prefixed aliases from configured server names", () => {
842
757
  const { manager, cleanup } = createManager(
843
758
  {
844
- defaultPolicy: {
845
- tools: "ask",
846
- bash: "ask",
847
- mcp: "ask",
848
- skills: "ask",
849
- special: "ask",
850
- },
851
- mcp: {
852
- "exa_*": "deny",
853
- exa_get_code_context_exa: "allow",
759
+ permission: {
760
+ "*": "ask",
761
+ mcp: { "exa_*": "deny", exa_get_code_context_exa: "allow" },
854
762
  },
855
763
  },
856
764
  {},
@@ -880,25 +788,8 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
880
788
  const agentsDir = join(baseDir, "agents");
881
789
  mkdirSync(agentsDir, { recursive: true });
882
790
 
883
- // Policy: allow any target prefixed with legacy-server, default mcp is ask.
884
- // If legacy-server were known as a configured server name, a tool named
885
- // "some_tool_legacy-server" would derive "legacy-server_some_tool_legacy-server"
886
- // which matches this rule and returns "allow".
887
- // After the fix, settings.json is ignored, so no server name is derived and the
888
- // result falls through to the default mcp policy ("ask").
889
791
  const config: ScopeConfig = {
890
- defaultPolicy: {
891
- tools: "ask",
892
- bash: "ask",
893
- mcp: "ask",
894
- skills: "ask",
895
- special: "ask",
896
- },
897
- tools: {},
898
- bash: {},
899
- mcp: { "legacy-server_*": "allow" },
900
- skills: {},
901
- special: {},
792
+ permission: { "*": "ask", mcp: { "legacy-server_*": "allow" } },
902
793
  };
903
794
 
904
795
  writeFileSync(
@@ -906,9 +797,7 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
906
797
  `${JSON.stringify(config, null, 2)}\n`,
907
798
  "utf8",
908
799
  );
909
- // mcp.json does not know about legacy-server.
910
800
  writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers: {} }), "utf8");
911
- // settings.json has legacy-server — the legacy source that must now be ignored.
912
801
  writeFileSync(
913
802
  settingsJsonPath,
914
803
  JSON.stringify({ mcpServers: { "legacy-server": {} } }),
@@ -922,8 +811,6 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
922
811
  });
923
812
 
924
813
  try {
925
- // "legacy-server" must not be derived from settings.json.
926
- // The bare tool name falls through to the default mcp policy → "ask".
927
814
  const result = manager.checkPermission("mcp", {
928
815
  tool: "some_tool_legacy-server",
929
816
  });
@@ -936,16 +823,9 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
936
823
  test("MCP describe mode normalizes qualified tool names without duplicating server prefixes", () => {
937
824
  const { manager, cleanup } = createManager(
938
825
  {
939
- defaultPolicy: {
940
- tools: "ask",
941
- bash: "ask",
942
- mcp: "ask",
943
- skills: "ask",
944
- special: "ask",
945
- },
946
- mcp: {
947
- "exa_*": "deny",
948
- exa_web_search_exa: "allow",
826
+ permission: {
827
+ "*": "ask",
828
+ mcp: { "exa_*": "deny", exa_web_search_exa: "allow" },
949
829
  },
950
830
  },
951
831
  {},
@@ -970,17 +850,7 @@ test("MCP describe mode normalizes qualified tool names without duplicating serv
970
850
 
971
851
  test("Canonical tools map directly without legacy aliases", () => {
972
852
  const { manager, cleanup } = createManager({
973
- defaultPolicy: {
974
- tools: "ask",
975
- bash: "ask",
976
- mcp: "ask",
977
- skills: "ask",
978
- special: "ask",
979
- },
980
- tools: {
981
- find: "allow",
982
- ls: "deny",
983
- },
853
+ permission: { "*": "ask", find: "allow", ls: "deny" },
984
854
  });
985
855
 
986
856
  try {
@@ -996,23 +866,16 @@ test("Canonical tools map directly without legacy aliases", () => {
996
866
  }
997
867
  });
998
868
 
999
- test("tools.mcp acts as fallback allow for unmatched MCP targets", () => {
869
+ test("mcp catch-all acts as fallback for unmatched MCP targets", () => {
1000
870
  const { manager, cleanup } = createManager(
1001
871
  {
1002
- defaultPolicy: {
1003
- tools: "ask",
1004
- bash: "ask",
1005
- mcp: "ask",
1006
- skills: "ask",
1007
- special: "ask",
1008
- },
872
+ permission: { "*": "ask" },
1009
873
  },
1010
874
  {
1011
875
  reviewer: `---
1012
876
  name: reviewer
1013
877
  permission:
1014
- tools:
1015
- mcp: allow
878
+ mcp: allow
1016
879
  ---
1017
880
  `,
1018
881
  },
@@ -1025,31 +888,24 @@ permission:
1025
888
  "reviewer",
1026
889
  );
1027
890
  assert.equal(result.state, "allow");
1028
- assert.equal(result.source, "tool");
891
+ assert.equal(result.source, "mcp");
1029
892
  assert.equal(result.target, "exa_web_search_exa");
1030
893
  } finally {
1031
894
  cleanup();
1032
895
  }
1033
896
  });
1034
897
 
1035
- test("specific MCP rules override tools.mcp fallback", () => {
898
+ test("specific MCP rules override mcp catch-all", () => {
1036
899
  const { manager, cleanup } = createManager(
1037
900
  {
1038
- defaultPolicy: {
1039
- tools: "ask",
1040
- bash: "ask",
1041
- mcp: "ask",
1042
- skills: "ask",
1043
- special: "ask",
1044
- },
901
+ permission: { "*": "ask" },
1045
902
  },
1046
903
  {
1047
904
  reviewer: `---
1048
905
  name: reviewer
1049
906
  permission:
1050
- tools:
1051
- mcp: allow
1052
907
  mcp:
908
+ "*": allow
1053
909
  exa_web_search_exa: deny
1054
910
  ---
1055
911
  `,
@@ -1074,24 +930,17 @@ permission:
1074
930
  }
1075
931
  });
1076
932
 
1077
- test("specific MCP rules still win when tools.mcp is deny", () => {
933
+ test("specific MCP rules still win when mcp catch-all is deny", () => {
1078
934
  const { manager, cleanup } = createManager(
1079
935
  {
1080
- defaultPolicy: {
1081
- tools: "ask",
1082
- bash: "ask",
1083
- mcp: "ask",
1084
- skills: "ask",
1085
- special: "ask",
1086
- },
936
+ permission: { "*": "ask" },
1087
937
  },
1088
938
  {
1089
939
  reviewer: `---
1090
940
  name: reviewer
1091
941
  permission:
1092
- tools:
1093
- mcp: deny
1094
942
  mcp:
943
+ "*": deny
1095
944
  exa_web_search_exa: allow
1096
945
  ---
1097
946
  `,
@@ -1118,30 +967,23 @@ permission:
1118
967
  "reviewer",
1119
968
  );
1120
969
  assert.equal(fallback.state, "deny");
1121
- assert.equal(fallback.source, "tool");
970
+ assert.equal(fallback.source, "mcp");
1122
971
  assert.equal(fallback.target, "exa_other_exa");
1123
972
  } finally {
1124
973
  cleanup();
1125
974
  }
1126
975
  });
1127
976
 
1128
- test("partial agent defaultPolicy overrides preserve global defaults", () => {
977
+ test("mcp catch-all in agent frontmatter overrides global default", () => {
1129
978
  const { manager, cleanup } = createManager(
1130
979
  {
1131
- defaultPolicy: {
1132
- tools: "deny",
1133
- bash: "deny",
1134
- mcp: "deny",
1135
- skills: "deny",
1136
- special: "deny",
1137
- },
980
+ permission: { "*": "deny" },
1138
981
  },
1139
982
  {
1140
983
  reviewer: `---
1141
984
  name: reviewer
1142
985
  permission:
1143
- defaultPolicy:
1144
- mcp: allow
986
+ mcp: allow
1145
987
  ---
1146
988
  `,
1147
989
  },
@@ -1158,7 +1000,7 @@ permission:
1158
1000
  "reviewer",
1159
1001
  );
1160
1002
  assert.equal(mcpResult.state, "allow");
1161
- assert.equal(mcpResult.source, "default");
1003
+ assert.equal(mcpResult.source, "mcp");
1162
1004
  } finally {
1163
1005
  cleanup();
1164
1006
  }
@@ -1167,13 +1009,7 @@ permission:
1167
1009
  test("Agent frontmatter canonical tools resolve correctly", () => {
1168
1010
  const { manager, cleanup } = createManager(
1169
1011
  {
1170
- defaultPolicy: {
1171
- tools: "deny",
1172
- bash: "ask",
1173
- mcp: "ask",
1174
- skills: "ask",
1175
- special: "ask",
1176
- },
1012
+ permission: { "*": "deny" },
1177
1013
  },
1178
1014
  {
1179
1015
  reviewer: `---
@@ -1199,16 +1035,10 @@ permission:
1199
1035
  }
1200
1036
  });
1201
1037
 
1202
- test("Only canonical built-ins support top-level shorthand in agent frontmatter", () => {
1038
+ test("All surface names work in agent frontmatter flat permission format", () => {
1203
1039
  const { manager, cleanup } = createManager(
1204
1040
  {
1205
- defaultPolicy: {
1206
- tools: "deny",
1207
- bash: "ask",
1208
- mcp: "deny",
1209
- skills: "ask",
1210
- special: "ask",
1211
- },
1041
+ permission: { "*": "deny" },
1212
1042
  },
1213
1043
  {
1214
1044
  reviewer: `---
@@ -1227,17 +1057,18 @@ permission:
1227
1057
  assert.equal(findResult.state, "allow");
1228
1058
  assert.equal(findResult.source, "tool");
1229
1059
 
1060
+ // In flat format any surface key works, including extension tools
1230
1061
  const taskResult = manager.checkPermission("task", {}, "reviewer");
1231
- assert.equal(taskResult.state, "deny");
1232
- assert.equal(taskResult.source, "default");
1062
+ assert.equal(taskResult.state, "allow");
1063
+ assert.equal(taskResult.source, "tool");
1233
1064
 
1065
+ // mcp: allow catches all MCP targets
1234
1066
  const mcpResult = manager.checkPermission(
1235
1067
  "mcp",
1236
1068
  { tool: "exa:web_search_exa" },
1237
1069
  "reviewer",
1238
1070
  );
1239
- assert.equal(mcpResult.state, "deny");
1240
- assert.equal(mcpResult.source, "default");
1071
+ assert.equal(mcpResult.state, "allow");
1241
1072
  } finally {
1242
1073
  cleanup();
1243
1074
  }
@@ -1245,16 +1076,7 @@ permission:
1245
1076
 
1246
1077
  test("task uses exact-name tool permissions like any registered extension tool", () => {
1247
1078
  const { manager, cleanup } = createManager({
1248
- defaultPolicy: {
1249
- tools: "deny",
1250
- bash: "ask",
1251
- mcp: "allow",
1252
- skills: "ask",
1253
- special: "ask",
1254
- },
1255
- tools: {
1256
- task: "allow",
1257
- },
1079
+ permission: { "*": "deny", task: "allow" },
1258
1080
  });
1259
1081
 
1260
1082
  try {
@@ -1307,22 +1129,15 @@ test("Tool registry blocks unregistered tools and handles aliases", () => {
1307
1129
  test("getToolPermission returns tool-level policy for canonical and extension tools", () => {
1308
1130
  const { manager, cleanup } = createManager(
1309
1131
  {
1310
- defaultPolicy: {
1311
- tools: "ask",
1312
- bash: "ask",
1313
- mcp: "ask",
1314
- skills: "ask",
1315
- special: "ask",
1316
- },
1132
+ permission: { "*": "ask" },
1317
1133
  },
1318
1134
  {
1319
1135
  reviewer: `---
1320
1136
  name: reviewer
1321
1137
  permission:
1322
- tools:
1323
- bash: deny
1324
- read: deny
1325
- task: allow
1138
+ bash: deny
1139
+ read: deny
1140
+ task: allow
1326
1141
  ---
1327
1142
  `,
1328
1143
  },
@@ -1342,16 +1157,7 @@ permission:
1342
1157
  assert.equal(defaultBashPermission, "ask");
1343
1158
 
1344
1159
  const { manager: manager2, cleanup: cleanup2 } = createManager({
1345
- defaultPolicy: {
1346
- tools: "deny",
1347
- bash: "ask",
1348
- mcp: "ask",
1349
- skills: "ask",
1350
- special: "ask",
1351
- },
1352
- tools: {
1353
- bash: "allow",
1354
- },
1160
+ permission: { "*": "deny", bash: "allow" },
1355
1161
  });
1356
1162
 
1357
1163
  try {
@@ -1367,16 +1173,7 @@ permission:
1367
1173
 
1368
1174
  test("getToolPermission supports arbitrary extension tool names", () => {
1369
1175
  const { manager, cleanup } = createManager({
1370
- defaultPolicy: {
1371
- tools: "deny",
1372
- bash: "ask",
1373
- mcp: "allow",
1374
- skills: "ask",
1375
- special: "ask",
1376
- },
1377
- tools: {
1378
- third_party_tool: "allow",
1379
- },
1176
+ permission: { "*": "deny", third_party_tool: "allow" },
1380
1177
  });
1381
1178
 
1382
1179
  try {
@@ -1485,6 +1282,10 @@ test("Permission forwarding rejects unresolved sentinel session ids", () => {
1485
1282
  assert.equal(targetSessionId, null);
1486
1283
  });
1487
1284
 
1285
+ // ---------------------------------------------------------------------------
1286
+ // Project-level and per-agent config scope tests
1287
+ // ---------------------------------------------------------------------------
1288
+
1488
1289
  type CreateManagerWithProjectOptions = CreateManagerOptions & {
1489
1290
  projectConfig?: ScopeConfig;
1490
1291
  projectAgentFiles?: Record<string, string>;
@@ -1549,23 +1350,15 @@ function createManagerWithProject(
1549
1350
  test("Project-level config overrides base bash patterns", () => {
1550
1351
  const { manager, cleanup } = createManagerWithProject(
1551
1352
  {
1552
- defaultPolicy: {
1553
- tools: "allow",
1554
- bash: "ask",
1555
- mcp: "ask",
1556
- skills: "ask",
1557
- special: "ask",
1558
- },
1559
- bash: {
1560
- "rm -rf *": "deny",
1353
+ permission: {
1354
+ "*": "allow",
1355
+ bash: { "*": "ask", "rm -rf *": "deny" },
1561
1356
  },
1562
1357
  },
1563
1358
  {},
1564
1359
  {
1565
1360
  projectConfig: {
1566
- bash: {
1567
- "rm -rf build": "allow",
1568
- },
1361
+ permission: { bash: { "rm -rf build": "allow" } },
1569
1362
  },
1570
1363
  },
1571
1364
  );
@@ -1590,13 +1383,7 @@ test("Project-level config overrides base bash patterns", () => {
1590
1383
  test("System-agent config overrides project-level bash patterns", () => {
1591
1384
  const { manager, cleanup } = createManagerWithProject(
1592
1385
  {
1593
- defaultPolicy: {
1594
- tools: "allow",
1595
- bash: "ask",
1596
- mcp: "ask",
1597
- skills: "ask",
1598
- special: "ask",
1599
- },
1386
+ permission: { "*": "allow", bash: "ask" },
1600
1387
  },
1601
1388
  {
1602
1389
  reviewer: `---
@@ -1609,9 +1396,7 @@ permission:
1609
1396
  },
1610
1397
  {
1611
1398
  projectConfig: {
1612
- bash: {
1613
- "git *": "deny",
1614
- },
1399
+ permission: { bash: { "git *": "deny" } },
1615
1400
  },
1616
1401
  },
1617
1402
  );
@@ -1640,20 +1425,13 @@ permission:
1640
1425
  test("Project-agent config overrides system-agent tool rules", () => {
1641
1426
  const { manager, cleanup } = createManagerWithProject(
1642
1427
  {
1643
- defaultPolicy: {
1644
- tools: "ask",
1645
- bash: "ask",
1646
- mcp: "ask",
1647
- skills: "ask",
1648
- special: "ask",
1649
- },
1428
+ permission: { "*": "ask" },
1650
1429
  },
1651
1430
  {
1652
1431
  reviewer: `---
1653
1432
  name: reviewer
1654
1433
  permission:
1655
- tools:
1656
- read: deny
1434
+ read: deny
1657
1435
  ---
1658
1436
  `,
1659
1437
  },
@@ -1662,8 +1440,7 @@ permission:
1662
1440
  reviewer: `---
1663
1441
  name: reviewer
1664
1442
  permission:
1665
- tools:
1666
- read: allow
1443
+ read: allow
1667
1444
  ---
1668
1445
  `,
1669
1446
  },
@@ -1679,38 +1456,28 @@ permission:
1679
1456
  }
1680
1457
  });
1681
1458
 
1682
- test("Full precedence chain base < project < system-agent < project-agent for defaultPolicy", () => {
1459
+ test("Full precedence chain base < project < system-agent < project-agent for universal default", () => {
1683
1460
  const { manager, cleanup } = createManagerWithProject(
1684
1461
  {
1685
- defaultPolicy: {
1686
- tools: "deny",
1687
- bash: "ask",
1688
- mcp: "ask",
1689
- skills: "ask",
1690
- special: "ask",
1691
- },
1462
+ permission: { "*": "deny" },
1692
1463
  },
1693
1464
  {
1694
1465
  reviewer: `---
1695
1466
  name: reviewer
1696
1467
  permission:
1697
- defaultPolicy:
1698
- tools: ask
1468
+ "*": ask
1699
1469
  ---
1700
1470
  `,
1701
1471
  },
1702
1472
  {
1703
1473
  projectConfig: {
1704
- defaultPolicy: {
1705
- tools: "allow",
1706
- },
1474
+ permission: { "*": "allow" },
1707
1475
  },
1708
1476
  projectAgentFiles: {
1709
1477
  reviewer: `---
1710
1478
  name: reviewer
1711
1479
  permission:
1712
- defaultPolicy:
1713
- tools: deny
1480
+ "*": deny
1714
1481
  ---
1715
1482
  `,
1716
1483
  },
@@ -1737,13 +1504,7 @@ permission:
1737
1504
  test("Project-agent applies even without a matching system-agent file", () => {
1738
1505
  const { manager, cleanup } = createManagerWithProject(
1739
1506
  {
1740
- defaultPolicy: {
1741
- tools: "allow",
1742
- bash: "ask",
1743
- mcp: "ask",
1744
- skills: "ask",
1745
- special: "ask",
1746
- },
1507
+ permission: { "*": "allow" },
1747
1508
  },
1748
1509
  {},
1749
1510
  {
@@ -1751,8 +1512,7 @@ test("Project-agent applies even without a matching system-agent file", () => {
1751
1512
  reviewer: `---
1752
1513
  name: reviewer
1753
1514
  permission:
1754
- tools:
1755
- read: deny
1515
+ read: deny
1756
1516
  ---
1757
1517
  `,
1758
1518
  },
@@ -1784,18 +1544,7 @@ test("PermissionManager reads config from PI_CODING_AGENT_DIR when set", () => {
1784
1544
  mkdirSync(dirname(newConfigPath), { recursive: true });
1785
1545
 
1786
1546
  const config: ScopeConfig = {
1787
- defaultPolicy: {
1788
- tools: "deny",
1789
- bash: "deny",
1790
- mcp: "deny",
1791
- skills: "deny",
1792
- special: "deny",
1793
- },
1794
- tools: { read: "allow" },
1795
- bash: {},
1796
- mcp: {},
1797
- skills: {},
1798
- special: {},
1547
+ permission: { "*": "deny", read: "allow" },
1799
1548
  };
1800
1549
  writeFileSync(newConfigPath, JSON.stringify(config), "utf8");
1801
1550
 
@@ -1852,15 +1601,9 @@ test("parseAllSkillPromptSections finds every available_skills block", () => {
1852
1601
 
1853
1602
  test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills block", () => {
1854
1603
  const { manager, cleanup } = createManager({
1855
- defaultPolicy: {
1856
- tools: "ask",
1857
- bash: "ask",
1858
- mcp: "ask",
1859
- skills: "ask",
1860
- special: "ask",
1861
- },
1862
- skills: {
1863
- "denied-skill": "deny",
1604
+ permission: {
1605
+ "*": "ask",
1606
+ skill: { "denied-skill": "deny" },
1864
1607
  },
1865
1608
  });
1866
1609
 
@@ -1919,15 +1662,9 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
1919
1662
 
1920
1663
  test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available for path matching", () => {
1921
1664
  const { manager, cleanup } = createManager({
1922
- defaultPolicy: {
1923
- tools: "ask",
1924
- bash: "ask",
1925
- mcp: "ask",
1926
- skills: "ask",
1927
- special: "ask",
1928
- },
1929
- skills: {
1930
- "blocked-skill": "deny",
1665
+ permission: {
1666
+ "*": "ask",
1667
+ skill: { "blocked-skill": "deny" },
1931
1668
  },
1932
1669
  });
1933
1670
 
@@ -1979,16 +1716,9 @@ test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available
1979
1716
  // external_directory special permission
1980
1717
  // ---------------------------------------------------------------------------
1981
1718
 
1982
- test("external_directory permission falls back to special default policy when not explicitly configured", () => {
1983
- const { manager, cleanup } = createManager({
1984
- defaultPolicy: {
1985
- tools: "allow",
1986
- bash: "allow",
1987
- mcp: "allow",
1988
- skills: "allow",
1989
- special: "ask",
1990
- },
1991
- });
1719
+ test("external_directory permission falls back to universal default when not explicitly configured", () => {
1720
+ // Empty permission: everything defaults to "ask" (least privilege).
1721
+ const { manager, cleanup } = createManager({ permission: {} });
1992
1722
 
1993
1723
  try {
1994
1724
  const result = manager.checkPermission("external_directory", {});
@@ -2000,25 +1730,16 @@ test("external_directory permission falls back to special default policy when no
2000
1730
  }
2001
1731
  });
2002
1732
 
2003
- test("external_directory permission respects explicit deny in special config", () => {
1733
+ test("external_directory permission respects explicit deny", () => {
2004
1734
  const { manager, cleanup } = createManager({
2005
- defaultPolicy: {
2006
- tools: "allow",
2007
- bash: "allow",
2008
- mcp: "allow",
2009
- skills: "allow",
2010
- special: "ask",
2011
- },
2012
- special: {
2013
- external_directory: "deny",
2014
- },
1735
+ permission: { "*": "allow", external_directory: "deny" },
2015
1736
  });
2016
1737
 
2017
1738
  try {
2018
1739
  const result = manager.checkPermission("external_directory", {});
2019
1740
  assert.equal(result.state, "deny");
2020
1741
  assert.equal(result.source, "special");
2021
- assert.equal(result.matchedPattern, "external_directory");
1742
+ assert.equal(result.matchedPattern, "*");
2022
1743
  } finally {
2023
1744
  cleanup();
2024
1745
  }
@@ -2026,23 +1747,14 @@ test("external_directory permission respects explicit deny in special config", (
2026
1747
 
2027
1748
  test("external_directory permission can be explicitly allowed", () => {
2028
1749
  const { manager, cleanup } = createManager({
2029
- defaultPolicy: {
2030
- tools: "allow",
2031
- bash: "allow",
2032
- mcp: "allow",
2033
- skills: "allow",
2034
- special: "deny",
2035
- },
2036
- special: {
2037
- external_directory: "allow",
2038
- },
1750
+ permission: { "*": "allow", external_directory: "allow" },
2039
1751
  });
2040
1752
 
2041
1753
  try {
2042
1754
  const result = manager.checkPermission("external_directory", {});
2043
1755
  assert.equal(result.state, "allow");
2044
1756
  assert.equal(result.source, "special");
2045
- assert.equal(result.matchedPattern, "external_directory");
1757
+ assert.equal(result.matchedPattern, "*");
2046
1758
  } finally {
2047
1759
  cleanup();
2048
1760
  }
@@ -2051,23 +1763,13 @@ test("external_directory permission can be explicitly allowed", () => {
2051
1763
  test("external_directory permission respects per-agent override", () => {
2052
1764
  const { manager, cleanup } = createManager(
2053
1765
  {
2054
- defaultPolicy: {
2055
- tools: "allow",
2056
- bash: "allow",
2057
- mcp: "allow",
2058
- skills: "allow",
2059
- special: "ask",
2060
- },
2061
- special: {
2062
- external_directory: "deny",
2063
- },
1766
+ permission: { "*": "allow", external_directory: "deny" },
2064
1767
  },
2065
1768
  {
2066
1769
  trusted: `---
2067
1770
  name: trusted
2068
1771
  permission:
2069
- special:
2070
- external_directory: allow
1772
+ external_directory: allow
2071
1773
  ---
2072
1774
  `,
2073
1775
  },
@@ -2091,31 +1793,18 @@ permission:
2091
1793
  }
2092
1794
  });
2093
1795
 
2094
- test("external_directory permission is unaffected when doom_loop key is present in config (deprecated and ignored)", () => {
1796
+ test("external_directory permission is not affected by unrelated surface keys", () => {
1797
+ // Flat format: unknown surface keys are just rules for that surface.
1798
+ // external_directory resolves from its own rule, not from unrelated keys.
2095
1799
  const { manager, cleanup } = createManager({
2096
- defaultPolicy: {
2097
- tools: "allow",
2098
- bash: "allow",
2099
- mcp: "allow",
2100
- skills: "allow",
2101
- special: "ask",
2102
- },
2103
- special: {
2104
- doom_loop: "deny",
2105
- external_directory: "allow",
2106
- },
1800
+ permission: { "*": "allow", external_directory: "allow" },
2107
1801
  });
2108
1802
 
2109
1803
  try {
2110
- // doom_loop is deprecated and stripped — falls through to defaultPolicy.tools
2111
- const doomResult = manager.checkPermission("doom_loop", {});
2112
- assert.equal(doomResult.state, "allow"); // defaultPolicy.tools, not the stripped doom_loop: "deny"
2113
- assert.equal(doomResult.matchedPattern, undefined);
2114
-
2115
1804
  // external_directory still resolves from its own entry
2116
1805
  const extResult = manager.checkPermission("external_directory", {});
2117
1806
  assert.equal(extResult.state, "allow");
2118
- assert.equal(extResult.matchedPattern, "external_directory");
1807
+ assert.equal(extResult.matchedPattern, "*");
2119
1808
  } finally {
2120
1809
  cleanup();
2121
1810
  }
@@ -2129,14 +1818,7 @@ test("tool_call blocks path-bearing tools outside cwd when external_directory is
2129
1818
 
2130
1819
  const harness = createToolCallHarness(
2131
1820
  {
2132
- defaultPolicy: {
2133
- tools: "allow",
2134
- bash: "allow",
2135
- mcp: "allow",
2136
- skills: "allow",
2137
- special: "ask",
2138
- },
2139
- special: { external_directory: "deny" },
1821
+ permission: { "*": "allow", external_directory: "deny" },
2140
1822
  },
2141
1823
  ["read"],
2142
1824
  { cwd },
@@ -2164,14 +1846,7 @@ test("tool_call blocks path-bearing tools outside cwd when external_directory is
2164
1846
  test("tool_call allows path-bearing tools inside cwd without external_directory prompt", async () => {
2165
1847
  const harness = createToolCallHarness(
2166
1848
  {
2167
- defaultPolicy: {
2168
- tools: "allow",
2169
- bash: "allow",
2170
- mcp: "allow",
2171
- skills: "allow",
2172
- special: "ask",
2173
- },
2174
- special: { external_directory: "deny" },
1849
+ permission: { "*": "allow", external_directory: "deny" },
2175
1850
  },
2176
1851
  ["read"],
2177
1852
  );
@@ -2193,14 +1868,7 @@ test("tool_call allows path-bearing tools inside cwd without external_directory
2193
1868
  test("tool_call blocks external_directory ask when no confirmation channel is available", async () => {
2194
1869
  const harness = createToolCallHarness(
2195
1870
  {
2196
- defaultPolicy: {
2197
- tools: "allow",
2198
- bash: "allow",
2199
- mcp: "allow",
2200
- skills: "allow",
2201
- special: "ask",
2202
- },
2203
- special: { external_directory: "ask" },
1871
+ permission: { "*": "allow", external_directory: "ask" },
2204
1872
  },
2205
1873
  ["write"],
2206
1874
  );
@@ -2228,14 +1896,7 @@ test("tool_call blocks external_directory ask when no confirmation channel is av
2228
1896
  test("tool_call prompts for external_directory and then falls through to normal tool policy", async () => {
2229
1897
  const harness = createToolCallHarness(
2230
1898
  {
2231
- defaultPolicy: {
2232
- tools: "allow",
2233
- bash: "allow",
2234
- mcp: "allow",
2235
- skills: "allow",
2236
- special: "ask",
2237
- },
2238
- special: { external_directory: "ask" },
1899
+ permission: { "*": "allow", external_directory: "ask" },
2239
1900
  },
2240
1901
  ["grep"],
2241
1902
  );
@@ -2265,14 +1926,7 @@ test("tool_call prompts for external_directory and then falls through to normal
2265
1926
  test("tool_call skips external_directory checks for optional path tools without a path", async () => {
2266
1927
  const harness = createToolCallHarness(
2267
1928
  {
2268
- defaultPolicy: {
2269
- tools: "allow",
2270
- bash: "allow",
2271
- mcp: "allow",
2272
- skills: "allow",
2273
- special: "ask",
2274
- },
2275
- special: { external_directory: "deny" },
1929
+ permission: { "*": "allow", external_directory: "deny" },
2276
1930
  },
2277
1931
  ["find"],
2278
1932
  );
@@ -2296,14 +1950,7 @@ test("tool_call skips external_directory checks for optional path tools without
2296
1950
  test("tool_call blocks bash command with external path when external_directory is denied", async () => {
2297
1951
  const harness = createToolCallHarness(
2298
1952
  {
2299
- defaultPolicy: {
2300
- tools: "allow",
2301
- bash: "allow",
2302
- mcp: "allow",
2303
- skills: "allow",
2304
- special: "ask",
2305
- },
2306
- special: { external_directory: "deny" },
1953
+ permission: { "*": "allow", external_directory: "deny" },
2307
1954
  },
2308
1955
  ["bash"],
2309
1956
  );
@@ -2329,14 +1976,7 @@ test("tool_call blocks bash command with external path when external_directory i
2329
1976
  test("tool_call allows bash command with only internal paths when external_directory is denied", async () => {
2330
1977
  const harness = createToolCallHarness(
2331
1978
  {
2332
- defaultPolicy: {
2333
- tools: "allow",
2334
- bash: "allow",
2335
- mcp: "allow",
2336
- skills: "allow",
2337
- special: "ask",
2338
- },
2339
- special: { external_directory: "deny" },
1979
+ permission: { "*": "allow", external_directory: "deny" },
2340
1980
  },
2341
1981
  ["bash"],
2342
1982
  );
@@ -2357,14 +1997,7 @@ test("tool_call allows bash command with only internal paths when external_direc
2357
1997
  test("tool_call prompts for bash command with external path when external_directory is ask", async () => {
2358
1998
  const harness = createToolCallHarness(
2359
1999
  {
2360
- defaultPolicy: {
2361
- tools: "allow",
2362
- bash: "allow",
2363
- mcp: "allow",
2364
- skills: "allow",
2365
- special: "ask",
2366
- },
2367
- special: { external_directory: "ask" },
2000
+ permission: { "*": "allow", external_directory: "ask" },
2368
2001
  },
2369
2002
  ["bash"],
2370
2003
  );
@@ -2390,14 +2023,7 @@ test("tool_call prompts for bash command with external path when external_direct
2390
2023
  test("tool_call allows bash command with external path when external_directory is allow", async () => {
2391
2024
  const harness = createToolCallHarness(
2392
2025
  {
2393
- defaultPolicy: {
2394
- tools: "allow",
2395
- bash: "allow",
2396
- mcp: "allow",
2397
- skills: "allow",
2398
- special: "ask",
2399
- },
2400
- special: { external_directory: "allow" },
2026
+ permission: { "*": "allow", external_directory: "allow" },
2401
2027
  },
2402
2028
  ["bash"],
2403
2029
  );
@@ -2419,15 +2045,11 @@ test("tool_call allows bash command with external path when external_directory i
2419
2045
  test("tool_call applies bash pattern permissions after external_directory allow", async () => {
2420
2046
  const harness = createToolCallHarness(
2421
2047
  {
2422
- defaultPolicy: {
2423
- tools: "allow",
2424
- bash: "allow",
2425
- mcp: "allow",
2426
- skills: "allow",
2427
- special: "ask",
2048
+ permission: {
2049
+ "*": "allow",
2050
+ external_directory: "allow",
2051
+ bash: { "*": "allow", "cat *": "deny" },
2428
2052
  },
2429
- special: { external_directory: "allow" },
2430
- bash: { "cat *": "deny" },
2431
2053
  },
2432
2054
  ["bash"],
2433
2055
  );
@@ -2450,13 +2072,7 @@ test("tool_call applies bash pattern permissions after external_directory allow"
2450
2072
  test("generic ask prompts include serialized tool input for informed approval", async () => {
2451
2073
  const harness = createToolCallHarness(
2452
2074
  {
2453
- defaultPolicy: {
2454
- tools: "ask",
2455
- bash: "ask",
2456
- mcp: "ask",
2457
- skills: "ask",
2458
- special: "ask",
2459
- },
2075
+ permission: { "*": "ask" },
2460
2076
  },
2461
2077
  ["weather_lookup"],
2462
2078
  );
@@ -2543,86 +2159,11 @@ test("getResolvedPolicyPaths returns false for missing files and null for absent
2543
2159
  }
2544
2160
  });
2545
2161
 
2546
- // --- tool_call_limit deprecation tests (#18) ---
2547
-
2548
- test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (integer)", () => {
2549
- const result = normalizeRawPermission({ special: { tool_call_limit: 5 } });
2550
- assert.equal(result.configIssues.length, 1);
2551
- assert.ok(result.configIssues[0].includes("tool_call_limit"));
2552
- assert.equal(result.permissions.special?.tool_call_limit, undefined);
2553
- });
2554
-
2555
- test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (string)", () => {
2556
- const result = normalizeRawPermission({
2557
- special: { tool_call_limit: "allow" },
2558
- });
2559
- assert.equal(result.configIssues.length, 1);
2560
- assert.ok(result.configIssues[0].includes("tool_call_limit"));
2561
- assert.equal(result.permissions.special?.tool_call_limit, undefined);
2562
- });
2563
-
2564
- test("normalizeRawPermission emits deprecation issue for special.doom_loop (string)", () => {
2565
- const result = normalizeRawPermission({
2566
- special: { doom_loop: "ask" },
2567
- });
2568
- assert.equal(result.configIssues.length, 1);
2569
- assert.ok(result.configIssues[0].includes("doom_loop"));
2570
- assert.equal(result.permissions.special?.doom_loop, undefined);
2571
- });
2572
-
2573
- test("normalizeRawPermission emits deprecation issue for special.doom_loop (deny)", () => {
2574
- const result = normalizeRawPermission({
2575
- special: { doom_loop: "deny" },
2576
- });
2577
- assert.equal(result.configIssues.length, 1);
2578
- assert.ok(result.configIssues[0].includes("doom_loop"));
2579
- assert.equal(result.permissions.special?.doom_loop, undefined);
2580
- });
2581
-
2582
- test("normalizeRawPermission emits no issues when special is absent", () => {
2583
- const result = normalizeRawPermission({ tools: { read: "allow" } });
2584
- assert.equal(result.configIssues.length, 0);
2585
- });
2586
-
2587
- test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit in global config", () => {
2588
- const config: ScopeConfig = {
2589
- defaultPolicy: {
2590
- tools: "ask",
2591
- bash: "ask",
2592
- mcp: "ask",
2593
- skills: "ask",
2594
- special: "ask",
2595
- },
2596
- tools: {},
2597
- bash: {},
2598
- mcp: {},
2599
- skills: {},
2600
- special: { tool_call_limit: "allow" as PermissionState },
2601
- };
2602
- const { manager, cleanup } = createManager(config);
2603
- try {
2604
- const issues = manager.getConfigIssues();
2605
- assert.equal(issues.length, 1);
2606
- assert.ok(issues[0].includes("tool_call_limit"));
2607
- } finally {
2608
- cleanup();
2609
- }
2610
- });
2162
+ // --- config issues tests ---
2611
2163
 
2612
2164
  test("PermissionManager.getConfigIssues returns empty array for clean config", () => {
2613
2165
  const config: ScopeConfig = {
2614
- defaultPolicy: {
2615
- tools: "ask",
2616
- bash: "ask",
2617
- mcp: "ask",
2618
- skills: "ask",
2619
- special: "ask",
2620
- },
2621
- tools: {},
2622
- bash: {},
2623
- mcp: {},
2624
- skills: {},
2625
- special: { external_directory: "ask" },
2166
+ permission: { "*": "ask", external_directory: "ask" },
2626
2167
  };
2627
2168
  const { manager, cleanup } = createManager(config);
2628
2169
  try {
@@ -2633,59 +2174,19 @@ test("PermissionManager.getConfigIssues returns empty array for clean config", (
2633
2174
  }
2634
2175
  });
2635
2176
 
2636
- // --- doom_loop config-loader deprecation tests (#54) ---
2637
-
2638
- test("PermissionManager.getConfigIssues returns deprecation for doom_loop in global config", () => {
2639
- const config: ScopeConfig = {
2640
- defaultPolicy: {
2641
- tools: "ask",
2642
- bash: "ask",
2643
- mcp: "ask",
2644
- skills: "ask",
2645
- special: "ask",
2646
- },
2647
- tools: {},
2648
- bash: {},
2649
- mcp: {},
2650
- skills: {},
2651
- special: { doom_loop: "deny" },
2652
- };
2653
- const { manager, cleanup } = createManager(config);
2177
+ test("PermissionManager.getConfigIssues returns empty array for empty config", () => {
2178
+ const { manager, cleanup } = createManager({});
2654
2179
  try {
2655
2180
  const issues = manager.getConfigIssues();
2656
- assert.equal(issues.length, 1);
2657
- assert.ok(issues[0].includes("doom_loop"));
2658
- } finally {
2659
- cleanup();
2660
- }
2661
- });
2662
-
2663
- test("checkPermission doom_loop falls through to defaultPolicy.tools when stripped by config-loader", () => {
2664
- const { manager, cleanup } = createManager({
2665
- defaultPolicy: {
2666
- tools: "allow",
2667
- bash: "ask",
2668
- mcp: "ask",
2669
- skills: "ask",
2670
- special: "deny",
2671
- },
2672
- tools: {},
2673
- bash: {},
2674
- mcp: {},
2675
- skills: {},
2676
- special: { doom_loop: "ask" },
2677
- });
2678
- try {
2679
- const result = manager.checkPermission("doom_loop", {});
2680
- // doom_loop stripped by config-loader — falls through to defaultPolicy.tools
2681
- assert.equal(result.state, "allow");
2682
- assert.equal(result.matchedPattern, undefined);
2181
+ assert.equal(issues.length, 0);
2683
2182
  } finally {
2684
2183
  cleanup();
2685
2184
  }
2686
2185
  });
2687
2186
 
2688
- // --- session-scoped approval tests (#45) ---
2187
+ // ---------------------------------------------------------------------------
2188
+ // Session-scoped approval tests (#45)
2189
+ // ---------------------------------------------------------------------------
2689
2190
 
2690
2191
  test("session approval: first prompt with 'Yes, for this session' skips subsequent prompts under same prefix", async () => {
2691
2192
  const rootDir = mkdtempSync(join(tmpdir(), "pi-session-approval-"));
@@ -2696,14 +2197,7 @@ test("session approval: first prompt with 'Yes, for this session' skips subseque
2696
2197
 
2697
2198
  const harness = createToolCallHarness(
2698
2199
  {
2699
- defaultPolicy: {
2700
- tools: "allow",
2701
- bash: "allow",
2702
- mcp: "allow",
2703
- skills: "allow",
2704
- special: "ask",
2705
- },
2706
- special: { external_directory: "ask" },
2200
+ permission: { "*": "allow", external_directory: "ask" },
2707
2201
  },
2708
2202
  ["read", "grep"],
2709
2203
  { cwd },
@@ -2766,14 +2260,7 @@ test("session approval: different directory prefix still prompts", async () => {
2766
2260
 
2767
2261
  const harness = createToolCallHarness(
2768
2262
  {
2769
- defaultPolicy: {
2770
- tools: "allow",
2771
- bash: "allow",
2772
- mcp: "allow",
2773
- skills: "allow",
2774
- special: "ask",
2775
- },
2776
- special: { external_directory: "ask" },
2263
+ permission: { "*": "allow", external_directory: "ask" },
2777
2264
  },
2778
2265
  ["read"],
2779
2266
  { cwd },
@@ -2818,14 +2305,7 @@ test("session approval: session_shutdown clears session approvals", async () =>
2818
2305
 
2819
2306
  const harness = createToolCallHarness(
2820
2307
  {
2821
- defaultPolicy: {
2822
- tools: "allow",
2823
- bash: "allow",
2824
- mcp: "allow",
2825
- skills: "allow",
2826
- special: "ask",
2827
- },
2828
- special: { external_directory: "ask" },
2308
+ permission: { "*": "allow", external_directory: "ask" },
2829
2309
  },
2830
2310
  ["read"],
2831
2311
  { cwd },
@@ -2876,14 +2356,7 @@ test("session approval: bash external directory with 'Yes, for this session' ski
2876
2356
 
2877
2357
  const harness = createToolCallHarness(
2878
2358
  {
2879
- defaultPolicy: {
2880
- tools: "allow",
2881
- bash: "allow",
2882
- mcp: "allow",
2883
- skills: "allow",
2884
- special: "ask",
2885
- },
2886
- special: { external_directory: "ask" },
2359
+ permission: { "*": "allow", external_directory: "ask" },
2887
2360
  },
2888
2361
  ["bash"],
2889
2362
  { cwd },
@@ -2931,14 +2404,7 @@ test("session approval: regular 'Yes' does not create session approval", async (
2931
2404
 
2932
2405
  const harness = createToolCallHarness(
2933
2406
  {
2934
- defaultPolicy: {
2935
- tools: "allow",
2936
- bash: "allow",
2937
- mcp: "allow",
2938
- skills: "allow",
2939
- special: "ask",
2940
- },
2941
- special: { external_directory: "ask" },
2407
+ permission: { "*": "allow", external_directory: "ask" },
2942
2408
  },
2943
2409
  ["read"],
2944
2410
  { cwd },
@@ -2980,13 +2446,7 @@ test("session approval: regular 'Yes' does not create session approval", async (
2980
2446
 
2981
2447
  test("checkPermission returns source 'session' when session rules cover the external_directory path", () => {
2982
2448
  const { manager, cleanup } = createManager({
2983
- defaultPolicy: {
2984
- tools: "allow",
2985
- bash: "allow",
2986
- mcp: "allow",
2987
- skills: "allow",
2988
- special: "ask",
2989
- },
2449
+ permission: { "*": "allow" },
2990
2450
  });
2991
2451
 
2992
2452
  try {
@@ -3015,13 +2475,7 @@ test("checkPermission returns source 'session' when session rules cover the exte
3015
2475
 
3016
2476
  test("checkPermission falls back to config policy when session rules do not cover the path", () => {
3017
2477
  const { manager, cleanup } = createManager({
3018
- defaultPolicy: {
3019
- tools: "allow",
3020
- bash: "allow",
3021
- mcp: "allow",
3022
- skills: "allow",
3023
- special: "deny",
3024
- },
2478
+ permission: { "*": "allow", external_directory: "deny" },
3025
2479
  });
3026
2480
 
3027
2481
  try {
@@ -3050,14 +2504,7 @@ test("checkPermission falls back to config policy when session rules do not cove
3050
2504
 
3051
2505
  test("checkPermission with empty session rules is identical to call without sessionRules arg", () => {
3052
2506
  const { manager, cleanup } = createManager({
3053
- defaultPolicy: {
3054
- tools: "allow",
3055
- bash: "allow",
3056
- mcp: "allow",
3057
- skills: "allow",
3058
- special: "ask",
3059
- },
3060
- special: { external_directory: "deny" },
2507
+ permission: { "*": "allow", external_directory: "deny" },
3061
2508
  });
3062
2509
 
3063
2510
  try {
@@ -3073,7 +2520,7 @@ test("checkPermission with empty session rules is identical to call without sess
3073
2520
  const expected: PermissionCheckResult = {
3074
2521
  toolName: "external_directory",
3075
2522
  state: "deny",
3076
- matchedPattern: "external_directory",
2523
+ matchedPattern: "*",
3077
2524
  source: "special",
3078
2525
  };
3079
2526
  assert.deepEqual(withEmpty, expected);
@@ -3085,13 +2532,8 @@ test("checkPermission with empty session rules is identical to call without sess
3085
2532
 
3086
2533
  test("session rules for one surface do not affect checks on other surfaces", () => {
3087
2534
  const { manager, cleanup } = createManager({
3088
- defaultPolicy: {
3089
- tools: "ask",
3090
- bash: "ask",
3091
- mcp: "ask",
3092
- skills: "ask",
3093
- special: "ask",
3094
- },
2535
+ // Empty permission: universal default is "ask" from DEFAULT_UNIVERSAL_FALLBACK.
2536
+ permission: {},
3095
2537
  });
3096
2538
 
3097
2539
  try {
@@ -3130,14 +2572,7 @@ test("session rules for one surface do not affect checks on other surfaces", ()
3130
2572
 
3131
2573
  test("session rules override config deny for external_directory", () => {
3132
2574
  const { manager, cleanup } = createManager({
3133
- defaultPolicy: {
3134
- tools: "allow",
3135
- bash: "allow",
3136
- mcp: "allow",
3137
- skills: "allow",
3138
- special: "ask",
3139
- },
3140
- special: { external_directory: "deny" },
2575
+ permission: { "*": "allow", external_directory: "deny" },
3141
2576
  });
3142
2577
 
3143
2578
  try {
@@ -3163,3 +2598,7 @@ test("session rules override config deny for external_directory", () => {
3163
2598
  cleanup();
3164
2599
  }
3165
2600
  });
2601
+
2602
+ // Suppress unused import warning — PermissionState used in type annotations
2603
+ const _unused: PermissionState = "ask";
2604
+ void _unused;