Skip to content

Commit 188cc24

Browse files
authored
chore: cleanup external_dir perm logic (#11845)
1 parent 5e3162b commit 188cc24

4 files changed

Lines changed: 53 additions & 16 deletions

File tree

packages/opencode/src/agent/agent.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ export namespace Agent {
5555
doom_loop: "ask",
5656
external_directory: {
5757
"*": "ask",
58-
[Truncate.DIR]: "allow",
5958
[Truncate.GLOB]: "allow",
6059
},
6160
question: "deny",
@@ -140,7 +139,6 @@ export namespace Agent {
140139
codesearch: "allow",
141140
read: "allow",
142141
external_directory: {
143-
[Truncate.DIR]: "allow",
144142
[Truncate.GLOB]: "allow",
145143
},
146144
}),
@@ -229,19 +227,19 @@ export namespace Agent {
229227
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
230228
}
231229

232-
// Ensure Truncate.DIR is allowed unless explicitly configured
230+
// Ensure Truncate.GLOB is allowed unless explicitly configured
233231
for (const name in result) {
234232
const agent = result[name]
235233
const explicit = agent.permission.some((r) => {
236234
if (r.permission !== "external_directory") return false
237235
if (r.action !== "deny") return false
238-
return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB
236+
return r.pattern === Truncate.GLOB
239237
})
240238
if (explicit) continue
241239

242240
result[name].permission = PermissionNext.merge(
243241
result[name].permission,
244-
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }),
242+
PermissionNext.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
245243
)
246244
}
247245

packages/opencode/src/tool/bash.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ export const BashTool = Tool.define("bash", async () => {
128128
process.platform === "win32" && resolved.match(/^\/[a-z]\//)
129129
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
130130
: resolved
131-
if (!Instance.containsPath(normalized)) directories.add(normalized)
131+
if (!Instance.containsPath(normalized)) {
132+
const dir = (await Filesystem.isDir(normalized)) ? normalized : path.dirname(normalized)
133+
directories.add(dir)
134+
}
132135
}
133136
}
134137
}
@@ -141,10 +144,11 @@ export const BashTool = Tool.define("bash", async () => {
141144
}
142145

143146
if (directories.size > 0) {
147+
const globs = Array.from(directories).map((dir) => path.join(dir, "*"))
144148
await ctx.ask({
145149
permission: "external_directory",
146-
patterns: Array.from(directories),
147-
always: Array.from(directories).map((x) => path.dirname(x) + "*"),
150+
patterns: globs,
151+
always: globs,
148152
metadata: {},
149153
})
150154
}

packages/opencode/test/agent/agent.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a
447447
})
448448
})
449449

450-
test("Truncate.DIR is allowed even when user denies external_directory globally", async () => {
450+
test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => {
451451
const { Truncate } = await import("../../src/tool/truncation")
452452
await using tmp = await tmpdir({
453453
config: {
@@ -460,14 +460,14 @@ test("Truncate.DIR is allowed even when user denies external_directory globally"
460460
directory: tmp.path,
461461
fn: async () => {
462462
const build = await Agent.get("build")
463-
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow")
464463
expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
464+
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
465465
expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
466466
},
467467
})
468468
})
469469

470-
test("Truncate.DIR is allowed even when user denies external_directory per-agent", async () => {
470+
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
471471
const { Truncate } = await import("../../src/tool/truncation")
472472
await using tmp = await tmpdir({
473473
config: {
@@ -484,21 +484,21 @@ test("Truncate.DIR is allowed even when user denies external_directory per-agent
484484
directory: tmp.path,
485485
fn: async () => {
486486
const build = await Agent.get("build")
487-
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow")
488487
expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
488+
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
489489
expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
490490
},
491491
})
492492
})
493493

494-
test("explicit Truncate.DIR deny is respected", async () => {
494+
test("explicit Truncate.GLOB deny is respected", async () => {
495495
const { Truncate } = await import("../../src/tool/truncation")
496496
await using tmp = await tmpdir({
497497
config: {
498498
permission: {
499499
external_directory: {
500500
"*": "deny",
501-
[Truncate.DIR]: "deny",
501+
[Truncate.GLOB]: "deny",
502502
},
503503
},
504504
},
@@ -507,8 +507,8 @@ test("explicit Truncate.DIR deny is respected", async () => {
507507
directory: tmp.path,
508508
fn: async () => {
509509
const build = await Agent.get("build")
510-
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
511510
expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
511+
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
512512
},
513513
})
514514
})

packages/opencode/test/tool/bash.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,42 @@ describe("tool.bash permissions", () => {
144144
)
145145
const extDirReq = requests.find((r) => r.permission === "external_directory")
146146
expect(extDirReq).toBeDefined()
147-
expect(extDirReq!.patterns).toContain("/tmp")
147+
expect(extDirReq!.patterns).toContain("/tmp/*")
148+
},
149+
})
150+
})
151+
152+
test("asks for external_directory permission when file arg is outside project", async () => {
153+
await using outerTmp = await tmpdir({
154+
init: async (dir) => {
155+
await Bun.write(path.join(dir, "outside.txt"), "x")
156+
},
157+
})
158+
await using tmp = await tmpdir({ git: true })
159+
await Instance.provide({
160+
directory: tmp.path,
161+
fn: async () => {
162+
const bash = await BashTool.init()
163+
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
164+
const testCtx = {
165+
...ctx,
166+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
167+
requests.push(req)
168+
},
169+
}
170+
const filepath = path.join(outerTmp.path, "outside.txt")
171+
await bash.execute(
172+
{
173+
command: `cat ${filepath}`,
174+
description: "Read external file",
175+
},
176+
testCtx,
177+
)
178+
const extDirReq = requests.find((r) => r.permission === "external_directory")
179+
const expected = path.join(outerTmp.path, "*")
180+
expect(extDirReq).toBeDefined()
181+
expect(extDirReq!.patterns).toContain(expected)
182+
expect(extDirReq!.always).toContain(expected)
148183
},
149184
})
150185
})

0 commit comments

Comments
 (0)