147 lines
3.9 KiB
Rust
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())
|
|
})
|
|
}
|