diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 115d8f8b29d..fb59da5ca25 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -84,7 +84,7 @@ export const BashTool = Tool.define("bash", async () => { const agent = await Agent.get(ctx.agent) const checkExternalDirectory = async (dir: string) => { - if (Filesystem.contains(Instance.directory, dir)) return + if (Filesystem.isAllowedPath(Instance.directory, dir)) return const title = `This command references paths outside of ${Instance.directory}` if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index b49bd7abe00..0b754dfe138 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -44,7 +44,7 @@ export const EditTool = Tool.define("edit", { const agent = await Agent.get(ctx.agent) const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) - if (!Filesystem.contains(Instance.directory, filePath)) { + if (!Filesystem.isAllowedPath(Instance.directory, filePath)) { const parentDir = path.dirname(filePath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts index 93888f60bd2..fc0528ef644 100644 --- a/packages/opencode/src/tool/patch.ts +++ b/packages/opencode/src/tool/patch.ts @@ -53,7 +53,7 @@ export const PatchTool = Tool.define("patch", { for (const hunk of hunks) { const filePath = path.resolve(Instance.directory, hunk.path) - if (!Filesystem.contains(Instance.directory, filePath)) { + if (!Filesystem.isAllowedPath(Instance.directory, filePath)) { const parentDir = path.dirname(filePath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 27426ad2412..f8ed0df84bb 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -30,7 +30,7 @@ export const ReadTool = Tool.define("read", { const title = path.relative(Instance.worktree, filepath) const agent = await Agent.get(ctx.agent) - if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) { + if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.isAllowedPath(Instance.directory, filepath)) { const parentDir = path.dirname(filepath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 6b8fd3dd111..9760d0947e7 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -24,7 +24,7 @@ export const WriteTool = Tool.define("write", { const agent = await Agent.get(ctx.agent) const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) - if (!Filesystem.contains(Instance.directory, filepath)) { + if (!Filesystem.isAllowedPath(Instance.directory, filepath)) { const parentDir = path.dirname(filepath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 98fbe533de3..67262f4fbf6 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,8 +1,28 @@ import { realpathSync } from "fs" import { exists } from "fs/promises" -import { dirname, join, relative } from "path" +import { dirname, join, normalize, relative } from "path" +import { tmpdir } from "os" export namespace Filesystem { + const systemTmpDir = normalize(tmpdir()) + // on macOS /tmp is a symlink to /private/tmp, resolve it + const tmpDirResolved = (() => { + try { + return realpathSync("/tmp") + } catch { + return null + } + })() + + export function isAllowedPath(projectDir: string, filepath: string) { + const normalized = normalize(filepath) + if (contains(projectDir, normalized)) return true + if (contains(systemTmpDir, normalized)) return true + if (contains("/tmp", normalized)) return true + if (tmpDirResolved && contains(tmpDirResolved, normalized)) return true + return false + } + /** * On Windows, normalize a path to its canonical casing using the filesystem. * This is needed because Windows paths are case-insensitive but LSP servers diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 9ef7dfb9d8f..b8ccc34f772 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -376,8 +376,8 @@ describe("tool.bash permissions", () => { bash.execute( { command: "ls", - workdir: "/tmp", - description: "List /tmp", + workdir: "/usr", + description: "List /usr", }, ctx, ),