@gotgenes/pi-permission-system 3.7.0 → 3.9.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 +39 -0
- package/package.json +1 -1
- package/src/defaults.ts +60 -0
- package/src/forwarded-permissions/io.ts +47 -12
- package/src/forwarded-permissions/polling.ts +33 -11
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/input.ts +7 -5
- package/src/handlers/lifecycle.ts +25 -24
- package/src/handlers/tool-call.ts +32 -22
- package/src/handlers/types.ts +7 -30
- package/src/index.ts +47 -417
- package/src/normalize.ts +70 -0
- package/src/permission-manager.ts +127 -254
- package/src/rule.ts +7 -23
- package/src/runtime.ts +484 -0
- package/src/types.ts +13 -18
- package/tests/defaults.test.ts +105 -0
- package/tests/forwarded-permissions/io.test.ts +135 -0
- package/tests/handlers/before-agent-start.test.ts +47 -31
- package/tests/handlers/input.test.ts +69 -39
- package/tests/handlers/lifecycle.test.ts +86 -65
- package/tests/handlers/tool-call.test.ts +92 -69
- package/tests/normalize.test.ts +121 -0
- package/tests/permission-system.test.ts +11 -39
- package/tests/rule.test.ts +24 -42
- package/tests/runtime.test.ts +618 -0
- package/tests/session-start.test.ts +2 -2
- package/src/bash-filter.ts +0 -51
- package/tests/bash-filter.test.ts +0 -142
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { normalizeConfig } from "../src/normalize";
|
|
3
|
+
|
|
4
|
+
describe("normalizeConfig", () => {
|
|
5
|
+
describe("tools entries", () => {
|
|
6
|
+
test("converts tools entries to tool-name-as-surface rules", () => {
|
|
7
|
+
const result = normalizeConfig({
|
|
8
|
+
tools: { read: "allow", write: "deny" },
|
|
9
|
+
});
|
|
10
|
+
expect(result).toEqual([
|
|
11
|
+
{ surface: "read", pattern: "*", action: "allow" },
|
|
12
|
+
{ surface: "write", pattern: "*", action: "deny" },
|
|
13
|
+
]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("tools.bash is excluded (handled as fallback override)", () => {
|
|
17
|
+
const result = normalizeConfig({
|
|
18
|
+
tools: { bash: "allow", read: "allow" },
|
|
19
|
+
});
|
|
20
|
+
expect(result).toEqual([
|
|
21
|
+
{ surface: "read", pattern: "*", action: "allow" },
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("tools.mcp is excluded (handled as fallback override)", () => {
|
|
26
|
+
const result = normalizeConfig({
|
|
27
|
+
tools: { mcp: "ask", read: "allow" },
|
|
28
|
+
});
|
|
29
|
+
expect(result).toEqual([
|
|
30
|
+
{ surface: "read", pattern: "*", action: "allow" },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("bash entries", () => {
|
|
36
|
+
test("converts bash entries to surface 'bash' rules", () => {
|
|
37
|
+
const result = normalizeConfig({
|
|
38
|
+
bash: { "git *": "allow", "rm -rf *": "deny" },
|
|
39
|
+
});
|
|
40
|
+
expect(result).toEqual([
|
|
41
|
+
{ surface: "bash", pattern: "git *", action: "allow" },
|
|
42
|
+
{ surface: "bash", pattern: "rm -rf *", action: "deny" },
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("mcp entries", () => {
|
|
48
|
+
test("converts mcp entries to surface 'mcp' rules", () => {
|
|
49
|
+
const result = normalizeConfig({
|
|
50
|
+
mcp: { "exa:*": "allow", mcp_status: "allow" },
|
|
51
|
+
});
|
|
52
|
+
expect(result).toEqual([
|
|
53
|
+
{ surface: "mcp", pattern: "exa:*", action: "allow" },
|
|
54
|
+
{ surface: "mcp", pattern: "mcp_status", action: "allow" },
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("skills entries", () => {
|
|
60
|
+
test("converts skills entries to surface 'skill' rules", () => {
|
|
61
|
+
const result = normalizeConfig({
|
|
62
|
+
skills: { "*": "ask", librarian: "allow" },
|
|
63
|
+
});
|
|
64
|
+
expect(result).toEqual([
|
|
65
|
+
{ surface: "skill", pattern: "*", action: "ask" },
|
|
66
|
+
{ surface: "skill", pattern: "librarian", action: "allow" },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("special entries", () => {
|
|
72
|
+
test("converts special entries to surface 'special' with key as pattern", () => {
|
|
73
|
+
const result = normalizeConfig({
|
|
74
|
+
special: { external_directory: "ask" },
|
|
75
|
+
});
|
|
76
|
+
expect(result).toEqual([
|
|
77
|
+
{ surface: "special", pattern: "external_directory", action: "ask" },
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("ordering", () => {
|
|
83
|
+
test("tools.bash excluded; bash entries come after tools", () => {
|
|
84
|
+
const result = normalizeConfig({
|
|
85
|
+
tools: { bash: "allow", read: "deny" },
|
|
86
|
+
bash: { "git *": "ask" },
|
|
87
|
+
});
|
|
88
|
+
expect(result).toEqual([
|
|
89
|
+
{ surface: "read", pattern: "*", action: "deny" },
|
|
90
|
+
{ surface: "bash", pattern: "git *", action: "ask" },
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("full ordering: tools → bash → mcp → skills → special", () => {
|
|
95
|
+
const result = normalizeConfig({
|
|
96
|
+
tools: { read: "allow" },
|
|
97
|
+
bash: { "git *": "allow" },
|
|
98
|
+
mcp: { "exa:*": "allow" },
|
|
99
|
+
skills: { librarian: "allow" },
|
|
100
|
+
special: { external_directory: "ask" },
|
|
101
|
+
});
|
|
102
|
+
expect(result).toEqual([
|
|
103
|
+
{ surface: "read", pattern: "*", action: "allow" },
|
|
104
|
+
{ surface: "bash", pattern: "git *", action: "allow" },
|
|
105
|
+
{ surface: "mcp", pattern: "exa:*", action: "allow" },
|
|
106
|
+
{ surface: "skill", pattern: "librarian", action: "allow" },
|
|
107
|
+
{ surface: "special", pattern: "external_directory", action: "ask" },
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("empty and missing sections", () => {
|
|
113
|
+
test("empty config produces empty ruleset", () => {
|
|
114
|
+
expect(normalizeConfig({})).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("undefined sections are skipped", () => {
|
|
118
|
+
expect(normalizeConfig({ tools: undefined })).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { test } from "vitest";
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
import {
|
|
15
15
|
createActiveToolsCacheKey,
|
|
16
16
|
createBeforeAgentStartPromptStateKey,
|
|
@@ -46,11 +46,7 @@ import {
|
|
|
46
46
|
checkRequestedToolRegistration,
|
|
47
47
|
getToolNameFromValue,
|
|
48
48
|
} from "../src/tool-registry";
|
|
49
|
-
import type {
|
|
50
|
-
AgentPermissions,
|
|
51
|
-
GlobalPermissionConfig,
|
|
52
|
-
PermissionState,
|
|
53
|
-
} from "../src/types";
|
|
49
|
+
import type { PermissionState, ScopeConfig } from "../src/types";
|
|
54
50
|
import {
|
|
55
51
|
canResolveAskPermissionRequest,
|
|
56
52
|
shouldAutoApprovePermissionState,
|
|
@@ -61,7 +57,7 @@ type CreateManagerOptions = {
|
|
|
61
57
|
};
|
|
62
58
|
|
|
63
59
|
function createManager(
|
|
64
|
-
config:
|
|
60
|
+
config: ScopeConfig,
|
|
65
61
|
agentFiles: Record<string, string> = {},
|
|
66
62
|
options: CreateManagerOptions = {},
|
|
67
63
|
) {
|
|
@@ -146,7 +142,7 @@ async function withIsolatedSubagentEnv<T>(
|
|
|
146
142
|
}
|
|
147
143
|
|
|
148
144
|
function createToolCallHarness(
|
|
149
|
-
config:
|
|
145
|
+
config: ScopeConfig,
|
|
150
146
|
toolNames: readonly string[],
|
|
151
147
|
options: ExtensionHarnessOptions = {},
|
|
152
148
|
): ExtensionHarness {
|
|
@@ -652,30 +648,6 @@ test("Permission-system logger respects debug toggle and keeps review log enable
|
|
|
652
648
|
}
|
|
653
649
|
});
|
|
654
650
|
|
|
655
|
-
test("BashFilter uses opencode-style last-match hierarchy", () => {
|
|
656
|
-
const filter = new BashFilter(
|
|
657
|
-
{
|
|
658
|
-
"*": "ask",
|
|
659
|
-
"git *": "deny",
|
|
660
|
-
"git status *": "ask",
|
|
661
|
-
"git status": "allow",
|
|
662
|
-
},
|
|
663
|
-
"deny",
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
const exact = filter.check("git status");
|
|
667
|
-
assert.equal(exact.state, "allow");
|
|
668
|
-
assert.equal(exact.matchedPattern, "git status");
|
|
669
|
-
|
|
670
|
-
const subcommand = filter.check("git status --short");
|
|
671
|
-
assert.equal(subcommand.state, "ask");
|
|
672
|
-
assert.equal(subcommand.matchedPattern, "git status *");
|
|
673
|
-
|
|
674
|
-
const generic = filter.check("git commit -m test");
|
|
675
|
-
assert.equal(generic.state, "deny");
|
|
676
|
-
assert.equal(generic.matchedPattern, "git *");
|
|
677
|
-
});
|
|
678
|
-
|
|
679
651
|
test("PermissionManager canonical built-in permission checking", () => {
|
|
680
652
|
const { manager, cleanup } = createManager({
|
|
681
653
|
defaultPolicy: {
|
|
@@ -910,7 +882,7 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
|
|
|
910
882
|
// which matches this rule and returns "allow".
|
|
911
883
|
// After the fix, settings.json is ignored, so no server name is derived and the
|
|
912
884
|
// result falls through to the default mcp policy ("ask").
|
|
913
|
-
const config:
|
|
885
|
+
const config: ScopeConfig = {
|
|
914
886
|
defaultPolicy: {
|
|
915
887
|
tools: "ask",
|
|
916
888
|
bash: "ask",
|
|
@@ -1510,12 +1482,12 @@ test("Permission forwarding rejects unresolved sentinel session ids", () => {
|
|
|
1510
1482
|
});
|
|
1511
1483
|
|
|
1512
1484
|
type CreateManagerWithProjectOptions = CreateManagerOptions & {
|
|
1513
|
-
projectConfig?:
|
|
1485
|
+
projectConfig?: ScopeConfig;
|
|
1514
1486
|
projectAgentFiles?: Record<string, string>;
|
|
1515
1487
|
};
|
|
1516
1488
|
|
|
1517
1489
|
function createManagerWithProject(
|
|
1518
|
-
config:
|
|
1490
|
+
config: ScopeConfig,
|
|
1519
1491
|
agentFiles: Record<string, string> = {},
|
|
1520
1492
|
options: CreateManagerWithProjectOptions = {},
|
|
1521
1493
|
) {
|
|
@@ -1807,7 +1779,7 @@ test("PermissionManager reads config from PI_CODING_AGENT_DIR when set", () => {
|
|
|
1807
1779
|
mkdirSync(agentsDir, { recursive: true });
|
|
1808
1780
|
mkdirSync(dirname(newConfigPath), { recursive: true });
|
|
1809
1781
|
|
|
1810
|
-
const config:
|
|
1782
|
+
const config: ScopeConfig = {
|
|
1811
1783
|
defaultPolicy: {
|
|
1812
1784
|
tools: "deny",
|
|
1813
1785
|
bash: "deny",
|
|
@@ -2609,7 +2581,7 @@ test("normalizeRawPermission emits no issues when special is absent", () => {
|
|
|
2609
2581
|
});
|
|
2610
2582
|
|
|
2611
2583
|
test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit in global config", () => {
|
|
2612
|
-
const config:
|
|
2584
|
+
const config: ScopeConfig = {
|
|
2613
2585
|
defaultPolicy: {
|
|
2614
2586
|
tools: "ask",
|
|
2615
2587
|
bash: "ask",
|
|
@@ -2634,7 +2606,7 @@ test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit
|
|
|
2634
2606
|
});
|
|
2635
2607
|
|
|
2636
2608
|
test("PermissionManager.getConfigIssues returns empty array for clean config", () => {
|
|
2637
|
-
const config:
|
|
2609
|
+
const config: ScopeConfig = {
|
|
2638
2610
|
defaultPolicy: {
|
|
2639
2611
|
tools: "ask",
|
|
2640
2612
|
bash: "ask",
|
|
@@ -2660,7 +2632,7 @@ test("PermissionManager.getConfigIssues returns empty array for clean config", (
|
|
|
2660
2632
|
// --- doom_loop config-loader deprecation tests (#54) ---
|
|
2661
2633
|
|
|
2662
2634
|
test("PermissionManager.getConfigIssues returns deprecation for doom_loop in global config", () => {
|
|
2663
|
-
const config:
|
|
2635
|
+
const config: ScopeConfig = {
|
|
2664
2636
|
defaultPolicy: {
|
|
2665
2637
|
tools: "ask",
|
|
2666
2638
|
bash: "ask",
|
package/tests/rule.test.ts
CHANGED
|
@@ -1,38 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
2
|
import type { Rule, Ruleset } from "../src/rule";
|
|
3
|
-
import { evaluate
|
|
4
|
-
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
vi.restoreAllMocks();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
describe("getDefaultAction", () => {
|
|
10
|
-
test("returns 'ask' for bash surface", () => {
|
|
11
|
-
expect(getDefaultAction("bash")).toBe("ask");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("returns 'ask' for mcp surface", () => {
|
|
15
|
-
expect(getDefaultAction("mcp")).toBe("ask");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("returns 'ask' for skill surface", () => {
|
|
19
|
-
expect(getDefaultAction("skill")).toBe("ask");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("returns 'ask' for special surface", () => {
|
|
23
|
-
expect(getDefaultAction("special")).toBe("ask");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("returns 'ask' for tools surface", () => {
|
|
27
|
-
expect(getDefaultAction("tools")).toBe("ask");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("returns 'ask' for unknown surface (least privilege)", () => {
|
|
31
|
-
expect(getDefaultAction("unknown_surface")).toBe("ask");
|
|
32
|
-
expect(getDefaultAction("")).toBe("ask");
|
|
33
|
-
expect(getDefaultAction("external_directory")).toBe("ask");
|
|
34
|
-
});
|
|
35
|
-
});
|
|
3
|
+
import { evaluate } from "../src/rule";
|
|
36
4
|
|
|
37
5
|
describe("evaluate", () => {
|
|
38
6
|
const allowBashGit: Rule = {
|
|
@@ -64,11 +32,23 @@ describe("evaluate", () => {
|
|
|
64
32
|
expect(result).toEqual(allowBashGit);
|
|
65
33
|
});
|
|
66
34
|
|
|
67
|
-
test("returns synthetic rule with
|
|
35
|
+
test("returns synthetic rule with 'ask' when no rules match and no defaultAction", () => {
|
|
68
36
|
const result = evaluate("bash", "npm install", [allowBashGit]);
|
|
69
37
|
expect(result.surface).toBe("bash");
|
|
70
38
|
expect(result.pattern).toBe("npm install");
|
|
71
|
-
expect(result.action).toBe("ask");
|
|
39
|
+
expect(result.action).toBe("ask");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("returns synthetic rule with custom defaultAction when no rules match", () => {
|
|
43
|
+
const result = evaluate("bash", "npm install", [allowBashGit], "deny");
|
|
44
|
+
expect(result.surface).toBe("bash");
|
|
45
|
+
expect(result.pattern).toBe("npm install");
|
|
46
|
+
expect(result.action).toBe("deny");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("defaultAction does not affect matched rules", () => {
|
|
50
|
+
const result = evaluate("bash", "git status", [allowBashGit], "deny");
|
|
51
|
+
expect(result).toEqual(allowBashGit);
|
|
72
52
|
});
|
|
73
53
|
|
|
74
54
|
test("returns synthetic rule for empty ruleset", () => {
|
|
@@ -126,18 +106,19 @@ describe("evaluate", () => {
|
|
|
126
106
|
expect(result.action).toBe("ask"); // falls back to default
|
|
127
107
|
});
|
|
128
108
|
|
|
129
|
-
test("
|
|
109
|
+
test("merged rulesets: rules from later scope take priority", () => {
|
|
130
110
|
const globalRules: Ruleset = [
|
|
131
111
|
{ surface: "bash", pattern: "git *", action: "ask" },
|
|
132
112
|
];
|
|
133
113
|
const agentRules: Ruleset = [
|
|
134
114
|
{ surface: "bash", pattern: "git *", action: "allow" },
|
|
135
115
|
];
|
|
136
|
-
const
|
|
116
|
+
const merged = [...globalRules, ...agentRules];
|
|
117
|
+
const result = evaluate("bash", "git status", merged);
|
|
137
118
|
expect(result.action).toBe("allow"); // agent rule wins
|
|
138
119
|
});
|
|
139
120
|
|
|
140
|
-
test("
|
|
121
|
+
test("merged rulesets: earlier scope used when later scope has no match", () => {
|
|
141
122
|
const globalRules: Ruleset = [
|
|
142
123
|
{ surface: "bash", pattern: "git *", action: "allow" },
|
|
143
124
|
];
|
|
@@ -145,12 +126,13 @@ describe("evaluate", () => {
|
|
|
145
126
|
{ surface: "bash", pattern: "npm *", action: "deny" },
|
|
146
127
|
];
|
|
147
128
|
// git status matches global but not agent rule
|
|
148
|
-
const
|
|
129
|
+
const merged = [...globalRules, ...agentRules];
|
|
130
|
+
const result = evaluate("bash", "git status", merged);
|
|
149
131
|
expect(result.action).toBe("allow"); // global rule is the last match for this pattern
|
|
150
132
|
});
|
|
151
133
|
|
|
152
|
-
test("
|
|
153
|
-
const result = evaluate("bash", "git status");
|
|
134
|
+
test("empty ruleset returns synthetic default", () => {
|
|
135
|
+
const result = evaluate("bash", "git status", []);
|
|
154
136
|
expect(result.surface).toBe("bash");
|
|
155
137
|
expect(result.pattern).toBe("git status");
|
|
156
138
|
expect(result.action).toBe("ask");
|