Accept a list of profiles on the CLI and in the config
This commit is contained in:
+217
-20
@@ -1,5 +1,6 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use clap::Parser;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
@@ -122,7 +123,7 @@ fn build_profile_overrides_globals() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("relaxed".into()),
|
||||
profile: vec!["relaxed".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
@@ -142,7 +143,7 @@ fn build_cli_flag_overrides_profile() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("nonet".into()),
|
||||
profile: vec!["nonet".into()],
|
||||
unshare_net: true,
|
||||
..args_with_command()
|
||||
};
|
||||
@@ -267,7 +268,7 @@ fn build_cli_mode_overrides_profile() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("wl".into()),
|
||||
profile: vec!["wl".into()],
|
||||
blacklist: true,
|
||||
..args_with_command()
|
||||
};
|
||||
@@ -292,7 +293,7 @@ fn build_rw_paths_accumulate() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("extra".into()),
|
||||
profile: vec!["extra".into()],
|
||||
extra_rw: vec!["/var".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
@@ -316,7 +317,7 @@ fn build_command_from_profile() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("test".into()),
|
||||
profile: vec!["test".into()],
|
||||
..Args::default()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
@@ -337,7 +338,7 @@ fn build_cli_entrypoint_overrides_profile() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("test".into()),
|
||||
profile: vec!["test".into()],
|
||||
entrypoint: Some("/usr/bin/true".into()),
|
||||
..Args::default()
|
||||
};
|
||||
@@ -455,7 +456,7 @@ fn build_cli_profile_overrides_top_level_profile() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("other".into()),
|
||||
profile: vec!["other".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
@@ -480,7 +481,7 @@ fn build_missing_top_level_profile_errors() {
|
||||
#[test]
|
||||
fn build_missing_profile_errors() {
|
||||
let args = Args {
|
||||
profile: Some("nope".into()),
|
||||
profile: vec!["nope".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -516,7 +517,7 @@ fn inheritance_merges_parent_into_child() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("child".into()),
|
||||
profile: vec!["child".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
@@ -565,7 +566,7 @@ fn inheritance_walks_multi_level_chain() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("child".into()),
|
||||
profile: vec!["child".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
@@ -632,7 +633,7 @@ fn inheritance_cycle_errors() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("a".into()),
|
||||
profile: vec!["a".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -654,7 +655,7 @@ fn missing_parent_profile_errors() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("child".into()),
|
||||
profile: vec!["child".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -695,7 +696,7 @@ fn inheritance_chain_spans_extra_config() {
|
||||
|
||||
let file_config = FileConfig::load(&base_path).unwrap();
|
||||
let args = Args {
|
||||
profile: Some("child".into()),
|
||||
profile: vec!["child".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
@@ -940,7 +941,7 @@ fn build_env_accumulates_disjoint_keys() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("p".into()),
|
||||
profile: vec!["p".into()],
|
||||
env: vec!["D".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
@@ -973,7 +974,7 @@ fn build_env_later_tier_overrides_earlier() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("p".into()),
|
||||
profile: vec!["p".into()],
|
||||
env: vec!["A=cli".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
@@ -1058,7 +1059,7 @@ fn build_unsetenv_accumulates() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("p".into()),
|
||||
profile: vec!["p".into()],
|
||||
unsetenv: vec!["C".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
@@ -1184,11 +1185,11 @@ fn persistent_tmp_uses_leaf_profile_name() {
|
||||
};
|
||||
|
||||
let leaf_args = Args {
|
||||
profile: Some("leaf".into()),
|
||||
profile: vec!["leaf".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let parent_args = Args {
|
||||
profile: Some("leaf".into()),
|
||||
profile: vec!["leaf".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
// Same profile selected: same key.
|
||||
@@ -1211,7 +1212,7 @@ fn persistent_tmp_uses_leaf_profile_name() {
|
||||
.unwrap()
|
||||
.persistent_tmp = Some(true);
|
||||
let parent_direct = Args {
|
||||
profile: Some("parent".into()),
|
||||
profile: vec!["parent".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let key3 = build(parent_direct, Some(file_config2))
|
||||
@@ -1246,7 +1247,7 @@ fn build_mask_accumulates() {
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("extra".into()),
|
||||
profile: vec!["extra".into()],
|
||||
mask: vec![PathBuf::from("/tmp/c")],
|
||||
..args_with_command()
|
||||
};
|
||||
@@ -1619,6 +1620,202 @@ fn load_extra_relative_path_rejected() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_profile_splits_on_commas() {
|
||||
let args = Args::try_parse_from(["agent-sandbox", "--profile", "claude,gpu"]).unwrap();
|
||||
assert_eq!(args.profile, vec!["claude", "gpu"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_repeated_profiles_concatenate_in_order() {
|
||||
let args = Args::try_parse_from([
|
||||
"agent-sandbox",
|
||||
"--profile",
|
||||
"claude",
|
||||
"--profile=gpu,wayland",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(args.profile, vec!["claude", "gpu", "wayland"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_profile_accepts_a_list() {
|
||||
let config: FileConfig = toml::from_str(r#"profile = ["claude", "gpu"]"#).unwrap();
|
||||
assert_eq!(
|
||||
config.options.profile,
|
||||
Some(ProfileNames(vec!["claude".into(), "gpu".into()]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_profile_accepts_a_single_name() {
|
||||
let config: FileConfig = toml::from_str(r#"profile = "claude""#).unwrap();
|
||||
assert_eq!(config.options.profile, Some("claude".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_merges_selected_profiles_in_order() {
|
||||
let args = Args {
|
||||
profile: vec!["claude".into(), "gpu".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(claude_and_gpu_profiles())).unwrap();
|
||||
|
||||
assert!(!config.hardened);
|
||||
assert!(config.unshare_net);
|
||||
assert_eq!(
|
||||
config.env,
|
||||
vec![
|
||||
EnvEntry::Set("FROM_CLAUDE".into(), "1".into()),
|
||||
EnvEntry::Set("FROM_GPU".into(), "1".into()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_merges_top_level_profile_list() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
profile: Some(ProfileNames(vec!["claude".into(), "gpu".into()])),
|
||||
..Options::default()
|
||||
},
|
||||
..claude_and_gpu_profiles()
|
||||
};
|
||||
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||
|
||||
assert!(!config.hardened);
|
||||
assert!(config.unshare_net);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_cli_profile_list_replaces_top_level_list() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
profile: Some(ProfileNames(vec!["claude".into(), "gpu".into()])),
|
||||
..Options::default()
|
||||
},
|
||||
..claude_and_gpu_profiles()
|
||||
};
|
||||
let args = Args {
|
||||
profile: vec!["gpu".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.env,
|
||||
vec![EnvEntry::Set("FROM_GPU".into(), "1".into())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_shared_parent_merges_once() {
|
||||
let file_config = FileConfig {
|
||||
profiles: HashMap::from([
|
||||
(
|
||||
"base".into(),
|
||||
Options {
|
||||
ro: vec!["/etc".into()],
|
||||
..Options::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"claude".into(),
|
||||
Options {
|
||||
profile: Some("base".into()),
|
||||
..Options::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"gpu".into(),
|
||||
Options {
|
||||
profile: Some("base".into()),
|
||||
..Options::default()
|
||||
},
|
||||
),
|
||||
]),
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: vec!["claude".into(), "gpu".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
|
||||
assert_eq!(config.extra_ro, vec![same_bind(Path::new("/etc"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_profile_inherits_from_a_parent_list() {
|
||||
let mut file_config = claude_and_gpu_profiles();
|
||||
file_config.profiles.insert(
|
||||
"both".into(),
|
||||
Options {
|
||||
profile: Some(ProfileNames(vec!["claude".into(), "gpu".into()])),
|
||||
..Options::default()
|
||||
},
|
||||
);
|
||||
let args = Args {
|
||||
profile: vec!["both".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
|
||||
assert!(!config.hardened);
|
||||
assert!(config.unshare_net);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_tmp_key_differs_per_profile_list() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
persistent_tmp: Some(true),
|
||||
..Options::default()
|
||||
},
|
||||
..claude_and_gpu_profiles()
|
||||
};
|
||||
let key_for = |profiles: Vec<String>| {
|
||||
let args = Args {
|
||||
profile: profiles,
|
||||
..args_with_command()
|
||||
};
|
||||
build(args, Some(clone_file_config(&file_config)))
|
||||
.unwrap()
|
||||
.persistent_tmp_key
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
assert_ne!(
|
||||
key_for(vec!["claude".into()]),
|
||||
key_for(vec!["claude".into(), "gpu".into()])
|
||||
);
|
||||
}
|
||||
|
||||
fn claude_and_gpu_profiles() -> FileConfig {
|
||||
FileConfig {
|
||||
profiles: HashMap::from([
|
||||
(
|
||||
"claude".into(),
|
||||
Options {
|
||||
hardened: Some(true),
|
||||
env: vec!["FROM_CLAUDE=1".into()],
|
||||
..Options::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"gpu".into(),
|
||||
Options {
|
||||
hardened: Some(false),
|
||||
unshare_net: Some(true),
|
||||
env: vec!["FROM_GPU=1".into()],
|
||||
..Options::default()
|
||||
},
|
||||
),
|
||||
]),
|
||||
..FileConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_paths(actual: &[String], expected: &[&str]) {
|
||||
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
|
||||
assert_eq!(actual, &expected);
|
||||
|
||||
Reference in New Issue
Block a user