Allow setting entrypoint from CLI
This commit is contained in:
@@ -62,6 +62,10 @@ pub struct Args {
|
|||||||
#[arg(long = "bwrap-arg", value_name = "ARG", action = clap::ArgAction::Append)]
|
#[arg(long = "bwrap-arg", value_name = "ARG", action = clap::ArgAction::Append)]
|
||||||
pub bwrap_args: Vec<String>,
|
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
|
/// Command and arguments to run inside the sandbox
|
||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
pub command_and_args: Vec<OsString>,
|
pub command_and_args: Vec<OsString>,
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
|||||||
globals.validate_paths()?;
|
globals.validate_paths()?;
|
||||||
profile.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)
|
let command = resolve_binary(&command)
|
||||||
.ok_or_else(|| SandboxError::CommandNotFound(PathBuf::from(&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(
|
fn resolve_command(
|
||||||
|
cli_entrypoint: Option<String>,
|
||||||
mut passthrough_args: Vec<OsString>,
|
mut passthrough_args: Vec<OsString>,
|
||||||
profile: &Options,
|
profile: &Options,
|
||||||
globals: &Options,
|
globals: &Options,
|
||||||
) -> Result<(OsString, Vec<OsString>), SandboxError> {
|
) -> 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());
|
let command = profile.command.clone().or(globals.command.clone());
|
||||||
|
|
||||||
if let Some(ep) = entrypoint {
|
if let Some(ep) = entrypoint {
|
||||||
@@ -568,6 +573,67 @@ mod tests {
|
|||||||
assert_eq!(config.command_args, vec![OsString::from("--flag")]);
|
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]
|
#[test]
|
||||||
fn build_cli_command_overrides_config() {
|
fn build_cli_command_overrides_config() {
|
||||||
let file_config = FileConfig {
|
let file_config = FileConfig {
|
||||||
@@ -754,6 +820,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn entrypoint_with_passthrough_args() {
|
fn entrypoint_with_passthrough_args() {
|
||||||
let (cmd, args) = resolve_command(
|
let (cmd, args) = resolve_command(
|
||||||
|
None,
|
||||||
vec!["--verbose".into(), "--model".into(), "opus".into()],
|
vec!["--verbose".into(), "--model".into(), "opus".into()],
|
||||||
&Options {
|
&Options {
|
||||||
entrypoint: Some(CommandValue::WithArgs(vec![
|
entrypoint: Some(CommandValue::WithArgs(vec![
|
||||||
@@ -784,6 +851,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn entrypoint_without_passthrough_uses_command_defaults() {
|
fn entrypoint_without_passthrough_uses_command_defaults() {
|
||||||
let (cmd, args) = resolve_command(
|
let (cmd, args) = resolve_command(
|
||||||
|
None,
|
||||||
vec![],
|
vec![],
|
||||||
&Options {
|
&Options {
|
||||||
entrypoint: Some(CommandValue::Simple("claude".into())),
|
entrypoint: Some(CommandValue::Simple("claude".into())),
|
||||||
@@ -809,6 +877,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn entrypoint_without_passthrough_or_command() {
|
fn entrypoint_without_passthrough_or_command() {
|
||||||
let (cmd, args) = resolve_command(
|
let (cmd, args) = resolve_command(
|
||||||
|
None,
|
||||||
vec![],
|
vec![],
|
||||||
&Options {
|
&Options {
|
||||||
entrypoint: Some(CommandValue::WithArgs(vec![
|
entrypoint: Some(CommandValue::WithArgs(vec![
|
||||||
@@ -827,6 +896,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn profile_entrypoint_overrides_global() {
|
fn profile_entrypoint_overrides_global() {
|
||||||
let (cmd, _) = resolve_command(
|
let (cmd, _) = resolve_command(
|
||||||
|
None,
|
||||||
vec![],
|
vec![],
|
||||||
&Options {
|
&Options {
|
||||||
entrypoint: Some(CommandValue::Simple("claude".into())),
|
entrypoint: Some(CommandValue::Simple("claude".into())),
|
||||||
@@ -844,6 +914,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn global_entrypoint_with_profile_command() {
|
fn global_entrypoint_with_profile_command() {
|
||||||
let (cmd, args) = resolve_command(
|
let (cmd, args) = resolve_command(
|
||||||
|
None,
|
||||||
vec![],
|
vec![],
|
||||||
&Options {
|
&Options {
|
||||||
command: Some(CommandValue::WithArgs(vec![
|
command: Some(CommandValue::WithArgs(vec![
|
||||||
@@ -869,7 +940,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn no_command_errors() {
|
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)));
|
assert!(matches!(result, Err(SandboxError::NoCommand)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn config_command_alone_without_passthrough() {
|
fn config_command_alone_without_passthrough() {
|
||||||
let cfg = ConfigFile::new(
|
let cfg = ConfigFile::new(
|
||||||
|
|||||||
Reference in New Issue
Block a user