Add entrypoint option

This commit is contained in:
2026-04-04 10:16:57 +02:00
parent 8ecba5d6dc
commit 062ddab5f8
2 changed files with 278 additions and 24 deletions

View File

@@ -21,11 +21,7 @@ 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.command.clone(),
globals.command.clone(),
);
let (command, command_args) = resolve_command(args.command_and_args, &profile, &globals);
let command = resolve_binary(&command)
.ok_or_else(|| SandboxError::CommandNotFound(PathBuf::from(&command)))?;
@@ -119,20 +115,31 @@ fn merge_vecs<T: Clone>(cli: Vec<T>, profile: &[T], globals: &[T]) -> Vec<T> {
}
fn resolve_command(
mut positional: Vec<OsString>,
profile_cmd: Option<CommandValue>,
globals_cmd: Option<CommandValue>,
mut passthrough_args: Vec<OsString>,
profile: &Options,
globals: &Options,
) -> (OsString, Vec<OsString>) {
if !positional.is_empty() {
let cmd = positional.remove(0);
return (cmd, positional);
}
if let Some(config_cmd) = profile_cmd.or(globals_cmd) {
let parts = config_cmd.into_vec();
let cmd = OsString::from(&parts[0]);
let args = parts[1..].iter().map(OsString::from).collect();
let entrypoint = profile.entrypoint.clone().or(globals.entrypoint.clone());
let command = profile.command.clone().or(globals.command.clone());
if let Some(ep) = entrypoint {
let (cmd, mut args) = ep.into_binary_and_args();
if !passthrough_args.is_empty() {
args.extend(passthrough_args);
} else if let Some(default_cmd) = command {
args.extend(default_cmd.into_vec().into_iter().map(OsString::from));
}
return (cmd, args);
}
if !passthrough_args.is_empty() {
return (passthrough_args.remove(0), passthrough_args);
}
if let Some(config_cmd) = command {
return config_cmd.into_binary_and_args();
}
if let Ok(cmd) = std::env::var("SANDBOX_CMD") {
return (OsString::from(cmd), vec![]);
}
@@ -210,6 +217,7 @@ pub struct Options {
pub whitelist: Option<bool>,
pub hardened: Option<bool>,
pub no_net: Option<bool>,
pub entrypoint: Option<CommandValue>,
pub command: Option<CommandValue>,
pub dry_run: Option<bool>,
pub chdir: Option<PathBuf>,
@@ -261,6 +269,13 @@ impl CommandValue {
Self::WithArgs(v) => v,
}
}
fn into_binary_and_args(self) -> (OsString, Vec<OsString>) {
let mut parts = self.into_vec();
let binary = OsString::from(parts.remove(0));
let args = parts.into_iter().map(OsString::from).collect();
(binary, args)
}
}
pub fn find_config_path(explicit: Option<&Path>) -> Option<PathBuf> {
@@ -334,6 +349,7 @@ mod tests {
[profile.claude]
rw = ["/home/user/.config/claude"]
ro = ["/etc/claude", "/etc/shared"]
entrypoint = ["claude", "--dangerously-skip-permissions"]
command = ["bash", "-c", "echo hi"]
[profile.codex]
@@ -380,6 +396,14 @@ mod tests {
);
}
#[test]
fn profile_entrypoint_with_args() {
assert_command(
&CONFIG.profile["claude"].entrypoint,
&["claude", "--dangerously-skip-permissions"],
);
}
#[test]
fn profile_command_with_args() {
assert_command(
@@ -675,6 +699,117 @@ mod tests {
));
}
#[test]
fn entrypoint_with_passthrough_args() {
let (cmd, args) = resolve_command(
vec!["--verbose".into(), "--model".into(), "opus".into()],
&Options {
entrypoint: Some(CommandValue::WithArgs(vec![
"claude".into(),
"--dangerously-skip-permissions".into(),
])),
command: Some(CommandValue::Simple("--default-flag".into())),
..Options::default()
},
&Options::default(),
);
assert_eq!(cmd, "claude");
assert_eq!(
args,
vec![
"--dangerously-skip-permissions",
"--verbose",
"--model",
"opus"
]
.into_iter()
.map(OsString::from)
.collect::<Vec<_>>()
);
}
#[test]
fn entrypoint_without_passthrough_uses_command_defaults() {
let (cmd, args) = resolve_command(
vec![],
&Options {
entrypoint: Some(CommandValue::Simple("claude".into())),
command: Some(CommandValue::WithArgs(vec![
"--dangerously-skip-permissions".into(),
"--verbose".into(),
])),
..Options::default()
},
&Options::default(),
);
assert_eq!(cmd, "claude");
assert_eq!(
args,
vec!["--dangerously-skip-permissions", "--verbose"]
.into_iter()
.map(OsString::from)
.collect::<Vec<_>>()
);
}
#[test]
fn entrypoint_without_passthrough_or_command() {
let (cmd, args) = resolve_command(
vec![],
&Options {
entrypoint: Some(CommandValue::WithArgs(vec![
"claude".into(),
"--dangerously-skip-permissions".into(),
])),
..Options::default()
},
&Options::default(),
);
assert_eq!(cmd, "claude");
assert_eq!(args, vec![OsString::from("--dangerously-skip-permissions")]);
}
#[test]
fn profile_entrypoint_overrides_global() {
let (cmd, _) = resolve_command(
vec![],
&Options {
entrypoint: Some(CommandValue::Simple("claude".into())),
..Options::default()
},
&Options {
entrypoint: Some(CommandValue::Simple("codex".into())),
..Options::default()
},
);
assert_eq!(cmd, "claude");
}
#[test]
fn global_entrypoint_with_profile_command() {
let (cmd, args) = resolve_command(
vec![],
&Options {
command: Some(CommandValue::WithArgs(vec![
"--dangerously-skip-permissions".into(),
"--verbose".into(),
])),
..Options::default()
},
&Options {
entrypoint: Some(CommandValue::Simple("claude".into())),
..Options::default()
},
);
assert_eq!(cmd, "claude");
assert_eq!(
args,
vec!["--dangerously-skip-permissions", "--verbose"]
.into_iter()
.map(OsString::from)
.collect::<Vec<_>>()
);
}
fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
assert_eq!(actual, &expected);