@gotgenes/pi-permission-system 3.11.0 → 4.0.1
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 +49 -0
- package/README.md +135 -168
- package/config/config.example.json +11 -21
- package/package.json +1 -1
- package/schemas/permissions.schema.json +34 -102
- package/src/config-loader.ts +87 -118
- package/src/defaults.ts +6 -62
- package/src/extension-config.ts +3 -4
- package/src/external-directory.ts +4 -0
- package/src/normalize.ts +22 -60
- package/src/permission-manager.ts +244 -348
- package/src/synthesize.ts +17 -82
- package/src/types.ts +12 -18
- package/tests/bash-external-directory.test.ts +31 -0
- package/tests/config-loader.test.ts +113 -63
- package/tests/defaults.test.ts +8 -101
- package/tests/extension-config.test.ts +12 -4
- package/tests/normalize.test.ts +67 -64
- package/tests/permission-system.test.ts +153 -714
- package/tests/session-start.test.ts +1 -7
- package/tests/synthesize.test.ts +46 -219
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
|
680
|
+
const resolve2 = manager.checkPermission("mcp", {
|
|
756
681
|
tool: "research:resolve-context",
|
|
757
682
|
});
|
|
758
|
-
assert.equal(
|
|
759
|
-
assert.equal(
|
|
760
|
-
assert.equal(
|
|
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
|
-
|
|
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
|
-
|
|
804
|
-
tools: "ask",
|
|
805
|
-
bash: "ask",
|
|
806
|
-
mcp: "ask",
|
|
807
|
-
skills: "ask",
|
|
808
|
-
special: "ask",
|
|
809
|
-
},
|
|
810
|
-
skills: {
|
|
722
|
+
permission: {
|
|
811
723
|
"*": "ask",
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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("
|
|
869
|
+
test("mcp catch-all acts as fallback for unmatched MCP targets", () => {
|
|
1000
870
|
const { manager, cleanup } = createManager(
|
|
1001
871
|
{
|
|
1002
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
|
898
|
+
test("specific MCP rules override mcp catch-all", () => {
|
|
1036
899
|
const { manager, cleanup } = createManager(
|
|
1037
900
|
{
|
|
1038
|
-
|
|
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
|
|
933
|
+
test("specific MCP rules still win when mcp catch-all is deny", () => {
|
|
1078
934
|
const { manager, cleanup } = createManager(
|
|
1079
935
|
{
|
|
1080
|
-
|
|
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, "
|
|
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("
|
|
977
|
+
test("mcp catch-all in agent frontmatter overrides global default", () => {
|
|
1129
978
|
const { manager, cleanup } = createManager(
|
|
1130
979
|
{
|
|
1131
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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("
|
|
1038
|
+
test("All surface names work in agent frontmatter flat permission format", () => {
|
|
1203
1039
|
const { manager, cleanup } = createManager(
|
|
1204
1040
|
{
|
|
1205
|
-
|
|
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, "
|
|
1232
|
-
assert.equal(taskResult.source, "
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1459
|
+
test("Full precedence chain base < project < system-agent < project-agent for universal default", () => {
|
|
1683
1460
|
const { manager, cleanup } = createManagerWithProject(
|
|
1684
1461
|
{
|
|
1685
|
-
|
|
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
|
-
|
|
1698
|
-
tools: ask
|
|
1468
|
+
"*": ask
|
|
1699
1469
|
---
|
|
1700
1470
|
`,
|
|
1701
1471
|
},
|
|
1702
1472
|
{
|
|
1703
1473
|
projectConfig: {
|
|
1704
|
-
|
|
1705
|
-
tools: "allow",
|
|
1706
|
-
},
|
|
1474
|
+
permission: { "*": "allow" },
|
|
1707
1475
|
},
|
|
1708
1476
|
projectAgentFiles: {
|
|
1709
1477
|
reviewer: `---
|
|
1710
1478
|
name: reviewer
|
|
1711
1479
|
permission:
|
|
1712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
|
1983
|
-
|
|
1984
|
-
|
|
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
|
|
1733
|
+
test("external_directory permission respects explicit deny", () => {
|
|
2004
1734
|
const { manager, cleanup } = createManager({
|
|
2005
|
-
|
|
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, "
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
3089
|
-
|
|
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
|
-
|
|
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;
|