diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index ff342ff2..2a1f1cb4 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -163,6 +163,41 @@ export const layer = Layer.effect( external_directory: { [path.join(Global.Path.data, "plans", "*")]: "allow", }, + bash: { + "*": "deny", + "pwd *": "allow", + "ls *": "allow", + "cat *": "allow", + "grep *": "allow", + "rg *": "allow", + "find *": "allow", + "git status *": "allow", + "git diff *": "allow", + "git log *": "allow", + "git show *": "allow", + "Get-ChildItem *": "allow", + "Get-Content *": "allow", + "*>*": "deny", + "*>>*": "deny", + "* > *": "deny", + "* >> *": "deny", + "*|*": "deny", + "*<(*": "deny", + "*\n*": "deny", + "*;*": "deny", + "*&*": "deny", + "*&&*": "deny", + "*`*": "deny", + "*$(*": "deny", + "find * -delete": "deny", + "find * -delete *": "deny", + "find * -exec *": "deny", + "find * -execdir *": "deny", + "git diff --output *": "deny", + "git diff --output=*": "deny", + "git diff * --output *": "deny", + "git diff * --output=*": "deny", + }, edit: { "*": "deny", [path.join(".mimocode", "plans", "*.md")]: "allow", diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 9befab0a..cdef54f4 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -75,6 +75,56 @@ test("plan agent denies edits except .mimocode/plans/*", async () => { }) }) +test("plan agent allows only read-only bash commands by default", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const plan = await load(tmp.path, (svc) => svc.get("plan")) + expect(plan).toBeDefined() + + for (const pattern of [ + "mkdir test", + "touch output.txt", + "echo test > output.txt", + "cat package.json; touch out.txt", + "cat package.json\ntouch out.txt", + "cat package.json & touch out.txt", + "cat package.json > out.txt", + "cat package.json>out.txt", + "cat package.json `touch out.txt`", + "cat package.json $(touch out.txt)", + "cat <(touch out.txt)", + "cat package.json <(touch out.txt)", + "cat < <(touch out.txt)", + "grep foo file > out.txt", + "rg needle src >> out.txt", + "rg needle src>>out.txt", + "ls && touch out.txt", + "cat package.json | tee out.txt", + "rg needle src | tee out.txt", + "find . -delete", + "find . -delete -print", + "find . -exec rm {} \\;", + "find . -execdir rm {} \\;", + "find . -print0 | xargs rm -f", + "git diff --output=out.patch", + "git diff --output out.patch", + "git diff | tee out.patch", + "git status && rm -rf x", + "Get-Content package.json; Set-Content out.txt", + "Get-Content package.json | Set-Content out.txt", + ]) { + expect(Permission.evaluate("bash", pattern, plan!.permission).action).toBe("deny") + } + + for (const pattern of ["ls -la", "git status --short", "rg needle src", "Get-ChildItem ."]) { + expect(Permission.evaluate("bash", pattern, plan!.permission).action).toBe("allow") + } + }, + }) +}) + test("explore agent denies edit and write", async () => { await using tmp = await tmpdir() await Instance.provide({