2025-05-31 18:41:00 +00:00
import { z } from "zod"
2025-08-03 19:34:37 +00:00
import { exec } from "child_process"
2025-08-11 05:23:00 +00:00
2025-05-31 18:41:00 +00:00
import { Tool } from "./tool"
2025-06-04 17:12:13 +00:00
import DESCRIPTION from "./bash.txt"
2025-07-31 14:34:43 +00:00
import { Permission } from "../permission"
2025-07-31 20:38:31 +00:00
import { Filesystem } from "../util/filesystem"
2025-07-31 21:19:56 +00:00
import { lazy } from "../util/lazy"
2025-08-01 00:40:05 +00:00
import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard"
2025-08-01 01:41:48 +00:00
import { $ } from "bun"
2025-09-01 21:15:49 +00:00
import { Instance } from "../project/instance"
2025-08-12 15:39:39 +00:00
import { Agent } from "../agent/agent"
2025-05-19 23:29:38 +00:00
2025-08-12 20:14:40 +00:00
const MAX_OUTPUT_LENGTH = 30 _000
2025-05-31 18:41:00 +00:00
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
2025-05-19 23:29:38 +00:00
2025-08-01 01:41:48 +00:00
const log = Log . create ( { service : "bash-tool" } )
2025-07-31 21:19:56 +00:00
const parser = lazy ( async ( ) = > {
2025-08-13 19:46:21 +00:00
try {
const { default : Parser } = await import ( "tree-sitter" )
const Bash = await import ( "tree-sitter-bash" )
const p = new Parser ( )
p . setLanguage ( Bash . language as any )
return p
} catch ( e ) {
const { default : Parser } = await import ( "web-tree-sitter" )
const { default : treeWasm } = await import ( "web-tree-sitter/tree-sitter.wasm" as string , { with : { type : "wasm" } } )
await Parser . init ( {
locateFile() {
return treeWasm
} ,
} )
const { default : bashWasm } = await import ( "tree-sitter-bash/tree-sitter-bash.wasm" as string , {
with : { type : "wasm" } ,
} )
const bashLanguage = await Parser . Language . load ( bashWasm )
const p = new Parser ( )
p . setLanguage ( bashLanguage )
return p
}
2025-07-31 21:19:56 +00:00
} )
2025-07-31 00:57:52 +00:00
2025-07-25 17:29:29 +00:00
export const BashTool = Tool . define ( "bash" , {
2025-05-19 23:29:38 +00:00
description : DESCRIPTION ,
parameters : z.object ( {
2025-06-04 17:12:13 +00:00
command : z.string ( ) . describe ( "The command to execute" ) ,
2025-07-29 21:39:31 +00:00
timeout : z.number ( ) . describe ( "Optional timeout in milliseconds" ) . optional ( ) ,
2025-06-04 17:12:13 +00:00
description : z
. string ( )
. describe (
"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'" ,
) ,
2025-05-19 23:29:38 +00:00
} ) ,
2025-06-10 21:56:05 +00:00
async execute ( params , ctx ) {
2025-05-31 18:41:00 +00:00
const timeout = Math . min ( params . timeout ? ? DEFAULT_TIMEOUT , MAX_TIMEOUT )
2025-07-31 21:19:56 +00:00
const tree = await parser ( ) . then ( ( p ) = > p . parse ( params . command ) )
2025-08-12 15:39:39 +00:00
const permissions = await Agent . get ( ctx . agent ) . then ( ( x ) = > x . permission . bash )
2025-07-31 00:57:52 +00:00
let needsAsk = false
for ( const node of tree . rootNode . descendantsOfType ( "command" ) ) {
const command = [ ]
for ( let i = 0 ; i < node . childCount ; i ++ ) {
const child = node . child ( i )
if ( ! child ) continue
if (
child . type !== "command_name" &&
child . type !== "word" &&
child . type !== "string" &&
child . type !== "raw_string" &&
child . type !== "concatenation"
) {
continue
}
command . push ( child . text )
}
// not an exhaustive list, but covers most common cases
if ( [ "cd" , "rm" , "cp" , "mv" , "mkdir" , "touch" , "chmod" , "chown" ] . includes ( command [ 0 ] ) ) {
for ( const arg of command . slice ( 1 ) ) {
2025-08-01 14:10:09 +00:00
if ( arg . startsWith ( "-" ) || ( command [ 0 ] === "chmod" && arg . startsWith ( "+" ) ) ) continue
2025-08-03 14:30:00 +00:00
const resolved = await $ ` realpath ${ arg } `
. quiet ( )
. nothrow ( )
. text ( )
. then ( ( x ) = > x . trim ( ) )
2025-08-01 01:41:48 +00:00
log . info ( "resolved path" , { arg , resolved } )
2025-09-01 21:15:49 +00:00
if ( resolved && ! Filesystem . contains ( Instance . directory , resolved ) ) {
2025-07-31 00:57:52 +00:00
throw new Error (
2025-09-01 21:15:49 +00:00
` This command references paths outside of ${ Instance . directory } so it is not allowed to be executed. ` ,
2025-07-31 00:57:52 +00:00
)
}
}
}
// always allow cd if it passes above check
if ( ! needsAsk && command [ 0 ] !== "cd" ) {
2025-08-12 15:39:39 +00:00
const action = Wildcard . all ( node . text , permissions )
2025-08-06 00:14:28 +00:00
if ( action === "deny" ) {
throw new Error (
2025-08-12 15:39:39 +00:00
` The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${ JSON . stringify ( permissions ) } ` ,
2025-08-06 00:14:28 +00:00
)
}
if ( action === "ask" ) needsAsk = true
2025-07-31 00:57:52 +00:00
}
}
if ( needsAsk ) {
await Permission . ask ( {
2025-07-31 20:38:31 +00:00
type : "bash" ,
2025-08-13 11:34:06 +00:00
pattern : params.command ,
2025-07-31 00:57:52 +00:00
sessionID : ctx.sessionID ,
2025-07-31 14:34:43 +00:00
messageID : ctx.messageID ,
2025-07-31 20:38:31 +00:00
callID : ctx.callID ,
2025-07-31 00:57:52 +00:00
title : params.command ,
metadata : {
command : params.command ,
} ,
} )
}
2025-07-31 14:34:43 +00:00
2025-08-03 19:34:37 +00:00
const process = exec ( params . command , {
2025-09-01 21:15:49 +00:00
cwd : Instance.directory ,
2025-06-10 21:56:05 +00:00
signal : ctx.abort ,
2025-08-03 17:51:59 +00:00
timeout ,
2025-05-31 18:41:00 +00:00
} )
2025-08-03 19:34:37 +00:00
2025-08-11 05:23:00 +00:00
let output = ""
// Initialize metadata with empty output
ctx . metadata ( {
metadata : {
output : "" ,
description : params.description ,
} ,
} )
process . stdout ? . on ( "data" , ( chunk ) = > {
output += chunk . toString ( )
ctx . metadata ( {
metadata : {
output : output ,
description : params.description ,
} ,
} )
} )
process . stderr ? . on ( "data" , ( chunk ) = > {
output += chunk . toString ( )
ctx . metadata ( {
metadata : {
output : output ,
description : params.description ,
} ,
} )
} )
2025-08-03 19:34:37 +00:00
2025-08-03 17:51:59 +00:00
await new Promise < void > ( ( resolve ) = > {
process . on ( "close" , ( ) = > {
resolve ( )
} )
} )
2025-08-03 19:34:37 +00:00
2025-08-11 05:23:00 +00:00
ctx . metadata ( {
metadata : {
output : output ,
exit : process.exitCode ,
description : params.description ,
} ,
} )
2025-06-03 17:08:47 +00:00
2025-08-12 20:14:40 +00:00
if ( output . length > MAX_OUTPUT_LENGTH ) {
output = output . slice ( 0 , MAX_OUTPUT_LENGTH )
output += "\n\n(Output was truncated due to length limit)"
2025-08-12 18:51:13 +00:00
}
2025-05-19 23:29:38 +00:00
return {
2025-07-07 19:53:43 +00:00
title : params.command ,
2025-06-03 17:08:47 +00:00
metadata : {
2025-08-11 05:23:00 +00:00
output ,
2025-06-22 18:24:35 +00:00
exit : process.exitCode ,
2025-06-04 17:12:13 +00:00
description : params.description ,
2025-06-03 17:08:47 +00:00
} ,
2025-08-11 05:23:00 +00:00
output ,
2025-05-31 18:41:00 +00:00
}
2025-05-19 23:29:38 +00:00
} ,
2025-05-31 18:41:00 +00:00
} )