0e2339a3c8359640694df3e3e0d35166447bb187
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/README.md b/README.md
2index 7fa80ee6ea7ffbbb187f2823796fe0f8c4cace8e..191e2394e4d44c4220ea6d2d5aa88a8299660edf 100644
3--- a/README.md
4+++ b/README.md
5@@ -90,12 +90,12 @@ Pi uses a root-level `tools` map and `externalDirectories` list. Any tool name c
6 "my_extension_tool": "allow"
7 },
8 "externalDirectories": [
9- "/tmp/pi/*"
10+ "/tmp/pi"
11 ]
12 }
13 ```
14
15-`externalDirectories` applies to Pi path tools. Every listed path is allowed. Its entries are also included in the LLM reviewer prompt so checked tools can allow matching external paths.
16diff --git a/src/core/rules.ts b/src/core/rules.ts
17index 99fb9316d600ec6a0cdcbe32bcc821a96ec917bc..2a04b35b333d744b5f73cb61c12d5c5afeed36fe 100644
18--- a/src/core/rules.ts
19+++ b/src/core/rules.ts
20@@ -237,7 +237,7 @@ export function llmPolicyPrompt(
21 .map((pattern) => ` - ${JSON.stringify(pattern)}`)
22 .join("\n")}
23
24-Treat these as trusted policy rules. Allow access when a tool call matches a listed path. This overrides the general directory guidance above.`;
25+Treat these as trusted policy rules. Allow access when a tool-call path starts with a listed path. This overrides the general directory guidance above.`;
26 return LLM_POLICY_PROMPT.replace(
27 EXTERNAL_DIRECTORIES_PLACEHOLDER,
28 directories,
29diff --git a/src/pi/static-permissions.ts b/src/pi/static-permissions.ts
30index 16914838013988cb884f2d72156befd072f13125..d40960bc80e1a09603967b22871ffa9131a92490 100644
31--- a/src/pi/static-permissions.ts
32+++ b/src/pi/static-permissions.ts
33@@ -8,11 +8,6 @@ const PATH_TOOLS = new Set(["read", "write", "edit", "find", "grep", "ls"])
34
35 type ToolInput = Record<string, unknown>
36
37-function patternRegex(pattern: string): RegExp {
38- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
39- return new RegExp(`^${escaped.replaceAll("*", ".*").replaceAll("?", ".")}$`)
40-}
41-
42 function expandedPattern(pattern: string): string {
43 if (pattern === "~") return homedir()
44 if (pattern.startsWith("~/")) return join(homedir(), pattern.slice(2))
45@@ -22,7 +17,7 @@ function expandedPattern(pattern: string): string {
46 }
47
48 export function matchesExternalDirectory(directories: ExternalDirectories | undefined, value: string): boolean {
49- return directories?.some((pattern) => patternRegex(expandedPattern(pattern)).test(value)) ?? false
50+ return directories?.some((directory) => value.startsWith(expandedPattern(directory))) ?? false
51 }
52
53 async function canonicalPath(path: string, cwd: string): Promise<string> {
54diff --git a/test/pi/extension.test.ts b/test/pi/extension.test.ts
55index 6930d592b7fa2658156a283f943a7181c8348915..6888f46910823eb663bccd20b051508380ff10e5 100644
56--- a/test/pi/extension.test.ts
57+++ b/test/pi/extension.test.ts
58@@ -4,6 +4,7 @@ import { tmpdir } from "node:os"
59 import { join } from "node:path"
60 import PolicyEngine from "../../src/pi/index"
61 import { loadPiPolicyConfig } from "../../src/pi/config"
62+import { matchesExternalDirectory } from "../../src/pi/static-permissions"
63
64 const originalAgentDirectory = process.env.PI_CODING_AGENT_DIR
65 const originalKey = process.env.OPENAI_API_KEY
66@@ -74,18 +75,18 @@ describe("Pi policy extension", () => {
67 test("merges project configuration over global configuration", async () => {
68 writeFileSync(join(tempDir, "policy-engine.json"), JSON.stringify({
69 tools: { bash: "deny", global_tool: "allow" },
70- externalDirectories: ["/global/*"],
71+ externalDirectories: ["/global"],
72 }))
73 const projectDirectory = join(tempDir, "project")
74 mkdirSync(join(projectDirectory, ".pi"), { recursive: true })
75 writeFileSync(join(projectDirectory, ".pi", "policy-engine.json"), JSON.stringify({
76 tools: { bash: "allow", project_tool: "deny" },
77- externalDirectories: ["/project/*"],
78+ externalDirectories: ["/project"],
79 }))
80
81 expect(loadPiPolicyConfig(projectDirectory)).toMatchObject({
82 tools: { bash: "allow", global_tool: "allow", project_tool: "deny" },
83- externalDirectories: ["/global/*", "/project/*"],
84+ externalDirectories: ["/global", "/project"],
85 })
86
87 const extension = loadExtension()
88@@ -95,12 +96,18 @@ describe("Pi policy extension", () => {
89 .resolves.toMatchObject({ block: true })
90 })
91
92+ test("matches external directories by prefix", () => {
93+ expect(matchesExternalDirectory(["/tmp/pi"], "/tmp/pi/file")).toBe(true)
94+ expect(matchesExternalDirectory(["/tmp/pi"], "/tmp/pilot/file")).toBe(true)
95+ expect(matchesExternalDirectory(["/tmp/pi/*"], "/tmp/pi/file")).toBe(false)
96+ })
97+
98 test("applies configured, deterministic, external-path, cached LLM, UI, and MCP policies", async () => {
99 const externalDirectory = join(tempDir, "external")
100 mkdirSync(externalDirectory)
101 writeFileSync(join(tempDir, "policy-engine.json"), JSON.stringify({
102 tools: { custom_tool: "allow", blocked_tool: "deny", bash: "check", read: "allow" },
103- externalDirectories: [`${externalDirectory}/*`],
104+ externalDirectories: [externalDirectory],
105 }))
106
107 let reviews = 0
108@@ -126,7 +133,7 @@ describe("Pi policy extension", () => {
109 await expect(extension.toolCall({ toolName: "read", input: { path: externalFile } }, confirmed)).resolves.toBeUndefined()
110
111 expect(reviews).toBe(1)
112- expect(prompt).toContain(`- ${JSON.stringify(`${externalDirectory}/*`)}`)
113+ expect(prompt).toContain(`- ${JSON.stringify(externalDirectory)}`)
114 expect(extension.events).toEqual([
115 { name: "herdr:blocked", data: { active: true, label: "Policy approval required" } },
116 { name: "herdr:blocked", data: { active: false } },