use a3s_sandbox::{default_guest_relay_addr, CapabilityReport, NativeSandbox, TcpUnixRelay};
use anyhow::{bail, Context, Result};
use std::env;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::ExitCode;
#[tokio::main]
async fn main() -> ExitCode {
match run().await {
Ok(code) => code,
Err(error) => {
eprintln!("a3s-sandbox: {error:#}");
ExitCode::from(1)
}
}
}
async fn run() -> Result<ExitCode> {
let mut args = env::args().skip(1).collect::<Vec<_>>();
if args.is_empty() {
print_usage();
bail!("missing command");
}
let command = args.remove(0);
match command.as_str() {
"help" | "-h" | "--help" => {
print_usage();
Ok(ExitCode::SUCCESS)
}
"probe" => {
let workspace = parse_workspace(&args)?;
let sandbox = NativeSandbox::new(&workspace)?;
sandbox.probe().await?;
let report = sandbox.capability_report();
println!("ok backend={}", report.backend);
print_unavailable(&report);
Ok(ExitCode::SUCCESS)
}
"digest" => {
let workspace = parse_workspace(&args)?;
let sandbox = NativeSandbox::new(&workspace)?;
println!("{}", sandbox.policy_digest());
Ok(ExitCode::SUCCESS)
}
"capabilities" => {
let workspace = parse_workspace(&args)?;
let sandbox = NativeSandbox::new(&workspace)?;
let report = sandbox.capability_report();
print_capabilities_json(&report)?;
Ok(ExitCode::SUCCESS)
}
"exec" => {
let (workspace, command) = parse_exec(&args)?;
let sandbox = NativeSandbox::new(&workspace)?;
let output = sandbox.exec_command(command).await?;
print!("{}", output.stdout);
eprint!("{}", output.stderr);
if output.timed_out {
bail!("command timed out");
}
Ok(ExitCode::from(output.exit_code.clamp(0, 255) as u8))
}
"relay" => {
let (unix, listen) = parse_relay(&args)?;
TcpUnixRelay::run_forever(listen, unix).await?;
Ok(ExitCode::SUCCESS)
}
other => {
print_usage();
bail!("unknown command {other:?}");
}
}
}
fn print_usage() {
eprintln!(
"Usage:\n \
a3s-sandbox probe [--workspace PATH]\n \
a3s-sandbox digest [--workspace PATH]\n \
a3s-sandbox capabilities [--workspace PATH]\n \
a3s-sandbox exec [--workspace PATH] -- <command>\n \
a3s-sandbox relay --unix PATH [--listen 127.0.0.1:24731]\n"
);
}
fn parse_relay(args: &[String]) -> Result<(PathBuf, SocketAddr)> {
let mut unix = None;
let mut listen = default_guest_relay_addr();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--unix" => {
i += 1;
let path = args.get(i).context("--unix requires a path")?;
unix = Some(PathBuf::from(path));
}
"--listen" => {
i += 1;
let addr = args.get(i).context("--listen requires host:port")?;
listen = addr
.parse::<SocketAddr>()
.with_context(|| format!("invalid --listen address {addr:?}"))?;
}
other => bail!("unexpected argument {other:?}"),
}
i += 1;
}
let unix = unix.context("relay requires --unix PATH")?;
Ok((unix, listen))
}
fn parse_workspace(args: &[String]) -> Result<PathBuf> {
let mut workspace = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--workspace" | "-w" => {
i += 1;
let path = args.get(i).context("--workspace requires a path")?;
workspace = Some(PathBuf::from(path));
}
other => bail!("unexpected argument {other:?}"),
}
i += 1;
}
Ok(workspace.unwrap_or_else(|| env::current_dir().expect("cwd")))
}
fn parse_exec(args: &[String]) -> Result<(PathBuf, String)> {
let mut workspace = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--workspace" | "-w" => {
i += 1;
let path = args.get(i).context("--workspace requires a path")?;
workspace = Some(PathBuf::from(path));
}
"--" => {
i += 1;
break;
}
other => bail!("unexpected argument {other:?}; use -- before the command"),
}
i += 1;
}
if i >= args.len() {
bail!("exec requires a command after --");
}
let command = args[i..].join(" ");
let workspace = workspace.unwrap_or_else(|| env::current_dir().expect("cwd"));
Ok((workspace, command))
}
fn print_unavailable(report: &CapabilityReport) {
if report.unavailable.is_empty() {
println!("unavailable=");
} else {
println!("unavailable={}", report.unavailable.join(","));
}
}
fn print_capabilities_json(report: &CapabilityReport) -> Result<()> {
let caps = &report.capabilities;
let payload = serde_json::json!({
"backend": report.backend,
"policy_digest": report.policy_digest,
"unavailable": report.unavailable,
"capabilities": {
"filesystem_path_policy": caps.filesystem_path_policy,
"filesystem_readonly_mounts": caps.filesystem_readonly_mounts,
"filesystem_ephemeral_writes": caps.filesystem_ephemeral_writes,
"network_deny_all": caps.network_deny_all,
"mediated_http": caps.mediated_http,
"mediated_socks": caps.mediated_socks,
"unix_socket_allowlist": caps.unix_socket_allowlist,
"resource_timeout": caps.resource_timeout,
"resource_output_limit": caps.resource_output_limit,
"resource_memory_limit": caps.resource_memory_limit,
"resource_process_limit": caps.resource_process_limit,
}
});
println!("{payload}");
Ok(())
}