use std::ffi::OsString;
use std::path::PathBuf;
use clap::Parser;
use clap::builder::styling::{AnsiColor, Effects, Styles};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Basic,
Opencode,
Claude,
Codex,
}
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::BrightBlue.on_default())
.error(AnsiColor::Red.on_default().effects(Effects::BOLD))
.valid(AnsiColor::Green.on_default().effects(Effects::BOLD))
.invalid(AnsiColor::Yellow.on_default());
const LONG_ABOUT: &str = "\
Run a command inside a filesystem sandbox so a coding agent can only write to \
a small set of directories.
The sandbox grants write access to the context directory (the current \
directory unless -C is given) plus its real git directory, and makes the rest \
of the filesystem read-only via Landlock. The agent presets (opencode, claude, \
codex) additionally allow that agent's own config and cache directories.";
const AFTER_HELP: &str = "\
Examples:
agent-locker -- cargo test Run a command in the current directory
agent-locker -C ./project claude Run claude scoped to ./project
Notes:
- The current backend uses Landlock to allow writes only in the context
directory and a few extra paths; everything else is read-only.";
#[derive(Parser, Debug)]
#[command(
name = "agent-locker",
version,
about = "A sandbox for running coding agents with restricted filesystem access.",
long_about = LONG_ABOUT,
after_help = AFTER_HELP,
styles = STYLES,
)]
struct RawCli {
#[arg(short = 'C', long = "context-dir", value_name = "DIR")]
context_dir: Option<PathBuf>,
#[arg(
value_name = "COMMAND",
trailing_var_arg = true,
allow_hyphen_values = true,
required = true
)]
command_and_args: Vec<OsString>,
}
#[derive(Debug, Clone)]
pub struct Cli {
pub context_dir: Option<PathBuf>,
pub mode: Mode,
pub command_and_args: Vec<OsString>,
}
impl Cli {
pub fn parse() -> Self {
Self::from_raw(RawCli::parse())
}
fn from_raw(raw: RawCli) -> Self {
let mode = match raw.command_and_args.first().and_then(|s| s.to_str()) {
Some("opencode") => Mode::Opencode,
Some("claude") => Mode::Claude,
Some("codex") => Mode::Codex,
_ => Mode::Basic,
};
let command_and_args = match mode {
Mode::Basic => raw.command_and_args,
Mode::Opencode | Mode::Claude | Mode::Codex => raw.command_and_args[1..].to_vec(),
};
Cli {
context_dir: raw.context_dir,
mode,
command_and_args,
}
}
}