| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- import { describe, expect, test } from "bun:test"
- 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: "",
- callID: "",
- agent: "build",
- abort: AbortSignal.any([]),
- metadata: () => {},
- }
- const projectRoot = path.join(__dirname, "../..")
- describe("tool.bash", () => {
- test("basic", async () => {
- await Instance.provide({
- directory: projectRoot,
- fn: async () => {
- const bash = await BashTool.init()
- const result = await bash.execute(
- {
- command: "echo 'test'",
- description: "Echo test message",
- },
- ctx,
- )
- expect(result.metadata.exit).toBe(0)
- expect(result.metadata.output).toContain("test")
- },
- })
- })
- })
- 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")
- },
- })
- })
- 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")
- },
- })
- })
- })
|