summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/agent/agent.ts8
-rw-r--r--packages/opencode/src/tool/bash.ts10
-rw-r--r--packages/opencode/test/agent/agent.test.ts14
-rw-r--r--packages/opencode/test/tool/bash.test.ts37
4 files changed, 53 insertions, 16 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 72e7f8985..0eaa410e0 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -55,7 +55,6 @@ export namespace Agent {
doom_loop: "ask",
external_directory: {
"*": "ask",
- [Truncate.DIR]: "allow",
[Truncate.GLOB]: "allow",
},
question: "deny",
@@ -140,7 +139,6 @@ export namespace Agent {
codesearch: "allow",
read: "allow",
external_directory: {
- [Truncate.DIR]: "allow",
[Truncate.GLOB]: "allow",
},
}),
@@ -229,19 +227,19 @@ export namespace Agent {
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
}
- // Ensure Truncate.DIR is allowed unless explicitly configured
+ // Ensure Truncate.GLOB is allowed unless explicitly configured
for (const name in result) {
const agent = result[name]
const explicit = agent.permission.some((r) => {
if (r.permission !== "external_directory") return false
if (r.action !== "deny") return false
- return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB
+ return r.pattern === Truncate.GLOB
})
if (explicit) continue
result[name].permission = PermissionNext.merge(
result[name].permission,
- PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }),
+ PermissionNext.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
)
}
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index 5d073fd68..ff208ff3f 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -128,7 +128,10 @@ export const BashTool = Tool.define("bash", async () => {
process.platform === "win32" && resolved.match(/^\/[a-z]\//)
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
: resolved
- if (!Instance.containsPath(normalized)) directories.add(normalized)
+ if (!Instance.containsPath(normalized)) {
+ const dir = (await Filesystem.isDir(normalized)) ? normalized : path.dirname(normalized)
+ directories.add(dir)
+ }
}
}
}
@@ -141,10 +144,11 @@ export const BashTool = Tool.define("bash", async () => {
}
if (directories.size > 0) {
+ const globs = Array.from(directories).map((dir) => path.join(dir, "*"))
await ctx.ask({
permission: "external_directory",
- patterns: Array.from(directories),
- always: Array.from(directories).map((x) => path.dirname(x) + "*"),
+ patterns: globs,
+ always: globs,
metadata: {},
})
}
diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts
index 1ff303b76..05b842739 100644
--- a/packages/opencode/test/agent/agent.test.ts
+++ b/packages/opencode/test/agent/agent.test.ts
@@ -447,7 +447,7 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a
})
})
-test("Truncate.DIR is allowed even when user denies external_directory globally", async () => {
+test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => {
const { Truncate } = await import("../../src/tool/truncation")
await using tmp = await tmpdir({
config: {
@@ -460,14 +460,14 @@ test("Truncate.DIR is allowed even when user denies external_directory globally"
directory: tmp.path,
fn: async () => {
const build = await Agent.get("build")
- expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow")
expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
+ expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
},
})
})
-test("Truncate.DIR is allowed even when user denies external_directory per-agent", async () => {
+test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
const { Truncate } = await import("../../src/tool/truncation")
await using tmp = await tmpdir({
config: {
@@ -484,21 +484,21 @@ test("Truncate.DIR is allowed even when user denies external_directory per-agent
directory: tmp.path,
fn: async () => {
const build = await Agent.get("build")
- expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow")
expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
+ expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
},
})
})
-test("explicit Truncate.DIR deny is respected", async () => {
+test("explicit Truncate.GLOB deny is respected", async () => {
const { Truncate } = await import("../../src/tool/truncation")
await using tmp = await tmpdir({
config: {
permission: {
external_directory: {
"*": "deny",
- [Truncate.DIR]: "deny",
+ [Truncate.GLOB]: "deny",
},
},
},
@@ -507,8 +507,8 @@ test("explicit Truncate.DIR deny is respected", async () => {
directory: tmp.path,
fn: async () => {
const build = await Agent.get("build")
- expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
+ expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
},
})
})
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index ff12c4368..fd03b7f98 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -144,7 +144,42 @@ describe("tool.bash permissions", () => {
)
const extDirReq = requests.find((r) => r.permission === "external_directory")
expect(extDirReq).toBeDefined()
- expect(extDirReq!.patterns).toContain("/tmp")
+ expect(extDirReq!.patterns).toContain("/tmp/*")
+ },
+ })
+ })
+
+ test("asks for external_directory permission when file arg is outside project", async () => {
+ await using outerTmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(path.join(dir, "outside.txt"), "x")
+ },
+ })
+ await using tmp = await tmpdir({ git: true })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
+ const testCtx = {
+ ...ctx,
+ ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
+ requests.push(req)
+ },
+ }
+ const filepath = path.join(outerTmp.path, "outside.txt")
+ await bash.execute(
+ {
+ command: `cat ${filepath}`,
+ description: "Read external file",
+ },
+ testCtx,
+ )
+ const extDirReq = requests.find((r) => r.permission === "external_directory")
+ const expected = path.join(outerTmp.path, "*")
+ expect(extDirReq).toBeDefined()
+ expect(extDirReq!.patterns).toContain(expected)
+ expect(extDirReq!.always).toContain(expected)
},
})
})