182 lines
5.4 KiB
Rust
182 lines
5.4 KiB
Rust
|
|
use std::ffi::OsString;
|
||
|
|
use std::path::PathBuf;
|
||
|
|
|
||
|
|
use clap::{Args, Parser, Subcommand};
|
||
|
|
use lazaret_host_coordinator::domain_store::StateResetScopeV1;
|
||
|
|
|
||
|
|
use crate::ClientCommand;
|
||
|
|
use crate::state::{StateCommand, StateOperation};
|
||
|
|
|
||
|
|
#[derive(Debug, Parser)]
|
||
|
|
#[command(
|
||
|
|
name = "lazaret",
|
||
|
|
version,
|
||
|
|
about = "Run development tools in an isolated agent sandbox",
|
||
|
|
long_about = "Run development tools in an isolated gVisor sandbox while keeping workspace editing, persistent tool state, controlled network access, and developer-approved host actions available."
|
||
|
|
)]
|
||
|
|
struct Cli {
|
||
|
|
#[command(subcommand)]
|
||
|
|
command: PublicCommand,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Subcommand)]
|
||
|
|
enum PublicCommand {
|
||
|
|
/// Run a command in the sandbox for a profile and workspace.
|
||
|
|
Run(RunArguments),
|
||
|
|
/// Inspect, verify, or reset persistent sandbox state.
|
||
|
|
State {
|
||
|
|
#[command(subcommand)]
|
||
|
|
operation: StateArguments,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Args)]
|
||
|
|
struct RunArguments {
|
||
|
|
#[command(flatten)]
|
||
|
|
domain: DomainArguments,
|
||
|
|
/// Command and arguments to run inside the sandbox.
|
||
|
|
#[arg(last = true, required = true, num_args = 1.., value_name = "COMMAND")]
|
||
|
|
command: Vec<OsString>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Args)]
|
||
|
|
struct DomainArguments {
|
||
|
|
/// Agent profile to use from the Lazaret configuration.
|
||
|
|
#[arg(long, value_name = "NAME")]
|
||
|
|
profile: String,
|
||
|
|
/// Project directory to share with the sandbox.
|
||
|
|
#[arg(long, value_name = "PATH")]
|
||
|
|
workspace: Option<PathBuf>,
|
||
|
|
/// Lazaret configuration file to load instead of the user default.
|
||
|
|
#[arg(long, value_name = "PATH")]
|
||
|
|
config: Option<PathBuf>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Subcommand)]
|
||
|
|
enum StateArguments {
|
||
|
|
/// List configured persistent sandbox paths without reading file contents.
|
||
|
|
List(StateReadArguments),
|
||
|
|
/// Show structural changes in persistent sandbox paths without file contents.
|
||
|
|
Inspect(StateReadArguments),
|
||
|
|
/// Verify persistent sandbox state while the domain is inactive.
|
||
|
|
Verify(StateReadArguments),
|
||
|
|
/// Discard persistent sandbox state for selected configured paths.
|
||
|
|
Reset(StateResetArguments),
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Args)]
|
||
|
|
struct StateReadArguments {
|
||
|
|
#[command(flatten)]
|
||
|
|
domain: DomainArguments,
|
||
|
|
/// Write a stable machine-readable JSON report.
|
||
|
|
#[arg(long)]
|
||
|
|
json: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Args)]
|
||
|
|
struct StateResetArguments {
|
||
|
|
#[command(flatten)]
|
||
|
|
domain: DomainArguments,
|
||
|
|
/// Write a stable machine-readable JSON report.
|
||
|
|
#[arg(long)]
|
||
|
|
json: bool,
|
||
|
|
/// Reset without asking for interactive confirmation.
|
||
|
|
#[arg(long)]
|
||
|
|
yes: bool,
|
||
|
|
#[command(flatten)]
|
||
|
|
scope: ResetScope,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Args)]
|
||
|
|
#[group(id = "scope", required = true, multiple = false)]
|
||
|
|
struct ResetScope {
|
||
|
|
/// Reset every configured persistent sandbox path in the domain.
|
||
|
|
#[arg(long)]
|
||
|
|
all: bool,
|
||
|
|
/// Reset one exact configured container path, such as `/usr/local`.
|
||
|
|
#[arg(long, value_name = "CONTAINER_PATH")]
|
||
|
|
path: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub enum Command {
|
||
|
|
Run(ClientCommand),
|
||
|
|
State(StateCommand),
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Parses the public command tree while preserving non-UTF-8 host paths and command arguments.
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
/// Returns clap's structured usage or help result.
|
||
|
|
pub fn parse(
|
||
|
|
arguments: impl IntoIterator<Item = OsString>,
|
||
|
|
current_workspace: PathBuf,
|
||
|
|
) -> Result<Command, clap::Error> {
|
||
|
|
let cli = Cli::try_parse_from(std::iter::once(OsString::from("lazaret")).chain(arguments))?;
|
||
|
|
Ok(match cli.command {
|
||
|
|
PublicCommand::Run(arguments) => Command::Run(ClientCommand {
|
||
|
|
profile: arguments.domain.profile,
|
||
|
|
workspace: arguments.domain.workspace.unwrap_or(current_workspace),
|
||
|
|
configuration: arguments.domain.config,
|
||
|
|
argv: arguments
|
||
|
|
.command
|
||
|
|
.into_iter()
|
||
|
|
.map(std::os::unix::ffi::OsStringExt::into_vec)
|
||
|
|
.collect(),
|
||
|
|
}),
|
||
|
|
PublicCommand::State { operation } => {
|
||
|
|
Command::State(state_command(operation, current_workspace))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn state_command(operation: StateArguments, current_workspace: PathBuf) -> StateCommand {
|
||
|
|
let (operation, domain, machine, yes, scope) = match operation {
|
||
|
|
StateArguments::List(arguments) => (
|
||
|
|
StateOperation::List,
|
||
|
|
arguments.domain,
|
||
|
|
arguments.json,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
),
|
||
|
|
StateArguments::Inspect(arguments) => (
|
||
|
|
StateOperation::Inspect,
|
||
|
|
arguments.domain,
|
||
|
|
arguments.json,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
),
|
||
|
|
StateArguments::Verify(arguments) => (
|
||
|
|
StateOperation::Verify,
|
||
|
|
arguments.domain,
|
||
|
|
arguments.json,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
),
|
||
|
|
StateArguments::Reset(arguments) => {
|
||
|
|
let scope = match arguments.scope.path {
|
||
|
|
Some(path) => StateResetScopeV1::Selected(path.into_bytes()),
|
||
|
|
None => StateResetScopeV1::All,
|
||
|
|
};
|
||
|
|
(
|
||
|
|
StateOperation::Reset,
|
||
|
|
arguments.domain,
|
||
|
|
arguments.json,
|
||
|
|
arguments.yes,
|
||
|
|
Some(scope),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
};
|
||
|
|
StateCommand {
|
||
|
|
operation,
|
||
|
|
context: ClientCommand {
|
||
|
|
profile: domain.profile,
|
||
|
|
workspace: domain.workspace.unwrap_or(current_workspace),
|
||
|
|
configuration: domain.config,
|
||
|
|
argv: Vec::new(),
|
||
|
|
},
|
||
|
|
machine,
|
||
|
|
yes,
|
||
|
|
scope,
|
||
|
|
}
|
||
|
|
}
|