feat(tools): implement Bash tool with persistent sessions and timeouts (M4 complete)
This commit implements the complete M4 milestone (Bash tool) including: Bash Session: - Persistent bash session using tokio::process - Environment variables persist between commands - Current working directory persists between commands - Session-based execution (not one-off commands) - Automatic cleanup on session close Key Features: - Command timeout support (default: 2 minutes, configurable per-command) - Output truncation (max 2000 lines for stdout/stderr) - Exit code capture and propagation - Stderr capture alongside stdout - Command delimiter system to reliably detect command completion - Automatic backup of exit codes to temp files Implementation Details: - Uses tokio::process for async command execution - BashSession maintains single bash process across multiple commands - stdio handles (stdin/stdout/stderr) are taken and restored for each command - Non-blocking stderr reading with timeout to avoid deadlocks - Mutex protection for concurrent access safety CLI Integration: - Added `bash` subcommand: `owlen bash <command> [--timeout <ms>]` - Permission checks with command context for pattern matching - Stdout/stderr properly routed to respective streams - Exit code propagation (exits with same code as bash command) Permission Enforcement: - Plan mode (default): blocks Bash (asks for approval) - Code mode: allows Bash - Pattern matching support for command-specific rules (e.g., "npm test*") Testing: - 7 tests in tools-bash for session behavior - bash_persists_env_between_calls ✅ - bash_persists_cwd_between_calls ✅ - bash_command_timeout ✅ - bash_output_truncation ✅ - bash_command_failure_returns_error_code ✅ - bash_stderr_captured ✅ - bash_multiple_commands_in_sequence ✅ - 3 new tests in CLI for permission enforcement - plan_mode_blocks_bash_operations ✅ - code_mode_allows_bash ✅ - bash_command_timeout_works ✅ - All 43 workspace tests passing ✅ Dependencies Added: - tokio with process, io-util, time, sync features M4 milestone complete! ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ enum Cmd {
|
||||
Grep { root: String, pattern: String },
|
||||
Write { path: String, content: String },
|
||||
Edit { path: String, old_string: String, new_string: String },
|
||||
Bash { command: String, #[arg(long)] timeout: Option<u64> },
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -143,6 +144,42 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Bash { command, timeout } => {
|
||||
// Check permission with command context for pattern matching
|
||||
match perms.check(Tool::Bash, Some(&command)) {
|
||||
PermissionDecision::Allow => {
|
||||
let mut session = tools_bash::BashSession::new().await?;
|
||||
let output = session.execute(&command, timeout).await?;
|
||||
|
||||
// Print stdout
|
||||
if !output.stdout.is_empty() {
|
||||
print!("{}", output.stdout);
|
||||
}
|
||||
|
||||
// Print stderr to stderr
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!("{}", output.stderr);
|
||||
}
|
||||
|
||||
session.close().await?;
|
||||
|
||||
// Exit with same code as command
|
||||
if !output.success {
|
||||
std::process::exit(output.exit_code);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
PermissionDecision::Ask => {
|
||||
return Err(eyre!(
|
||||
"Permission denied: Bash operation requires approval. Use --mode code to allow."
|
||||
));
|
||||
}
|
||||
PermissionDecision::Deny => {
|
||||
return Err(eyre!("Permission denied: Bash operation is blocked."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user