use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use crate::sandbox::SandboxInputs;
pub const DEFAULT_IMAGE: &str = "alpine:3";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContainerRuntime {
Docker,
Podman,
Nerdctl,
AppleContainer,
}
impl ContainerRuntime {
const PREFERENCE_ORDER: &'static [ContainerRuntime] = &[
ContainerRuntime::Docker,
ContainerRuntime::Podman,
ContainerRuntime::Nerdctl,
ContainerRuntime::AppleContainer,
];
pub fn binary(self) -> &'static str {
match self {
ContainerRuntime::Docker => "docker",
ContainerRuntime::Podman => "podman",
ContainerRuntime::Nerdctl => "nerdctl",
ContainerRuntime::AppleContainer => "container",
}
}
pub(crate) fn client_env(self) -> std::collections::HashMap<String, String> {
let mut keys = vec![
"PATH",
"HOME",
"USER",
"LOGNAME",
"LANG",
"LC_ALL",
"LC_CTYPE",
"TMPDIR",
"XDG_CONFIG_HOME",
"XDG_RUNTIME_DIR",
"SSH_AUTH_SOCK",
"USERPROFILE",
"SystemRoot",
"ComSpec",
"APPDATA",
"LOCALAPPDATA",
"TEMP",
"TMP",
];
match self {
Self::Docker => keys.extend([
"DOCKER_HOST",
"DOCKER_CONTEXT",
"DOCKER_CONFIG",
"DOCKER_TLS",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
"DOCKER_API_VERSION",
]),
Self::Podman => {
keys.extend(["CONTAINER_HOST", "CONTAINER_CONNECTION", "CONTAINER_SSHKEY"])
}
Self::Nerdctl => {
keys.extend(["CONTAINERD_ADDRESS", "CONTAINERD_NAMESPACE", "NERDCTL_TOML"])
}
Self::AppleContainer => {}
}
keys.into_iter()
.filter_map(|key| {
std::env::var(key)
.ok()
.map(|value| (key.to_string(), value))
})
.collect()
}
}
pub fn detect() -> Option<ContainerRuntime> {
detect_with(crate::sandbox::command_available)
}
pub fn host_supports_container_contract() -> bool {
if cfg!(target_os = "linux") {
return true;
}
if cfg!(target_os = "windows") {
return false;
}
let Some(runtime) = detect() else {
return false;
};
matches!(host_mount_contract_proof(runtime), MountProof::Proven)
}
pub fn host_mount_contract_proof(runtime: ContainerRuntime) -> MountProof {
let cwd = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
for root in [cwd.as_path(), std::env::temp_dir().as_path()] {
match cached_bind_mount_proof(runtime, root, DEFAULT_IMAGE) {
MountProof::Proven => {}
failed => return failed,
}
}
MountProof::Proven
}
pub const MOUNT_PROOF_GUEST_DIR: &str = "/kranz-mount-proof";
const MOUNT_PROOF_TIMEOUT: Duration = Duration::from_secs(90);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MountProof {
Proven,
Failed(String),
}
pub fn mount_proof_argv(host_dir: &Path, image: &str, guest_sentinel: &str) -> Vec<String> {
vec![
"run".to_string(),
"--rm".to_string(),
"-v".to_string(),
format!(
"{}:{MOUNT_PROOF_GUEST_DIR}",
container_host_path(host_dir)
),
image.to_string(),
"sh".to_string(),
"-c".to_string(),
format!(
"if [ -r {MOUNT_PROOF_GUEST_DIR}/host.txt ]; then cat {MOUNT_PROOF_GUEST_DIR}/host.txt; \
else printf %s no-host-sentinel; fi; \
printf %s {guest_sentinel} > {MOUNT_PROOF_GUEST_DIR}/guest.txt 2>/dev/null || true"
),
]
}
pub fn prove_bind_mount(runtime: ContainerRuntime, host_dir: &Path, image: &str) -> MountProof {
let probe = host_dir.join(format!(
"kranz-mount-proof-{}",
uuid::Uuid::new_v4().simple()
));
if let Err(error) = std::fs::create_dir_all(&probe) {
return MountProof::Failed(format!(
"could not create the mount probe directory {}: {error}",
probe.display()
));
}
let host_sentinel = uuid::Uuid::new_v4().simple().to_string();
let guest_sentinel = uuid::Uuid::new_v4().simple().to_string();
let proof = run_mount_proof(
runtime,
host_dir,
&probe,
image,
&host_sentinel,
&guest_sentinel,
);
let _ = std::fs::remove_dir_all(&probe);
proof
}
fn run_mount_proof(
runtime: ContainerRuntime,
host_dir: &Path,
probe: &Path,
image: &str,
host_sentinel: &str,
guest_sentinel: &str,
) -> MountProof {
if let Err(error) = std::fs::write(probe.join("host.txt"), host_sentinel) {
return MountProof::Failed(format!(
"could not write the host sentinel in {}: {error}",
probe.display()
));
}
let argv = mount_proof_argv(probe, image, guest_sentinel);
let Some(output) = crate::command_exec::run_with_timeout(
Path::new(runtime.binary()),
&argv,
MOUNT_PROOF_TIMEOUT,
) else {
return MountProof::Failed(format!(
"the {} mount proof did not finish within {}s: {}",
runtime.binary(),
MOUNT_PROOF_TIMEOUT.as_secs(),
argv.join(" ")
));
};
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !output.status.success() {
return MountProof::Failed(format!(
"the {} mount proof exited {:?}: {}",
runtime.binary(),
output.status.code(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
if stdout != host_sentinel {
return MountProof::Failed(unshared_path_reason(
runtime,
host_dir,
"the host sentinel was not visible inside the container",
));
}
match std::fs::read_to_string(probe.join("guest.txt")) {
Ok(written) if written.trim() == guest_sentinel => MountProof::Proven,
Ok(_) | Err(_) => MountProof::Failed(unshared_path_reason(
runtime,
host_dir,
"the container's write did not reach the host",
)),
}
}
fn unshared_path_reason(runtime: ContainerRuntime, host_dir: &Path, symptom: &str) -> String {
let mut reason = format!(
"{} accepted a bind mount of {} and shared nothing: {symptom}. \
The runtime's daemon cannot see this host path, so the declared write set would \
not exist inside the container and a worker's output would be lost silently. \
Share this path with the runtime (Colima mounts only the home directory by \
default: `colima start --mount {}:w`; Docker Desktop keeps its own file-sharing \
list)",
runtime.binary(),
host_dir.display(),
host_dir.display()
);
if host_dir == crate::backend_claude::scratch_root_base() {
reason.push_str(&format!(
", or move kranz's own scratch to a directory the runtime already shares by \
setting {}=<path> (this root is scratch, not your workspace)",
crate::backend_claude::SCRATCH_ROOT_ENV
));
} else {
reason.push_str(" or point the mission's workspace at a path it already shares");
}
reason
}
fn proof_cache() -> &'static Mutex<HashMap<(String, String), MountProof>> {
static CACHE: OnceLock<Mutex<HashMap<(String, String), MountProof>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn cached_bind_mount_proof(
runtime: ContainerRuntime,
host_dir: &Path,
image: &str,
) -> MountProof {
let key = (
runtime.binary().to_string(),
host_dir.to_string_lossy().into_owned(),
);
if let Ok(cache) = proof_cache().lock() {
if let Some(proof) = cache.get(&key) {
return proof.clone();
}
}
let proof = prove_bind_mount(runtime, host_dir, image);
if let Ok(mut cache) = proof_cache().lock() {
cache.insert(key, proof.clone());
}
proof
}
pub fn prove_mount_roots(runtime: ContainerRuntime, roots: &[PathBuf], image: &str) -> MountProof {
let mut seen = Vec::new();
for root in roots {
if root.as_os_str().is_empty() || seen.iter().any(|prior| prior == root) {
continue;
}
seen.push(root.clone());
match cached_bind_mount_proof(runtime, root, image) {
MountProof::Proven => {}
failed => return failed,
}
}
MountProof::Proven
}
pub fn declared_mount_roots(
session_cwd: &Path,
mission_dir: &Path,
extra_write: &[PathBuf],
) -> Vec<PathBuf> {
let mut roots = vec![
session_cwd.parent().unwrap_or(session_cwd).to_path_buf(),
mission_dir.to_path_buf(),
crate::backend_claude::scratch_root_base(),
];
roots.extend(extra_write.iter().cloned());
roots
}
pub fn container_contract_skip_detail() -> String {
if cfg!(target_os = "windows") {
return "the container provider refuses Windows: POSIX guest paths, Linux images, \
and /dev/null authority masks are not honored there"
.to_string();
}
match detect() {
None => "no docker/podman/nerdctl/container on PATH".to_string(),
Some(runtime) => match host_mount_contract_proof(runtime) {
MountProof::Proven => {
"the host contract is supported; this skip should not have fired".to_string()
}
MountProof::Failed(reason) => reason,
},
}
}
pub fn detect_with(lookup: impl Fn(&str) -> bool) -> Option<ContainerRuntime> {
ContainerRuntime::PREFERENCE_ORDER
.iter()
.copied()
.find(|runtime| lookup(runtime.binary()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerSpec {
pub runtime: ContainerRuntime,
pub image: String,
pub network: Option<String>,
pub name: Option<String>,
}
fn mount_arg(host_abs: &str, read_only: bool) -> String {
format!(
"{host_abs}:{host_abs}{}",
if read_only { ":ro" } else { "" }
)
}
fn container_host_path(path: &Path) -> String {
let absolute = crate::sandbox::absolutize(path);
let rendered = absolute.as_os_str().to_string_lossy();
#[cfg(windows)]
if let Some(rest) = rendered.strip_prefix(r"\\?\") {
if !rest.starts_with("UNC") {
return rest.to_string();
}
}
rendered.into_owned()
}
const CONTAINER_PIDS_LIMIT: &str = "512";
fn run_prologue(inputs: &SandboxInputs) -> Vec<String> {
let mut out = vec![
"run".to_string(),
"--rm".to_string(),
"-i".to_string(),
"--read-only".to_string(),
"--cap-drop".to_string(),
"ALL".to_string(),
"--security-opt".to_string(),
"no-new-privileges".to_string(),
"--pids-limit".to_string(),
CONTAINER_PIDS_LIMIT.to_string(),
];
if let Some(owner) = crate::container_egress::mount_owner(&inputs.session_cwd) {
out.push("--user".to_string());
out.push(owner);
}
for key in [
"HTTP_PROXY",
"HTTPS_PROXY",
"FTP_PROXY",
"ALL_PROXY",
"NO_PROXY",
"http_proxy",
"https_proxy",
"ftp_proxy",
"all_proxy",
"no_proxy",
] {
out.extend(["-e".to_string(), format!("{key}=")]);
}
out
}
fn push_policy_mounts(out: &mut Vec<String>, inputs: &SandboxInputs) {
let mut mounts: Vec<(String, bool)> = Vec::new();
let denied_dirs: Vec<_> = crate::sandbox::authority_read_deny_dirs(inputs)
.iter()
.map(|path| crate::sandbox::absolutize(path))
.collect();
let denied_files: Vec<_> = crate::sandbox::authority_read_deny_paths(inputs)
.iter()
.map(|path| crate::sandbox::absolutize(path))
.collect();
let mut add_mount = |path: &Path, ro: bool| {
let path = crate::sandbox::absolutize(path);
if denied_dirs.iter().any(|dir| path.starts_with(dir))
|| denied_files.iter().any(|file| path.starts_with(file))
{
return;
}
let host = container_host_path(&path);
if !mounts.iter().any(|(existing, _)| existing == &host) {
mounts.push((host, ro));
}
};
add_mount(&inputs.session_cwd, false);
if let Some(missions) = inputs
.mission_dir
.parent()
.filter(|path| path.ends_with("missions") && path.is_dir())
{
add_mount(missions, true);
}
add_mount(&inputs.mission_dir, true);
add_mount(&inputs.tmpdir, false);
for extra in &inputs.extra_write {
if inputs
.mission_dir
.parent()
.filter(|p| p.ends_with("missions"))
.is_some_and(|missions| {
crate::sandbox::absolutize(extra).starts_with(crate::sandbox::absolutize(missions))
})
{
continue;
}
add_mount(extra, false);
}
for (host, ro) in mounts {
out.push("-v".to_string());
out.push(mount_arg(&host, ro));
}
}
fn under_writable_mount(path: &Path, inputs: &SandboxInputs) -> bool {
let candidate = crate::sandbox::absolutize(path);
std::iter::once(&inputs.session_cwd)
.chain(std::iter::once(&inputs.tmpdir))
.chain(inputs.extra_write.iter())
.any(|root| candidate.starts_with(crate::sandbox::absolutize(root)))
}
fn push_authority_masks(out: &mut Vec<String>, inputs: &SandboxInputs) {
for node in crate::sandbox::git_metadata_mount_nodes(inputs) {
let node = container_host_path(&node);
if !out.windows(2).any(|pair| {
pair[0] == "-v"
&& (pair[1] == mount_arg(&node, false) || pair[1] == mount_arg(&node, true))
}) {
out.extend(["-v".to_string(), mount_arg(&node, false)]);
}
}
let masks: Vec<_> = crate::sandbox::authority_directory_masks(inputs)
.into_iter()
.filter(|mask| {
mask.path.ancestors().any(|ancestor| {
let path = container_host_path(ancestor);
out.windows(2).any(|pair| {
pair[0] == "-v"
&& (pair[1] == mount_arg(&path, false) || pair[1] == mount_arg(&path, true))
})
})
})
.collect();
let masked_paths: std::collections::BTreeSet<_> =
masks.iter().map(|mask| mask.path.clone()).collect();
let mut filtered = Vec::new();
let mut index = 0;
while index < out.len() {
if out[index] == "-v" && index + 1 < out.len() {
let mount = &out[index + 1];
if masks.iter().any(|mask| {
let path = container_host_path(&mask.path);
mount == &mount_arg(&path, false) || mount == &mount_arg(&path, true)
}) {
index += 2;
continue;
}
}
filtered.push(out[index].clone());
index += 1;
}
*out = filtered;
for mask in &masks {
out.push("--tmpfs".to_string());
out.push(format!(
"{}:ro,noexec,nosuid,nodev,mode=755",
container_host_path(&mask.path)
));
for path in &mask.visible_entries {
if masked_paths.contains(path) {
continue;
}
let path = container_host_path(path);
if !out.windows(2).any(|pair| {
pair[0] == "-v"
&& (pair[1] == mount_arg(&path, false) || pair[1] == mount_arg(&path, true))
}) {
out.push("-v".to_string());
out.push(mount_arg(&path, true));
}
}
}
let writes = crate::sandbox::authority_write_denies(inputs);
let git = crate::sandbox::git_metadata_write_denies(inputs);
for path in writes
.files
.iter()
.chain(writes.dirs.iter())
.chain(git.files.iter().filter(|path| path.is_file()))
.chain(git.dirs.iter())
{
if !under_writable_mount(path, inputs)
|| path.is_symlink()
|| !path.exists()
|| masks
.iter()
.any(|mask| crate::sandbox::absolutize(path).starts_with(&mask.path))
{
continue;
}
let host = container_host_path(path);
if !out
.windows(2)
.any(|pair| pair[0] == "-v" && pair[1] == mount_arg(&host, true))
{
out.extend(["-v".to_string(), mount_arg(&host, true)]);
}
}
}
fn push_workdir_and_scratch_env(out: &mut Vec<String>, inputs: &SandboxInputs) {
out.push("-w".to_string());
out.push(container_host_path(&inputs.session_cwd));
let scratch = container_host_path(&inputs.tmpdir);
out.push("-e".to_string());
out.push(format!("HOME={scratch}"));
out.push("-e".to_string());
out.push(format!("TMPDIR={scratch}"));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ToolchainMount {
Session,
Gate,
}
fn push_toolchain_caches(out: &mut Vec<String>, mode: ToolchainMount) {
let global = crate::sandbox::global_authority_dir();
for (var, default_subdir) in [
("RUSTUP_HOME", ".rustup"),
("CARGO_HOME", ".cargo"),
("NPM_CONFIG_CACHE", ".npm"),
] {
let host = std::env::var_os(var)
.map(std::path::PathBuf::from)
.or_else(|| {
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(default_subdir))
});
if let Some(host) = host {
if global
.as_ref()
.is_some_and(|dir| crate::sandbox::absolutize(&host).starts_with(dir))
{
continue;
}
if var == "CARGO_HOME" {
let leaves: &[&str] = match mode {
ToolchainMount::Gate => &["bin"],
ToolchainMount::Session => &["bin", "registry", "git"],
};
let mut mounted_any = false;
for leaf in leaves {
let dir = host.join(leaf);
if dir.is_dir() {
let mounted = container_host_path(&dir);
out.push("-v".to_string());
out.push(mount_arg(&mounted, true));
mounted_any = true;
}
}
if mode == ToolchainMount::Session && mounted_any {
out.push("-e".to_string());
out.push(format!("CARGO_HOME={}", container_host_path(&host)));
}
continue;
}
if host.is_dir() {
let mounted = container_host_path(&host);
out.push("-v".to_string());
out.push(mount_arg(&mounted, true));
out.push("-e".to_string());
out.push(format!("{var}={mounted}"));
}
}
}
}
fn push_network(out: &mut Vec<String>, inputs: &SandboxInputs, proxy_url: Option<&str>) {
if inputs.enforce == crate::types::SandboxEnforce::FsNet {
if inputs.egress.is_empty() {
out.push("--network".to_string());
out.push("none".to_string());
} else if let Some(proxy_url) = proxy_url {
out.push("-e".to_string());
out.push(format!(
"{}={proxy_url}",
crate::egress_proxy::HTTPS_PROXY_ENV
));
out.push("-e".to_string());
out.push(format!(
"{}={proxy_url}",
crate::egress_proxy::HTTP_PROXY_ENV
));
out.push("-e".to_string());
out.push(format!(
"{}={}",
crate::egress_proxy::NO_PROXY_ENV,
crate::egress_proxy::NO_PROXY_VALUE
));
}
}
}
pub fn container_run_args(
inputs: &SandboxInputs,
spec: &ContainerSpec,
binary: &Path,
args: &[String],
proxy_url: Option<&str>,
) -> Vec<String> {
let mut out = run_prologue(inputs);
if let Some(name) = &spec.name {
out.push("--name".to_string());
out.push(name.clone());
}
push_policy_mounts(&mut out, inputs);
push_workdir_and_scratch_env(&mut out, inputs);
push_toolchain_caches(&mut out, ToolchainMount::Session);
push_authority_masks(&mut out, inputs);
if inputs.enforce == crate::types::SandboxEnforce::FsNet && !inputs.egress.is_empty() {
if let (Some(network), Some(_)) = (&spec.network, proxy_url) {
out.push("--network".to_string());
out.push(network.clone());
push_network(&mut out, inputs, proxy_url);
} else {
out.push("--network".to_string());
out.push("none".to_string());
}
} else {
push_network(&mut out, inputs, proxy_url);
}
out.push(spec.image.clone());
out.push(binary.display().to_string());
out.extend(args.iter().cloned());
out
}
const GATE_FORWARD_ENV_SKIP: &[&str] = &[
"HOME",
"TMPDIR",
"TMP",
"TEMP",
"RUSTUP_HOME",
"NPM_CONFIG_CACHE",
];
pub fn container_gate_run_args(
inputs: &SandboxInputs,
spec: &ContainerSpec,
command: &str,
env: &std::collections::HashMap<String, String>,
container_name: &str,
) -> Vec<String> {
let mut out = run_prologue(inputs);
out.push("--name".to_string());
out.push(container_name.to_string());
push_policy_mounts(&mut out, inputs);
push_workdir_and_scratch_env(&mut out, inputs);
push_toolchain_caches(&mut out, ToolchainMount::Gate);
push_authority_masks(&mut out, inputs);
push_network(&mut out, inputs, None);
let mut forwarded: Vec<(&String, &String)> = env.iter().collect();
forwarded.sort_by_key(|(key, _)| *key);
for (key, value) in forwarded {
if GATE_FORWARD_ENV_SKIP.contains(&key.as_str()) {
continue;
}
out.push("-e".to_string());
out.push(format!("{key}={value}"));
}
out.push(spec.image.clone());
out.push("sh".to_string());
out.push("-c".to_string());
out.push(command.to_string());
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sandbox::SandboxInputs;
use crate::types::SandboxEnforce;
use std::path::PathBuf;
#[test]
fn declared_roots_follow_the_scratch_override_not_the_temp_dir() {
let case =
"sandbox_container::tests::declared_roots_follow_the_scratch_override_not_the_temp_dir";
if std::env::var("KRANZ_SCRATCH_TEST_CASE").as_deref() != Ok(case) {
let shared = tempfile::tempdir().unwrap();
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([case, "--exact", "--nocapture"])
.env("KRANZ_SCRATCH_TEST_CASE", case)
.env(crate::backend_claude::SCRATCH_ROOT_ENV, shared.path())
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("test result: ok. 1 passed;"));
return;
}
let checkout = std::path::Path::new("/repos/app/worktree");
let mission = std::path::Path::new("/repos/app/.kranz/missions/m-1");
let shared =
PathBuf::from(std::env::var_os(crate::backend_claude::SCRATCH_ROOT_ENV).unwrap());
let roots = declared_mount_roots(checkout, mission, &[]);
assert!(roots.contains(&shared), "{roots:?}");
assert!(!roots.contains(&std::env::temp_dir()), "{roots:?}");
assert!(
roots.contains(&std::path::PathBuf::from("/repos/app")),
"the checkout's parent is mounted, not the worktree itself: {roots:?}"
);
}
#[test]
fn mount_proof_argv_reads_the_host_sentinel_and_writes_the_guest_one() {
let host = std::env::temp_dir();
let argv = mount_proof_argv(&host, "alpine:3", "guestsentinel");
let rendered = argv.join(" ");
let expected_mount = format!("{}:/kranz-mount-proof", container_host_path(&host));
assert!(rendered.contains(&expected_mount), "{rendered}");
assert!(!expected_mount.starts_with(r"\\?\"), "{expected_mount}");
assert!(
rendered.contains("cat /kranz-mount-proof/host.txt"),
"{rendered}"
);
assert!(
rendered.contains("printf %s guestsentinel > /kranz-mount-proof/guest.txt"),
"{rendered}"
);
assert!(rendered.contains("no-host-sentinel"), "{rendered}");
assert!(rendered.starts_with("run --rm "), "{rendered}");
}
#[test]
fn live_bind_mount_round_trip_closes_under_the_checkout() {
if cfg!(target_os = "windows") {
crate::test_capability::skip(
crate::test_capability::capability::CONTAINER,
"the container provider refuses Windows, so a bind-mount probe proves nothing",
);
return;
}
let Some(runtime) = detect() else {
crate::test_capability::skip(
crate::test_capability::capability::CONTAINER,
"no container runtime on PATH, so the bind-mount round trip cannot be proven",
);
return;
};
let checkout = std::env::current_dir().expect("a working directory");
let root = checkout.parent().unwrap_or(&checkout);
match prove_bind_mount(runtime, root, DEFAULT_IMAGE) {
MountProof::Proven => {}
MountProof::Failed(reason) => panic!(
"the bind-mount round trip under {} did not close, so a mission's \
declared write set cannot be trusted here: {reason}",
root.display()
),
}
}
#[test]
fn detect_prefers_docker_then_podman_then_nerdctl_then_apple_container() {
assert_eq!(detect_with(|_| false), None);
assert_eq!(
detect_with(|name| name == "container"),
Some(ContainerRuntime::AppleContainer)
);
assert_eq!(
detect_with(|name| name == "nerdctl" || name == "container"),
Some(ContainerRuntime::Nerdctl)
);
assert_eq!(
detect_with(|name| name == "podman" || name == "nerdctl"),
Some(ContainerRuntime::Podman)
);
assert_eq!(
detect_with(|name| name == "docker" || name == "podman"),
Some(ContainerRuntime::Docker)
);
}
fn inputs(enforce: SandboxEnforce) -> SandboxInputs {
SandboxInputs {
enforce,
session_cwd: PathBuf::from("/work/session"),
mission_dir: PathBuf::from("/work/mission"),
tmpdir: PathBuf::from("/work/scratch"),
extra_write: vec![PathBuf::from("/home/op/.cargo")],
egress: Vec::new(),
validator_read_deny_roots: Vec::new(),
}
}
fn spec() -> ContainerSpec {
ContainerSpec {
runtime: ContainerRuntime::Docker,
image: DEFAULT_IMAGE.to_string(),
network: None,
name: None,
}
}
fn live_fixture() -> tempfile::TempDir {
tempfile::tempdir_in(std::env::current_dir().unwrap()).unwrap()
}
#[test]
fn container_run_args_fs_net_with_empty_egress_disables_network() {
let args = container_run_args(
&inputs(SandboxEnforce::FsNet),
&spec(),
Path::new("claude"),
&["-p".to_string(), "hi".to_string()],
None,
);
let network = args
.windows(2)
.find(|w| w[0] == "--network")
.expect("fs+net must pass a --network flag");
assert_eq!(network[1], "none");
}
#[test]
fn container_run_args_fs_net_with_egress_uses_internal_network_and_relay_env() {
let mut inputs = inputs(SandboxEnforce::FsNet);
inputs.egress = vec!["crates.io:443".to_string()];
let mut spec = spec();
spec.network = Some("kranz-egress-test".to_string());
spec.name = Some("kranz-egress-worker-test".to_string());
let args = container_run_args(
&inputs,
&spec,
Path::new("claude"),
&["-p".to_string(), "hi".to_string()],
Some("http://kranz-egress:3128"),
);
assert!(
args.windows(2)
.any(|w| w[0] == "--network" && w[1] == "kranz-egress-test"),
"proxy-routed fs+net must use the per-run internal network: {args:?}"
);
assert!(
args.windows(2)
.any(|w| w[0] == "--name" && w[1] == "kranz-egress-worker-test"),
"the daemon-owned worker must be named for timeout teardown: {args:?}"
);
for var in ["HTTPS_PROXY", "HTTP_PROXY"] {
assert!(
args.windows(2)
.any(|w| w[0] == "-e" && w[1] == format!("{var}=http://kranz-egress:3128")),
"missing -e {var}=…: {args:?}"
);
}
assert!(
args.windows(2)
.any(|w| w[0] == "-e" && w[1] == "NO_PROXY=localhost,127.0.0.1"),
"missing -e NO_PROXY…: {args:?}"
);
}
#[test]
fn container_run_args_fs_net_with_egress_fails_closed_without_boundary() {
let mut inputs = inputs(SandboxEnforce::FsNet);
inputs.egress = vec!["crates.io:443".to_string()];
let args = container_run_args(
&inputs,
&spec(),
Path::new("claude"),
&[],
Some("http://kranz-egress:3128"),
);
assert!(
args.windows(2)
.any(|w| w[0] == "--network" && w[1] == "none"),
"missing boundary state must disable networking: {args:?}"
);
assert!(
args.iter()
.filter(|a| a.starts_with("HTTPS_PROXY="))
.all(|a| a == "HTTPS_PROXY="),
"a relay env must not be emitted without its internal network: {args:?}"
);
}
#[test]
fn container_run_args_fs_keeps_runtime_default_network() {
let args = container_run_args(
&inputs(SandboxEnforce::Fs),
&spec(),
Path::new("claude"),
&[],
None,
);
assert!(
!args.iter().any(|a| a == "--network"),
"fs must not restrict the network (runtime default bridge): {args:?}"
);
}
#[test]
fn container_run_args_mounts_policy_and_runs_image() {
let dir = tempfile::tempdir().unwrap();
let session = dir.path().join("session");
let mission = dir.path().join("mission");
let scratch = dir.path().join("scratch");
let cargo = dir.path().join("cargo");
for path in [&session, &mission, &scratch, &cargo] {
std::fs::create_dir_all(path).unwrap();
}
let inputs = SandboxInputs {
enforce: SandboxEnforce::Fs,
session_cwd: session.clone(),
mission_dir: mission.clone(),
tmpdir: scratch.clone(),
extra_write: vec![cargo.clone()],
egress: Vec::new(),
validator_read_deny_roots: Vec::new(),
};
let args = container_run_args(
&inputs,
&spec(),
Path::new("claude"),
&["--print".to_string()],
None,
);
let joined = args.join(" ");
let abs = |p: &std::path::Path| container_host_path(p);
assert!(args.contains(&"--rm".to_string()));
assert!(args.contains(&"--read-only".to_string()));
assert!(joined.contains(&mount_arg(&abs(&session), false)));
assert!(joined.contains(&format!("--tmpfs {}:ro,", abs(&mission))));
assert!(joined.contains(&mount_arg(&abs(&scratch), false)));
assert!(joined.contains(&mount_arg(&abs(&cargo), false)));
assert!(joined.contains(&format!("-w {}", abs(&session))));
assert!(joined.contains(&format!("-e HOME={}", abs(&scratch))));
assert!(
joined.ends_with(&format!("{DEFAULT_IMAGE} claude --print")),
"image then binary then args: {args:?}"
);
}
#[test]
fn container_run_args_mask_authority_material_under_session_root() {
let dir = tempfile::tempdir().unwrap();
let session = dir.path().join("session");
let kranz_dir = session.join(".kranz");
std::fs::create_dir_all(&kranz_dir).unwrap();
let masked_token_file = kranz_dir.join("serve.token");
let config = kranz_dir.join("config.json");
std::fs::write(&masked_token_file, "secret").unwrap();
std::fs::write(&config, "{}").unwrap();
let mut inputs = inputs(SandboxEnforce::Fs);
inputs.session_cwd = session;
let args = container_run_args(
&inputs,
&spec(),
Path::new("claude"),
&["--print".to_string()],
None,
);
let joined = args.join(" ");
let abs = |p: &std::path::Path| container_host_path(p);
assert!(joined.contains(&format!("--tmpfs {}:ro,", abs(&kranz_dir))));
for name in ["serve.token", "serve.read.token", "config.json"] {
assert!(
!joined.contains(&abs(&kranz_dir.join(name))),
"authority must stay outside the private view: {args:?}"
);
}
}
#[test]
fn container_run_args_mask_the_whole_process_tier_authority_set() {
let dir = tempfile::tempdir().unwrap();
let session = dir.path().join("session");
let kranz = session.join(".kranz");
let mission = kranz.join("missions").join("m-x");
std::fs::create_dir_all(mission.join("control")).unwrap();
std::fs::create_dir_all(kranz.join("hook-status")).unwrap();
std::fs::create_dir_all(kranz.join("missions").join("m-other")).unwrap();
std::fs::create_dir_all(kranz.join("queue")).unwrap();
for name in ["serve.token", "config.json", "domain-terms.local"] {
std::fs::write(kranz.join(name), "secret").unwrap();
}
let mut inputs = inputs(SandboxEnforce::Fs);
inputs.session_cwd = session;
inputs.mission_dir = mission.clone();
let args = container_run_args(&inputs, &spec(), Path::new("claude"), &[], None);
let joined = args.join(" ");
let abs = |p: &std::path::Path| container_host_path(p);
for name in [
"serve.token",
"config.json",
"domain-terms.local",
"hook-status",
] {
assert!(
!joined.contains(&abs(&kranz.join(name))),
"authority must not be rebound: {args:?}"
);
}
assert!(joined.contains(&format!("--tmpfs {}:ro,", abs(&kranz))));
assert!(joined.contains(&format!("--tmpfs {}:ro,", abs(&mission))));
assert!(!joined.contains(&abs(&mission.join("control"))));
for readable in [kranz.join("queue"), kranz.join("missions")] {
assert!(
joined.contains(&mount_arg(&abs(&readable), true)),
"missing :ro self-bind for {}: {args:?}",
readable.display()
);
}
}
#[test]
fn container_run_args_keep_write_denied_kranz_content_readable() {
let dir = tempfile::tempdir().unwrap();
let session = dir.path().join("repo");
let kranz = session.join(".kranz");
let mission = kranz.join("missions").join("m-x");
std::fs::create_dir_all(mission.join("control")).unwrap();
std::fs::create_dir_all(kranz.join("hook-status")).unwrap();
std::fs::create_dir_all(kranz.join("tickets")).unwrap();
std::fs::create_dir_all(kranz.join("lessons")).unwrap();
std::fs::create_dir_all(kranz.join("queue")).unwrap();
std::fs::create_dir_all(kranz.join("missions").join("m-other")).unwrap();
std::fs::write(kranz.join("tickets").join("some-ticket.md"), "# tracked").unwrap();
std::fs::write(kranz.join("merge-gates.json"), "{}").unwrap();
std::fs::write(kranz.join("secret-allowlist"), "OK_TOKEN\n").unwrap();
for name in ["serve.token", "config.json"] {
std::fs::write(kranz.join(name), "secret").unwrap();
}
let mut inputs = inputs(SandboxEnforce::Fs);
inputs.session_cwd = session;
inputs.mission_dir = mission.clone();
let args = container_run_args(&inputs, &spec(), Path::new("claude"), &[], None);
let joined = args.join(" ");
let abs = |p: &std::path::Path| container_host_path(p);
for readable in [
kranz.join("tickets"),
kranz.join("lessons"),
kranz.join("queue"),
kranz.join("missions"),
] {
assert!(
joined.contains(&mount_arg(&abs(&readable), true)),
"{} must be a :ro self-bind, not a mask: {args:?}",
readable.display()
);
assert!(
!joined.contains(&format!("--tmpfs {}:ro", abs(&readable))),
"{} must not be shadowed by an empty tmpfs: {args:?}",
readable.display()
);
}
for readable in [
kranz.join("merge-gates.json"),
kranz.join("secret-allowlist"),
] {
assert!(
joined.contains(&mount_arg(&abs(&readable), true)),
"{} must be a :ro self-bind: {args:?}",
readable.display()
);
assert!(
!joined.contains(&format!("/dev/null:{}:ro", abs(&readable))),
"{} must not read as zero bytes: {args:?}",
readable.display()
);
}
for hidden in [
kranz.join("serve.token"),
kranz.join("config.json"),
mission.join("control"),
kranz.join("hook-status"),
] {
assert!(
!joined.contains(&abs(&hidden)),
"read-denied entry was mounted: {args:?}"
);
}
}
#[test]
fn container_run_args_harden_the_worker_like_the_egress_relay() {
let session = tempfile::tempdir().unwrap();
let mut inputs = inputs(SandboxEnforce::Fs);
inputs.session_cwd = session.path().to_path_buf();
let args = container_run_args(&inputs, &spec(), Path::new("claude"), &[], None);
assert!(args
.windows(2)
.any(|w| w[0] == "--cap-drop" && w[1] == "ALL"));
assert!(args
.windows(2)
.any(|w| w[0] == "--security-opt" && w[1] == "no-new-privileges"));
assert!(args
.windows(2)
.any(|w| w[0] == "--pids-limit" && w[1] == CONTAINER_PIDS_LIMIT));
#[cfg(unix)]
{
let expected = crate::container_egress::mount_owner(session.path())
.expect("a stat-able path yields an owner");
assert!(
args.windows(2)
.any(|w| w[0] == "--user" && w[1] == expected),
"missing --user {expected}: {args:?}"
);
}
}
#[test]
fn container_run_args_never_mount_the_real_cargo_root_for_a_session() {
let home = tempfile::tempdir().unwrap();
let cargo = home.path().join(".cargo");
for leaf in ["bin", "registry", "git"] {
std::fs::create_dir_all(cargo.join(leaf)).unwrap();
}
std::fs::write(cargo.join("credentials.toml"), "[registry]\ntoken=\"x\"\n").unwrap();
let _guard = crate::agent_env::EnvTestGuard::engage(&[
("CARGO_HOME", cargo.to_str().unwrap()),
("HOME", home.path().to_str().unwrap()),
]);
let mut out = Vec::new();
push_toolchain_caches(&mut out, ToolchainMount::Session);
let joined = out.join(" ");
let root = container_host_path(&cargo);
assert!(
!joined.contains(&mount_arg(&root, true)),
"the credential-bearing Cargo root must never be mounted: {out:?}"
);
for leaf in ["bin", "registry", "git"] {
let mounted = container_host_path(&cargo.join(leaf));
assert!(
joined.contains(&mount_arg(&mounted, true)),
"the {leaf} cache leaf must still cross read-only: {out:?}"
);
}
assert!(
out.windows(2)
.any(|w| w[0] == "-e" && w[1] == format!("CARGO_HOME={root}")),
"session mode must forward the cache-only CARGO_HOME: {out:?}"
);
}
#[test]
fn container_gate_wrap_args_mounts_policy_forwards_env_and_payload() {
let dir = tempfile::tempdir().unwrap();
let gate = dir.path().join("gate");
let mission = dir.path().join("mission");
let scratch = dir.path().join("scratch");
let extra = dir.path().join("extra");
for dir in [&gate, &mission, &scratch, &extra] {
std::fs::create_dir_all(dir).unwrap();
}
let kranz_dir = gate.join(".kranz");
std::fs::create_dir_all(&kranz_dir).unwrap();
let masked_token_file = kranz_dir.join("serve.token");
std::fs::write(&masked_token_file, "secret").unwrap();
let inputs = SandboxInputs {
enforce: SandboxEnforce::Fs,
session_cwd: gate.clone(),
mission_dir: mission.clone(),
tmpdir: scratch.clone(),
extra_write: vec![extra.clone()],
egress: Vec::new(),
validator_read_deny_roots: Vec::new(),
};
let env: std::collections::HashMap<String, String> = [
("ZZZ_BASE".to_string(), "deadbeef".to_string()),
("AAA_FIRST".to_string(), "1".to_string()),
("CARGO_HOME".to_string(), "/scratch/cache-only".to_string()),
("PATH".to_string(), "/usr/bin:/bin".to_string()),
("HOME".to_string(), "/caller/home".to_string()),
("TMPDIR".to_string(), "/caller/tmp".to_string()),
("RUSTUP_HOME".to_string(), "/caller/rustup".to_string()),
("NPM_CONFIG_CACHE".to_string(), "/caller/npm".to_string()),
]
.into_iter()
.collect();
let args = container_gate_run_args(
&inputs,
&spec(),
"cargo test --workspace",
&env,
"kranz-gate-test",
);
let joined = args.join(" ");
let abs = |p: &std::path::Path| container_host_path(p);
assert!(args.contains(&"--read-only".to_string()));
assert!(joined.contains(&mount_arg(&abs(&gate), false)));
assert!(joined.contains(&format!("--tmpfs {}:ro,", abs(&mission))));
assert!(joined.contains(&mount_arg(&abs(&scratch), false)));
assert!(joined.contains(&mount_arg(&abs(&extra), false)));
assert!(joined.contains(&format!("-w {}", abs(&gate))));
assert!(joined.contains(&format!("-e HOME={}", abs(&scratch))));
assert!(joined.contains(&format!("-e TMPDIR={}", abs(&scratch))));
assert!(
joined.contains(&format!("--tmpfs {}:ro,", abs(&kranz_dir)))
&& !joined.contains(&abs(&masked_token_file)),
"authority material must stay outside the private directory: {args:?}"
);
assert!(
args.windows(2)
.any(|w| w[0] == "--name" && w[1] == "kranz-gate-test"),
"the gate container must carry the caller-chosen name: {args:?}"
);
assert!(
joined.ends_with(&format!("{DEFAULT_IMAGE} sh -c cargo test --workspace")),
"image then sh -c payload: {args:?}"
);
let index_of = |needle: &str| {
args.windows(2)
.position(|w| w[0] == "-e" && w[1] == needle)
.unwrap_or_else(|| panic!("missing -e {needle}: {args:?}"))
};
assert!(index_of("AAA_FIRST=1") < index_of("ZZZ_BASE=deadbeef"));
index_of("CARGO_HOME=/scratch/cache-only");
index_of("PATH=/usr/bin:/bin");
for skipped in [
"-e HOME=/caller/home",
"-e TMPDIR=/caller/tmp",
"-e RUSTUP_HOME=/caller/rustup",
"-e NPM_CONFIG_CACHE=/caller/npm",
] {
assert!(
!joined.contains(skipped),
"builder-owned env key must not be forwarded with the caller value: {skipped}\n{args:?}"
);
}
}
#[test]
fn container_gate_wrap_args_never_mounts_the_real_cargo_root() {
let cargo = tempfile::tempdir().unwrap();
std::fs::create_dir_all(cargo.path().join("bin")).unwrap();
std::fs::write(cargo.path().join("credentials.toml"), "operator-secret").unwrap();
let _guard = crate::agent_env::EnvTestGuard::engage(&[(
"CARGO_HOME",
cargo.path().to_str().expect("utf-8 temp path"),
)]);
let dir = tempfile::tempdir().unwrap();
let inputs = SandboxInputs {
enforce: SandboxEnforce::Fs,
session_cwd: dir.path().join("gate"),
mission_dir: dir.path().join("mission"),
tmpdir: dir.path().join("scratch"),
extra_write: Vec::new(),
egress: Vec::new(),
validator_read_deny_roots: Vec::new(),
};
let env: std::collections::HashMap<String, String> =
[("CARGO_HOME".to_string(), "/scratch/cache-only".to_string())]
.into_iter()
.collect();
let args = container_gate_run_args(&inputs, &spec(), "true", &env, "kranz-gate-test");
let joined = args.join(" ");
let abs = |p: &std::path::Path| container_host_path(p);
let root = abs(cargo.path());
let bin = abs(&cargo.path().join("bin"));
assert!(
joined.contains(&mount_arg(&bin, true)),
"the shim dir must cross read-only: {args:?}"
);
assert!(
!joined.contains(&mount_arg(&root, true)),
"the credential-bearing Cargo root must NEVER be mounted: {args:?}"
);
assert!(
!joined.contains(&format!("-e CARGO_HOME={root}")),
"no -e may point CARGO_HOME at the real root: {args:?}"
);
assert!(
joined.contains("-e CARGO_HOME=/scratch/cache-only"),
"the caller's cache-only CARGO_HOME crosses instead: {args:?}"
);
}
#[test]
fn container_gate_wrap_args_fs_net_empty_egress_disables_network() {
let env = std::collections::HashMap::new();
let fs_net = container_gate_run_args(
&inputs(SandboxEnforce::FsNet),
&spec(),
"true",
&env,
"kranz-gate-test",
);
let network = fs_net
.windows(2)
.find(|w| w[0] == "--network")
.expect("fs+net must pass a --network flag");
assert_eq!(network[1], "none");
assert!(
fs_net
.iter()
.filter(|a| a.starts_with("HTTPS_PROXY="))
.all(|a| a == "HTTPS_PROXY="),
"offline gates must suppress inherited proxy configuration: {fs_net:?}"
);
let fs = container_gate_run_args(
&inputs(SandboxEnforce::Fs),
&spec(),
"true",
&env,
"kranz-gate-test",
);
assert!(
!fs.iter().any(|a| a == "--network"),
"fs must not restrict the network (runtime default bridge): {fs:?}"
);
}
#[test]
fn container_run_args_respects_image_override() {
let spec = ContainerSpec {
runtime: ContainerRuntime::Podman,
image: "ghcr.io/example/kranz-worker:1".to_string(),
network: None,
name: None,
};
let args = container_run_args(
&inputs(SandboxEnforce::Fs),
&spec,
Path::new("claude"),
&[],
None,
);
assert!(
args.iter().any(|a| a == "ghcr.io/example/kranz-worker:1"),
"configured image must be used: {args:?}"
);
assert!(!args.iter().any(|a| a == DEFAULT_IMAGE));
}
#[test]
fn container_provider_runs_a_trivial_worker_and_enforces_the_write_boundary() {
if !host_supports_container_contract() {
crate::test_capability::skip(
crate::test_capability::capability::CONTAINER,
&container_contract_skip_detail(),
);
return;
}
let Some(runtime) = detect() else {
crate::test_capability::skip(
crate::test_capability::capability::CONTAINER,
"no docker/podman/nerdctl/container on PATH",
);
return;
};
let session = live_fixture();
let mission = live_fixture();
let scratch = live_fixture();
let kranz_dir = session.path().join(".kranz");
std::fs::create_dir_all(&kranz_dir).unwrap();
std::fs::write(kranz_dir.join("serve.token"), "secret").unwrap();
let inputs = SandboxInputs {
enforce: SandboxEnforce::FsNet,
session_cwd: session.path().to_path_buf(),
mission_dir: mission.path().to_path_buf(),
tmpdir: scratch.path().to_path_buf(),
extra_write: Vec::new(),
egress: Vec::new(),
validator_read_deny_roots: Vec::new(),
};
let spec = ContainerSpec {
runtime,
image: DEFAULT_IMAGE.to_string(),
network: None,
name: None,
};
let ok_file = session.path().join("ok.txt");
let args = container_run_args(
&inputs,
&spec,
Path::new("sh"),
&[
"-c".to_string(),
format!(
"echo ok > {} && ! cat {} && echo nope > /etc/nope.txt",
ok_file.display(),
kranz_dir.join("serve.token").display()
),
],
None,
);
let output = std::process::Command::new(runtime.binary())
.args(&args)
.stdin(std::process::Stdio::null())
.output()
.expect("failed to spawn container runtime");
assert!(
ok_file.exists(),
"write inside the mounted session_cwd must land on the host: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!output.status.success(),
"write outside the declared policy (/etc) must be denied, failing the worker: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!String::from_utf8_lossy(&output.stdout).contains("secret"),
"the /dev/null mask must hide serve.token content inside the container"
);
}
#[test]
fn container_authority_directory_mask_covers_absent_and_future_tokens() {
if crate::agent_env::isolated_global_home_test("sandbox_container::tests::container_authority_directory_mask_covers_absent_and_future_tokens") { return; }
let home = tempfile::tempdir().unwrap();
let _env = crate::agent_env::EnvTestGuard::engage(&[(
if cfg!(windows) { "USERPROFILE" } else { "HOME" },
home.path().to_str().unwrap(),
)]);
let global = home.path().join(".kranz");
assert!(!global.exists());
let mut inputs = inputs(SandboxEnforce::Fs);
inputs.extra_write.extend([
home.path().to_path_buf(),
global.clone(),
global.join("serve"),
]);
for args in [
container_run_args(&inputs, &spec(), Path::new("sh"), &[], None),
container_gate_run_args(&inputs, &spec(), "true", &Default::default(), "test"),
] {
assert!(
args.windows(2).any(|pair| pair[0] == "--tmpfs"
&& pair[1]
== format!(
"{}:ro,noexec,nosuid,nodev,mode=755",
container_host_path(&global)
)),
"authority mask missing: {args:?}"
);
assert!(
!args
.windows(2)
.any(|pair| pair[0] == "-v"
&& pair[1].starts_with(&container_host_path(&global))),
"nested mounts must not reopen global authority: {args:?}"
);
}
assert!(!global.exists());
}
#[cfg(unix)]
#[test]
fn container_authority_directory_hides_tokens_created_after_start() {
if crate::agent_env::isolated_global_home_test("sandbox_container::tests::container_authority_directory_hides_tokens_created_after_start") { return; }
use std::io::{BufRead as _, Write as _};
let Some(runtime) = detect() else {
eprintln!("no container runtime; skipping live authority test");
return;
};
let dir = live_fixture();
let home = dir.path().join("operator");
let session = dir.path().join("session");
let mission = session.join(".kranz/missions/m-test");
let scratch = dir.path().join("scratch");
std::fs::create_dir_all(&home).unwrap();
let authority_target = dir.path().join("private-authority");
std::fs::create_dir(&authority_target).unwrap();
std::os::unix::fs::symlink(&authority_target, home.join(".kranz")).unwrap();
std::fs::create_dir_all(&mission).unwrap();
std::fs::create_dir(&scratch).unwrap();
let authority = home.join(".kranz/serve/later.token");
let global_config = home.join(".kranz/config.json");
let cargo = home.join(".cargo");
std::fs::create_dir(&cargo).unwrap();
let repo_token_path = session.join(".kranz/serve.token");
let repo_read_token_path = session.join(".kranz/serve.read.token");
let repo_config = session.join(".kranz/config.json");
let cargo_credentials = cargo.join("credentials.toml");
let policy = session.join(".kranz/merge-gates.json");
std::fs::write(&repo_token_path, "original-token").unwrap();
std::fs::write(&policy, "visible-policy").unwrap();
let input = SandboxInputs {
enforce: SandboxEnforce::FsNet,
session_cwd: session.clone(),
mission_dir: mission,
tmpdir: scratch,
extra_write: vec![home.clone(), home.join(".kranz/serve")],
egress: Vec::new(),
validator_read_deny_roots: Vec::new(),
};
let args = {
let _env = crate::agent_env::EnvTestGuard::engage(&[
("HOME", home.to_str().unwrap()),
("CARGO_HOME", cargo.to_str().unwrap()),
]);
container_run_args(
&input,
&ContainerSpec {
runtime,
network: None,
name: None,
image: DEFAULT_IMAGE.to_string(),
},
Path::new("sh"),
&[
"-c".to_string(),
"printf 'ready\\n'; read -r proceed; test -s \"$1\" || exit 2; \
for secret in \"$2\" \"$3\" \"$4\" \"$5\" \"$6\" \"$7\"; do \
if cat \"$secret\"; then exit 3; fi; \
if printf forged > \"$secret\"; then exit 4; fi; done; \
if rm \"$9\"; then exit 5; fi; \
test \"$(cat \"$8\")\" = visible-policy || exit 8; \
printf work > \"$1-worker\""
.to_string(),
"test".to_string(),
session.join("host-witness").display().to_string(),
authority.display().to_string(),
global_config.display().to_string(),
repo_token_path.display().to_string(),
repo_read_token_path.display().to_string(),
repo_config.display().to_string(),
cargo_credentials.display().to_string(),
policy.display().to_string(),
home.join(".kranz").display().to_string(),
],
None,
)
};
let _env = crate::agent_env::EnvTestGuard::engage(&[]);
let mut child = std::process::Command::new(runtime.binary())
.args(args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let mut stdout = std::io::BufReader::new(child.stdout.take().unwrap());
let mut line = String::new();
stdout.read_line(&mut line).unwrap();
if line != "ready\n" {
let _ = child.kill();
let output = child.wait_with_output().unwrap();
panic!(
"container did not start: {line:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
std::fs::create_dir(authority.parent().unwrap()).unwrap();
std::fs::write(&authority, "fake-authority").unwrap();
std::fs::write(&global_config, "fake-config").unwrap();
for path in [&repo_read_token_path, &repo_config, &cargo_credentials] {
assert!(
!path.exists(),
"mount setup created a placeholder credential"
);
std::fs::write(path, "fake-authority").unwrap();
}
let rotated = session.join(".kranz/rotated.tmp");
std::fs::write(&rotated, "rotated-token").unwrap();
std::fs::rename(rotated, &repo_token_path).unwrap();
std::fs::write(session.join("host-witness"), "visible").unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"continue\n")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success(), "{output:?}");
assert!(session.join("host-witness-worker").exists());
assert!(
home.join(".kranz").is_symlink(),
"authority alias was replaced"
);
assert_eq!(
std::fs::read_to_string(repo_token_path).unwrap(),
"rotated-token"
);
for path in [&repo_read_token_path, &repo_config, &cargo_credentials] {
assert_eq!(std::fs::read_to_string(path).unwrap(), "fake-authority");
}
assert_eq!(
std::fs::read_to_string(global_config).unwrap(),
"fake-config"
);
}
}
#[cfg(test)]
mod git_mount_tests {
use super::*;
#[test]
fn git_config_mount_nodes_preserve_existing_readonly_destinations() {
let root = tempfile::tempdir().unwrap();
let root = crate::sandbox::absolutize(root.path());
let git = root.join(".git");
std::fs::create_dir(&git).unwrap();
std::fs::write(git.join("config"), "[core]\nrepositoryformatversion = 0\n").unwrap();
let inputs = SandboxInputs {
enforce: crate::types::SandboxEnforce::Fs,
session_cwd: root.clone(),
mission_dir: root.join(".kranz/missions/m-fixture"),
tmpdir: root.join("scratch"),
extra_write: Vec::new(),
egress: Vec::new(),
validator_read_deny_roots: Vec::new(),
};
let root = container_host_path(&root);
let git = container_host_path(&git);
let mut args = vec![
"-v".into(),
mount_arg(&root, false),
"-v".into(),
mount_arg(&git, true),
];
push_authority_masks(&mut args, &inputs);
let duplicates = args
.windows(2)
.filter(|part| {
part[0] == "-v"
&& (part[1] == mount_arg(&git, false) || part[1] == mount_arg(&git, true))
})
.count();
assert_eq!(duplicates, 1, "{args:?}");
assert!(args
.windows(2)
.any(|part| part[0] == "-v" && part[1] == mount_arg(&git, true)));
}
}