summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2025-12-10 15:07:32 -0600
committerAiden Cline <[email protected]>2025-12-10 15:07:40 -0600
commit59fb3ae606764e8bd3dc8f8d9fc40b952aee8257 (patch)
tree149be4119ef0ebd926cb6709b3b50fbbeee4f2eb
parent0ab3b882507c44522b574a4cf5e4199ee58f93d1 (diff)
downloadopencode-59fb3ae606764e8bd3dc8f8d9fc40b952aee8257.tar.gz
opencode-59fb3ae606764e8bd3dc8f8d9fc40b952aee8257.zip
ignore: add bash tests
-rw-r--r--packages/opencode/test/tool/bash.test.ts417
1 files changed, 398 insertions, 19 deletions
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index 0116f47cf..9ef7dfb9d 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -3,11 +3,12 @@ import path from "path"
import { BashTool } from "../../src/tool/bash"
import { Instance } from "../../src/project/instance"
import { Permission } from "../../src/permission"
+import { tmpdir } from "../fixture/fixture"
const ctx = {
sessionID: "test",
messageID: "",
- toolCallID: "",
+ callID: "",
agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
@@ -33,23 +34,401 @@ describe("tool.bash", () => {
},
})
})
+})
+
+describe("tool.bash permissions", () => {
+ test("allows command matching allow pattern", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ bash: {
+ "echo *": "allow",
+ "*": "deny",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ const result = await bash.execute(
+ {
+ command: "echo hello",
+ description: "Echo hello",
+ },
+ ctx,
+ )
+ expect(result.metadata.exit).toBe(0)
+ expect(result.metadata.output).toContain("hello")
+ },
+ })
+ })
- // TODO: better test
- // test("cd ../ should ask for permission for external directory", async () => {
- // await Instance.provide({
- // directory: projectRoot,
- // fn: async () => {
- // bash.execute(
- // {
- // command: "cd ../",
- // description: "Try to cd to parent directory",
- // },
- // ctx,
- // )
- // // Give time for permission to be asked
- // await new Promise((resolve) => setTimeout(resolve, 1000))
- // expect(Permission.pending()[ctx.sessionID]).toBeDefined()
- // },
- // })
- // })
+ test("denies command matching deny pattern", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ bash: {
+ "curl *": "deny",
+ "*": "allow",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ await expect(
+ bash.execute(
+ {
+ command: "curl https://example.com",
+ description: "Fetch URL",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+ },
+ })
+ })
+
+ test("denies all commands with wildcard deny", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ bash: {
+ "*": "deny",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ await expect(
+ bash.execute(
+ {
+ command: "ls",
+ description: "List files",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+ },
+ })
+ })
+
+ test("more specific pattern overrides general pattern", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ bash: {
+ "*": "deny",
+ "ls *": "allow",
+ "pwd*": "allow",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ // ls should be allowed
+ const result = await bash.execute(
+ {
+ command: "ls -la",
+ description: "List files",
+ },
+ ctx,
+ )
+ expect(result.metadata.exit).toBe(0)
+
+ // pwd should be allowed
+ const pwd = await bash.execute(
+ {
+ command: "pwd",
+ description: "Print working directory",
+ },
+ ctx,
+ )
+ expect(pwd.metadata.exit).toBe(0)
+
+ // cat should be denied
+ await expect(
+ bash.execute(
+ {
+ command: "cat /etc/passwd",
+ description: "Read file",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+ },
+ })
+ })
+
+ test("denies dangerous subcommands while allowing safe ones", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ bash: {
+ "find *": "allow",
+ "find * -delete*": "deny",
+ "find * -exec*": "deny",
+ "*": "deny",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ // Basic find should work
+ const result = await bash.execute(
+ {
+ command: "find . -name '*.ts'",
+ description: "Find typescript files",
+ },
+ ctx,
+ )
+ expect(result.metadata.exit).toBe(0)
+
+ // find -delete should be denied
+ await expect(
+ bash.execute(
+ {
+ command: "find . -name '*.tmp' -delete",
+ description: "Delete temp files",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+
+ // find -exec should be denied
+ await expect(
+ bash.execute(
+ {
+ command: "find . -name '*.ts' -exec cat {} \\;",
+ description: "Find and cat files",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+ },
+ })
+ })
+
+ test("allows git read commands while denying writes", async () => {
+ await using tmp = await tmpdir({
+ git: true,
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ bash: {
+ "git status*": "allow",
+ "git log*": "allow",
+ "git diff*": "allow",
+ "git branch": "allow",
+ "git commit *": "deny",
+ "git push *": "deny",
+ "*": "deny",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ // git status should work
+ const status = await bash.execute(
+ {
+ command: "git status",
+ description: "Git status",
+ },
+ ctx,
+ )
+ expect(status.metadata.exit).toBe(0)
+
+ // git log should work
+ const log = await bash.execute(
+ {
+ command: "git log --oneline -5",
+ description: "Git log",
+ },
+ ctx,
+ )
+ expect(log.metadata.exit).toBe(0)
+
+ // git commit should be denied
+ await expect(
+ bash.execute(
+ {
+ command: "git commit -m 'test'",
+ description: "Git commit",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+
+ // git push should be denied
+ await expect(
+ bash.execute(
+ {
+ command: "git push origin main",
+ description: "Git push",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+ },
+ })
+ })
+
+ test("denies external directory access when permission is deny", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ external_directory: "deny",
+ bash: {
+ "*": "allow",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ // Should deny cd to parent directory (cd is checked for external paths)
+ await expect(
+ bash.execute(
+ {
+ command: "cd ../",
+ description: "Change to parent directory",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow()
+ },
+ })
+ })
+
+ test("denies workdir outside project when external_directory is deny", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ external_directory: "deny",
+ bash: {
+ "*": "allow",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ await expect(
+ bash.execute(
+ {
+ command: "ls",
+ workdir: "/tmp",
+ description: "List /tmp",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow()
+ },
+ })
+ })
+
+ test("handles multiple commands in sequence", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ permission: {
+ bash: {
+ "echo *": "allow",
+ "curl *": "deny",
+ "*": "deny",
+ },
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const bash = await BashTool.init()
+ // echo && echo should work
+ const result = await bash.execute(
+ {
+ command: "echo foo && echo bar",
+ description: "Echo twice",
+ },
+ ctx,
+ )
+ expect(result.metadata.output).toContain("foo")
+ expect(result.metadata.output).toContain("bar")
+
+ // echo && curl should fail (curl is denied)
+ await expect(
+ bash.execute(
+ {
+ command: "echo hi && curl https://example.com",
+ description: "Echo then curl",
+ },
+ ctx,
+ ),
+ ).rejects.toThrow("restricted")
+ },
+ })
+ })
})