Skip to content

Commit 75a4dcb

Browse files
authored
tweak: make bash give agent more awareness of cwd, bump default timeout, drop max timeout (#5140)
1 parent 3a179fc commit 75a4dcb

File tree

4 files changed

+6251
-42
lines changed

4 files changed

+6251
-42
lines changed

packages/opencode/src/tool/bash.ts

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ const MAX_OUTPUT_LENGTH = (() => {
2121
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
2222
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
2323
})()
24-
const DEFAULT_TIMEOUT = 1 * 60 * 1000
25-
const MAX_TIMEOUT = 10 * 60 * 1000
24+
const DEFAULT_TIMEOUT = 2 * 60 * 1000
2625
const SIGKILL_TIMEOUT_MS = 200
2726

2827
export const log = Log.create({ service: "bash-tool" })
@@ -90,22 +89,60 @@ export const BashTool = Tool.define("bash", async () => {
9089
parameters: z.object({
9190
command: z.string().describe("The command to execute"),
9291
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
92+
workdir: z
93+
.string()
94+
.describe(
95+
`The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
96+
)
97+
.optional(),
9398
description: z
9499
.string()
95100
.describe(
96101
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
97102
),
98103
}),
99104
async execute(params, ctx) {
105+
const cwd = params.workdir || Instance.directory
100106
if (params.timeout !== undefined && params.timeout < 0) {
101107
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
102108
}
103-
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
109+
const timeout = params.timeout ?? DEFAULT_TIMEOUT
104110
const tree = await parser().then((p) => p.parse(params.command))
105111
if (!tree) {
106112
throw new Error("Failed to parse command")
107113
}
108114
const agent = await Agent.get(ctx.agent)
115+
116+
const checkExternalDirectory = async (dir: string) => {
117+
if (Filesystem.contains(Instance.directory, dir)) return
118+
const title = `This command references paths outside of ${Instance.directory}`
119+
if (agent.permission.external_directory === "ask") {
120+
await Permission.ask({
121+
type: "external_directory",
122+
pattern: [dir, path.join(dir, "*")],
123+
sessionID: ctx.sessionID,
124+
messageID: ctx.messageID,
125+
callID: ctx.callID,
126+
title,
127+
metadata: {
128+
command: params.command,
129+
},
130+
})
131+
} else if (agent.permission.external_directory === "deny") {
132+
throw new Permission.RejectedError(
133+
ctx.sessionID,
134+
"external_directory",
135+
ctx.callID,
136+
{
137+
command: params.command,
138+
},
139+
`${title} so this command is not allowed to be executed.`,
140+
)
141+
}
142+
}
143+
144+
await checkExternalDirectory(cwd)
145+
109146
const permissions = agent.permission.bash
110147

111148
const askPatterns = new Set<string>()
@@ -144,32 +181,7 @@ export const BashTool = Tool.define("bash", async () => {
144181
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
145182
: resolved
146183

147-
if (!Filesystem.contains(Instance.directory, normalized)) {
148-
const parentDir = path.dirname(normalized)
149-
if (agent.permission.external_directory === "ask") {
150-
await Permission.ask({
151-
type: "external_directory",
152-
pattern: [parentDir, path.join(parentDir, "*")],
153-
sessionID: ctx.sessionID,
154-
messageID: ctx.messageID,
155-
callID: ctx.callID,
156-
title: `This command references paths outside of ${Instance.directory}`,
157-
metadata: {
158-
command: params.command,
159-
},
160-
})
161-
} else if (agent.permission.external_directory === "deny") {
162-
throw new Permission.RejectedError(
163-
ctx.sessionID,
164-
"external_directory",
165-
ctx.callID,
166-
{
167-
command: params.command,
168-
},
169-
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
170-
)
171-
}
172-
}
184+
await checkExternalDirectory(normalized)
173185
}
174186
}
175187
}
@@ -215,7 +227,7 @@ export const BashTool = Tool.define("bash", async () => {
215227

216228
const proc = spawn(params.command, {
217229
shell,
218-
cwd: Instance.directory,
230+
cwd,
219231
env: {
220232
...process.env,
221233
},

packages/opencode/src/tool/bash.txt

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,38 @@ Before executing the command, please follow these steps:
77
- For example, before running "mkdir foo/bar", first use List to check that "foo" exists and is the intended parent directory
88

99
2. Command Execution:
10-
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
10+
- Always quote file paths that contain spaces with double quotes (e.g., rm "path with spaces/file.txt")
1111
- Examples of proper quoting:
12-
- cd "/Users/name/My Documents" (correct)
13-
- cd /Users/name/My Documents (incorrect - will fail)
12+
- mkdir "/Users/name/My Documents" (correct)
13+
- mkdir /Users/name/My Documents (incorrect - will fail)
1414
- python "/path/with spaces/script.py" (correct)
1515
- python /path/with spaces/script.py (incorrect - will fail)
1616
- After ensuring proper quoting, execute the command.
1717
- Capture the output of the command.
1818

1919
Usage notes:
2020
- The command argument is required.
21-
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
21+
- You can specify an optional timeout in milliseconds. If not specified, commands will timeout after 120000ms (2 minutes). Use the `timeout` parameter to control execution time.
22+
- The `workdir` parameter specifies the working directory for the command. Defaults to the current working directory. Prefer setting `workdir` over using `cd` in your commands.
2223
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
2324
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
2425
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files.
2526
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
2627
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
27-
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
28-
<good-example>
29-
pytest /foo/bar/tests
30-
</good-example>
31-
<bad-example>
32-
cd /foo/bar && pytest tests
33-
</bad-example>
3428

29+
# Working Directory
30+
31+
The `workdir` parameter sets the working directory for command execution. Prefer using `workdir` over `cd <dir> &&` command chains when you simply need to run a command in a different directory.
32+
33+
<good-example>
34+
workdir="/foo/bar", command="pytest tests"
35+
</good-example>
36+
<good-example>
37+
command="pytest /foo/bar/tests"
38+
</good-example>
39+
<bad-example>
40+
command="cd /foo/bar && pytest tests"
41+
</bad-example>
3542

3643
# Committing changes with git
3744

packages/opencode/test/tool/bash.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ const ctx = {
1313
metadata: () => {},
1414
}
1515

16-
const bash = await BashTool.init()
1716
const projectRoot = path.join(__dirname, "../..")
1817

1918
describe("tool.bash", () => {
2019
test("basic", async () => {
2120
await Instance.provide({
2221
directory: projectRoot,
2322
fn: async () => {
23+
const bash = await BashTool.init()
2424
const result = await bash.execute(
2525
{
2626
command: "echo 'test'",

0 commit comments

Comments
 (0)