Restore profile-first interactive launches
This commit is contained in:
parent
b9c3bcfbbc
commit
b9dbd78087
3 changed files with 67 additions and 15 deletions
|
|
@ -12,7 +12,9 @@ use crate::state::{StateCommand, StateOperation};
|
||||||
name = "lazaret",
|
name = "lazaret",
|
||||||
version,
|
version,
|
||||||
about = "Run development tools in an isolated agent sandbox",
|
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."
|
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.",
|
||||||
|
override_usage = "lazaret <PROFILE> [OPTIONS] [-- <COMMAND>...]\n lazaret <COMMAND>",
|
||||||
|
after_help = "Launch a profile directly with `lazaret PROFILE`. Add `-- COMMAND...` to override the default /bin/bash environment entry."
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
|
@ -34,8 +36,8 @@ enum PublicCommand {
|
||||||
struct RunArguments {
|
struct RunArguments {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
domain: DomainArguments,
|
domain: DomainArguments,
|
||||||
/// Command and arguments to run inside the sandbox.
|
/// Command and arguments to run instead of the default `/bin/bash`.
|
||||||
#[arg(last = true, required = true, num_args = 1.., value_name = "COMMAND")]
|
#[arg(last = true, num_args = 1.., value_name = "COMMAND")]
|
||||||
command: Vec<OsString>,
|
command: Vec<OsString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,17 +114,14 @@ pub fn parse(
|
||||||
arguments: impl IntoIterator<Item = OsString>,
|
arguments: impl IntoIterator<Item = OsString>,
|
||||||
current_workspace: PathBuf,
|
current_workspace: PathBuf,
|
||||||
) -> Result<Command, clap::Error> {
|
) -> Result<Command, clap::Error> {
|
||||||
|
let arguments = expand_profile_launch(arguments);
|
||||||
let cli = Cli::try_parse_from(std::iter::once(OsString::from("lazaret")).chain(arguments))?;
|
let cli = Cli::try_parse_from(std::iter::once(OsString::from("lazaret")).chain(arguments))?;
|
||||||
Ok(match cli.command {
|
Ok(match cli.command {
|
||||||
PublicCommand::Run(arguments) => Command::Run(ClientCommand {
|
PublicCommand::Run(arguments) => Command::Run(ClientCommand {
|
||||||
profile: arguments.domain.profile,
|
profile: arguments.domain.profile,
|
||||||
workspace: arguments.domain.workspace.unwrap_or(current_workspace),
|
workspace: arguments.domain.workspace.unwrap_or(current_workspace),
|
||||||
configuration: arguments.domain.config,
|
configuration: arguments.domain.config,
|
||||||
argv: arguments
|
argv: command_argv(arguments.command),
|
||||||
.command
|
|
||||||
.into_iter()
|
|
||||||
.map(std::os::unix::ffi::OsStringExt::into_vec)
|
|
||||||
.collect(),
|
|
||||||
}),
|
}),
|
||||||
PublicCommand::State { operation } => {
|
PublicCommand::State { operation } => {
|
||||||
Command::State(state_command(operation, current_workspace))
|
Command::State(state_command(operation, current_workspace))
|
||||||
|
|
@ -130,6 +129,29 @@ pub fn parse(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expand_profile_launch(arguments: impl IntoIterator<Item = OsString>) -> Vec<OsString> {
|
||||||
|
let mut arguments = arguments.into_iter().collect::<Vec<_>>();
|
||||||
|
if arguments.first().is_some_and(|first| {
|
||||||
|
first != "run"
|
||||||
|
&& first != "state"
|
||||||
|
&& first.to_str().is_some_and(|first| !first.starts_with('-'))
|
||||||
|
}) {
|
||||||
|
arguments.insert(0, OsString::from("--profile"));
|
||||||
|
arguments.insert(0, OsString::from("run"));
|
||||||
|
}
|
||||||
|
arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_argv(command: Vec<OsString>) -> Vec<Vec<u8>> {
|
||||||
|
if command.is_empty() {
|
||||||
|
return vec![b"/bin/bash".to_vec()];
|
||||||
|
}
|
||||||
|
command
|
||||||
|
.into_iter()
|
||||||
|
.map(std::os::unix::ffi::OsStringExt::into_vec)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn state_command(operation: StateArguments, current_workspace: PathBuf) -> StateCommand {
|
fn state_command(operation: StateArguments, current_workspace: PathBuf) -> StateCommand {
|
||||||
let (operation, domain, machine, yes, scope) = match operation {
|
let (operation, domain, machine, yes, scope) = match operation {
|
||||||
StateArguments::List(arguments) => (
|
StateArguments::List(arguments) => (
|
||||||
|
|
|
||||||
|
|
@ -233,11 +233,31 @@ fn unsupported_host_and_configuration_errors_keep_their_cause() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_line_requires_an_explicit_profile_and_preserves_command_bytes() {
|
fn command_line_accepts_profile_entry_and_preserves_explicit_command_bytes() {
|
||||||
|
let parsed =
|
||||||
|
lazaret_client::cli::parse(["agent"].map(OsString::from), PathBuf::from("/current"))
|
||||||
|
.unwrap();
|
||||||
|
let lazaret_client::cli::Command::Run(parsed) = parsed else {
|
||||||
|
panic!("expected run command");
|
||||||
|
};
|
||||||
|
assert_eq!(parsed.profile, "agent");
|
||||||
|
assert_eq!(parsed.workspace, PathBuf::from("/current"));
|
||||||
|
assert_eq!(parsed.configuration, None);
|
||||||
|
assert_eq!(parsed.argv, [b"/bin/bash".to_vec()]);
|
||||||
|
|
||||||
|
let parsed = lazaret_client::cli::parse(
|
||||||
|
["run", "--profile", "state"].map(OsString::from),
|
||||||
|
PathBuf::from("/current"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let lazaret_client::cli::Command::Run(parsed) = parsed else {
|
||||||
|
panic!("expected run command");
|
||||||
|
};
|
||||||
|
assert_eq!(parsed.profile, "state");
|
||||||
|
assert_eq!(parsed.argv, [b"/bin/bash".to_vec()]);
|
||||||
|
|
||||||
let parsed = lazaret_client::cli::parse(
|
let parsed = lazaret_client::cli::parse(
|
||||||
[
|
[
|
||||||
"run",
|
|
||||||
"--profile",
|
|
||||||
"agent",
|
"agent",
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"/project",
|
"/project",
|
||||||
|
|
@ -305,6 +325,8 @@ fn public_help_explains_commands_and_options() {
|
||||||
&["--help"][..],
|
&["--help"][..],
|
||||||
&[
|
&[
|
||||||
"Run development tools in an isolated gVisor sandbox",
|
"Run development tools in an isolated gVisor sandbox",
|
||||||
|
"Usage: lazaret <PROFILE>",
|
||||||
|
"lazaret PROFILE",
|
||||||
"run",
|
"run",
|
||||||
"state",
|
"state",
|
||||||
][..],
|
][..],
|
||||||
|
|
@ -314,7 +336,7 @@ fn public_help_explains_commands_and_options() {
|
||||||
&[
|
&[
|
||||||
"Agent profile to use",
|
"Agent profile to use",
|
||||||
"Project directory to share",
|
"Project directory to share",
|
||||||
"Command and arguments to run",
|
"default `/bin/bash`",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
@ -347,11 +369,10 @@ fn public_help_explains_commands_and_options() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clap_rejects_missing_profile_command_and_ambiguous_reset_scope() {
|
fn clap_rejects_missing_profile_and_ambiguous_reset_scope() {
|
||||||
let workspace = PathBuf::from("/current");
|
let workspace = PathBuf::from("/current");
|
||||||
for arguments in [
|
for arguments in [
|
||||||
vec!["run", "--", "tool"],
|
vec!["run", "--", "tool"],
|
||||||
vec!["run", "--profile", "agent"],
|
|
||||||
vec!["state", "reset", "--profile", "agent"],
|
vec!["state", "reset", "--profile", "agent"],
|
||||||
vec![
|
vec![
|
||||||
"state",
|
"state",
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,15 @@ and retains the descriptor. The v1 Domain ID is SHA-256 over the
|
||||||
`lazaret-domain-id-v1` context followed by the selected profile name and
|
`lazaret-domain-id-v1` context followed by the selected profile name and
|
||||||
canonical workspace bytes, each framed by an unsigned 32-bit big-endian length.
|
canonical workspace bytes, each framed by an unsigned 32-bit big-endian length.
|
||||||
|
|
||||||
|
The primary developer launch form is `lazaret <profile>`. It selects the current
|
||||||
|
working directory as the Agent Workspace and starts `/bin/bash` as a new
|
||||||
|
interactive Client Session. `lazaret <profile> -- <command> [arguments...]`
|
||||||
|
replaces that default with the exact supplied argument vector. Host options may
|
||||||
|
select another workspace or configuration source. The explicit
|
||||||
|
`lazaret run --profile <profile>` form has the same default and remains an
|
||||||
|
equivalent compatibility interface. `state` is reserved as the maintenance
|
||||||
|
command name; a profile with that name is launched through the explicit form.
|
||||||
|
|
||||||
The explicit configuration path follows this refinement's strict host-path grammar.
|
The explicit configuration path follows this refinement's strict host-path grammar.
|
||||||
The loader resolves any declared symlink once to a canonical target and builds
|
The loader resolves any declared symlink once to a canonical target and builds
|
||||||
a no-symlink confined plan to that target. The source is limited to 1,048,576
|
a no-symlink confined plan to that target. The source is limited to 1,048,576
|
||||||
|
|
@ -1067,7 +1076,7 @@ not duplicated here.
|
||||||
|
|
||||||
| ID | Requirement |
|
| ID | Requirement |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
| **DX-1** | A developer shall be able to launch an Agent Profile in the current workspace with one command. |
|
| **DX-1** | `lazaret <profile>` shall launch the explicitly named Agent Profile in the current workspace and start `/bin/bash` as a new interactive Client Session. A trailing `-- <command> [arguments...]` shall replace that default with the exact supplied argument vector. |
|
||||||
| **DX-2** | Tools installed in configured persistent sandbox paths and dependencies stored in Ignored Workspace State shall remain available on later launches of the same Domain. |
|
| **DX-2** | Tools installed in configured persistent sandbox paths and dependencies stored in Ignored Workspace State shall remain available on later launches of the same Domain. |
|
||||||
| **DX-3** | Normal source editing, `git status`, staging, committing, and reference updates shall work from inside the Agent Sandbox. |
|
| **DX-3** | Normal source editing, `git status`, staging, committing, and reference updates shall work from inside the Agent Sandbox. |
|
||||||
| **DX-4** | Escalation prompts shall identify the requesting profile, session, command, and justification without mixing terminal input or output between sessions. |
|
| **DX-4** | Escalation prompts shall identify the requesting profile, session, command, and justification without mixing terminal input or output between sessions. |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue