@gotgenes/pi-permission-system 5.4.0 → 5.5.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 +28 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/gates/bash-external-directory.ts +22 -24
- package/src/handlers/gates/external-directory.ts +32 -41
- package/src/handlers/gates/skill-read.ts +10 -12
- package/src/handlers/gates/tool.ts +20 -27
- package/src/handlers/gates/types.ts +75 -0
- package/src/handlers/input.ts +3 -3
- package/src/handlers/lifecycle.ts +21 -21
- package/src/handlers/tool-call.ts +77 -7
- package/src/handlers/types.ts +20 -7
- package/src/index.ts +6 -1
- package/src/permission-manager.ts +28 -279
- package/src/policy-loader.ts +350 -0
- package/src/runtime.ts +17 -9
- package/tests/handlers/before-agent-start.test.ts +17 -27
- package/tests/handlers/gates/bash-external-directory.test.ts +48 -105
- package/tests/handlers/gates/external-directory.test.ts +65 -140
- package/tests/handlers/gates/skill-read.test.ts +50 -65
- package/tests/handlers/gates/tool.test.ts +90 -334
- package/tests/handlers/input-events.test.ts +10 -21
- package/tests/handlers/input.test.ts +26 -43
- package/tests/handlers/lifecycle.test.ts +47 -66
- package/tests/handlers/tool-call-events.test.ts +29 -40
- package/tests/handlers/tool-call.test.ts +19 -30
- package/tests/permission-manager-unified.test.ts +319 -0
- package/tests/policy-loader.test.ts +561 -0
|
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
|
|
4
4
|
import { getEventInput, handleToolCall } from "../../src/handlers/tool-call";
|
|
5
5
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
6
|
-
import type {
|
|
6
|
+
import type { SessionState } from "../../src/runtime";
|
|
7
7
|
import type { PermissionCheckResult } from "../../src/types";
|
|
8
8
|
|
|
9
9
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
@@ -55,43 +55,32 @@ function makePermissionResult(
|
|
|
55
55
|
return { state, toolName: "read", source: "tool", origin: "builtin" };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
overrides: Partial<ExtensionRuntime> = {},
|
|
60
|
-
): ExtensionRuntime {
|
|
58
|
+
function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
61
59
|
return {
|
|
62
|
-
agentDir: "/test/agent",
|
|
63
|
-
sessionsDir: "/test/agent/sessions",
|
|
64
|
-
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
65
|
-
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
66
|
-
globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
|
|
67
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
68
|
-
config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
|
|
69
60
|
runtimeContext: null,
|
|
70
61
|
permissionManager: {
|
|
71
62
|
checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
|
|
72
|
-
} as unknown as
|
|
63
|
+
} as unknown as SessionState["permissionManager"],
|
|
73
64
|
activeSkillEntries: [],
|
|
74
65
|
lastKnownActiveAgentName: null,
|
|
75
66
|
lastActiveToolsCacheKey: null,
|
|
76
67
|
lastPromptStateCacheKey: null,
|
|
77
|
-
lastConfigWarning: null,
|
|
78
68
|
sessionRules: {
|
|
79
69
|
approve: vi.fn(),
|
|
80
70
|
getRuleset: vi.fn().mockReturnValue([]),
|
|
81
71
|
clear: vi.fn(),
|
|
82
|
-
} as unknown as
|
|
83
|
-
permissionForwardingContext: null,
|
|
84
|
-
permissionForwardingTimer: null,
|
|
85
|
-
isProcessingForwardedRequests: false,
|
|
86
|
-
writeDebugLog: vi.fn(),
|
|
87
|
-
writeReviewLog: vi.fn(),
|
|
72
|
+
} as unknown as SessionState["sessionRules"],
|
|
88
73
|
...overrides,
|
|
89
|
-
}
|
|
74
|
+
};
|
|
90
75
|
}
|
|
91
76
|
|
|
92
77
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
93
78
|
return {
|
|
94
|
-
|
|
79
|
+
session: makeSession(),
|
|
80
|
+
writeDebugLog: vi.fn(),
|
|
81
|
+
writeReviewLog: vi.fn(),
|
|
82
|
+
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
83
|
+
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
95
84
|
createPermissionManagerForCwd: vi.fn(),
|
|
96
85
|
refreshExtensionConfig: vi.fn(),
|
|
97
86
|
notifyWarning: vi.fn(),
|
|
@@ -145,7 +134,7 @@ describe("handleToolCall", () => {
|
|
|
145
134
|
const ctx = makeCtx();
|
|
146
135
|
const deps = makeDeps();
|
|
147
136
|
await handleToolCall(deps, makeToolCallEvent("read"), ctx);
|
|
148
|
-
expect(deps.
|
|
137
|
+
expect(deps.session.runtimeContext).toBe(ctx);
|
|
149
138
|
});
|
|
150
139
|
|
|
151
140
|
it("starts forwarded permission polling", async () => {
|
|
@@ -190,12 +179,12 @@ describe("handleToolCall", () => {
|
|
|
190
179
|
|
|
191
180
|
it("blocks when tool is denied by policy", async () => {
|
|
192
181
|
const deps = makeDeps({
|
|
193
|
-
|
|
182
|
+
session: makeSession({
|
|
194
183
|
permissionManager: {
|
|
195
184
|
checkPermission: vi
|
|
196
185
|
.fn()
|
|
197
186
|
.mockReturnValue(makePermissionResult("deny")),
|
|
198
|
-
} as unknown as
|
|
187
|
+
} as unknown as SessionState["permissionManager"],
|
|
199
188
|
}),
|
|
200
189
|
});
|
|
201
190
|
const result = await handleToolCall(
|
|
@@ -220,7 +209,7 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
220
209
|
normalizedBaseDir: "/skills/librarian",
|
|
221
210
|
};
|
|
222
211
|
const deps = makeDeps({
|
|
223
|
-
|
|
212
|
+
session: makeSession({ activeSkillEntries: [skillEntry] }),
|
|
224
213
|
getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
225
214
|
});
|
|
226
215
|
const event = {
|
|
@@ -243,7 +232,7 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
243
232
|
normalizedBaseDir: "/skills/librarian",
|
|
244
233
|
};
|
|
245
234
|
const deps = makeDeps({
|
|
246
|
-
|
|
235
|
+
session: makeSession({ activeSkillEntries: [skillEntry] }),
|
|
247
236
|
getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
248
237
|
});
|
|
249
238
|
const event = {
|
|
@@ -262,12 +251,12 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
262
251
|
describe("handleToolCall — external-directory gate", () => {
|
|
263
252
|
it("blocks a read of a path outside cwd when policy is deny", async () => {
|
|
264
253
|
const deps = makeDeps({
|
|
265
|
-
|
|
254
|
+
session: makeSession({
|
|
266
255
|
permissionManager: {
|
|
267
256
|
checkPermission: vi
|
|
268
257
|
.fn()
|
|
269
258
|
.mockReturnValue(makePermissionResult("deny")),
|
|
270
|
-
} as unknown as
|
|
259
|
+
} as unknown as SessionState["permissionManager"],
|
|
271
260
|
}),
|
|
272
261
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
273
262
|
});
|
|
@@ -287,12 +276,12 @@ describe("handleToolCall — external-directory gate", () => {
|
|
|
287
276
|
describe("handleToolCall — bash external-directory gate", () => {
|
|
288
277
|
it("blocks a bash command referencing an external path when policy is deny", async () => {
|
|
289
278
|
const deps = makeDeps({
|
|
290
|
-
|
|
279
|
+
session: makeSession({
|
|
291
280
|
permissionManager: {
|
|
292
281
|
checkPermission: vi
|
|
293
282
|
.fn()
|
|
294
283
|
.mockReturnValue(makePermissionResult("deny")),
|
|
295
|
-
} as unknown as
|
|
284
|
+
} as unknown as SessionState["permissionManager"],
|
|
296
285
|
}),
|
|
297
286
|
getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
298
287
|
});
|
|
@@ -661,3 +661,322 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
661
661
|
expect(result.origin).toBe("builtin");
|
|
662
662
|
});
|
|
663
663
|
});
|
|
664
|
+
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
// In-memory PolicyLoader stub tests — no filesystem required
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
|
|
669
|
+
import type { PolicyLoader } from "../src/permission-manager";
|
|
670
|
+
import type { ResolvedPolicyPaths } from "../src/policy-loader";
|
|
671
|
+
import type { ScopeConfig } from "../src/types";
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Minimal in-memory PolicyLoader for testing merge + evaluation logic
|
|
675
|
+
* without touching the filesystem.
|
|
676
|
+
*/
|
|
677
|
+
function createInMemoryPolicyLoader(
|
|
678
|
+
scopes: {
|
|
679
|
+
global?: ScopeConfig;
|
|
680
|
+
project?: ScopeConfig;
|
|
681
|
+
agent?: Record<string, ScopeConfig>;
|
|
682
|
+
projectAgent?: Record<string, ScopeConfig>;
|
|
683
|
+
} = {},
|
|
684
|
+
mcpServerNames: readonly string[] = [],
|
|
685
|
+
): PolicyLoader {
|
|
686
|
+
const issues: string[] = [];
|
|
687
|
+
return {
|
|
688
|
+
loadGlobalConfig: () => scopes.global ?? {},
|
|
689
|
+
loadProjectConfig: () => scopes.project ?? {},
|
|
690
|
+
loadAgentConfig: (name?: string) => (name && scopes.agent?.[name]) || {},
|
|
691
|
+
loadProjectAgentConfig: (name?: string) =>
|
|
692
|
+
(name && scopes.projectAgent?.[name]) || {},
|
|
693
|
+
getConfiguredMcpServerNames: () => mcpServerNames,
|
|
694
|
+
getCacheStamp: () => "in-memory",
|
|
695
|
+
getConfigIssues: () => issues,
|
|
696
|
+
getResolvedPolicyPaths: (): ResolvedPolicyPaths => ({
|
|
697
|
+
globalConfigPath: "/in-memory/config.json",
|
|
698
|
+
globalConfigExists: true,
|
|
699
|
+
projectConfigPath: null,
|
|
700
|
+
projectConfigExists: false,
|
|
701
|
+
agentsDir: "/in-memory/agents",
|
|
702
|
+
agentsDirExists: false,
|
|
703
|
+
projectAgentsDir: null,
|
|
704
|
+
projectAgentsDirExists: false,
|
|
705
|
+
}),
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Create a PermissionManager backed by an in-memory PolicyLoader. */
|
|
710
|
+
function makeInMemoryManager(
|
|
711
|
+
scopes: Parameters<typeof createInMemoryPolicyLoader>[0] = {},
|
|
712
|
+
mcpServerNames: readonly string[] = [],
|
|
713
|
+
): PermissionManager {
|
|
714
|
+
return new PermissionManager({
|
|
715
|
+
policyLoader: createInMemoryPolicyLoader(scopes, mcpServerNames),
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
720
|
+
describe("universal fallback", () => {
|
|
721
|
+
it("defaults to ask when no config is provided", () => {
|
|
722
|
+
const manager = makeInMemoryManager();
|
|
723
|
+
const result = manager.checkPermission("read", {});
|
|
724
|
+
expect(result.state).toBe("ask");
|
|
725
|
+
expect(result.origin).toBe("builtin");
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("respects permission['*'] = 'allow' from global config", () => {
|
|
729
|
+
const manager = makeInMemoryManager({
|
|
730
|
+
global: { permission: { "*": "allow" } },
|
|
731
|
+
});
|
|
732
|
+
const result = manager.checkPermission("read", {});
|
|
733
|
+
expect(result.state).toBe("allow");
|
|
734
|
+
expect(result.origin).toBe("global");
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it("respects permission['*'] = 'deny' from global config", () => {
|
|
738
|
+
const manager = makeInMemoryManager({
|
|
739
|
+
global: { permission: { "*": "deny" } },
|
|
740
|
+
});
|
|
741
|
+
const result = manager.checkPermission("write", {});
|
|
742
|
+
expect(result.state).toBe("deny");
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
describe("surface routing", () => {
|
|
747
|
+
it("bash surface routes correctly", () => {
|
|
748
|
+
const manager = makeInMemoryManager({
|
|
749
|
+
global: {
|
|
750
|
+
permission: { "*": "ask", bash: { "git *": "allow" } },
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
const result = manager.checkPermission("bash", {
|
|
754
|
+
command: "git status",
|
|
755
|
+
});
|
|
756
|
+
expect(result.state).toBe("allow");
|
|
757
|
+
expect(result.source).toBe("bash");
|
|
758
|
+
expect(result.matchedPattern).toBe("git *");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("tool surface routes correctly for built-in tools", () => {
|
|
762
|
+
const manager = makeInMemoryManager({
|
|
763
|
+
global: { permission: { "*": "deny", read: "allow" } },
|
|
764
|
+
});
|
|
765
|
+
const result = manager.checkPermission("read", {});
|
|
766
|
+
expect(result.state).toBe("allow");
|
|
767
|
+
expect(result.source).toBe("tool");
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it("skill surface routes correctly", () => {
|
|
771
|
+
const manager = makeInMemoryManager({
|
|
772
|
+
global: {
|
|
773
|
+
permission: { "*": "ask", skill: { librarian: "allow" } },
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
const result = manager.checkPermission("skill", { name: "librarian" });
|
|
777
|
+
expect(result.state).toBe("allow");
|
|
778
|
+
expect(result.source).toBe("skill");
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("mcp surface routes correctly", () => {
|
|
782
|
+
const manager = makeInMemoryManager(
|
|
783
|
+
{
|
|
784
|
+
global: {
|
|
785
|
+
permission: { "*": "ask", mcp: { exa_search: "allow" } },
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
["exa"],
|
|
789
|
+
);
|
|
790
|
+
const result = manager.checkPermission("mcp", {
|
|
791
|
+
tool: "exa:search",
|
|
792
|
+
server: "exa",
|
|
793
|
+
});
|
|
794
|
+
expect(result.state).toBe("allow");
|
|
795
|
+
expect(result.source).toBe("mcp");
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("external_directory surface routes correctly", () => {
|
|
799
|
+
const manager = makeInMemoryManager({
|
|
800
|
+
global: {
|
|
801
|
+
permission: {
|
|
802
|
+
"*": "ask",
|
|
803
|
+
external_directory: { "/trusted/*": "allow" },
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
const result = manager.checkPermission("external_directory", {
|
|
808
|
+
path: "/trusted/repo",
|
|
809
|
+
});
|
|
810
|
+
expect(result.state).toBe("allow");
|
|
811
|
+
expect(result.source).toBe("special");
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it("extension tools use 'default' source when no config rule matches", () => {
|
|
815
|
+
const manager = makeInMemoryManager({
|
|
816
|
+
global: { permission: { "*": "ask" } },
|
|
817
|
+
});
|
|
818
|
+
const result = manager.checkPermission("my_custom_tool", {});
|
|
819
|
+
expect(result.state).toBe("ask");
|
|
820
|
+
expect(result.source).toBe("default");
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
describe("multi-scope merge", () => {
|
|
825
|
+
it("project overrides global", () => {
|
|
826
|
+
const manager = makeInMemoryManager({
|
|
827
|
+
global: { permission: { read: "ask" } },
|
|
828
|
+
project: { permission: { read: "allow" } },
|
|
829
|
+
});
|
|
830
|
+
const result = manager.checkPermission("read", {});
|
|
831
|
+
expect(result.state).toBe("allow");
|
|
832
|
+
expect(result.origin).toBe("project");
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("agent overrides project", () => {
|
|
836
|
+
const manager = makeInMemoryManager({
|
|
837
|
+
global: { permission: { read: "ask" } },
|
|
838
|
+
project: { permission: { read: "allow" } },
|
|
839
|
+
agent: { coder: { permission: { read: "deny" } } },
|
|
840
|
+
});
|
|
841
|
+
const result = manager.checkPermission("read", {}, "coder");
|
|
842
|
+
expect(result.state).toBe("deny");
|
|
843
|
+
expect(result.origin).toBe("agent");
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it("project-agent overrides agent", () => {
|
|
847
|
+
const manager = makeInMemoryManager({
|
|
848
|
+
global: { permission: { read: "deny" } },
|
|
849
|
+
agent: { coder: { permission: { read: "deny" } } },
|
|
850
|
+
projectAgent: { coder: { permission: { read: "allow" } } },
|
|
851
|
+
});
|
|
852
|
+
const result = manager.checkPermission("read", {}, "coder");
|
|
853
|
+
expect(result.state).toBe("allow");
|
|
854
|
+
expect(result.origin).toBe("project-agent");
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("deep-shallow merge preserves patterns from different scopes", () => {
|
|
858
|
+
const manager = makeInMemoryManager({
|
|
859
|
+
global: { permission: { bash: { "git *": "allow" } } },
|
|
860
|
+
project: { permission: { bash: { "rm *": "deny" } } },
|
|
861
|
+
});
|
|
862
|
+
const gitResult = manager.checkPermission("bash", {
|
|
863
|
+
command: "git status",
|
|
864
|
+
});
|
|
865
|
+
expect(gitResult.state).toBe("allow");
|
|
866
|
+
expect(gitResult.origin).toBe("global");
|
|
867
|
+
|
|
868
|
+
const rmResult = manager.checkPermission("bash", {
|
|
869
|
+
command: "rm -rf /",
|
|
870
|
+
});
|
|
871
|
+
expect(rmResult.state).toBe("deny");
|
|
872
|
+
expect(rmResult.origin).toBe("project");
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it("string replaces object in override scope", () => {
|
|
876
|
+
const manager = makeInMemoryManager({
|
|
877
|
+
global: {
|
|
878
|
+
permission: { bash: { "git *": "ask", "npm *": "ask" } },
|
|
879
|
+
},
|
|
880
|
+
project: { permission: { bash: "allow" } },
|
|
881
|
+
});
|
|
882
|
+
const result = manager.checkPermission("bash", { command: "anything" });
|
|
883
|
+
expect(result.state).toBe("allow");
|
|
884
|
+
expect(result.origin).toBe("project");
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
describe("session rule composition", () => {
|
|
889
|
+
it("session rule wins over config", () => {
|
|
890
|
+
const manager = makeInMemoryManager({
|
|
891
|
+
global: { permission: { "*": "deny" } },
|
|
892
|
+
});
|
|
893
|
+
const sessionRules: Ruleset = [sessionAllow("read", "*")];
|
|
894
|
+
const result = manager.checkPermission(
|
|
895
|
+
"read",
|
|
896
|
+
{},
|
|
897
|
+
undefined,
|
|
898
|
+
sessionRules,
|
|
899
|
+
);
|
|
900
|
+
expect(result.state).toBe("allow");
|
|
901
|
+
expect(result.source).toBe("session");
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("session rule does not bleed across surfaces", () => {
|
|
905
|
+
const manager = makeInMemoryManager({
|
|
906
|
+
global: { permission: { "*": "ask" } },
|
|
907
|
+
});
|
|
908
|
+
const sessionRules: Ruleset = [sessionAllow("bash", "git *")];
|
|
909
|
+
const bashResult = manager.checkPermission(
|
|
910
|
+
"bash",
|
|
911
|
+
{ command: "git status" },
|
|
912
|
+
undefined,
|
|
913
|
+
sessionRules,
|
|
914
|
+
);
|
|
915
|
+
expect(bashResult.state).toBe("allow");
|
|
916
|
+
|
|
917
|
+
const readResult = manager.checkPermission(
|
|
918
|
+
"read",
|
|
919
|
+
{},
|
|
920
|
+
undefined,
|
|
921
|
+
sessionRules,
|
|
922
|
+
);
|
|
923
|
+
expect(readResult.state).toBe("ask");
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
describe("origin tracking", () => {
|
|
928
|
+
it("universal fallback from project carries origin 'project'", () => {
|
|
929
|
+
const manager = makeInMemoryManager({
|
|
930
|
+
global: { permission: { "*": "ask" } },
|
|
931
|
+
project: { permission: { "*": "allow" } },
|
|
932
|
+
});
|
|
933
|
+
const result = manager.checkPermission("read", {});
|
|
934
|
+
expect(result.state).toBe("allow");
|
|
935
|
+
expect(result.origin).toBe("project");
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it("session origin is 'session'", () => {
|
|
939
|
+
const manager = makeInMemoryManager();
|
|
940
|
+
const sessionRules: Ruleset = [sessionAllow("read", "*")];
|
|
941
|
+
const result = manager.checkPermission(
|
|
942
|
+
"read",
|
|
943
|
+
{},
|
|
944
|
+
undefined,
|
|
945
|
+
sessionRules,
|
|
946
|
+
);
|
|
947
|
+
expect(result.origin).toBe("session");
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
describe("getToolPermission", () => {
|
|
952
|
+
it("returns tool-level state for built-in tools", () => {
|
|
953
|
+
const manager = makeInMemoryManager({
|
|
954
|
+
global: { permission: { "*": "deny", read: "allow" } },
|
|
955
|
+
});
|
|
956
|
+
expect(manager.getToolPermission("read")).toBe("allow");
|
|
957
|
+
expect(manager.getToolPermission("write")).toBe("deny");
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("returns tool-level state for bash surface", () => {
|
|
961
|
+
const manager = makeInMemoryManager({
|
|
962
|
+
global: { permission: { "*": "deny", bash: "allow" } },
|
|
963
|
+
});
|
|
964
|
+
expect(manager.getToolPermission("bash")).toBe("allow");
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
describe("getComposedConfigRules", () => {
|
|
969
|
+
it("returns only config-layer rules", () => {
|
|
970
|
+
const manager = makeInMemoryManager({
|
|
971
|
+
global: {
|
|
972
|
+
permission: { "*": "ask", bash: { "git *": "allow" } },
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
const rules = manager.getComposedConfigRules();
|
|
976
|
+
expect(rules.every((r) => r.layer === "config")).toBe(true);
|
|
977
|
+
expect(
|
|
978
|
+
rules.some((r) => r.surface === "bash" && r.pattern === "git *"),
|
|
979
|
+
).toBe(true);
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
});
|