Diff
1diff --git a/src/config.ts b/src/config.ts
2index ffa817d4842d19a7cd43e04f034b5ae52319b313..00f60d27ed8e1182b5d082ef9906ddcb895cb398 100644
3--- a/src/config.ts
4+++ b/src/config.ts
5@@ -7,7 +7,12 @@ export type PermissionRules = PermissionAction | Record<string, PermissionAction
6
7 export type PermissionConfig = {
8 bash?: PermissionRules
9+ read?: PermissionRules
10+ write?: PermissionRules
11 edit?: PermissionRules
12+ find?: PermissionRules
13+ grep?: PermissionRules
14+ ls?: PermissionRules
15 external_directory?: PermissionRules
16 webfetch?: PermissionRules
17 }
18diff --git a/src/rules.ts b/src/rules.ts
19index 4c1753235d47e3b8a9bad3c834ca8d21c1439c1b..f614ddadd4da95afb9523e89885ce39862bca39f 100644
20--- a/src/rules.ts
21+++ b/src/rules.ts
22@@ -1,5 +1,5 @@
23 // Bump to invalidate all cached decisions when rules change.
24-export const POLICY_VERSION = 21;
25+export const POLICY_VERSION = 23;
26
27 // Trailing stderr redirections that are safe to strip before pattern matching.
28 // `2>&1` and `2>/dev/null` have no security implication but would otherwise
29diff --git a/src/static-permissions.ts b/src/static-permissions.ts
30index 18a19007c7d37864aa252b80d20b0e8eccdd4431..4d8f616a613ba70b53b1cb932286bfadccdc5309 100644
31--- a/src/static-permissions.ts
32+++ b/src/static-permissions.ts
33@@ -67,9 +67,18 @@ function toolRules(toolType: string, config: PermissionConfig): PermissionRules
34 switch (toolType) {
35 case "bash":
36 return config.bash
37- case "edit":
38+ case "read":
39+ return config.read
40 case "write":
41+ return config.write
42+ case "edit":
43 return config.edit
44+ case "find":
45+ return config.find
46+ case "grep":
47+ return config.grep
48+ case "ls":
49+ return config.ls
50 case "webfetch":
51 return config.webfetch
52 default:
53diff --git a/test/static-permissions.test.ts b/test/static-permissions.test.ts
54index 9f856baf81a0dbcde261700a3d464e5f1c9e8624..18a07ccd2f84f8bfec46956b6c415de39bae3103 100644
55--- a/test/static-permissions.test.ts
56+++ b/test/static-permissions.test.ts
57@@ -18,7 +18,12 @@ beforeEach(() => {
58 process.env.PI_POLICY_ENGINE_CONFIG = join(tempDir, "config.json")
59 writeFileSync(process.env.PI_POLICY_ENGINE_CONFIG, JSON.stringify({
60 permission: {
61+ read: "allow",
62+ write: "allow",
63 edit: "allow",
64+ find: "allow",
65+ grep: "allow",
66+ ls: "allow",
67 bash: { "*sudo*": "deny", "*terraform apply*": "deny" },
68 external_directory: { [`${externalDirectory}/*`]: "allow" },
69 webfetch: "check",
70@@ -46,13 +51,50 @@ describe("static permissions", () => {
71 .resolves.toMatchObject({ decision: "deny" })
72 })
73
74- test("allows edits in the workspace and actions in allowed external directories", async () => {
75+ test("uses separate path-tool permissions", async () => {
76+ writeFileSync(process.env.PI_POLICY_ENGINE_CONFIG!, JSON.stringify({
77+ permission: { read: "allow", write: "deny", edit: "deny", find: "deny", grep: "deny", ls: "deny" },
78+ }))
79+
80+ await expect(checkStaticPermission("read", { path: "file.ts" }, workspace))
81+ .resolves.toMatchObject({ decision: "allow" })
82+ await expect(checkStaticPermission("write", { path: "file.ts" }, workspace))
83+ .resolves.toMatchObject({ decision: "deny" })
84 await expect(checkStaticPermission("edit", { path: "file.ts" }, workspace))
85+ .resolves.toMatchObject({ decision: "deny" })
86+ await expect(checkStaticPermission("find", { path: "file.ts" }, workspace))
87+ .resolves.toMatchObject({ decision: "deny" })
88+ await expect(checkStaticPermission("grep", { path: "file.ts" }, workspace))
89+ .resolves.toMatchObject({ decision: "deny" })
90+ await expect(checkStaticPermission("ls", { path: "file.ts" }, workspace))
91+ .resolves.toMatchObject({ decision: "deny" })
92+ })
93+
94+ test("allows configured path tools in the workspace and allowed external directories", async () => {
95+ await expect(checkStaticPermission("read", { path: "file.ts" }, workspace))
96+ .resolves.toMatchObject({ decision: "allow" })
97+ await expect(checkStaticPermission("write", { path: "file.ts" }, workspace))
98+ .resolves.toMatchObject({ decision: "allow" })
99+ await expect(checkStaticPermission("edit", { path: "file.ts" }, workspace))
100+ .resolves.toMatchObject({ decision: "allow" })
101+ await expect(checkStaticPermission("find", { path: "file.ts" }, workspace))
102+ .resolves.toMatchObject({ decision: "allow" })
103+ await expect(checkStaticPermission("grep", { path: "file.ts" }, workspace))
104+ .resolves.toMatchObject({ decision: "allow" })
105+ await expect(checkStaticPermission("ls", { path: "file.ts" }, workspace))
106 .resolves.toMatchObject({ decision: "allow" })
107 await expect(checkStaticPermission("write", { path: join(externalDirectory, "file.ts") }, workspace))
108 .resolves.toMatchObject({ decision: "allow" })
109+ await expect(checkStaticPermission("edit", { path: join(externalDirectory, "file.ts") }, workspace))
110+ .resolves.toMatchObject({ decision: "allow" })
111 await expect(checkStaticPermission("read", { path: join(externalDirectory, "file.ts") }, workspace))
112 .resolves.toMatchObject({ decision: "allow" })
113+ await expect(checkStaticPermission("find", { path: join(externalDirectory, "file.ts") }, workspace))
114+ .resolves.toMatchObject({ decision: "allow" })
115+ await expect(checkStaticPermission("grep", { path: join(externalDirectory, "file.ts") }, workspace))
116+ .resolves.toMatchObject({ decision: "allow" })
117+ await expect(checkStaticPermission("ls", { path: join(externalDirectory, "file.ts") }, workspace))
118+ .resolves.toMatchObject({ decision: "allow" })
119 })
120
121 test("passes unlisted external paths to the policy pipeline", async () => {