Allow setting entrypoint from CLI

This commit is contained in:
2026-04-07 18:02:03 +02:00
parent 83bd4305c7
commit 17f0e84005
3 changed files with 112 additions and 3 deletions

View File

@@ -62,6 +62,10 @@ pub struct Args {
#[arg(long = "bwrap-arg", value_name = "ARG", action = clap::ArgAction::Append)]
pub bwrap_args: Vec<String>,
/// Run this binary with the trailing args as its arguments
#[arg(long, value_name = "CMD")]
pub entrypoint: Option<String>,
/// Command and arguments to run inside the sandbox
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub command_and_args: Vec<OsString>,

View File

@@ -21,7 +21,8 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
globals.validate_paths()?;
profile.validate_paths()?;
let (command, command_args) = resolve_command(args.command_and_args, &profile, &globals)?;
let (command, command_args) =
resolve_command(args.entrypoint, args.command_and_args, &profile, &globals)?;
let command = resolve_binary(&command)
.ok_or_else(|| SandboxError::CommandNotFound(PathBuf::from(&command)))?;
@@ -124,11 +125,15 @@ fn merge_vecs<T: Clone>(cli: Vec<T>, profile: &[T], globals: &[T]) -> Vec<T> {
}
fn resolve_command(
cli_entrypoint: Option<String>,
mut passthrough_args: Vec<OsString>,
profile: &Options,
globals: &Options,
) -> Result<(OsString, Vec<OsString>), SandboxError> {
let entrypoint = profile.entrypoint.clone().or(globals.entrypoint.clone());
let entrypoint = cli_entrypoint
.map(CommandValue::Simple)
.or_else(|| profile.entrypoint.clone())
.or_else(|| globals.entrypoint.clone());
let command = profile.command.clone().or(globals.command.clone());
if let Some(ep) = entrypoint {
@@ -568,6 +573,67 @@ mod tests {
assert_eq!(config.command_args, vec![OsString::from("--flag")]);
}
#[test]
fn build_cli_entrypoint_overrides_profile() {
let file_config = FileConfig {
profile: HashMap::from([(
"test".into(),
Options {
entrypoint: Some(CommandValue::Simple("/usr/bin/false".into())),
..Options::default()
},
)]),
..FileConfig::default()
};
let args = Args {
profile: Some("test".into()),
entrypoint: Some("/usr/bin/true".into()),
..Args::default()
};
let config = build(args, Some(file_config)).unwrap();
assert_eq!(config.command, PathBuf::from("/usr/bin/true"));
assert!(config.command_args.is_empty());
}
#[test]
fn build_cli_entrypoint_combines_with_passthrough_args() {
let args = Args {
entrypoint: Some("/usr/bin/true".into()),
command_and_args: vec!["--flag".into(), "value".into()],
..Args::default()
};
let config = build(args, None).unwrap();
assert_eq!(config.command, PathBuf::from("/usr/bin/true"));
assert_eq!(
config.command_args,
vec![OsString::from("--flag"), OsString::from("value")]
);
}
#[test]
fn build_cli_entrypoint_falls_back_to_config_command_defaults() {
let file_config = FileConfig {
options: Options {
command: Some(CommandValue::WithArgs(vec![
"--default".into(),
"args".into(),
])),
..Options::default()
},
..FileConfig::default()
};
let args = Args {
entrypoint: Some("/usr/bin/true".into()),
..Args::default()
};
let config = build(args, Some(file_config)).unwrap();
assert_eq!(config.command, PathBuf::from("/usr/bin/true"));
assert_eq!(
config.command_args,
vec![OsString::from("--default"), OsString::from("args")]
);
}
#[test]
fn build_cli_command_overrides_config() {
let file_config = FileConfig {
@@ -754,6 +820,7 @@ mod tests {
#[test]
fn entrypoint_with_passthrough_args() {
let (cmd, args) = resolve_command(
None,
vec!["--verbose".into(), "--model".into(), "opus".into()],
&Options {
entrypoint: Some(CommandValue::WithArgs(vec![
@@ -784,6 +851,7 @@ mod tests {
#[test]
fn entrypoint_without_passthrough_uses_command_defaults() {
let (cmd, args) = resolve_command(
None,
vec![],
&Options {
entrypoint: Some(CommandValue::Simple("claude".into())),
@@ -809,6 +877,7 @@ mod tests {
#[test]
fn entrypoint_without_passthrough_or_command() {
let (cmd, args) = resolve_command(
None,
vec![],
&Options {
entrypoint: Some(CommandValue::WithArgs(vec![
@@ -827,6 +896,7 @@ mod tests {
#[test]
fn profile_entrypoint_overrides_global() {
let (cmd, _) = resolve_command(
None,
vec![],
&Options {
entrypoint: Some(CommandValue::Simple("claude".into())),
@@ -844,6 +914,7 @@ mod tests {
#[test]
fn global_entrypoint_with_profile_command() {
let (cmd, args) = resolve_command(
None,
vec![],
&Options {
command: Some(CommandValue::WithArgs(vec![
@@ -869,7 +940,7 @@ mod tests {
}
#[test]
fn no_command_errors() {
let result = resolve_command(vec![], &Options::default(), &Options::default());
let result = resolve_command(None, vec![], &Options::default(), &Options::default());
assert!(matches!(result, Err(SandboxError::NoCommand)));
}

View File

@@ -755,6 +755,40 @@ fn config_entrypoint_alone_without_command_or_passthrough() {
);
}
#[test]
fn cli_entrypoint_appends_passthrough_args() {
let output = sandbox(&["--entrypoint", "bash"])
.args(["--", "-c", "echo cli-entrypoint-works"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "cli-entrypoint-works",
"expected --entrypoint to receive trailing args, got: {stdout}"
);
}
#[test]
fn cli_entrypoint_overrides_config_entrypoint() {
let cfg = ConfigFile::new(
r#"
entrypoint = ["/bin/false"]
"#,
);
let output = sandbox_withconfig(&["--config", &cfg, "--entrypoint", "bash"])
.args(["--", "-c", "echo override-works"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "override-works",
"expected CLI --entrypoint to override config entrypoint, got: {stdout}"
);
}
#[test]
fn config_command_alone_without_passthrough() {
let cfg = ConfigFile::new(