use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; use std::process; use clap::Parser; use agent_sandbox::{SandboxConfig, SandboxMode}; #[derive(Parser, Debug)] #[command( name = "agent-sandbox", version, about = "Sandbox agentic coding assistants with bubblewrap" )] struct Args { /// Blacklist mode: bind / read-only, overlay sensitive paths (default) #[arg(long, conflicts_with = "whitelist")] blacklist: bool, /// Whitelist mode: only explicitly listed minimal paths visible #[arg(long)] whitelist: bool, /// Harden: unshare IPC, PID, UTS; private /tmp, /dev, /run #[arg(long)] hardened: bool, /// Unshare the network namespace #[arg(long)] no_net: bool, /// Bind an extra path read-write (repeatable) #[arg(long = "rw", value_name = "PATH", action = clap::ArgAction::Append)] extra_rw: Vec, /// Bind an extra path read-only (repeatable) #[arg(long = "ro", value_name = "PATH", action = clap::ArgAction::Append)] extra_ro: Vec, /// Print the bwrap command without executing #[arg(long)] dry_run: bool, /// Working directory inside the sandbox (default: current directory) #[arg(long, value_name = "PATH")] chdir: Option, /// Command and arguments to run inside the sandbox #[arg(trailing_var_arg = true, allow_hyphen_values = true)] command_and_args: Vec, } fn main() { let args = Args::parse(); let (command, command_args) = resolve_command(args.command_and_args); let command = assert_binary_exists(&command); let chdir = assert_chdir(args.chdir); let mode = if args.whitelist { SandboxMode::Whitelist } else { SandboxMode::Blacklist }; let config = SandboxConfig { mode, hardened: args.hardened, no_net: args.no_net, extra_rw: args .extra_rw .iter() .map(|p| canonicalize_or_exit(p)) .collect(), extra_ro: args .extra_ro .iter() .map(|p| canonicalize_or_exit(p)) .collect(), command, command_args, chdir, dry_run: args.dry_run, }; if let Err(e) = agent_sandbox::run(config) { eprintln!("error: {e}"); process::exit(1); } } fn resolve_command(mut positional: Vec) -> (OsString, Vec) { if !positional.is_empty() { let cmd = positional.remove(0); return (cmd, positional); } if let Ok(cmd) = std::env::var("SANDBOX_CMD") { return (OsString::from(cmd), vec![]); } ( OsString::from("claude"), vec![OsString::from("--dangerously-skip-permissions")], ) } fn assert_binary_exists(name: &OsStr) -> PathBuf { resolve_binary(name).unwrap_or_else(|| { eprintln!("error: command not found: {}", name.to_string_lossy()); process::exit(1); }) } fn assert_chdir(explicit: Option) -> PathBuf { if let Some(p) = explicit { return canonicalize_or_exit(&p); } match std::env::current_dir() { Ok(p) => p, Err(e) => { eprintln!( "error: {}", agent_sandbox::SandboxError::CurrentDirUnavailable(e) ); process::exit(1); } } } fn canonicalize_or_exit(p: &Path) -> PathBuf { std::fs::canonicalize(p).unwrap_or_else(|e| { eprintln!("error: cannot resolve path '{}': {e}", p.display()); process::exit(1); }) } fn resolve_binary(name: &OsStr) -> Option { let path = PathBuf::from(name); if path.is_absolute() || path.components().count() > 1 { return path.is_file().then_some(path); } std::env::var_os("PATH").and_then(|path_var| { std::env::split_paths(&path_var) .map(|dir| dir.join(name)) .find(|p| p.is_file()) }) }