@gotgenes/pi-permission-system 3.10.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.
- package/CHANGELOG.md +55 -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 -56
- package/src/extension-config.ts +3 -4
- package/src/handlers/tool-call.ts +15 -18
- package/src/normalize.ts +22 -60
- package/src/permission-manager.ts +309 -431
- package/src/rule.ts +5 -0
- package/src/session-rules.ts +1 -1
- package/src/synthesize.ts +87 -0
- package/src/types.ts +13 -19
- 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 +310 -677
- package/tests/rule.test.ts +31 -0
- package/tests/session-rules.test.ts +1 -0
- package/tests/session-start.test.ts +1 -7
- package/tests/synthesize.test.ts +240 -0
|
@@ -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,
|
|
@@ -46,7 +43,11 @@ import {
|
|
|
46
43
|
checkRequestedToolRegistration,
|
|
47
44
|
getToolNameFromValue,
|
|
48
45
|
} from "../src/tool-registry";
|
|
49
|
-
import type {
|
|
46
|
+
import type {
|
|
47
|
+
PermissionCheckResult,
|
|
48
|
+
PermissionState,
|
|
49
|
+
ScopeConfig,
|
|
50
|
+
} from "../src/types";
|
|
50
51
|
import {
|
|
51
52
|
canResolveAskPermissionRequest,
|
|
52
53
|
shouldAutoApprovePermissionState,
|
|
@@ -514,20 +515,7 @@ test("Before-agent-start cache dedupes unchanged active-tool exposure and prompt
|
|
|
514
515
|
|
|
515
516
|
test("Before-agent-start prompt cache invalidates on permission changes while runtime enforcement stays authoritative", () => {
|
|
516
517
|
const { manager, globalConfigPath, cleanup } = createManager({
|
|
517
|
-
|
|
518
|
-
tools: "allow",
|
|
519
|
-
bash: "allow",
|
|
520
|
-
mcp: "allow",
|
|
521
|
-
skills: "allow",
|
|
522
|
-
special: "allow",
|
|
523
|
-
},
|
|
524
|
-
tools: {
|
|
525
|
-
write: "deny",
|
|
526
|
-
},
|
|
527
|
-
bash: {},
|
|
528
|
-
mcp: {},
|
|
529
|
-
skills: {},
|
|
530
|
-
special: {},
|
|
518
|
+
permission: { "*": "allow", write: "deny" },
|
|
531
519
|
});
|
|
532
520
|
|
|
533
521
|
try {
|
|
@@ -547,22 +535,7 @@ test("Before-agent-start prompt cache invalidates on permission changes while ru
|
|
|
547
535
|
assert.equal(manager.checkPermission("write", {}, undefined).state, "deny");
|
|
548
536
|
|
|
549
537
|
const updatedConfig = `${JSON.stringify(
|
|
550
|
-
{
|
|
551
|
-
defaultPolicy: {
|
|
552
|
-
tools: "allow",
|
|
553
|
-
bash: "allow",
|
|
554
|
-
mcp: "allow",
|
|
555
|
-
skills: "allow",
|
|
556
|
-
special: "allow",
|
|
557
|
-
},
|
|
558
|
-
tools: {
|
|
559
|
-
write: "allow",
|
|
560
|
-
},
|
|
561
|
-
bash: {},
|
|
562
|
-
mcp: {},
|
|
563
|
-
skills: {},
|
|
564
|
-
special: {},
|
|
565
|
-
},
|
|
538
|
+
{ permission: { "*": "allow", write: "allow" } },
|
|
566
539
|
null,
|
|
567
540
|
2,
|
|
568
541
|
)}\n`;
|
|
@@ -633,10 +606,6 @@ test("Permission-system logger respects debug toggle and keeps review log enable
|
|
|
633
606
|
assert.equal(reviewWarning, undefined);
|
|
634
607
|
assert.equal(existsSync(debugLogPath), false);
|
|
635
608
|
assert.equal(existsSync(reviewLogPath), true);
|
|
636
|
-
assert.match(
|
|
637
|
-
readFileSync(reviewLogPath, "utf8"),
|
|
638
|
-
/permission_request\.waiting/,
|
|
639
|
-
);
|
|
640
609
|
|
|
641
610
|
config.debugLog = true;
|
|
642
611
|
const enabledDebugWarning = logger.debug("debug.enabled", { sample: true });
|
|
@@ -650,16 +619,7 @@ test("Permission-system logger respects debug toggle and keeps review log enable
|
|
|
650
619
|
|
|
651
620
|
test("PermissionManager canonical built-in permission checking", () => {
|
|
652
621
|
const { manager, cleanup } = createManager({
|
|
653
|
-
|
|
654
|
-
tools: "deny",
|
|
655
|
-
bash: "ask",
|
|
656
|
-
mcp: "ask",
|
|
657
|
-
skills: "ask",
|
|
658
|
-
special: "ask",
|
|
659
|
-
},
|
|
660
|
-
tools: {
|
|
661
|
-
read: "allow",
|
|
662
|
-
},
|
|
622
|
+
permission: { "*": "deny", read: "allow" },
|
|
663
623
|
});
|
|
664
624
|
|
|
665
625
|
try {
|
|
@@ -675,49 +635,26 @@ test("PermissionManager canonical built-in permission checking", () => {
|
|
|
675
635
|
}
|
|
676
636
|
});
|
|
677
637
|
|
|
678
|
-
test("Bash patterns
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
skills: "ask",
|
|
686
|
-
special: "ask",
|
|
687
|
-
},
|
|
688
|
-
bash: {
|
|
689
|
-
"rm -rf *": "deny",
|
|
690
|
-
},
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
reviewer: `---
|
|
694
|
-
name: reviewer
|
|
695
|
-
permission:
|
|
696
|
-
tools:
|
|
697
|
-
bash: allow
|
|
698
|
-
---
|
|
699
|
-
`,
|
|
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" },
|
|
700
645
|
},
|
|
701
|
-
);
|
|
646
|
+
});
|
|
702
647
|
|
|
703
648
|
try {
|
|
704
|
-
const denied = manager.checkPermission(
|
|
705
|
-
"bash",
|
|
706
|
-
{ command: "rm -rf build" },
|
|
707
|
-
"reviewer",
|
|
708
|
-
);
|
|
649
|
+
const denied = manager.checkPermission("bash", { command: "rm -rf build" });
|
|
709
650
|
assert.equal(denied.state, "deny");
|
|
710
651
|
assert.equal(denied.source, "bash");
|
|
711
652
|
assert.equal(denied.matchedPattern, "rm -rf *");
|
|
712
653
|
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
);
|
|
718
|
-
assert.equal(fallback.state, "allow");
|
|
719
|
-
assert.equal(fallback.source, "bash");
|
|
720
|
-
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, "*");
|
|
721
658
|
} finally {
|
|
722
659
|
cleanup();
|
|
723
660
|
}
|
|
@@ -725,17 +662,9 @@ permission:
|
|
|
725
662
|
|
|
726
663
|
test("MCP wildcard matching uses the registered mcp tool", () => {
|
|
727
664
|
const { manager, cleanup } = createManager({
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
mcp: "ask",
|
|
732
|
-
skills: "ask",
|
|
733
|
-
special: "ask",
|
|
734
|
-
},
|
|
735
|
-
mcp: {
|
|
736
|
-
"*": "deny",
|
|
737
|
-
"research_*": "ask",
|
|
738
|
-
"research_query-*": "allow",
|
|
665
|
+
permission: {
|
|
666
|
+
"*": "ask",
|
|
667
|
+
mcp: { "*": "deny", "research_*": "ask", "research_query-*": "allow" },
|
|
739
668
|
},
|
|
740
669
|
});
|
|
741
670
|
|
|
@@ -748,12 +677,12 @@ test("MCP wildcard matching uses the registered mcp tool", () => {
|
|
|
748
677
|
assert.equal(queryDocs.matchedPattern, "research_query-*");
|
|
749
678
|
assert.equal(queryDocs.target, "research_query-docs");
|
|
750
679
|
|
|
751
|
-
const
|
|
680
|
+
const resolve2 = manager.checkPermission("mcp", {
|
|
752
681
|
tool: "research:resolve-context",
|
|
753
682
|
});
|
|
754
|
-
assert.equal(
|
|
755
|
-
assert.equal(
|
|
756
|
-
assert.equal(
|
|
683
|
+
assert.equal(resolve2.state, "ask");
|
|
684
|
+
assert.equal(resolve2.matchedPattern, "research_*");
|
|
685
|
+
assert.equal(resolve2.target, "research_resolve-context");
|
|
757
686
|
|
|
758
687
|
const unknown = manager.checkPermission("mcp", { tool: "search:provider" });
|
|
759
688
|
assert.equal(unknown.state, "deny");
|
|
@@ -766,18 +695,10 @@ test("MCP wildcard matching uses the registered mcp tool", () => {
|
|
|
766
695
|
|
|
767
696
|
test("Arbitrary extension tools use exact-name tool permissions instead of MCP fallback", () => {
|
|
768
697
|
const { manager, cleanup } = createManager({
|
|
769
|
-
|
|
770
|
-
tools: "deny",
|
|
771
|
-
bash: "ask",
|
|
772
|
-
mcp: "allow",
|
|
773
|
-
skills: "ask",
|
|
774
|
-
special: "ask",
|
|
775
|
-
},
|
|
776
|
-
tools: {
|
|
777
|
-
third_party_tool: "allow",
|
|
778
|
-
},
|
|
779
|
-
mcp: {
|
|
698
|
+
permission: {
|
|
780
699
|
"*": "deny",
|
|
700
|
+
third_party_tool: "allow",
|
|
701
|
+
mcp: { "*": "deny" },
|
|
781
702
|
},
|
|
782
703
|
});
|
|
783
704
|
|
|
@@ -786,6 +707,8 @@ test("Arbitrary extension tools use exact-name tool permissions instead of MCP f
|
|
|
786
707
|
assert.equal(allowed.state, "allow");
|
|
787
708
|
assert.equal(allowed.source, "tool");
|
|
788
709
|
|
|
710
|
+
// another_extension_tool has no explicit rule — falls through to the
|
|
711
|
+
// universal default (permission["*"] = "deny") with source "default".
|
|
789
712
|
const fallback = manager.checkPermission("another_extension_tool", {});
|
|
790
713
|
assert.equal(fallback.state, "deny");
|
|
791
714
|
assert.equal(fallback.source, "default");
|
|
@@ -796,17 +719,13 @@ test("Arbitrary extension tools use exact-name tool permissions instead of MCP f
|
|
|
796
719
|
|
|
797
720
|
test("Skill permission matching", () => {
|
|
798
721
|
const { manager, cleanup } = createManager({
|
|
799
|
-
|
|
800
|
-
tools: "ask",
|
|
801
|
-
bash: "ask",
|
|
802
|
-
mcp: "ask",
|
|
803
|
-
skills: "ask",
|
|
804
|
-
special: "ask",
|
|
805
|
-
},
|
|
806
|
-
skills: {
|
|
722
|
+
permission: {
|
|
807
723
|
"*": "ask",
|
|
808
|
-
|
|
809
|
-
|
|
724
|
+
skill: {
|
|
725
|
+
"*": "ask",
|
|
726
|
+
"web-*": "deny",
|
|
727
|
+
"requesting-code-review": "allow",
|
|
728
|
+
},
|
|
810
729
|
},
|
|
811
730
|
});
|
|
812
731
|
|
|
@@ -837,16 +756,9 @@ test("Skill permission matching", () => {
|
|
|
837
756
|
test("MCP proxy tool infers server-prefixed aliases from configured server names", () => {
|
|
838
757
|
const { manager, cleanup } = createManager(
|
|
839
758
|
{
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
mcp: "ask",
|
|
844
|
-
skills: "ask",
|
|
845
|
-
special: "ask",
|
|
846
|
-
},
|
|
847
|
-
mcp: {
|
|
848
|
-
"exa_*": "deny",
|
|
849
|
-
exa_get_code_context_exa: "allow",
|
|
759
|
+
permission: {
|
|
760
|
+
"*": "ask",
|
|
761
|
+
mcp: { "exa_*": "deny", exa_get_code_context_exa: "allow" },
|
|
850
762
|
},
|
|
851
763
|
},
|
|
852
764
|
{},
|
|
@@ -876,25 +788,8 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
|
|
|
876
788
|
const agentsDir = join(baseDir, "agents");
|
|
877
789
|
mkdirSync(agentsDir, { recursive: true });
|
|
878
790
|
|
|
879
|
-
// Policy: allow any target prefixed with legacy-server, default mcp is ask.
|
|
880
|
-
// If legacy-server were known as a configured server name, a tool named
|
|
881
|
-
// "some_tool_legacy-server" would derive "legacy-server_some_tool_legacy-server"
|
|
882
|
-
// which matches this rule and returns "allow".
|
|
883
|
-
// After the fix, settings.json is ignored, so no server name is derived and the
|
|
884
|
-
// result falls through to the default mcp policy ("ask").
|
|
885
791
|
const config: ScopeConfig = {
|
|
886
|
-
|
|
887
|
-
tools: "ask",
|
|
888
|
-
bash: "ask",
|
|
889
|
-
mcp: "ask",
|
|
890
|
-
skills: "ask",
|
|
891
|
-
special: "ask",
|
|
892
|
-
},
|
|
893
|
-
tools: {},
|
|
894
|
-
bash: {},
|
|
895
|
-
mcp: { "legacy-server_*": "allow" },
|
|
896
|
-
skills: {},
|
|
897
|
-
special: {},
|
|
792
|
+
permission: { "*": "ask", mcp: { "legacy-server_*": "allow" } },
|
|
898
793
|
};
|
|
899
794
|
|
|
900
795
|
writeFileSync(
|
|
@@ -902,9 +797,7 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
|
|
|
902
797
|
`${JSON.stringify(config, null, 2)}\n`,
|
|
903
798
|
"utf8",
|
|
904
799
|
);
|
|
905
|
-
// mcp.json does not know about legacy-server.
|
|
906
800
|
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers: {} }), "utf8");
|
|
907
|
-
// settings.json has legacy-server — the legacy source that must now be ignored.
|
|
908
801
|
writeFileSync(
|
|
909
802
|
settingsJsonPath,
|
|
910
803
|
JSON.stringify({ mcpServers: { "legacy-server": {} } }),
|
|
@@ -918,8 +811,6 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
|
|
|
918
811
|
});
|
|
919
812
|
|
|
920
813
|
try {
|
|
921
|
-
// "legacy-server" must not be derived from settings.json.
|
|
922
|
-
// The bare tool name falls through to the default mcp policy → "ask".
|
|
923
814
|
const result = manager.checkPermission("mcp", {
|
|
924
815
|
tool: "some_tool_legacy-server",
|
|
925
816
|
});
|
|
@@ -932,16 +823,9 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
|
|
|
932
823
|
test("MCP describe mode normalizes qualified tool names without duplicating server prefixes", () => {
|
|
933
824
|
const { manager, cleanup } = createManager(
|
|
934
825
|
{
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
mcp: "ask",
|
|
939
|
-
skills: "ask",
|
|
940
|
-
special: "ask",
|
|
941
|
-
},
|
|
942
|
-
mcp: {
|
|
943
|
-
"exa_*": "deny",
|
|
944
|
-
exa_web_search_exa: "allow",
|
|
826
|
+
permission: {
|
|
827
|
+
"*": "ask",
|
|
828
|
+
mcp: { "exa_*": "deny", exa_web_search_exa: "allow" },
|
|
945
829
|
},
|
|
946
830
|
},
|
|
947
831
|
{},
|
|
@@ -966,17 +850,7 @@ test("MCP describe mode normalizes qualified tool names without duplicating serv
|
|
|
966
850
|
|
|
967
851
|
test("Canonical tools map directly without legacy aliases", () => {
|
|
968
852
|
const { manager, cleanup } = createManager({
|
|
969
|
-
|
|
970
|
-
tools: "ask",
|
|
971
|
-
bash: "ask",
|
|
972
|
-
mcp: "ask",
|
|
973
|
-
skills: "ask",
|
|
974
|
-
special: "ask",
|
|
975
|
-
},
|
|
976
|
-
tools: {
|
|
977
|
-
find: "allow",
|
|
978
|
-
ls: "deny",
|
|
979
|
-
},
|
|
853
|
+
permission: { "*": "ask", find: "allow", ls: "deny" },
|
|
980
854
|
});
|
|
981
855
|
|
|
982
856
|
try {
|
|
@@ -992,23 +866,16 @@ test("Canonical tools map directly without legacy aliases", () => {
|
|
|
992
866
|
}
|
|
993
867
|
});
|
|
994
868
|
|
|
995
|
-
test("
|
|
869
|
+
test("mcp catch-all acts as fallback for unmatched MCP targets", () => {
|
|
996
870
|
const { manager, cleanup } = createManager(
|
|
997
871
|
{
|
|
998
|
-
|
|
999
|
-
tools: "ask",
|
|
1000
|
-
bash: "ask",
|
|
1001
|
-
mcp: "ask",
|
|
1002
|
-
skills: "ask",
|
|
1003
|
-
special: "ask",
|
|
1004
|
-
},
|
|
872
|
+
permission: { "*": "ask" },
|
|
1005
873
|
},
|
|
1006
874
|
{
|
|
1007
875
|
reviewer: `---
|
|
1008
876
|
name: reviewer
|
|
1009
877
|
permission:
|
|
1010
|
-
|
|
1011
|
-
mcp: allow
|
|
878
|
+
mcp: allow
|
|
1012
879
|
---
|
|
1013
880
|
`,
|
|
1014
881
|
},
|
|
@@ -1021,31 +888,24 @@ permission:
|
|
|
1021
888
|
"reviewer",
|
|
1022
889
|
);
|
|
1023
890
|
assert.equal(result.state, "allow");
|
|
1024
|
-
assert.equal(result.source, "
|
|
891
|
+
assert.equal(result.source, "mcp");
|
|
1025
892
|
assert.equal(result.target, "exa_web_search_exa");
|
|
1026
893
|
} finally {
|
|
1027
894
|
cleanup();
|
|
1028
895
|
}
|
|
1029
896
|
});
|
|
1030
897
|
|
|
1031
|
-
test("specific MCP rules override
|
|
898
|
+
test("specific MCP rules override mcp catch-all", () => {
|
|
1032
899
|
const { manager, cleanup } = createManager(
|
|
1033
900
|
{
|
|
1034
|
-
|
|
1035
|
-
tools: "ask",
|
|
1036
|
-
bash: "ask",
|
|
1037
|
-
mcp: "ask",
|
|
1038
|
-
skills: "ask",
|
|
1039
|
-
special: "ask",
|
|
1040
|
-
},
|
|
901
|
+
permission: { "*": "ask" },
|
|
1041
902
|
},
|
|
1042
903
|
{
|
|
1043
904
|
reviewer: `---
|
|
1044
905
|
name: reviewer
|
|
1045
906
|
permission:
|
|
1046
|
-
tools:
|
|
1047
|
-
mcp: allow
|
|
1048
907
|
mcp:
|
|
908
|
+
"*": allow
|
|
1049
909
|
exa_web_search_exa: deny
|
|
1050
910
|
---
|
|
1051
911
|
`,
|
|
@@ -1070,24 +930,17 @@ permission:
|
|
|
1070
930
|
}
|
|
1071
931
|
});
|
|
1072
932
|
|
|
1073
|
-
test("specific MCP rules still win when
|
|
933
|
+
test("specific MCP rules still win when mcp catch-all is deny", () => {
|
|
1074
934
|
const { manager, cleanup } = createManager(
|
|
1075
935
|
{
|
|
1076
|
-
|
|
1077
|
-
tools: "ask",
|
|
1078
|
-
bash: "ask",
|
|
1079
|
-
mcp: "ask",
|
|
1080
|
-
skills: "ask",
|
|
1081
|
-
special: "ask",
|
|
1082
|
-
},
|
|
936
|
+
permission: { "*": "ask" },
|
|
1083
937
|
},
|
|
1084
938
|
{
|
|
1085
939
|
reviewer: `---
|
|
1086
940
|
name: reviewer
|
|
1087
941
|
permission:
|
|
1088
|
-
tools:
|
|
1089
|
-
mcp: deny
|
|
1090
942
|
mcp:
|
|
943
|
+
"*": deny
|
|
1091
944
|
exa_web_search_exa: allow
|
|
1092
945
|
---
|
|
1093
946
|
`,
|
|
@@ -1114,30 +967,23 @@ permission:
|
|
|
1114
967
|
"reviewer",
|
|
1115
968
|
);
|
|
1116
969
|
assert.equal(fallback.state, "deny");
|
|
1117
|
-
assert.equal(fallback.source, "
|
|
970
|
+
assert.equal(fallback.source, "mcp");
|
|
1118
971
|
assert.equal(fallback.target, "exa_other_exa");
|
|
1119
972
|
} finally {
|
|
1120
973
|
cleanup();
|
|
1121
974
|
}
|
|
1122
975
|
});
|
|
1123
976
|
|
|
1124
|
-
test("
|
|
977
|
+
test("mcp catch-all in agent frontmatter overrides global default", () => {
|
|
1125
978
|
const { manager, cleanup } = createManager(
|
|
1126
979
|
{
|
|
1127
|
-
|
|
1128
|
-
tools: "deny",
|
|
1129
|
-
bash: "deny",
|
|
1130
|
-
mcp: "deny",
|
|
1131
|
-
skills: "deny",
|
|
1132
|
-
special: "deny",
|
|
1133
|
-
},
|
|
980
|
+
permission: { "*": "deny" },
|
|
1134
981
|
},
|
|
1135
982
|
{
|
|
1136
983
|
reviewer: `---
|
|
1137
984
|
name: reviewer
|
|
1138
985
|
permission:
|
|
1139
|
-
|
|
1140
|
-
mcp: allow
|
|
986
|
+
mcp: allow
|
|
1141
987
|
---
|
|
1142
988
|
`,
|
|
1143
989
|
},
|
|
@@ -1154,7 +1000,7 @@ permission:
|
|
|
1154
1000
|
"reviewer",
|
|
1155
1001
|
);
|
|
1156
1002
|
assert.equal(mcpResult.state, "allow");
|
|
1157
|
-
assert.equal(mcpResult.source, "
|
|
1003
|
+
assert.equal(mcpResult.source, "mcp");
|
|
1158
1004
|
} finally {
|
|
1159
1005
|
cleanup();
|
|
1160
1006
|
}
|
|
@@ -1163,13 +1009,7 @@ permission:
|
|
|
1163
1009
|
test("Agent frontmatter canonical tools resolve correctly", () => {
|
|
1164
1010
|
const { manager, cleanup } = createManager(
|
|
1165
1011
|
{
|
|
1166
|
-
|
|
1167
|
-
tools: "deny",
|
|
1168
|
-
bash: "ask",
|
|
1169
|
-
mcp: "ask",
|
|
1170
|
-
skills: "ask",
|
|
1171
|
-
special: "ask",
|
|
1172
|
-
},
|
|
1012
|
+
permission: { "*": "deny" },
|
|
1173
1013
|
},
|
|
1174
1014
|
{
|
|
1175
1015
|
reviewer: `---
|
|
@@ -1195,16 +1035,10 @@ permission:
|
|
|
1195
1035
|
}
|
|
1196
1036
|
});
|
|
1197
1037
|
|
|
1198
|
-
test("
|
|
1038
|
+
test("All surface names work in agent frontmatter flat permission format", () => {
|
|
1199
1039
|
const { manager, cleanup } = createManager(
|
|
1200
1040
|
{
|
|
1201
|
-
|
|
1202
|
-
tools: "deny",
|
|
1203
|
-
bash: "ask",
|
|
1204
|
-
mcp: "deny",
|
|
1205
|
-
skills: "ask",
|
|
1206
|
-
special: "ask",
|
|
1207
|
-
},
|
|
1041
|
+
permission: { "*": "deny" },
|
|
1208
1042
|
},
|
|
1209
1043
|
{
|
|
1210
1044
|
reviewer: `---
|
|
@@ -1223,17 +1057,18 @@ permission:
|
|
|
1223
1057
|
assert.equal(findResult.state, "allow");
|
|
1224
1058
|
assert.equal(findResult.source, "tool");
|
|
1225
1059
|
|
|
1060
|
+
// In flat format any surface key works, including extension tools
|
|
1226
1061
|
const taskResult = manager.checkPermission("task", {}, "reviewer");
|
|
1227
|
-
assert.equal(taskResult.state, "
|
|
1228
|
-
assert.equal(taskResult.source, "
|
|
1062
|
+
assert.equal(taskResult.state, "allow");
|
|
1063
|
+
assert.equal(taskResult.source, "tool");
|
|
1229
1064
|
|
|
1065
|
+
// mcp: allow catches all MCP targets
|
|
1230
1066
|
const mcpResult = manager.checkPermission(
|
|
1231
1067
|
"mcp",
|
|
1232
1068
|
{ tool: "exa:web_search_exa" },
|
|
1233
1069
|
"reviewer",
|
|
1234
1070
|
);
|
|
1235
|
-
assert.equal(mcpResult.state, "
|
|
1236
|
-
assert.equal(mcpResult.source, "default");
|
|
1071
|
+
assert.equal(mcpResult.state, "allow");
|
|
1237
1072
|
} finally {
|
|
1238
1073
|
cleanup();
|
|
1239
1074
|
}
|
|
@@ -1241,16 +1076,7 @@ permission:
|
|
|
1241
1076
|
|
|
1242
1077
|
test("task uses exact-name tool permissions like any registered extension tool", () => {
|
|
1243
1078
|
const { manager, cleanup } = createManager({
|
|
1244
|
-
|
|
1245
|
-
tools: "deny",
|
|
1246
|
-
bash: "ask",
|
|
1247
|
-
mcp: "allow",
|
|
1248
|
-
skills: "ask",
|
|
1249
|
-
special: "ask",
|
|
1250
|
-
},
|
|
1251
|
-
tools: {
|
|
1252
|
-
task: "allow",
|
|
1253
|
-
},
|
|
1079
|
+
permission: { "*": "deny", task: "allow" },
|
|
1254
1080
|
});
|
|
1255
1081
|
|
|
1256
1082
|
try {
|
|
@@ -1303,22 +1129,15 @@ test("Tool registry blocks unregistered tools and handles aliases", () => {
|
|
|
1303
1129
|
test("getToolPermission returns tool-level policy for canonical and extension tools", () => {
|
|
1304
1130
|
const { manager, cleanup } = createManager(
|
|
1305
1131
|
{
|
|
1306
|
-
|
|
1307
|
-
tools: "ask",
|
|
1308
|
-
bash: "ask",
|
|
1309
|
-
mcp: "ask",
|
|
1310
|
-
skills: "ask",
|
|
1311
|
-
special: "ask",
|
|
1312
|
-
},
|
|
1132
|
+
permission: { "*": "ask" },
|
|
1313
1133
|
},
|
|
1314
1134
|
{
|
|
1315
1135
|
reviewer: `---
|
|
1316
1136
|
name: reviewer
|
|
1317
1137
|
permission:
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
task: allow
|
|
1138
|
+
bash: deny
|
|
1139
|
+
read: deny
|
|
1140
|
+
task: allow
|
|
1322
1141
|
---
|
|
1323
1142
|
`,
|
|
1324
1143
|
},
|
|
@@ -1338,16 +1157,7 @@ permission:
|
|
|
1338
1157
|
assert.equal(defaultBashPermission, "ask");
|
|
1339
1158
|
|
|
1340
1159
|
const { manager: manager2, cleanup: cleanup2 } = createManager({
|
|
1341
|
-
|
|
1342
|
-
tools: "deny",
|
|
1343
|
-
bash: "ask",
|
|
1344
|
-
mcp: "ask",
|
|
1345
|
-
skills: "ask",
|
|
1346
|
-
special: "ask",
|
|
1347
|
-
},
|
|
1348
|
-
tools: {
|
|
1349
|
-
bash: "allow",
|
|
1350
|
-
},
|
|
1160
|
+
permission: { "*": "deny", bash: "allow" },
|
|
1351
1161
|
});
|
|
1352
1162
|
|
|
1353
1163
|
try {
|
|
@@ -1363,16 +1173,7 @@ permission:
|
|
|
1363
1173
|
|
|
1364
1174
|
test("getToolPermission supports arbitrary extension tool names", () => {
|
|
1365
1175
|
const { manager, cleanup } = createManager({
|
|
1366
|
-
|
|
1367
|
-
tools: "deny",
|
|
1368
|
-
bash: "ask",
|
|
1369
|
-
mcp: "allow",
|
|
1370
|
-
skills: "ask",
|
|
1371
|
-
special: "ask",
|
|
1372
|
-
},
|
|
1373
|
-
tools: {
|
|
1374
|
-
third_party_tool: "allow",
|
|
1375
|
-
},
|
|
1176
|
+
permission: { "*": "deny", third_party_tool: "allow" },
|
|
1376
1177
|
});
|
|
1377
1178
|
|
|
1378
1179
|
try {
|
|
@@ -1481,6 +1282,10 @@ test("Permission forwarding rejects unresolved sentinel session ids", () => {
|
|
|
1481
1282
|
assert.equal(targetSessionId, null);
|
|
1482
1283
|
});
|
|
1483
1284
|
|
|
1285
|
+
// ---------------------------------------------------------------------------
|
|
1286
|
+
// Project-level and per-agent config scope tests
|
|
1287
|
+
// ---------------------------------------------------------------------------
|
|
1288
|
+
|
|
1484
1289
|
type CreateManagerWithProjectOptions = CreateManagerOptions & {
|
|
1485
1290
|
projectConfig?: ScopeConfig;
|
|
1486
1291
|
projectAgentFiles?: Record<string, string>;
|
|
@@ -1545,23 +1350,15 @@ function createManagerWithProject(
|
|
|
1545
1350
|
test("Project-level config overrides base bash patterns", () => {
|
|
1546
1351
|
const { manager, cleanup } = createManagerWithProject(
|
|
1547
1352
|
{
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
bash: "ask",
|
|
1551
|
-
mcp: "ask",
|
|
1552
|
-
skills: "ask",
|
|
1553
|
-
special: "ask",
|
|
1554
|
-
},
|
|
1555
|
-
bash: {
|
|
1556
|
-
"rm -rf *": "deny",
|
|
1353
|
+
permission: {
|
|
1354
|
+
"*": "allow",
|
|
1355
|
+
bash: { "*": "ask", "rm -rf *": "deny" },
|
|
1557
1356
|
},
|
|
1558
1357
|
},
|
|
1559
1358
|
{},
|
|
1560
1359
|
{
|
|
1561
1360
|
projectConfig: {
|
|
1562
|
-
bash: {
|
|
1563
|
-
"rm -rf build": "allow",
|
|
1564
|
-
},
|
|
1361
|
+
permission: { bash: { "rm -rf build": "allow" } },
|
|
1565
1362
|
},
|
|
1566
1363
|
},
|
|
1567
1364
|
);
|
|
@@ -1586,13 +1383,7 @@ test("Project-level config overrides base bash patterns", () => {
|
|
|
1586
1383
|
test("System-agent config overrides project-level bash patterns", () => {
|
|
1587
1384
|
const { manager, cleanup } = createManagerWithProject(
|
|
1588
1385
|
{
|
|
1589
|
-
|
|
1590
|
-
tools: "allow",
|
|
1591
|
-
bash: "ask",
|
|
1592
|
-
mcp: "ask",
|
|
1593
|
-
skills: "ask",
|
|
1594
|
-
special: "ask",
|
|
1595
|
-
},
|
|
1386
|
+
permission: { "*": "allow", bash: "ask" },
|
|
1596
1387
|
},
|
|
1597
1388
|
{
|
|
1598
1389
|
reviewer: `---
|
|
@@ -1605,9 +1396,7 @@ permission:
|
|
|
1605
1396
|
},
|
|
1606
1397
|
{
|
|
1607
1398
|
projectConfig: {
|
|
1608
|
-
bash: {
|
|
1609
|
-
"git *": "deny",
|
|
1610
|
-
},
|
|
1399
|
+
permission: { bash: { "git *": "deny" } },
|
|
1611
1400
|
},
|
|
1612
1401
|
},
|
|
1613
1402
|
);
|
|
@@ -1636,20 +1425,13 @@ permission:
|
|
|
1636
1425
|
test("Project-agent config overrides system-agent tool rules", () => {
|
|
1637
1426
|
const { manager, cleanup } = createManagerWithProject(
|
|
1638
1427
|
{
|
|
1639
|
-
|
|
1640
|
-
tools: "ask",
|
|
1641
|
-
bash: "ask",
|
|
1642
|
-
mcp: "ask",
|
|
1643
|
-
skills: "ask",
|
|
1644
|
-
special: "ask",
|
|
1645
|
-
},
|
|
1428
|
+
permission: { "*": "ask" },
|
|
1646
1429
|
},
|
|
1647
1430
|
{
|
|
1648
1431
|
reviewer: `---
|
|
1649
1432
|
name: reviewer
|
|
1650
1433
|
permission:
|
|
1651
|
-
|
|
1652
|
-
read: deny
|
|
1434
|
+
read: deny
|
|
1653
1435
|
---
|
|
1654
1436
|
`,
|
|
1655
1437
|
},
|
|
@@ -1658,8 +1440,7 @@ permission:
|
|
|
1658
1440
|
reviewer: `---
|
|
1659
1441
|
name: reviewer
|
|
1660
1442
|
permission:
|
|
1661
|
-
|
|
1662
|
-
read: allow
|
|
1443
|
+
read: allow
|
|
1663
1444
|
---
|
|
1664
1445
|
`,
|
|
1665
1446
|
},
|
|
@@ -1675,38 +1456,28 @@ permission:
|
|
|
1675
1456
|
}
|
|
1676
1457
|
});
|
|
1677
1458
|
|
|
1678
|
-
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", () => {
|
|
1679
1460
|
const { manager, cleanup } = createManagerWithProject(
|
|
1680
1461
|
{
|
|
1681
|
-
|
|
1682
|
-
tools: "deny",
|
|
1683
|
-
bash: "ask",
|
|
1684
|
-
mcp: "ask",
|
|
1685
|
-
skills: "ask",
|
|
1686
|
-
special: "ask",
|
|
1687
|
-
},
|
|
1462
|
+
permission: { "*": "deny" },
|
|
1688
1463
|
},
|
|
1689
1464
|
{
|
|
1690
1465
|
reviewer: `---
|
|
1691
1466
|
name: reviewer
|
|
1692
1467
|
permission:
|
|
1693
|
-
|
|
1694
|
-
tools: ask
|
|
1468
|
+
"*": ask
|
|
1695
1469
|
---
|
|
1696
1470
|
`,
|
|
1697
1471
|
},
|
|
1698
1472
|
{
|
|
1699
1473
|
projectConfig: {
|
|
1700
|
-
|
|
1701
|
-
tools: "allow",
|
|
1702
|
-
},
|
|
1474
|
+
permission: { "*": "allow" },
|
|
1703
1475
|
},
|
|
1704
1476
|
projectAgentFiles: {
|
|
1705
1477
|
reviewer: `---
|
|
1706
1478
|
name: reviewer
|
|
1707
1479
|
permission:
|
|
1708
|
-
|
|
1709
|
-
tools: deny
|
|
1480
|
+
"*": deny
|
|
1710
1481
|
---
|
|
1711
1482
|
`,
|
|
1712
1483
|
},
|
|
@@ -1733,13 +1504,7 @@ permission:
|
|
|
1733
1504
|
test("Project-agent applies even without a matching system-agent file", () => {
|
|
1734
1505
|
const { manager, cleanup } = createManagerWithProject(
|
|
1735
1506
|
{
|
|
1736
|
-
|
|
1737
|
-
tools: "allow",
|
|
1738
|
-
bash: "ask",
|
|
1739
|
-
mcp: "ask",
|
|
1740
|
-
skills: "ask",
|
|
1741
|
-
special: "ask",
|
|
1742
|
-
},
|
|
1507
|
+
permission: { "*": "allow" },
|
|
1743
1508
|
},
|
|
1744
1509
|
{},
|
|
1745
1510
|
{
|
|
@@ -1747,8 +1512,7 @@ test("Project-agent applies even without a matching system-agent file", () => {
|
|
|
1747
1512
|
reviewer: `---
|
|
1748
1513
|
name: reviewer
|
|
1749
1514
|
permission:
|
|
1750
|
-
|
|
1751
|
-
read: deny
|
|
1515
|
+
read: deny
|
|
1752
1516
|
---
|
|
1753
1517
|
`,
|
|
1754
1518
|
},
|
|
@@ -1780,18 +1544,7 @@ test("PermissionManager reads config from PI_CODING_AGENT_DIR when set", () => {
|
|
|
1780
1544
|
mkdirSync(dirname(newConfigPath), { recursive: true });
|
|
1781
1545
|
|
|
1782
1546
|
const config: ScopeConfig = {
|
|
1783
|
-
|
|
1784
|
-
tools: "deny",
|
|
1785
|
-
bash: "deny",
|
|
1786
|
-
mcp: "deny",
|
|
1787
|
-
skills: "deny",
|
|
1788
|
-
special: "deny",
|
|
1789
|
-
},
|
|
1790
|
-
tools: { read: "allow" },
|
|
1791
|
-
bash: {},
|
|
1792
|
-
mcp: {},
|
|
1793
|
-
skills: {},
|
|
1794
|
-
special: {},
|
|
1547
|
+
permission: { "*": "deny", read: "allow" },
|
|
1795
1548
|
};
|
|
1796
1549
|
writeFileSync(newConfigPath, JSON.stringify(config), "utf8");
|
|
1797
1550
|
|
|
@@ -1848,15 +1601,9 @@ test("parseAllSkillPromptSections finds every available_skills block", () => {
|
|
|
1848
1601
|
|
|
1849
1602
|
test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills block", () => {
|
|
1850
1603
|
const { manager, cleanup } = createManager({
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
mcp: "ask",
|
|
1855
|
-
skills: "ask",
|
|
1856
|
-
special: "ask",
|
|
1857
|
-
},
|
|
1858
|
-
skills: {
|
|
1859
|
-
"denied-skill": "deny",
|
|
1604
|
+
permission: {
|
|
1605
|
+
"*": "ask",
|
|
1606
|
+
skill: { "denied-skill": "deny" },
|
|
1860
1607
|
},
|
|
1861
1608
|
});
|
|
1862
1609
|
|
|
@@ -1915,15 +1662,9 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
|
|
|
1915
1662
|
|
|
1916
1663
|
test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available for path matching", () => {
|
|
1917
1664
|
const { manager, cleanup } = createManager({
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
mcp: "ask",
|
|
1922
|
-
skills: "ask",
|
|
1923
|
-
special: "ask",
|
|
1924
|
-
},
|
|
1925
|
-
skills: {
|
|
1926
|
-
"blocked-skill": "deny",
|
|
1665
|
+
permission: {
|
|
1666
|
+
"*": "ask",
|
|
1667
|
+
skill: { "blocked-skill": "deny" },
|
|
1927
1668
|
},
|
|
1928
1669
|
});
|
|
1929
1670
|
|
|
@@ -1975,16 +1716,9 @@ test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available
|
|
|
1975
1716
|
// external_directory special permission
|
|
1976
1717
|
// ---------------------------------------------------------------------------
|
|
1977
1718
|
|
|
1978
|
-
test("external_directory permission falls back to
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
tools: "allow",
|
|
1982
|
-
bash: "allow",
|
|
1983
|
-
mcp: "allow",
|
|
1984
|
-
skills: "allow",
|
|
1985
|
-
special: "ask",
|
|
1986
|
-
},
|
|
1987
|
-
});
|
|
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: {} });
|
|
1988
1722
|
|
|
1989
1723
|
try {
|
|
1990
1724
|
const result = manager.checkPermission("external_directory", {});
|
|
@@ -1996,25 +1730,16 @@ test("external_directory permission falls back to special default policy when no
|
|
|
1996
1730
|
}
|
|
1997
1731
|
});
|
|
1998
1732
|
|
|
1999
|
-
test("external_directory permission respects explicit deny
|
|
1733
|
+
test("external_directory permission respects explicit deny", () => {
|
|
2000
1734
|
const { manager, cleanup } = createManager({
|
|
2001
|
-
|
|
2002
|
-
tools: "allow",
|
|
2003
|
-
bash: "allow",
|
|
2004
|
-
mcp: "allow",
|
|
2005
|
-
skills: "allow",
|
|
2006
|
-
special: "ask",
|
|
2007
|
-
},
|
|
2008
|
-
special: {
|
|
2009
|
-
external_directory: "deny",
|
|
2010
|
-
},
|
|
1735
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2011
1736
|
});
|
|
2012
1737
|
|
|
2013
1738
|
try {
|
|
2014
1739
|
const result = manager.checkPermission("external_directory", {});
|
|
2015
1740
|
assert.equal(result.state, "deny");
|
|
2016
1741
|
assert.equal(result.source, "special");
|
|
2017
|
-
assert.equal(result.matchedPattern, "
|
|
1742
|
+
assert.equal(result.matchedPattern, "*");
|
|
2018
1743
|
} finally {
|
|
2019
1744
|
cleanup();
|
|
2020
1745
|
}
|
|
@@ -2022,23 +1747,14 @@ test("external_directory permission respects explicit deny in special config", (
|
|
|
2022
1747
|
|
|
2023
1748
|
test("external_directory permission can be explicitly allowed", () => {
|
|
2024
1749
|
const { manager, cleanup } = createManager({
|
|
2025
|
-
|
|
2026
|
-
tools: "allow",
|
|
2027
|
-
bash: "allow",
|
|
2028
|
-
mcp: "allow",
|
|
2029
|
-
skills: "allow",
|
|
2030
|
-
special: "deny",
|
|
2031
|
-
},
|
|
2032
|
-
special: {
|
|
2033
|
-
external_directory: "allow",
|
|
2034
|
-
},
|
|
1750
|
+
permission: { "*": "allow", external_directory: "allow" },
|
|
2035
1751
|
});
|
|
2036
1752
|
|
|
2037
1753
|
try {
|
|
2038
1754
|
const result = manager.checkPermission("external_directory", {});
|
|
2039
1755
|
assert.equal(result.state, "allow");
|
|
2040
1756
|
assert.equal(result.source, "special");
|
|
2041
|
-
assert.equal(result.matchedPattern, "
|
|
1757
|
+
assert.equal(result.matchedPattern, "*");
|
|
2042
1758
|
} finally {
|
|
2043
1759
|
cleanup();
|
|
2044
1760
|
}
|
|
@@ -2047,23 +1763,13 @@ test("external_directory permission can be explicitly allowed", () => {
|
|
|
2047
1763
|
test("external_directory permission respects per-agent override", () => {
|
|
2048
1764
|
const { manager, cleanup } = createManager(
|
|
2049
1765
|
{
|
|
2050
|
-
|
|
2051
|
-
tools: "allow",
|
|
2052
|
-
bash: "allow",
|
|
2053
|
-
mcp: "allow",
|
|
2054
|
-
skills: "allow",
|
|
2055
|
-
special: "ask",
|
|
2056
|
-
},
|
|
2057
|
-
special: {
|
|
2058
|
-
external_directory: "deny",
|
|
2059
|
-
},
|
|
1766
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2060
1767
|
},
|
|
2061
1768
|
{
|
|
2062
1769
|
trusted: `---
|
|
2063
1770
|
name: trusted
|
|
2064
1771
|
permission:
|
|
2065
|
-
|
|
2066
|
-
external_directory: allow
|
|
1772
|
+
external_directory: allow
|
|
2067
1773
|
---
|
|
2068
1774
|
`,
|
|
2069
1775
|
},
|
|
@@ -2087,31 +1793,18 @@ permission:
|
|
|
2087
1793
|
}
|
|
2088
1794
|
});
|
|
2089
1795
|
|
|
2090
|
-
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.
|
|
2091
1799
|
const { manager, cleanup } = createManager({
|
|
2092
|
-
|
|
2093
|
-
tools: "allow",
|
|
2094
|
-
bash: "allow",
|
|
2095
|
-
mcp: "allow",
|
|
2096
|
-
skills: "allow",
|
|
2097
|
-
special: "ask",
|
|
2098
|
-
},
|
|
2099
|
-
special: {
|
|
2100
|
-
doom_loop: "deny",
|
|
2101
|
-
external_directory: "allow",
|
|
2102
|
-
},
|
|
1800
|
+
permission: { "*": "allow", external_directory: "allow" },
|
|
2103
1801
|
});
|
|
2104
1802
|
|
|
2105
1803
|
try {
|
|
2106
|
-
// doom_loop is deprecated and stripped — falls through to defaultPolicy.tools
|
|
2107
|
-
const doomResult = manager.checkPermission("doom_loop", {});
|
|
2108
|
-
assert.equal(doomResult.state, "allow"); // defaultPolicy.tools, not the stripped doom_loop: "deny"
|
|
2109
|
-
assert.equal(doomResult.matchedPattern, undefined);
|
|
2110
|
-
|
|
2111
1804
|
// external_directory still resolves from its own entry
|
|
2112
1805
|
const extResult = manager.checkPermission("external_directory", {});
|
|
2113
1806
|
assert.equal(extResult.state, "allow");
|
|
2114
|
-
assert.equal(extResult.matchedPattern, "
|
|
1807
|
+
assert.equal(extResult.matchedPattern, "*");
|
|
2115
1808
|
} finally {
|
|
2116
1809
|
cleanup();
|
|
2117
1810
|
}
|
|
@@ -2125,14 +1818,7 @@ test("tool_call blocks path-bearing tools outside cwd when external_directory is
|
|
|
2125
1818
|
|
|
2126
1819
|
const harness = createToolCallHarness(
|
|
2127
1820
|
{
|
|
2128
|
-
|
|
2129
|
-
tools: "allow",
|
|
2130
|
-
bash: "allow",
|
|
2131
|
-
mcp: "allow",
|
|
2132
|
-
skills: "allow",
|
|
2133
|
-
special: "ask",
|
|
2134
|
-
},
|
|
2135
|
-
special: { external_directory: "deny" },
|
|
1821
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2136
1822
|
},
|
|
2137
1823
|
["read"],
|
|
2138
1824
|
{ cwd },
|
|
@@ -2160,14 +1846,7 @@ test("tool_call blocks path-bearing tools outside cwd when external_directory is
|
|
|
2160
1846
|
test("tool_call allows path-bearing tools inside cwd without external_directory prompt", async () => {
|
|
2161
1847
|
const harness = createToolCallHarness(
|
|
2162
1848
|
{
|
|
2163
|
-
|
|
2164
|
-
tools: "allow",
|
|
2165
|
-
bash: "allow",
|
|
2166
|
-
mcp: "allow",
|
|
2167
|
-
skills: "allow",
|
|
2168
|
-
special: "ask",
|
|
2169
|
-
},
|
|
2170
|
-
special: { external_directory: "deny" },
|
|
1849
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2171
1850
|
},
|
|
2172
1851
|
["read"],
|
|
2173
1852
|
);
|
|
@@ -2189,14 +1868,7 @@ test("tool_call allows path-bearing tools inside cwd without external_directory
|
|
|
2189
1868
|
test("tool_call blocks external_directory ask when no confirmation channel is available", async () => {
|
|
2190
1869
|
const harness = createToolCallHarness(
|
|
2191
1870
|
{
|
|
2192
|
-
|
|
2193
|
-
tools: "allow",
|
|
2194
|
-
bash: "allow",
|
|
2195
|
-
mcp: "allow",
|
|
2196
|
-
skills: "allow",
|
|
2197
|
-
special: "ask",
|
|
2198
|
-
},
|
|
2199
|
-
special: { external_directory: "ask" },
|
|
1871
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2200
1872
|
},
|
|
2201
1873
|
["write"],
|
|
2202
1874
|
);
|
|
@@ -2224,14 +1896,7 @@ test("tool_call blocks external_directory ask when no confirmation channel is av
|
|
|
2224
1896
|
test("tool_call prompts for external_directory and then falls through to normal tool policy", async () => {
|
|
2225
1897
|
const harness = createToolCallHarness(
|
|
2226
1898
|
{
|
|
2227
|
-
|
|
2228
|
-
tools: "allow",
|
|
2229
|
-
bash: "allow",
|
|
2230
|
-
mcp: "allow",
|
|
2231
|
-
skills: "allow",
|
|
2232
|
-
special: "ask",
|
|
2233
|
-
},
|
|
2234
|
-
special: { external_directory: "ask" },
|
|
1899
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2235
1900
|
},
|
|
2236
1901
|
["grep"],
|
|
2237
1902
|
);
|
|
@@ -2261,14 +1926,7 @@ test("tool_call prompts for external_directory and then falls through to normal
|
|
|
2261
1926
|
test("tool_call skips external_directory checks for optional path tools without a path", async () => {
|
|
2262
1927
|
const harness = createToolCallHarness(
|
|
2263
1928
|
{
|
|
2264
|
-
|
|
2265
|
-
tools: "allow",
|
|
2266
|
-
bash: "allow",
|
|
2267
|
-
mcp: "allow",
|
|
2268
|
-
skills: "allow",
|
|
2269
|
-
special: "ask",
|
|
2270
|
-
},
|
|
2271
|
-
special: { external_directory: "deny" },
|
|
1929
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2272
1930
|
},
|
|
2273
1931
|
["find"],
|
|
2274
1932
|
);
|
|
@@ -2292,14 +1950,7 @@ test("tool_call skips external_directory checks for optional path tools without
|
|
|
2292
1950
|
test("tool_call blocks bash command with external path when external_directory is denied", async () => {
|
|
2293
1951
|
const harness = createToolCallHarness(
|
|
2294
1952
|
{
|
|
2295
|
-
|
|
2296
|
-
tools: "allow",
|
|
2297
|
-
bash: "allow",
|
|
2298
|
-
mcp: "allow",
|
|
2299
|
-
skills: "allow",
|
|
2300
|
-
special: "ask",
|
|
2301
|
-
},
|
|
2302
|
-
special: { external_directory: "deny" },
|
|
1953
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2303
1954
|
},
|
|
2304
1955
|
["bash"],
|
|
2305
1956
|
);
|
|
@@ -2325,14 +1976,7 @@ test("tool_call blocks bash command with external path when external_directory i
|
|
|
2325
1976
|
test("tool_call allows bash command with only internal paths when external_directory is denied", async () => {
|
|
2326
1977
|
const harness = createToolCallHarness(
|
|
2327
1978
|
{
|
|
2328
|
-
|
|
2329
|
-
tools: "allow",
|
|
2330
|
-
bash: "allow",
|
|
2331
|
-
mcp: "allow",
|
|
2332
|
-
skills: "allow",
|
|
2333
|
-
special: "ask",
|
|
2334
|
-
},
|
|
2335
|
-
special: { external_directory: "deny" },
|
|
1979
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2336
1980
|
},
|
|
2337
1981
|
["bash"],
|
|
2338
1982
|
);
|
|
@@ -2353,14 +1997,7 @@ test("tool_call allows bash command with only internal paths when external_direc
|
|
|
2353
1997
|
test("tool_call prompts for bash command with external path when external_directory is ask", async () => {
|
|
2354
1998
|
const harness = createToolCallHarness(
|
|
2355
1999
|
{
|
|
2356
|
-
|
|
2357
|
-
tools: "allow",
|
|
2358
|
-
bash: "allow",
|
|
2359
|
-
mcp: "allow",
|
|
2360
|
-
skills: "allow",
|
|
2361
|
-
special: "ask",
|
|
2362
|
-
},
|
|
2363
|
-
special: { external_directory: "ask" },
|
|
2000
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2364
2001
|
},
|
|
2365
2002
|
["bash"],
|
|
2366
2003
|
);
|
|
@@ -2386,14 +2023,7 @@ test("tool_call prompts for bash command with external path when external_direct
|
|
|
2386
2023
|
test("tool_call allows bash command with external path when external_directory is allow", async () => {
|
|
2387
2024
|
const harness = createToolCallHarness(
|
|
2388
2025
|
{
|
|
2389
|
-
|
|
2390
|
-
tools: "allow",
|
|
2391
|
-
bash: "allow",
|
|
2392
|
-
mcp: "allow",
|
|
2393
|
-
skills: "allow",
|
|
2394
|
-
special: "ask",
|
|
2395
|
-
},
|
|
2396
|
-
special: { external_directory: "allow" },
|
|
2026
|
+
permission: { "*": "allow", external_directory: "allow" },
|
|
2397
2027
|
},
|
|
2398
2028
|
["bash"],
|
|
2399
2029
|
);
|
|
@@ -2415,15 +2045,11 @@ test("tool_call allows bash command with external path when external_directory i
|
|
|
2415
2045
|
test("tool_call applies bash pattern permissions after external_directory allow", async () => {
|
|
2416
2046
|
const harness = createToolCallHarness(
|
|
2417
2047
|
{
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
skills: "allow",
|
|
2423
|
-
special: "ask",
|
|
2048
|
+
permission: {
|
|
2049
|
+
"*": "allow",
|
|
2050
|
+
external_directory: "allow",
|
|
2051
|
+
bash: { "*": "allow", "cat *": "deny" },
|
|
2424
2052
|
},
|
|
2425
|
-
special: { external_directory: "allow" },
|
|
2426
|
-
bash: { "cat *": "deny" },
|
|
2427
2053
|
},
|
|
2428
2054
|
["bash"],
|
|
2429
2055
|
);
|
|
@@ -2446,13 +2072,7 @@ test("tool_call applies bash pattern permissions after external_directory allow"
|
|
|
2446
2072
|
test("generic ask prompts include serialized tool input for informed approval", async () => {
|
|
2447
2073
|
const harness = createToolCallHarness(
|
|
2448
2074
|
{
|
|
2449
|
-
|
|
2450
|
-
tools: "ask",
|
|
2451
|
-
bash: "ask",
|
|
2452
|
-
mcp: "ask",
|
|
2453
|
-
skills: "ask",
|
|
2454
|
-
special: "ask",
|
|
2455
|
-
},
|
|
2075
|
+
permission: { "*": "ask" },
|
|
2456
2076
|
},
|
|
2457
2077
|
["weather_lookup"],
|
|
2458
2078
|
);
|
|
@@ -2539,86 +2159,11 @@ test("getResolvedPolicyPaths returns false for missing files and null for absent
|
|
|
2539
2159
|
}
|
|
2540
2160
|
});
|
|
2541
2161
|
|
|
2542
|
-
// ---
|
|
2543
|
-
|
|
2544
|
-
test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (integer)", () => {
|
|
2545
|
-
const result = normalizeRawPermission({ special: { tool_call_limit: 5 } });
|
|
2546
|
-
assert.equal(result.configIssues.length, 1);
|
|
2547
|
-
assert.ok(result.configIssues[0].includes("tool_call_limit"));
|
|
2548
|
-
assert.equal(result.permissions.special?.tool_call_limit, undefined);
|
|
2549
|
-
});
|
|
2550
|
-
|
|
2551
|
-
test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (string)", () => {
|
|
2552
|
-
const result = normalizeRawPermission({
|
|
2553
|
-
special: { tool_call_limit: "allow" },
|
|
2554
|
-
});
|
|
2555
|
-
assert.equal(result.configIssues.length, 1);
|
|
2556
|
-
assert.ok(result.configIssues[0].includes("tool_call_limit"));
|
|
2557
|
-
assert.equal(result.permissions.special?.tool_call_limit, undefined);
|
|
2558
|
-
});
|
|
2559
|
-
|
|
2560
|
-
test("normalizeRawPermission emits deprecation issue for special.doom_loop (string)", () => {
|
|
2561
|
-
const result = normalizeRawPermission({
|
|
2562
|
-
special: { doom_loop: "ask" },
|
|
2563
|
-
});
|
|
2564
|
-
assert.equal(result.configIssues.length, 1);
|
|
2565
|
-
assert.ok(result.configIssues[0].includes("doom_loop"));
|
|
2566
|
-
assert.equal(result.permissions.special?.doom_loop, undefined);
|
|
2567
|
-
});
|
|
2568
|
-
|
|
2569
|
-
test("normalizeRawPermission emits deprecation issue for special.doom_loop (deny)", () => {
|
|
2570
|
-
const result = normalizeRawPermission({
|
|
2571
|
-
special: { doom_loop: "deny" },
|
|
2572
|
-
});
|
|
2573
|
-
assert.equal(result.configIssues.length, 1);
|
|
2574
|
-
assert.ok(result.configIssues[0].includes("doom_loop"));
|
|
2575
|
-
assert.equal(result.permissions.special?.doom_loop, undefined);
|
|
2576
|
-
});
|
|
2577
|
-
|
|
2578
|
-
test("normalizeRawPermission emits no issues when special is absent", () => {
|
|
2579
|
-
const result = normalizeRawPermission({ tools: { read: "allow" } });
|
|
2580
|
-
assert.equal(result.configIssues.length, 0);
|
|
2581
|
-
});
|
|
2582
|
-
|
|
2583
|
-
test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit in global config", () => {
|
|
2584
|
-
const config: ScopeConfig = {
|
|
2585
|
-
defaultPolicy: {
|
|
2586
|
-
tools: "ask",
|
|
2587
|
-
bash: "ask",
|
|
2588
|
-
mcp: "ask",
|
|
2589
|
-
skills: "ask",
|
|
2590
|
-
special: "ask",
|
|
2591
|
-
},
|
|
2592
|
-
tools: {},
|
|
2593
|
-
bash: {},
|
|
2594
|
-
mcp: {},
|
|
2595
|
-
skills: {},
|
|
2596
|
-
special: { tool_call_limit: "allow" as PermissionState },
|
|
2597
|
-
};
|
|
2598
|
-
const { manager, cleanup } = createManager(config);
|
|
2599
|
-
try {
|
|
2600
|
-
const issues = manager.getConfigIssues();
|
|
2601
|
-
assert.equal(issues.length, 1);
|
|
2602
|
-
assert.ok(issues[0].includes("tool_call_limit"));
|
|
2603
|
-
} finally {
|
|
2604
|
-
cleanup();
|
|
2605
|
-
}
|
|
2606
|
-
});
|
|
2162
|
+
// --- config issues tests ---
|
|
2607
2163
|
|
|
2608
2164
|
test("PermissionManager.getConfigIssues returns empty array for clean config", () => {
|
|
2609
2165
|
const config: ScopeConfig = {
|
|
2610
|
-
|
|
2611
|
-
tools: "ask",
|
|
2612
|
-
bash: "ask",
|
|
2613
|
-
mcp: "ask",
|
|
2614
|
-
skills: "ask",
|
|
2615
|
-
special: "ask",
|
|
2616
|
-
},
|
|
2617
|
-
tools: {},
|
|
2618
|
-
bash: {},
|
|
2619
|
-
mcp: {},
|
|
2620
|
-
skills: {},
|
|
2621
|
-
special: { external_directory: "ask" },
|
|
2166
|
+
permission: { "*": "ask", external_directory: "ask" },
|
|
2622
2167
|
};
|
|
2623
2168
|
const { manager, cleanup } = createManager(config);
|
|
2624
2169
|
try {
|
|
@@ -2629,59 +2174,19 @@ test("PermissionManager.getConfigIssues returns empty array for clean config", (
|
|
|
2629
2174
|
}
|
|
2630
2175
|
});
|
|
2631
2176
|
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
test("PermissionManager.getConfigIssues returns deprecation for doom_loop in global config", () => {
|
|
2635
|
-
const config: ScopeConfig = {
|
|
2636
|
-
defaultPolicy: {
|
|
2637
|
-
tools: "ask",
|
|
2638
|
-
bash: "ask",
|
|
2639
|
-
mcp: "ask",
|
|
2640
|
-
skills: "ask",
|
|
2641
|
-
special: "ask",
|
|
2642
|
-
},
|
|
2643
|
-
tools: {},
|
|
2644
|
-
bash: {},
|
|
2645
|
-
mcp: {},
|
|
2646
|
-
skills: {},
|
|
2647
|
-
special: { doom_loop: "deny" },
|
|
2648
|
-
};
|
|
2649
|
-
const { manager, cleanup } = createManager(config);
|
|
2177
|
+
test("PermissionManager.getConfigIssues returns empty array for empty config", () => {
|
|
2178
|
+
const { manager, cleanup } = createManager({});
|
|
2650
2179
|
try {
|
|
2651
2180
|
const issues = manager.getConfigIssues();
|
|
2652
|
-
assert.equal(issues.length,
|
|
2653
|
-
assert.ok(issues[0].includes("doom_loop"));
|
|
2654
|
-
} finally {
|
|
2655
|
-
cleanup();
|
|
2656
|
-
}
|
|
2657
|
-
});
|
|
2658
|
-
|
|
2659
|
-
test("checkPermission doom_loop falls through to defaultPolicy.tools when stripped by config-loader", () => {
|
|
2660
|
-
const { manager, cleanup } = createManager({
|
|
2661
|
-
defaultPolicy: {
|
|
2662
|
-
tools: "allow",
|
|
2663
|
-
bash: "ask",
|
|
2664
|
-
mcp: "ask",
|
|
2665
|
-
skills: "ask",
|
|
2666
|
-
special: "deny",
|
|
2667
|
-
},
|
|
2668
|
-
tools: {},
|
|
2669
|
-
bash: {},
|
|
2670
|
-
mcp: {},
|
|
2671
|
-
skills: {},
|
|
2672
|
-
special: { doom_loop: "ask" },
|
|
2673
|
-
});
|
|
2674
|
-
try {
|
|
2675
|
-
const result = manager.checkPermission("doom_loop", {});
|
|
2676
|
-
// doom_loop stripped by config-loader — falls through to defaultPolicy.tools
|
|
2677
|
-
assert.equal(result.state, "allow");
|
|
2678
|
-
assert.equal(result.matchedPattern, undefined);
|
|
2181
|
+
assert.equal(issues.length, 0);
|
|
2679
2182
|
} finally {
|
|
2680
2183
|
cleanup();
|
|
2681
2184
|
}
|
|
2682
2185
|
});
|
|
2683
2186
|
|
|
2684
|
-
//
|
|
2187
|
+
// ---------------------------------------------------------------------------
|
|
2188
|
+
// Session-scoped approval tests (#45)
|
|
2189
|
+
// ---------------------------------------------------------------------------
|
|
2685
2190
|
|
|
2686
2191
|
test("session approval: first prompt with 'Yes, for this session' skips subsequent prompts under same prefix", async () => {
|
|
2687
2192
|
const rootDir = mkdtempSync(join(tmpdir(), "pi-session-approval-"));
|
|
@@ -2692,14 +2197,7 @@ test("session approval: first prompt with 'Yes, for this session' skips subseque
|
|
|
2692
2197
|
|
|
2693
2198
|
const harness = createToolCallHarness(
|
|
2694
2199
|
{
|
|
2695
|
-
|
|
2696
|
-
tools: "allow",
|
|
2697
|
-
bash: "allow",
|
|
2698
|
-
mcp: "allow",
|
|
2699
|
-
skills: "allow",
|
|
2700
|
-
special: "ask",
|
|
2701
|
-
},
|
|
2702
|
-
special: { external_directory: "ask" },
|
|
2200
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2703
2201
|
},
|
|
2704
2202
|
["read", "grep"],
|
|
2705
2203
|
{ cwd },
|
|
@@ -2762,14 +2260,7 @@ test("session approval: different directory prefix still prompts", async () => {
|
|
|
2762
2260
|
|
|
2763
2261
|
const harness = createToolCallHarness(
|
|
2764
2262
|
{
|
|
2765
|
-
|
|
2766
|
-
tools: "allow",
|
|
2767
|
-
bash: "allow",
|
|
2768
|
-
mcp: "allow",
|
|
2769
|
-
skills: "allow",
|
|
2770
|
-
special: "ask",
|
|
2771
|
-
},
|
|
2772
|
-
special: { external_directory: "ask" },
|
|
2263
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2773
2264
|
},
|
|
2774
2265
|
["read"],
|
|
2775
2266
|
{ cwd },
|
|
@@ -2814,14 +2305,7 @@ test("session approval: session_shutdown clears session approvals", async () =>
|
|
|
2814
2305
|
|
|
2815
2306
|
const harness = createToolCallHarness(
|
|
2816
2307
|
{
|
|
2817
|
-
|
|
2818
|
-
tools: "allow",
|
|
2819
|
-
bash: "allow",
|
|
2820
|
-
mcp: "allow",
|
|
2821
|
-
skills: "allow",
|
|
2822
|
-
special: "ask",
|
|
2823
|
-
},
|
|
2824
|
-
special: { external_directory: "ask" },
|
|
2308
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2825
2309
|
},
|
|
2826
2310
|
["read"],
|
|
2827
2311
|
{ cwd },
|
|
@@ -2872,14 +2356,7 @@ test("session approval: bash external directory with 'Yes, for this session' ski
|
|
|
2872
2356
|
|
|
2873
2357
|
const harness = createToolCallHarness(
|
|
2874
2358
|
{
|
|
2875
|
-
|
|
2876
|
-
tools: "allow",
|
|
2877
|
-
bash: "allow",
|
|
2878
|
-
mcp: "allow",
|
|
2879
|
-
skills: "allow",
|
|
2880
|
-
special: "ask",
|
|
2881
|
-
},
|
|
2882
|
-
special: { external_directory: "ask" },
|
|
2359
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2883
2360
|
},
|
|
2884
2361
|
["bash"],
|
|
2885
2362
|
{ cwd },
|
|
@@ -2927,14 +2404,7 @@ test("session approval: regular 'Yes' does not create session approval", async (
|
|
|
2927
2404
|
|
|
2928
2405
|
const harness = createToolCallHarness(
|
|
2929
2406
|
{
|
|
2930
|
-
|
|
2931
|
-
tools: "allow",
|
|
2932
|
-
bash: "allow",
|
|
2933
|
-
mcp: "allow",
|
|
2934
|
-
skills: "allow",
|
|
2935
|
-
special: "ask",
|
|
2936
|
-
},
|
|
2937
|
-
special: { external_directory: "ask" },
|
|
2407
|
+
permission: { "*": "allow", external_directory: "ask" },
|
|
2938
2408
|
},
|
|
2939
2409
|
["read"],
|
|
2940
2410
|
{ cwd },
|
|
@@ -2969,3 +2439,166 @@ test("session approval: regular 'Yes' does not create session approval", async (
|
|
|
2969
2439
|
rmSync(rootDir, { recursive: true, force: true });
|
|
2970
2440
|
}
|
|
2971
2441
|
});
|
|
2442
|
+
|
|
2443
|
+
// ---------------------------------------------------------------------------
|
|
2444
|
+
// Session-aware checkPermission() integration
|
|
2445
|
+
// ---------------------------------------------------------------------------
|
|
2446
|
+
|
|
2447
|
+
test("checkPermission returns source 'session' when session rules cover the external_directory path", () => {
|
|
2448
|
+
const { manager, cleanup } = createManager({
|
|
2449
|
+
permission: { "*": "allow" },
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
try {
|
|
2453
|
+
const sessionRules = [
|
|
2454
|
+
{
|
|
2455
|
+
surface: "external_directory",
|
|
2456
|
+
pattern: "/other/project/*",
|
|
2457
|
+
action: "allow" as const,
|
|
2458
|
+
layer: "session" as const,
|
|
2459
|
+
},
|
|
2460
|
+
];
|
|
2461
|
+
|
|
2462
|
+
const result = manager.checkPermission(
|
|
2463
|
+
"external_directory",
|
|
2464
|
+
{ path: "/other/project/src/foo.ts" },
|
|
2465
|
+
undefined,
|
|
2466
|
+
sessionRules,
|
|
2467
|
+
);
|
|
2468
|
+
assert.equal(result.state, "allow");
|
|
2469
|
+
assert.equal(result.source, "session");
|
|
2470
|
+
assert.equal(result.matchedPattern, "/other/project/*");
|
|
2471
|
+
} finally {
|
|
2472
|
+
cleanup();
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
test("checkPermission falls back to config policy when session rules do not cover the path", () => {
|
|
2477
|
+
const { manager, cleanup } = createManager({
|
|
2478
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2481
|
+
try {
|
|
2482
|
+
const sessionRules = [
|
|
2483
|
+
{
|
|
2484
|
+
surface: "external_directory",
|
|
2485
|
+
pattern: "/other/project/*",
|
|
2486
|
+
action: "allow" as const,
|
|
2487
|
+
layer: "session" as const,
|
|
2488
|
+
},
|
|
2489
|
+
];
|
|
2490
|
+
|
|
2491
|
+
// Path NOT under /other/project/ — session rules don't match.
|
|
2492
|
+
const result = manager.checkPermission(
|
|
2493
|
+
"external_directory",
|
|
2494
|
+
{ path: "/completely/different/path.ts" },
|
|
2495
|
+
undefined,
|
|
2496
|
+
sessionRules,
|
|
2497
|
+
);
|
|
2498
|
+
assert.equal(result.state, "deny");
|
|
2499
|
+
assert.equal(result.source, "special");
|
|
2500
|
+
} finally {
|
|
2501
|
+
cleanup();
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
test("checkPermission with empty session rules is identical to call without sessionRules arg", () => {
|
|
2506
|
+
const { manager, cleanup } = createManager({
|
|
2507
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2508
|
+
});
|
|
2509
|
+
|
|
2510
|
+
try {
|
|
2511
|
+
const withEmpty = manager.checkPermission(
|
|
2512
|
+
"external_directory",
|
|
2513
|
+
{ path: "/other/project/foo.ts" },
|
|
2514
|
+
undefined,
|
|
2515
|
+
[],
|
|
2516
|
+
);
|
|
2517
|
+
const withoutArg = manager.checkPermission("external_directory", {
|
|
2518
|
+
path: "/other/project/foo.ts",
|
|
2519
|
+
});
|
|
2520
|
+
const expected: PermissionCheckResult = {
|
|
2521
|
+
toolName: "external_directory",
|
|
2522
|
+
state: "deny",
|
|
2523
|
+
matchedPattern: "*",
|
|
2524
|
+
source: "special",
|
|
2525
|
+
};
|
|
2526
|
+
assert.deepEqual(withEmpty, expected);
|
|
2527
|
+
assert.deepEqual(withoutArg, expected);
|
|
2528
|
+
} finally {
|
|
2529
|
+
cleanup();
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
test("session rules for one surface do not affect checks on other surfaces", () => {
|
|
2534
|
+
const { manager, cleanup } = createManager({
|
|
2535
|
+
// Empty permission: universal default is "ask" from DEFAULT_UNIVERSAL_FALLBACK.
|
|
2536
|
+
permission: {},
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
try {
|
|
2540
|
+
const sessionRules = [
|
|
2541
|
+
{
|
|
2542
|
+
surface: "external_directory",
|
|
2543
|
+
pattern: "/other/project/*",
|
|
2544
|
+
action: "allow" as const,
|
|
2545
|
+
layer: "session" as const,
|
|
2546
|
+
},
|
|
2547
|
+
];
|
|
2548
|
+
|
|
2549
|
+
// Bash check — session rules should not affect bash decisions.
|
|
2550
|
+
const bashResult = manager.checkPermission(
|
|
2551
|
+
"bash",
|
|
2552
|
+
{ command: "git status" },
|
|
2553
|
+
undefined,
|
|
2554
|
+
sessionRules,
|
|
2555
|
+
);
|
|
2556
|
+
assert.equal(bashResult.state, "ask");
|
|
2557
|
+
assert.equal(bashResult.source, "bash");
|
|
2558
|
+
|
|
2559
|
+
// MCP check — session rules should not affect MCP decisions.
|
|
2560
|
+
const mcpResult = manager.checkPermission(
|
|
2561
|
+
"mcp",
|
|
2562
|
+
{ tool: "exa:search" },
|
|
2563
|
+
undefined,
|
|
2564
|
+
sessionRules,
|
|
2565
|
+
);
|
|
2566
|
+
assert.equal(mcpResult.state, "ask");
|
|
2567
|
+
assert.equal(mcpResult.source, "default");
|
|
2568
|
+
} finally {
|
|
2569
|
+
cleanup();
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
test("session rules override config deny for external_directory", () => {
|
|
2574
|
+
const { manager, cleanup } = createManager({
|
|
2575
|
+
permission: { "*": "allow", external_directory: "deny" },
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
try {
|
|
2579
|
+
const sessionRules = [
|
|
2580
|
+
{
|
|
2581
|
+
surface: "external_directory",
|
|
2582
|
+
pattern: "/other/project/*",
|
|
2583
|
+
action: "allow" as const,
|
|
2584
|
+
layer: "session" as const,
|
|
2585
|
+
},
|
|
2586
|
+
];
|
|
2587
|
+
|
|
2588
|
+
// Session approval overrides config deny for the covered path.
|
|
2589
|
+
const result = manager.checkPermission(
|
|
2590
|
+
"external_directory",
|
|
2591
|
+
{ path: "/other/project/src/foo.ts" },
|
|
2592
|
+
undefined,
|
|
2593
|
+
sessionRules,
|
|
2594
|
+
);
|
|
2595
|
+
assert.equal(result.state, "allow");
|
|
2596
|
+
assert.equal(result.source, "session");
|
|
2597
|
+
} finally {
|
|
2598
|
+
cleanup();
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
|
|
2602
|
+
// Suppress unused import warning — PermissionState used in type annotations
|
|
2603
|
+
const _unused: PermissionState = "ask";
|
|
2604
|
+
void _unused;
|