2026-07-15 00:43:42 +09:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
2026-07-18 05:47:15 +09:00
|
|
|
use std::sync::{Arc, Barrier};
|
2026-07-15 00:43:42 +09:00
|
|
|
|
|
|
|
|
use lazaret_core::codec::{CodecError, decode, encode};
|
|
|
|
|
use lazaret_core::configuration::{
|
|
|
|
|
ConfigurationErrorKind, ConfigurationLoadRequest, ConfinedPathPlan, ConfinedPathType,
|
|
|
|
|
ResolvedDomainConfiguration, SandboxLayoutV1, derive_configuration_fingerprint,
|
|
|
|
|
derive_domain_id, load_trusted_configuration, parse_trusted_configuration,
|
|
|
|
|
};
|
|
|
|
|
use lazaret_core::identity::HostPath;
|
|
|
|
|
use lazaret_core::records::{
|
2026-07-24 23:34:02 +09:00
|
|
|
ConfigurationFingerprintRecordV1, DEFAULT_RUNSC_DCACHE, EnvironmentReference, MAX_RUNSC_DCACHE,
|
|
|
|
|
ResolvedAutoApprovalRule, ResolvedVolume, VolumeAccess, VolumeKind,
|
2026-07-15 00:43:42 +09:00
|
|
|
};
|
|
|
|
|
use static_assertions::assert_not_impl_any;
|
|
|
|
|
|
|
|
|
|
assert_not_impl_any!(ResolvedDomainConfiguration: serde::Serialize, serde::de::DeserializeOwned);
|
|
|
|
|
assert_not_impl_any!(SandboxLayoutV1: serde::Serialize, serde::de::DeserializeOwned);
|
|
|
|
|
assert_not_impl_any!(ConfinedPathPlan: serde::Serialize, serde::de::DeserializeOwned);
|
|
|
|
|
|
|
|
|
|
static NEXT_TREE: AtomicU64 = AtomicU64::new(0);
|
|
|
|
|
|
|
|
|
|
struct TestTree(PathBuf);
|
|
|
|
|
|
|
|
|
|
impl TestTree {
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
let sequence = NEXT_TREE.fetch_add(1, Ordering::Relaxed);
|
2026-07-19 05:12:57 +09:00
|
|
|
let parent = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
|
|
|
.join("../../target/test-artifacts/configuration");
|
|
|
|
|
fs::create_dir_all(&parent).unwrap();
|
|
|
|
|
let parent = parent.canonicalize().unwrap();
|
|
|
|
|
let path = parent.join(format!(
|
2026-07-15 00:43:42 +09:00
|
|
|
"lazaret-configuration-{}-{sequence}",
|
|
|
|
|
std::process::id()
|
|
|
|
|
));
|
|
|
|
|
fs::create_dir(&path).unwrap();
|
|
|
|
|
Self(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn path(&self) -> &Path {
|
|
|
|
|
&self.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for TestTree {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
fs::remove_dir_all(&self.0).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn write_configuration(tree: &TestTree, body: &str) -> PathBuf {
|
|
|
|
|
let directory = tree.path().join("config");
|
|
|
|
|
fs::create_dir(&directory).unwrap();
|
|
|
|
|
let path = directory.join("config.toml");
|
|
|
|
|
fs::write(&path, body).unwrap();
|
|
|
|
|
path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn layout() -> SandboxLayoutV1 {
|
2026-07-18 22:35:48 +09:00
|
|
|
SandboxLayoutV1::from_protected_destinations([
|
2026-07-15 00:43:42 +09:00
|
|
|
"/workspace".to_owned(),
|
|
|
|
|
"/proc".to_owned(),
|
|
|
|
|
"/sys".to_owned(),
|
|
|
|
|
"/dev".to_owned(),
|
|
|
|
|
"/run/lazaret".to_owned(),
|
|
|
|
|
])
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request(
|
|
|
|
|
tree: &TestTree,
|
|
|
|
|
config: PathBuf,
|
|
|
|
|
environment: BTreeMap<String, std::ffi::OsString>,
|
2026-07-22 03:07:42 +09:00
|
|
|
) -> ConfigurationLoadRequest {
|
|
|
|
|
request_with_layout(tree, config, environment, layout())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request_with_layout(
|
|
|
|
|
tree: &TestTree,
|
|
|
|
|
config: PathBuf,
|
|
|
|
|
environment: BTreeMap<String, std::ffi::OsString>,
|
|
|
|
|
sandbox_layout: SandboxLayoutV1,
|
2026-07-15 00:43:42 +09:00
|
|
|
) -> ConfigurationLoadRequest {
|
|
|
|
|
let workspace = tree.path().join("workspace");
|
|
|
|
|
let runtime = tree.path().join("runtime");
|
|
|
|
|
fs::create_dir_all(&workspace).unwrap();
|
|
|
|
|
fs::create_dir_all(&runtime).unwrap();
|
|
|
|
|
ConfigurationLoadRequest {
|
|
|
|
|
explicit_configuration_path: Some(config),
|
|
|
|
|
selected_profile: "default".into(),
|
|
|
|
|
workspace,
|
|
|
|
|
home: tree.path().join("home"),
|
|
|
|
|
xdg_config_home: None,
|
|
|
|
|
xdg_data_home: None,
|
|
|
|
|
xdg_runtime_dir: runtime,
|
|
|
|
|
environment,
|
2026-07-22 03:07:42 +09:00
|
|
|
sandbox_layout,
|
2026-07-15 00:43:42 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn valid_configuration(tree: &TestTree) -> String {
|
|
|
|
|
format!(
|
|
|
|
|
r#"schema = "lazaret-trusted-host-config-v1"
|
|
|
|
|
|
|
|
|
|
[storage]
|
|
|
|
|
data_root = "{}"
|
|
|
|
|
|
|
|
|
|
[network]
|
|
|
|
|
default_allow = ["192.0.2.4/24", "198.51.100.53/32"]
|
|
|
|
|
|
|
|
|
|
[profiles.default]
|
|
|
|
|
image = "example.invalid/agent:v1"
|
|
|
|
|
persistent_dirs = ["/root"]
|
|
|
|
|
allow_egress = ["10.1.2.3/24"]
|
|
|
|
|
env = {{ TOKEN = "env:HOST_TOKEN" }}
|
|
|
|
|
volumes = [
|
|
|
|
|
{{ host = "{{profile_dir}}/agent-state", container = "/root/.local/state/agent", kind = "directory", access = "read-write", create = true }},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
[profiles.default.escalation]
|
|
|
|
|
enabled = false
|
|
|
|
|
"#,
|
|
|
|
|
tree.path().join("data").display()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn strict_parser_applies_defaults_and_rejects_every_unknown_level() {
|
|
|
|
|
let minimal = r#"schema = "lazaret-trusted-host-config-v1"
|
|
|
|
|
[profiles.default]
|
|
|
|
|
image = "example.invalid/agent:v1"
|
|
|
|
|
"#;
|
|
|
|
|
let parsed = parse_trusted_configuration(minimal.as_bytes()).unwrap();
|
|
|
|
|
let profile = &parsed.profiles()["default"];
|
|
|
|
|
assert!(profile.isolate_gitignored);
|
2026-07-22 03:23:38 +09:00
|
|
|
assert_eq!(
|
|
|
|
|
profile.persistent_dirs,
|
|
|
|
|
["/root", "/usr", "/var", "/etc", "/opt"]
|
|
|
|
|
);
|
2026-07-15 00:43:42 +09:00
|
|
|
assert!(profile.allow_egress.is_empty());
|
|
|
|
|
assert!(profile.deny_egress.is_empty());
|
|
|
|
|
assert!(profile.env.is_empty());
|
|
|
|
|
assert!(profile.volumes.is_empty());
|
|
|
|
|
assert!(!profile.escalation.enabled);
|
|
|
|
|
assert!(profile.escalation.auto_approve.is_empty());
|
|
|
|
|
assert!(parsed.storage().data_root.is_none());
|
|
|
|
|
assert!(parsed.network().default_allow.is_empty());
|
|
|
|
|
assert!(parsed.network().default_deny.is_empty());
|
2026-07-24 23:34:02 +09:00
|
|
|
assert_eq!(parsed.runtime().runsc_dcache, DEFAULT_RUNSC_DCACHE);
|
2026-07-15 00:43:42 +09:00
|
|
|
|
2026-07-22 03:23:38 +09:00
|
|
|
for (value, expected) in [
|
|
|
|
|
("[]", Vec::<String>::new()),
|
|
|
|
|
("[\"/nix\"]", vec!["/nix".to_owned()]),
|
|
|
|
|
] {
|
|
|
|
|
let explicit = minimal.replace(
|
|
|
|
|
"image = \"example.invalid/agent:v1\"",
|
|
|
|
|
&format!("image = \"example.invalid/agent:v1\"\npersistent_dirs = {value}"),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_trusted_configuration(explicit.as_bytes())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.profiles()["default"]
|
|
|
|
|
.persistent_dirs,
|
|
|
|
|
expected
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 00:43:42 +09:00
|
|
|
for invalid in [
|
|
|
|
|
minimal.replace("schema", "unknown"),
|
|
|
|
|
format!("{minimal}unknown = true\n"),
|
|
|
|
|
minimal.replace(
|
|
|
|
|
"image = \"example.invalid/agent:v1\"",
|
|
|
|
|
"image = \"example.invalid/agent:v1\"\nunknown = true",
|
|
|
|
|
),
|
|
|
|
|
minimal.replace(
|
|
|
|
|
"example.invalid/agent:v1",
|
|
|
|
|
"https://example.invalid/agent:v1",
|
|
|
|
|
),
|
|
|
|
|
minimal.replace("example.invalid/agent:v1", ""),
|
|
|
|
|
minimal.replace("-v1", "-v2"),
|
|
|
|
|
] {
|
|
|
|
|
assert!(parse_trusted_configuration(invalid.as_bytes()).is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let duplicate = format!("{minimal}schema = \"lazaret-trusted-host-config-v1\"\n");
|
|
|
|
|
assert!(parse_trusted_configuration(duplicate.as_bytes()).is_err());
|
|
|
|
|
|
|
|
|
|
for invalid in [
|
|
|
|
|
format!("{minimal}\n[storage]\nunknown = true\n"),
|
|
|
|
|
format!("{minimal}\n[network]\nunknown = true\n"),
|
|
|
|
|
r#"schema = "lazaret-trusted-host-config-v1"
|
|
|
|
|
[profiles.default]
|
|
|
|
|
image = "example.invalid/agent:v1"
|
|
|
|
|
volumes = [{ host = "/source", container = "/root/source", kind = "directory", unknown = true }]
|
|
|
|
|
"#
|
|
|
|
|
.into(),
|
|
|
|
|
format!("{minimal}\n[profiles.default.escalation]\nunknown = true\n"),
|
|
|
|
|
format!(
|
|
|
|
|
"{minimal}\n[profiles.default.escalation]\nenabled = true\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"/bin/true\"\nunknown = true\n"
|
|
|
|
|
),
|
|
|
|
|
r#"schema = "lazaret-trusted-host-config-v1"
|
|
|
|
|
[profiles.default]
|
|
|
|
|
image = "example.invalid/agent:v1"
|
|
|
|
|
volumes = [{ host = "/source", container = "/root/source", kind = "socket" }]
|
|
|
|
|
"#
|
|
|
|
|
.into(),
|
|
|
|
|
] {
|
|
|
|
|
assert!(parse_trusted_configuration(invalid.as_bytes()).is_err());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 23:34:02 +09:00
|
|
|
#[test]
|
|
|
|
|
fn runtime_dcache_defaults_accepts_budget_bounds_and_rejects_excess() {
|
|
|
|
|
let minimal = r#"schema = "lazaret-trusted-host-config-v1"
|
|
|
|
|
[profiles.default]
|
|
|
|
|
image = "example.invalid/agent:v1"
|
|
|
|
|
"#;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_trusted_configuration(minimal.as_bytes())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.runtime()
|
|
|
|
|
.runsc_dcache,
|
|
|
|
|
DEFAULT_RUNSC_DCACHE
|
|
|
|
|
);
|
|
|
|
|
for value in [0, MAX_RUNSC_DCACHE] {
|
|
|
|
|
let explicit = minimal.replace(
|
|
|
|
|
"[profiles.default]",
|
|
|
|
|
&format!("[runtime]\nrunsc_dcache = {value}\n[profiles.default]"),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_trusted_configuration(explicit.as_bytes())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.runtime()
|
|
|
|
|
.runsc_dcache,
|
|
|
|
|
value
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let excessive = minimal.replace(
|
|
|
|
|
"[profiles.default]",
|
|
|
|
|
&format!(
|
|
|
|
|
"[runtime]\nrunsc_dcache = {}\n[profiles.default]",
|
|
|
|
|
MAX_RUNSC_DCACHE + 1
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_trusted_configuration(excessive.as_bytes())
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.kind(),
|
|
|
|
|
ConfigurationErrorKind::InvalidRuntime
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 00:43:42 +09:00
|
|
|
#[test]
|
|
|
|
|
fn sandbox_layout_is_closed_normalized_and_requires_workspace() {
|
2026-07-18 22:35:48 +09:00
|
|
|
assert!(SandboxLayoutV1::from_protected_destinations(["/proc".into()]).is_err());
|
2026-07-15 00:43:42 +09:00
|
|
|
assert!(
|
2026-07-18 22:35:48 +09:00
|
|
|
SandboxLayoutV1::from_protected_destinations(["/workspace".into(), "/workspace".into()])
|
2026-07-15 00:43:42 +09:00
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
for invalid in ["workspace", "/", "/workspace/", "/workspace/../control"] {
|
|
|
|
|
assert!(
|
2026-07-18 22:35:48 +09:00
|
|
|
SandboxLayoutV1::from_protected_destinations(["/workspace".into(), invalid.into(),])
|
2026-07-15 00:43:42 +09:00
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-22 03:07:42 +09:00
|
|
|
assert!(
|
|
|
|
|
SandboxLayoutV1::from_destination_policy(["/workspace".into()], ["/workspace".into()])
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn persistent_parents_allow_only_classified_transient_children() {
|
|
|
|
|
let layout = SandboxLayoutV1::from_destination_policy(
|
|
|
|
|
[
|
|
|
|
|
"/workspace".into(),
|
|
|
|
|
"/run/lazaret".into(),
|
|
|
|
|
"/proc".into(),
|
|
|
|
|
"/sys".into(),
|
|
|
|
|
"/dev".into(),
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
"/etc/resolv.conf".into(),
|
|
|
|
|
"/etc/hosts".into(),
|
|
|
|
|
"/etc/hostname".into(),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let body = valid_configuration(&tree).replace(
|
|
|
|
|
"persistent_dirs = [\"/root\"]",
|
|
|
|
|
"persistent_dirs = [\"/root\", \"/usr\", \"/var\", \"/etc\", \"/opt\"]",
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&tree, &body);
|
|
|
|
|
let environment = BTreeMap::from([("HOST_TOKEN".into(), "value".into())]);
|
|
|
|
|
let resolved =
|
|
|
|
|
load_trusted_configuration(request_with_layout(&tree, config, environment, layout))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolved.fingerprint_record().persistent_dirs,
|
|
|
|
|
["/root", "/usr", "/var", "/etc", "/opt"]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for persistent in ["/run", "/run/lazaret/state", "/etc/hosts"] {
|
|
|
|
|
let case = TestTree::new();
|
|
|
|
|
let body = valid_configuration(&case).replace(
|
|
|
|
|
"persistent_dirs = [\"/root\"]",
|
|
|
|
|
&format!("persistent_dirs = [\"{persistent}\"]"),
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&case, &body);
|
|
|
|
|
let environment = BTreeMap::from([("HOST_TOKEN".into(), "value".into())]);
|
|
|
|
|
let layout = SandboxLayoutV1::from_destination_policy(
|
|
|
|
|
["/workspace".into(), "/run/lazaret".into()],
|
|
|
|
|
["/etc/hosts".into()],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
load_trusted_configuration(request_with_layout(&case, config, environment, layout,))
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.kind(),
|
|
|
|
|
ConfigurationErrorKind::PathConflict
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-15 00:43:42 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn repository_example_loads_with_ipv4_normalization_and_private_denies() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let config = write_configuration(&tree, include_str!("../../../docs/example.toml"));
|
|
|
|
|
let resolved = load_trusted_configuration(request(&tree, config, BTreeMap::new())).unwrap();
|
|
|
|
|
assert_eq!(resolved.selected_profile(), "default");
|
2026-07-22 03:23:38 +09:00
|
|
|
assert_eq!(
|
|
|
|
|
resolved.fingerprint_record().persistent_dirs,
|
|
|
|
|
["/root", "/usr", "/var", "/etc", "/opt"]
|
|
|
|
|
);
|
2026-07-15 00:43:42 +09:00
|
|
|
assert!(
|
|
|
|
|
resolved
|
|
|
|
|
.effective_allow_prefixes()
|
|
|
|
|
.iter()
|
2026-07-15 02:49:39 +09:00
|
|
|
.any(|prefix| prefix.address() == [192, 0, 2, 53] && prefix.prefix_length() == 32)
|
2026-07-15 00:43:42 +09:00
|
|
|
);
|
|
|
|
|
for (address, length) in [
|
|
|
|
|
([10, 0, 0, 0], 8),
|
|
|
|
|
([172, 16, 0, 0], 12),
|
|
|
|
|
([192, 168, 0, 0], 16),
|
|
|
|
|
] {
|
|
|
|
|
assert!(
|
|
|
|
|
resolved
|
|
|
|
|
.effective_deny_prefixes()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|prefix| prefix.address() == address && prefix.prefix_length() == length)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn explicit_tilde_configuration_path_expands_only_against_home() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let home = tree.path().join("home");
|
|
|
|
|
fs::create_dir(&home).unwrap();
|
|
|
|
|
fs::set_permissions(&home, fs::Permissions::from_mode(0o700)).unwrap();
|
|
|
|
|
fs::write(home.join("trusted.toml"), valid_configuration(&tree)).unwrap();
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let resolved =
|
|
|
|
|
load_trusted_configuration(request(&tree, PathBuf::from("~/trusted.toml"), environment))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(resolved.selected_profile(), "default");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn nonselected_profiles_are_decoded_but_do_not_resolve_host_authority() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let body = format!(
|
|
|
|
|
"{}\n[profiles.other]\nimage = \"example.invalid/other:v1\"\nenv = {{ SECRET = \"env:UNSET\" }}\nvolumes = [{{ host = \"/does/not/exist\", container = \"/root/missing\", kind = \"directory\" }}]\n",
|
|
|
|
|
valid_configuration(&tree)
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
assert!(load_trusted_configuration(request(&tree, config, environment)).is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn nonselected_profiles_cannot_hide_invalid_approval_semantics() {
|
|
|
|
|
for other_profile in [
|
|
|
|
|
r#"[profiles.other]
|
|
|
|
|
image = "example.invalid/other:v1"
|
|
|
|
|
[[profiles.other.escalation.auto_approve]]
|
|
|
|
|
executable = "/bin/true"
|
|
|
|
|
"#,
|
|
|
|
|
r#"[profiles.other]
|
|
|
|
|
image = "example.invalid/other:v1"
|
|
|
|
|
[profiles.other.escalation]
|
|
|
|
|
enabled = true
|
|
|
|
|
[[profiles.other.escalation.auto_approve]]
|
|
|
|
|
executable = "/bin/true"
|
|
|
|
|
allow_extra_args = true
|
|
|
|
|
"#,
|
|
|
|
|
] {
|
|
|
|
|
let document = format!(
|
|
|
|
|
"schema = \"lazaret-trusted-host-config-v1\"\n[profiles.default]\nimage = \"example.invalid/default:v1\"\n{other_profile}"
|
|
|
|
|
);
|
|
|
|
|
let error = parse_trusted_configuration(document.as_bytes()).unwrap_err();
|
|
|
|
|
assert_eq!(error.kind(), ConfigurationErrorKind::InvalidAutoApproval);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 22:39:31 +09:00
|
|
|
#[test]
|
|
|
|
|
fn profiles_cannot_claim_the_session_identity_environment_name() {
|
|
|
|
|
let document = b"schema = \"lazaret-trusted-host-config-v1\"\n\
|
|
|
|
|
[profiles.default]\n\
|
|
|
|
|
image = \"example.invalid/default:v1\"\n\
|
|
|
|
|
env = { LAZARET_SESSION_ID = \"env:HOST_SESSION_ID\" }\n";
|
|
|
|
|
let error = parse_trusted_configuration(document).unwrap_err();
|
|
|
|
|
assert_eq!(error.kind(), ConfigurationErrorKind::InvalidProfile);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 00:43:42 +09:00
|
|
|
#[test]
|
|
|
|
|
fn selected_profile_loads_into_confined_nonserializable_authority() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let config = write_configuration(&tree, &valid_configuration(&tree));
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "top-secret".into());
|
|
|
|
|
let resolved = load_trusted_configuration(request(&tree, config, environment)).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(resolved.selected_profile(), "default");
|
2026-07-15 02:49:39 +09:00
|
|
|
assert!(
|
|
|
|
|
resolved
|
|
|
|
|
.effective_allow_prefixes()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|prefix| prefix.address() == [192, 0, 2, 0] && prefix.prefix_length() == 24)
|
2026-07-15 00:43:42 +09:00
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
resolved
|
|
|
|
|
.effective_deny_prefixes()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|prefix| prefix.address() == [10, 0, 0, 0] && prefix.prefix_length() == 8)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolved.ephemeral_environment()["TOKEN"].expose().to_str(),
|
|
|
|
|
Some("top-secret")
|
|
|
|
|
);
|
|
|
|
|
assert!(!format!("{resolved:?}").contains("top-secret"));
|
|
|
|
|
assert_eq!(resolved.volumes().len(), 1);
|
|
|
|
|
resolved.volumes()[0].open_source().unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fs::metadata(tree.path().join("data"))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.permissions()
|
|
|
|
|
.mode()
|
|
|
|
|
& 0o777,
|
|
|
|
|
0o700
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn environment_values_never_change_the_fingerprint() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let config = write_configuration(&tree, &valid_configuration(&tree));
|
|
|
|
|
let mut first = BTreeMap::new();
|
|
|
|
|
first.insert("HOST_TOKEN".into(), "first-secret".into());
|
|
|
|
|
let first = load_trusted_configuration(request(&tree, config.clone(), first)).unwrap();
|
|
|
|
|
let mut second = BTreeMap::new();
|
|
|
|
|
second.insert("HOST_TOKEN".into(), "second-secret".into());
|
|
|
|
|
let second = load_trusted_configuration(request(&tree, config, second)).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.configuration_fingerprint(),
|
|
|
|
|
second.configuration_fingerprint()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ignored_routing_is_explicit_and_fingerprinted() {
|
|
|
|
|
let first_tree = TestTree::new();
|
|
|
|
|
let first_config = write_configuration(&first_tree, &valid_configuration(&first_tree));
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let first =
|
|
|
|
|
load_trusted_configuration(request(&first_tree, first_config, environment)).unwrap();
|
|
|
|
|
assert!(first.fingerprint_record().isolate_gitignored);
|
|
|
|
|
|
|
|
|
|
let second_tree = TestTree::new();
|
|
|
|
|
let body = valid_configuration(&second_tree).replace(
|
|
|
|
|
"image = \"example.invalid/agent:v1\"",
|
|
|
|
|
"image = \"example.invalid/agent:v1\"\nisolate_gitignored = false",
|
|
|
|
|
);
|
|
|
|
|
let second_config = write_configuration(&second_tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let second =
|
|
|
|
|
load_trusted_configuration(request(&second_tree, second_config, environment)).unwrap();
|
|
|
|
|
assert!(!second.fingerprint_record().isolate_gitignored);
|
|
|
|
|
let disabled = derive_configuration_fingerprint(second.fingerprint_record()).unwrap();
|
|
|
|
|
let mut enabled_record = second.fingerprint_record().clone();
|
|
|
|
|
enabled_record.isolate_gitignored = true;
|
|
|
|
|
assert_ne!(
|
|
|
|
|
disabled,
|
|
|
|
|
derive_configuration_fingerprint(&enabled_record).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn retained_ancestor_authority_survives_path_replacement() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let config = write_configuration(&tree, &valid_configuration(&tree));
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let resolved = load_trusted_configuration(request(&tree, config, environment)).unwrap();
|
|
|
|
|
let original_source = tree.path().join("data/profiles/default/agent-state");
|
|
|
|
|
let original_inode = fs::metadata(&original_source).unwrap().ino();
|
|
|
|
|
let profile = tree.path().join("data/profiles/default");
|
|
|
|
|
let displaced = tree.path().join("data/profiles/displaced");
|
|
|
|
|
fs::rename(&profile, &displaced).unwrap();
|
|
|
|
|
fs::create_dir(&profile).unwrap();
|
|
|
|
|
fs::set_permissions(&profile, fs::Permissions::from_mode(0o700)).unwrap();
|
|
|
|
|
fs::create_dir(profile.join("agent-state")).unwrap();
|
|
|
|
|
let replacement_inode = fs::metadata(profile.join("agent-state")).unwrap().ino();
|
|
|
|
|
|
|
|
|
|
let opened = resolved.volumes()[0].open_source().unwrap();
|
|
|
|
|
let opened_inode = rustix::fs::fstat(&opened).unwrap().st_ino;
|
|
|
|
|
assert_eq!(opened_inode, original_inode);
|
|
|
|
|
assert_ne!(opened_inode, replacement_inode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn consumer_api_is_descriptor_relative_and_refuses_final_identity_drift() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let config = write_configuration(&tree, &valid_configuration(&tree));
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let load_request = request(&tree, config, environment);
|
|
|
|
|
let nested = load_request.workspace.join("nested");
|
|
|
|
|
fs::create_dir(&nested).unwrap();
|
|
|
|
|
fs::write(nested.join("data"), b"validated").unwrap();
|
|
|
|
|
symlink("data", nested.join("alias")).unwrap();
|
|
|
|
|
let expected_inode = fs::metadata(nested.join("data")).unwrap().ino();
|
|
|
|
|
let resolved = load_trusted_configuration(load_request).unwrap();
|
|
|
|
|
|
|
|
|
|
let opened = resolved
|
|
|
|
|
.workspace()
|
|
|
|
|
.open_beneath(Path::new("nested/data"), ConfinedPathType::RegularFile)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(rustix::fs::fstat(&opened).unwrap().st_ino, expected_inode);
|
|
|
|
|
for refused in ["../outside", "/absolute", "nested/./data", "nested/alias"] {
|
|
|
|
|
assert!(
|
|
|
|
|
resolved
|
|
|
|
|
.workspace()
|
|
|
|
|
.open_beneath(Path::new(refused), ConfinedPathType::RegularFile)
|
|
|
|
|
.is_err(),
|
|
|
|
|
"{refused} must not escape or redirect authority"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let workspace = tree.path().join("workspace");
|
|
|
|
|
fs::rename(&workspace, tree.path().join("displaced-workspace")).unwrap();
|
|
|
|
|
fs::create_dir(&workspace).unwrap();
|
|
|
|
|
assert!(resolved.workspace().open_authority().is_err());
|
|
|
|
|
|
|
|
|
|
let source = tree.path().join("data/profiles/default/agent-state");
|
|
|
|
|
fs::rename(&source, tree.path().join("displaced-agent-state")).unwrap();
|
|
|
|
|
fs::create_dir(&source).unwrap();
|
|
|
|
|
assert!(resolved.volumes()[0].open_source().is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn auto_approval_uses_only_the_canonical_direct_vector_and_safe_object() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let binary_directory = tree.path().join("bin");
|
|
|
|
|
fs::create_dir(&binary_directory).unwrap();
|
|
|
|
|
let executable = binary_directory.join("tool");
|
|
|
|
|
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
|
|
|
|
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
|
|
|
|
|
let body = valid_configuration(&tree).replace(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = false",
|
|
|
|
|
&format!(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = true\n\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"\nargs = [\"fixed\"]\nallow_extra_args = false",
|
|
|
|
|
executable.display()
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let resolved = load_trusted_configuration(request(&tree, config, environment)).unwrap();
|
|
|
|
|
let rule = &resolved.auto_approval_rules()[0];
|
|
|
|
|
let canonical = fs::canonicalize(&executable)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.to_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.to_vec();
|
|
|
|
|
assert!(rule.matches_command_argv(&[canonical.clone(), b"fixed".to_vec()]));
|
|
|
|
|
assert!(!rule.matches_command_argv(&[b"tool".to_vec(), b"fixed".to_vec()]));
|
|
|
|
|
assert!(!rule.matches_command_argv(std::slice::from_ref(&canonical)));
|
|
|
|
|
assert!(
|
|
|
|
|
!rule.matches_command_argv(&[canonical.clone(), b"fixed".to_vec(), b"extra".to_vec(),])
|
|
|
|
|
);
|
|
|
|
|
assert!(!rule.matches_command_argv(&[canonical, b"fixed\0extra".to_vec()]));
|
|
|
|
|
|
|
|
|
|
let prefix_tree = TestTree::new();
|
|
|
|
|
let binary_directory = prefix_tree.path().join("bin");
|
|
|
|
|
fs::create_dir(&binary_directory).unwrap();
|
|
|
|
|
let executable = binary_directory.join("tool");
|
|
|
|
|
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
|
|
|
|
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
|
|
|
|
|
let body = valid_configuration(&prefix_tree).replace(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = false",
|
|
|
|
|
&format!(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = true\n\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"\nargs = [\"fixed\"]\nallow_extra_args = true",
|
|
|
|
|
executable.display()
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&prefix_tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let resolved = load_trusted_configuration(request(&prefix_tree, config, environment)).unwrap();
|
|
|
|
|
let canonical = fs::canonicalize(executable)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.to_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.to_vec();
|
|
|
|
|
let rule = &resolved.auto_approval_rules()[0];
|
|
|
|
|
assert!(rule.matches_command_argv(&[canonical.clone(), b"fixed".to_vec()]));
|
|
|
|
|
assert!(rule.matches_command_argv(&[canonical, b"fixed".to_vec(), b"extra".to_vec(),]));
|
|
|
|
|
|
|
|
|
|
let hardlink_tree = TestTree::new();
|
|
|
|
|
let binary_directory = hardlink_tree.path().join("bin");
|
|
|
|
|
fs::create_dir(&binary_directory).unwrap();
|
|
|
|
|
let executable = binary_directory.join("tool");
|
|
|
|
|
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
|
|
|
|
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
|
|
|
|
|
fs::hard_link(&executable, binary_directory.join("alias")).unwrap();
|
|
|
|
|
let body = valid_configuration(&hardlink_tree).replace(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = false",
|
|
|
|
|
&format!(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = true\n\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"",
|
|
|
|
|
executable.display()
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&hardlink_tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
load_trusted_configuration(request(&hardlink_tree, config, environment))
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.kind(),
|
|
|
|
|
ConfigurationErrorKind::InvalidAutoApproval
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn path_network_environment_and_volume_conflicts_refuse_loading() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let base = valid_configuration(&tree);
|
|
|
|
|
let cases = [
|
|
|
|
|
(
|
|
|
|
|
base.replace(
|
|
|
|
|
"persistent_dirs = [\"/root\"]",
|
|
|
|
|
"persistent_dirs = [\"/workspace/cache\"]",
|
|
|
|
|
),
|
|
|
|
|
ConfigurationErrorKind::PathConflict,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
base.replace(
|
|
|
|
|
"allow_egress = [\"10.1.2.3/24\"]",
|
|
|
|
|
"allow_egress = [\"10.0.0.0/8\"]",
|
|
|
|
|
),
|
|
|
|
|
ConfigurationErrorKind::InvalidNetwork,
|
|
|
|
|
),
|
2026-07-19 19:52:14 +09:00
|
|
|
(
|
|
|
|
|
base.replace(
|
|
|
|
|
"allow_egress = [\"10.1.2.3/24\"]",
|
|
|
|
|
"allow_egress = [\"10.137.55.2/32\"]",
|
|
|
|
|
),
|
|
|
|
|
ConfigurationErrorKind::InvalidNetwork,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
base.replace(
|
|
|
|
|
"allow_egress = [\"10.1.2.3/24\"]",
|
|
|
|
|
"allow_egress = [\"10.1.2.3/24\"]\ndeny_egress = [\"10.137.54.3/32\"]",
|
|
|
|
|
),
|
|
|
|
|
ConfigurationErrorKind::InvalidNetwork,
|
|
|
|
|
),
|
2026-07-15 00:43:42 +09:00
|
|
|
(
|
|
|
|
|
base.replace("/root/.local/state/agent", "/root"),
|
|
|
|
|
ConfigurationErrorKind::InvalidVolume,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
base.replace(
|
|
|
|
|
"persistent_dirs = [\"/root\"]",
|
|
|
|
|
"persistent_dirs = [\"/root\", \"/root/cache\"]",
|
|
|
|
|
),
|
|
|
|
|
ConfigurationErrorKind::InvalidProfile,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
base.replace("kind = \"directory\"", "kind = \"file\""),
|
|
|
|
|
ConfigurationErrorKind::InvalidVolume,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
format!(
|
|
|
|
|
"{base}\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"/bin/true\"\n"
|
|
|
|
|
),
|
|
|
|
|
ConfigurationErrorKind::InvalidAutoApproval,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
base.replace(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = false",
|
|
|
|
|
"[profiles.default.escalation]\nenabled = true\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"/bin/true\"\nallow_extra_args = true",
|
|
|
|
|
),
|
|
|
|
|
ConfigurationErrorKind::InvalidAutoApproval,
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
for (index, (body, expected)) in cases.into_iter().enumerate() {
|
|
|
|
|
let case_tree = TestTree::new();
|
|
|
|
|
let body = body.replace(
|
|
|
|
|
&tree.path().join("data").display().to_string(),
|
|
|
|
|
&case_tree.path().join("data").display().to_string(),
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&case_tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
let error = load_trusted_configuration(request(&case_tree, config, environment))
|
|
|
|
|
.expect_err(&format!("case {index} must fail"));
|
|
|
|
|
assert_eq!(error.kind(), expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let config = write_configuration(&tree, &base);
|
|
|
|
|
let error = load_trusted_configuration(request(&tree, config, BTreeMap::new())).unwrap_err();
|
|
|
|
|
assert_eq!(error.kind(), ConfigurationErrorKind::MissingEnvironment);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 19:52:14 +09:00
|
|
|
#[test]
|
|
|
|
|
fn broad_private_deny_can_contain_the_reserved_topology() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let body = valid_configuration(&tree).replace(
|
|
|
|
|
"default_allow = [\"192.0.2.4/24\", \"198.51.100.53/32\"]",
|
|
|
|
|
"default_allow = [\"192.0.2.4/24\", \"198.51.100.53/32\"]\ndefault_deny = [\"10.0.0.0/8\"]",
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
load_trusted_configuration(request(&tree, config, environment)).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 00:43:42 +09:00
|
|
|
#[test]
|
|
|
|
|
fn secret_environment_resolution_occurs_after_authority_validation() {
|
|
|
|
|
let volume_tree = TestTree::new();
|
|
|
|
|
let body = valid_configuration(&volume_tree).replace("kind = \"directory\"", "kind = \"file\"");
|
|
|
|
|
let config = write_configuration(&volume_tree, &body);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
load_trusted_configuration(request(&volume_tree, config, BTreeMap::new()))
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.kind(),
|
|
|
|
|
ConfigurationErrorKind::InvalidVolume
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let executable_tree = TestTree::new();
|
|
|
|
|
let executable = executable_tree.path().join("unsafe-tool");
|
|
|
|
|
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
|
|
|
|
fs::set_permissions(&executable, fs::Permissions::from_mode(0o722)).unwrap();
|
|
|
|
|
let body = valid_configuration(&executable_tree).replace(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = false",
|
|
|
|
|
&format!(
|
|
|
|
|
"[profiles.default.escalation]\nenabled = true\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"",
|
|
|
|
|
executable.display()
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
let config = write_configuration(&executable_tree, &body);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
load_trusted_configuration(request(&executable_tree, config, BTreeMap::new()))
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.kind(),
|
|
|
|
|
ConfigurationErrorKind::InvalidAutoApproval
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn volume_symlinks_and_lazaret_owned_mode_drift_are_rejected() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let target = tree.path().join("target");
|
|
|
|
|
fs::create_dir(&target).unwrap();
|
|
|
|
|
let link = tree.path().join("volume-link");
|
|
|
|
|
symlink(&target, &link).unwrap();
|
|
|
|
|
let body = valid_configuration(&tree)
|
|
|
|
|
.replace("{profile_dir}/agent-state", link.to_str().unwrap())
|
|
|
|
|
.replace("create = true", "create = false");
|
|
|
|
|
let config = write_configuration(&tree, &body);
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
load_trusted_configuration(request(&tree, config, environment))
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.kind(),
|
|
|
|
|
ConfigurationErrorKind::InvalidVolume
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mode_tree = TestTree::new();
|
|
|
|
|
let data = mode_tree.path().join("data");
|
|
|
|
|
fs::create_dir(&data).unwrap();
|
|
|
|
|
fs::set_permissions(&data, fs::Permissions::from_mode(0o755)).unwrap();
|
|
|
|
|
let config = write_configuration(&mode_tree, &valid_configuration(&mode_tree));
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
load_trusted_configuration(request(&mode_tree, config, environment))
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.kind(),
|
|
|
|
|
ConfigurationErrorKind::InvalidPath
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 05:47:15 +09:00
|
|
|
#[test]
|
|
|
|
|
fn concurrent_loaders_share_exact_owned_directory_creation() {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let config = write_configuration(&tree, &valid_configuration(&tree));
|
|
|
|
|
let barrier = Arc::new(Barrier::new(8));
|
|
|
|
|
std::thread::scope(|scope| {
|
|
|
|
|
let handles = (0..8)
|
|
|
|
|
.map(|_| {
|
|
|
|
|
let barrier = Arc::clone(&barrier);
|
|
|
|
|
let config = config.clone();
|
|
|
|
|
let tree = &tree;
|
|
|
|
|
scope.spawn(move || {
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
barrier.wait();
|
|
|
|
|
load_trusted_configuration(request(tree, config, environment))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
for handle in handles {
|
|
|
|
|
handle.join().unwrap().unwrap();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 00:43:42 +09:00
|
|
|
#[test]
|
|
|
|
|
fn configuration_source_permissions_links_and_size_are_bounded() {
|
|
|
|
|
for mutation in 0..3 {
|
|
|
|
|
let tree = TestTree::new();
|
|
|
|
|
let config = write_configuration(&tree, &valid_configuration(&tree));
|
|
|
|
|
match mutation {
|
|
|
|
|
0 => fs::set_permissions(&config, fs::Permissions::from_mode(0o666)).unwrap(),
|
|
|
|
|
1 => fs::hard_link(&config, tree.path().join("config-link")).unwrap(),
|
|
|
|
|
2 => fs::write(&config, vec![b'x'; 1_048_577]).unwrap(),
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
|
}
|
|
|
|
|
let mut environment = BTreeMap::new();
|
|
|
|
|
environment.insert("HOST_TOKEN".into(), "value".into());
|
|
|
|
|
assert!(
|
|
|
|
|
load_trusted_configuration(request(&tree, config, environment)).is_err(),
|
|
|
|
|
"mutation {mutation} must fail"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn digest_vectors_are_framed_and_order_independent() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
derive_domain_id("default", b"/workspace/project")
|
|
|
|
|
.unwrap()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
&[
|
|
|
|
|
0x62, 0x64, 0xea, 0x6c, 0x25, 0x85, 0x2f, 0x5b, 0x77, 0x03, 0x90, 0x33, 0x9b, 0xb1,
|
|
|
|
|
0xd9, 0x75, 0x01, 0x9b, 0x70, 0xa6, 0x04, 0x55, 0xff, 0xfa, 0x2a, 0x1c, 0xf3, 0x0e,
|
|
|
|
|
0xf1, 0xc6, 0xbe, 0xf9,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut record = ConfigurationFingerprintRecordV1 {
|
|
|
|
|
selected_profile: "default".into(),
|
|
|
|
|
storage_root: HostPath::new(b"/data".to_vec()),
|
|
|
|
|
runtime_root: HostPath::new(b"/run/user/1000/lazaret".to_vec()),
|
2026-07-24 23:34:02 +09:00
|
|
|
runsc_dcache: DEFAULT_RUNSC_DCACHE,
|
2026-07-15 00:43:42 +09:00
|
|
|
image: "example.invalid/agent:v1".into(),
|
|
|
|
|
isolate_gitignored: true,
|
|
|
|
|
persistent_dirs: vec!["/var".into(), "/root".into()],
|
|
|
|
|
environment_references: vec![
|
|
|
|
|
EnvironmentReference {
|
|
|
|
|
sandbox_name: "B".into(),
|
|
|
|
|
host_name: "HOST_B".into(),
|
|
|
|
|
},
|
|
|
|
|
EnvironmentReference {
|
|
|
|
|
sandbox_name: "A".into(),
|
|
|
|
|
host_name: "HOST_A".into(),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
volumes: vec![ResolvedVolume {
|
|
|
|
|
canonical_host_source: HostPath::new(b"/source".to_vec()),
|
|
|
|
|
container_path: "/root/source".into(),
|
|
|
|
|
kind: VolumeKind::Directory,
|
|
|
|
|
access: VolumeAccess::ReadOnly,
|
|
|
|
|
create: false,
|
|
|
|
|
}],
|
|
|
|
|
effective_allow_prefixes: vec![([8, 8, 8, 8], 32), ([1, 1, 1, 1], 32)],
|
|
|
|
|
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)],
|
|
|
|
|
escalation_enabled: true,
|
|
|
|
|
auto_approval_rules: vec![ResolvedAutoApprovalRule {
|
|
|
|
|
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
|
|
|
|
args: vec!["a".into(), "b".into()],
|
|
|
|
|
allow_extra_args: false,
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
let first = derive_configuration_fingerprint(&record).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.as_bytes(),
|
|
|
|
|
&[
|
2026-07-24 23:34:02 +09:00
|
|
|
0x5f, 0x1d, 0xeb, 0x46, 0xa4, 0xb5, 0x90, 0xc3, 0x47, 0x0b, 0xa0, 0xdf, 0xd6, 0x6e,
|
|
|
|
|
0x9d, 0x91, 0xdb, 0xf3, 0x75, 0xf6, 0xbe, 0x50, 0x9a, 0x8d, 0xba, 0x4a, 0x3d, 0xae,
|
|
|
|
|
0xf3, 0x05, 0x97, 0x56,
|
2026-07-15 00:43:42 +09:00
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
record.persistent_dirs.reverse();
|
|
|
|
|
record.environment_references.reverse();
|
|
|
|
|
record.effective_allow_prefixes.reverse();
|
|
|
|
|
assert_eq!(first, derive_configuration_fingerprint(&record).unwrap());
|
|
|
|
|
record.auto_approval_rules[0].args.reverse();
|
|
|
|
|
assert_ne!(first, derive_configuration_fingerprint(&record).unwrap());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn serialized_configuration_records_use_strict_postcard() {
|
|
|
|
|
let reference = EnvironmentReference {
|
|
|
|
|
sandbox_name: "TOKEN".into(),
|
|
|
|
|
host_name: "HOST_TOKEN".into(),
|
|
|
|
|
};
|
|
|
|
|
let mut encoded = encode(&reference).unwrap();
|
|
|
|
|
encoded.push(0);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode::<EnvironmentReference>(&encoded),
|
|
|
|
|
Err(CodecError::TrailingPayload)
|
|
|
|
|
));
|
|
|
|
|
}
|