2025-05-31 18:41:00 +00:00
import { z } from "zod"
import { Tool } from "./tool"
2025-06-04 17:12:13 +00:00
import DESCRIPTION from "./bash.txt"
2025-06-30 19:28:47 +00:00
import { App } from "../app/app"
2025-07-31 14:34:43 +00:00
import { Permission } from "../permission"
import { Config } from "../config/config"
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-05-19 23:29:38 +00:00
2025-05-31 18:41:00 +00:00
const MAX_OUTPUT_LENGTH = 30000
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 ( ) = > {
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
} )
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 00:57:52 +00:00
const app = App . info ( )
2025-07-31 20:38:31 +00:00
const cfg = await Config . get ( )
2025-07-31 21:19:56 +00:00
const tree = await parser ( ) . then ( ( p ) = > p . parse ( params . command ) )
2025-07-31 00:57:52 +00:00
const permissions = ( ( ) = > {
const value = cfg . permission ? . bash
if ( ! value )
return {
"*" : "allow" ,
}
if ( typeof value === "string" )
return {
"*" : value ,
}
return value
} ) ( )
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-08-03 14:30:00 +00:00
if ( resolved && ! Filesystem . contains ( app . path . cwd , resolved ) ) {
2025-07-31 00:57:52 +00:00
throw new Error (
` This command references paths outside of ${ app . path . cwd } so it is not allowed to be executed. ` ,
)
}
}
}
// always allow cd if it passes above check
if ( ! needsAsk && command [ 0 ] !== "cd" ) {
const ask = ( ( ) = > {
for ( const [ pattern , value ] of Object . entries ( permissions ) ) {
2025-08-01 00:40:05 +00:00
const match = Wildcard . match ( node . text , pattern )
2025-08-01 01:41:48 +00:00
log . info ( "checking" , { text : node.text.trim ( ) , pattern , match } )
if ( match ) return value
2025-07-31 00:57:52 +00:00
}
return "ask"
} ) ( )
if ( ask === "ask" ) needsAsk = true
}
}
if ( needsAsk ) {
await Permission . ask ( {
2025-07-31 20:38:31 +00:00
type : "bash" ,
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-06-03 17:08:47 +00:00
const process = Bun . spawn ( {
2025-05-19 23:29:38 +00:00
cmd : [ "bash" , "-c" , params . command ] ,
2025-07-31 00:57:52 +00:00
cwd : app.path.cwd ,
2025-05-19 23:29:38 +00:00
maxBuffer : MAX_OUTPUT_LENGTH ,
2025-06-10 21:56:05 +00:00
signal : ctx.abort ,
2025-05-19 23:29:38 +00:00
timeout : timeout ,
2025-06-03 18:48:05 +00:00
stdout : "pipe" ,
stderr : "pipe" ,
2025-05-31 18:41:00 +00:00
} )
2025-06-03 17:08:47 +00:00
await process . exited
const stdout = await new Response ( process . stdout ) . text ( )
2025-06-03 18:48:05 +00:00
const stderr = await new Response ( process . stderr ) . text ( )
2025-06-03 17:08:47 +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 : {
stderr ,
stdout ,
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-07-07 19:53:43 +00:00
output : [ ` <stdout> ` , stdout ? ? "" , ` </stdout> ` , ` <stderr> ` , stderr ? ? "" , ` </stderr> ` ] . join ( "\n" ) ,
2025-05-31 18:41:00 +00:00
}
2025-05-19 23:29:38 +00:00
} ,
2025-05-31 18:41:00 +00:00
} )