Files
agent-sandbox/src/main.rs

147 lines
3.9 KiB
Rust

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<PathBuf>,
/// Bind an extra path read-only (repeatable)
#[arg(long = "ro", value_name = "PATH", action = clap::ArgAction::Append)]
extra_ro: Vec<PathBuf>,
/// 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<PathBuf>,
/// Command and arguments to run inside the sandbox
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command_and_args: Vec<OsString>,
}
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>) -> (OsString, Vec<OsString>) {
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>) -> 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<PathBuf> {
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())
})
}