Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 50 additions & 0 deletions packages/opencode/test/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down