Compare commits

...

19 Commits

Author SHA1 Message Date
GitHub Action
3a77f98ead chore: regen sdk 2025-12-07 05:55:22 +00:00
Aiden Cline
c8c29faf4d fix: typo 2025-12-06 23:54:50 -06:00
GitHub Action
e367c8439b chore: format code 2025-12-07 05:47:44 +00:00
Aiden Cline
e52d22039e rebase 2025-12-06 23:47:08 -06:00
Aiden Cline
9425eee09f fix: external dir 2025-12-06 23:47:08 -06:00
GitHub Action
320ebb35b7 chore: regen sdk 2025-12-07 05:46:16 +00:00
Aiden Cline
775ddd8c46 Merge branch 'dev' into bash-tweaks 2025-12-06 21:45:50 -08:00
Aiden Cline
32566a16a5 Revise bash.txt for command usage guidelines
Updated examples to avoid using 'cd' and clarified command usage.
2025-12-06 17:30:10 -06:00
Aiden Cline
414bbd50ac fix example
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-06 17:28:53 -06:00
Aiden Cline
b40bb950ae fix typo
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-06 17:28:43 -06:00
Aiden Cline
fcc256c3c9 fix: test 2025-12-06 17:04:53 -06:00
Aiden Cline
bfbcf978fa tweak: timeout defaults 2025-12-05 16:31:10 -06:00
Aiden Cline
c75dae680a remove max timeout 2025-12-05 16:28:32 -06:00
Aiden Cline
639824b1ea tweak 2025-12-05 16:18:52 -06:00
Aiden Cline
8ede7c59d2 fix 2025-12-05 16:12:40 -06:00
Aiden Cline
61cf0aeee2 fix 2025-12-05 16:12:18 -06:00
Aiden Cline
ae328d338e bash tweaks 2025-12-05 16:11:39 -06:00
Aiden Cline
2f5d6ba447 Merge branch 'dev' into bash-tweaks 2025-12-05 14:42:23 -06:00
Aiden Cline
6cf5fe481b wip 2025-12-05 14:42:10 -06:00
6 changed files with 8593 additions and 554 deletions

View File

@@ -21,8 +21,7 @@ const MAX_OUTPUT_LENGTH = (() => {
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
})()
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
const DEFAULT_TIMEOUT = 2 * 60 * 1000
const SIGKILL_TIMEOUT_MS = 200
export const log = Log.create({ service: "bash-tool" })
@@ -90,6 +89,12 @@ export const BashTool = Tool.define("bash", async () => {
parameters: z.object({
command: z.string().describe("The command to execute"),
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
workdir: z
.string()
.describe(
`The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
)
.optional(),
description: z
.string()
.describe(
@@ -97,15 +102,47 @@ export const BashTool = Tool.define("bash", async () => {
),
}),
async execute(params, ctx) {
const cwd = params.workdir || Instance.directory
if (params.timeout !== undefined && params.timeout < 0) {
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
}
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const timeout = params.timeout ?? DEFAULT_TIMEOUT
const tree = await parser().then((p) => p.parse(params.command))
if (!tree) {
throw new Error("Failed to parse command")
}
const agent = await Agent.get(ctx.agent)
const checkExternalDirectory = async (dir: string) => {
if (Filesystem.contains(Instance.directory, dir)) return
const title = `This command references paths outside of ${Instance.directory}`
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: [dir, path.join(dir, "*")],
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title,
metadata: {
command: params.command,
},
})
} else if (agent.permission.external_directory === "deny") {
throw new Permission.RejectedError(
ctx.sessionID,
"external_directory",
ctx.callID,
{
command: params.command,
},
`${title} so this command is not allowed to be executed.`,
)
}
}
await checkExternalDirectory(cwd)
const permissions = agent.permission.bash
const askPatterns = new Set<string>()
@@ -144,32 +181,7 @@ export const BashTool = Tool.define("bash", async () => {
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
: resolved
if (!Filesystem.contains(Instance.directory, normalized)) {
const parentDir = path.dirname(normalized)
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: [parentDir, path.join(parentDir, "*")],
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `This command references paths outside of ${Instance.directory}`,
metadata: {
command: params.command,
},
})
} else if (agent.permission.external_directory === "deny") {
throw new Permission.RejectedError(
ctx.sessionID,
"external_directory",
ctx.callID,
{
command: params.command,
},
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
)
}
}
await checkExternalDirectory(normalized)
}
}
}
@@ -215,7 +227,7 @@ export const BashTool = Tool.define("bash", async () => {
const proc = spawn(params.command, {
shell,
cwd: Instance.directory,
cwd,
env: {
...process.env,
},

View File

@@ -7,10 +7,10 @@ Before executing the command, please follow these steps:
- For example, before running "mkdir foo/bar", first use List to check that "foo" exists and is the intended parent directory
2. Command Execution:
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
- Always quote file paths that contain spaces with double quotes (e.g., rm "path with spaces/file.txt")
- Examples of proper quoting:
- cd "/Users/name/My Documents" (correct)
- cd /Users/name/My Documents (incorrect - will fail)
- mkdir "/Users/name/My Documents" (correct)
- mkdir /Users/name/My Documents (incorrect - will fail)
- python "/path/with spaces/script.py" (correct)
- python /path/with spaces/script.py (incorrect - will fail)
- After ensuring proper quoting, execute the command.
@@ -18,20 +18,27 @@ Before executing the command, please follow these steps:
Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
- 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.
- 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.
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
- 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.
- 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.
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
- 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.
<good-example>
pytest /foo/bar/tests
</good-example>
<bad-example>
cd /foo/bar && pytest tests
</bad-example>
# Working Directory
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.
<good-example>
workdir="/foo/bar", command="pytest tests"
</good-example>
<good-example>
command="pytest /foo/bar/tests"
</good-example>
<bad-example>
command="cd /foo/bar && pytest tests"
</bad-example>
# Committing changes with git

View File

@@ -13,7 +13,6 @@ const ctx = {
metadata: () => {},
}
const bash = await BashTool.init()
const projectRoot = path.join(__dirname, "../..")
describe("tool.bash", () => {
@@ -21,6 +20,7 @@ describe("tool.bash", () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
const result = await bash.execute(
{
command: "echo 'test'",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff