1use super::*;
7use crate::tool_primitives::process::{self as pproc, ExecOptions, Shell};
8use serde::Deserialize;
9
10fn parse_sentinel_output(stdout: &str, sentinel: &str) -> (String, Option<String>) {
13 if let Some(pos) = stdout.rfind(sentinel) {
14 let user_output = stdout[..pos].trim_end_matches('\n').to_string();
15 let state_section = &stdout[pos + sentinel.len()..];
16 let new_cwd = state_section
17 .trim()
18 .lines()
19 .next()
20 .map(|s| s.trim().to_string());
21 (user_output, new_cwd)
22 } else {
23 (stdout.to_string(), None)
25 }
26}
27
28pub struct BashTool;
29
30#[async_trait]
31impl Tool for BashTool {
32 fn name(&self) -> &str {
33 "Bash"
34 }
35
36 fn description(&self) -> &str {
37 "Execute a bash command and return its output. The working directory persists between commands."
38 }
39
40 fn permission_level(&self) -> PermissionLevel {
41 PermissionLevel::Execute
42 }
43 fn category(&self) -> ToolCategory {
44 ToolCategory::Shell
45 }
46
47 fn input_schema(&self) -> Value {
48 serde_json::json!({
49 "type": "object",
50 "properties": {
51 "command": {
52 "type": "string",
53 "description": "The bash command to execute"
54 },
55 "timeout": {
56 "type": "integer",
57 "description": "Optional timeout in milliseconds (max 600000)"
58 }
59 },
60 "required": ["command"]
61 })
62 }
63
64 async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
65 #[derive(Deserialize)]
66 struct Input {
67 command: String,
68 timeout: Option<u64>,
69 }
70
71 let input: Input = match serde_json::from_value(input) {
72 Ok(i) => i,
73 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
74 };
75
76 let shell_state = session_shell_state(&ctx.session_id);
77 let (cwd, env_vars) = {
78 let state = shell_state.lock();
79 (
80 state.cwd.clone().unwrap_or_else(|| ctx.working_dir.clone()),
81 state.env_vars.clone(),
82 )
83 };
84
85 let timeout_ms = input.timeout.unwrap_or(120_000).min(600_000);
86
87 #[cfg(feature = "vms")]
90 if let Some(sandbox) =
91 ctx.extensions.get::<std::sync::Arc<dyn cersei_vms::Sandbox>>()
92 {
93 let req = cersei_vms::RunRequest::new(input.command.clone())
94 .timeout(std::time::Duration::from_millis(timeout_ms));
95 return match sandbox.commands().run(req).await {
96 Ok(out) => {
97 if out.timed_out {
98 return ToolResult::error(format!(
99 "Command timed out after {}ms (sandbox: {})",
100 timeout_ms,
101 sandbox.id()
102 ));
103 }
104 let mut content = out.stdout;
105 if !out.stderr.is_empty() {
106 if !content.is_empty() {
107 content.push('\n');
108 }
109 content.push_str(&out.stderr);
110 }
111 if out.exit_code == 0 {
112 if content.is_empty() {
113 ToolResult::success("(Bash completed with no output)")
114 } else {
115 ToolResult::success(content)
116 }
117 } else {
118 ToolResult::error(format!("Exit code {}\n{}", out.exit_code, content))
119 }
120 }
121 Err(e) => ToolResult::error(format!("Sandbox exec failed: {e}")),
122 };
123 }
124
125 const SENTINEL: &str = "__ABSTRACT_STATE_7f2a9b__";
128 let wrapped_command = format!(
129 "cd '{}' 2>/dev/null; {} ; __abstract_exit=$?; echo '{}'; pwd; exit $__abstract_exit",
130 cwd.display(),
131 input.command,
132 SENTINEL,
133 );
134
135 let opts = ExecOptions {
136 cwd: Some(ctx.working_dir.clone()), env: env_vars,
138 timeout: Some(std::time::Duration::from_millis(timeout_ms)),
139 shell: Shell::Sh,
140 };
141
142 match pproc::exec(&wrapped_command, opts).await {
143 Ok(output) => {
144 if output.timed_out {
145 return ToolResult::error(format!("Command timed out after {}ms", timeout_ms));
146 }
147
148 let (user_output, new_cwd) = parse_sentinel_output(&output.stdout, SENTINEL);
150
151 if let Some(new_dir) = new_cwd {
153 let path = PathBuf::from(&new_dir);
154 if path.exists() {
155 shell_state.lock().cwd = Some(path);
156 }
157 }
158
159 let mut content = user_output;
160 if !output.stderr.is_empty() {
161 if !content.is_empty() {
162 content.push('\n');
163 }
164 content.push_str(&output.stderr);
165 }
166
167 if output.exit_code == 0 {
168 if content.is_empty() {
169 ToolResult::success("(Bash completed with no output)")
170 } else {
171 ToolResult::success(content)
172 }
173 } else {
174 ToolResult::error(format!("Exit code {}\n{}", output.exit_code, content))
175 }
176 }
177 Err(e) => ToolResult::error(format!("Failed to execute: {}", e)),
178 }
179 }
180}