use crate::container::Plan;
use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Caps {
pub file_mounts: bool,
pub free_guest_paths: bool,
}
pub trait Runtime: std::fmt::Debug {
#[allow(dead_code)]
fn name(&self) -> &'static str;
fn program(&self) -> &'static str;
fn caps(&self) -> Caps;
fn args(&self, plan: &Plan) -> Vec<String>;
fn up_args(&self, plan: &Plan, name: &str, port: u16, pubkey: &str) -> Vec<String>;
fn exec_args(&self, name: &str, argv: &[String], tty: bool) -> Vec<String>;
#[allow(dead_code)]
fn exec_detached_args(&self, name: &str, argv: &[String]) -> Vec<String>;
}
#[derive(Debug)]
pub struct Docker;
#[derive(Debug)]
pub struct Sbx;
pub const NAMES: [&str; 2] = ["docker", "sbx"];
impl Runtime for Docker {
fn name(&self) -> &'static str {
"docker"
}
fn program(&self) -> &'static str {
"docker"
}
fn caps(&self) -> Caps {
Caps {
file_mounts: true,
free_guest_paths: true,
}
}
fn args(&self, plan: &Plan) -> Vec<String> {
let mut a: Vec<String> = vec!["run".into(), "--rm".into()];
if plan.tty {
a.push("-it".into());
}
for m in &plan.mounts {
a.push("-v".into());
a.push(format!(
"{}:{}{}",
m.host.display(),
m.guest.display(),
if m.read_only { ":ro" } else { "" }
));
}
for (k, v) in &plan.env {
a.push("-e".into());
a.push(format!("{k}={v}"));
}
a.extend(["--network".into(), plan.network.clone()]);
a.extend(["-w".into(), plan.workdir.clone()]);
a.push(plan.image.clone());
a.extend(plan.argv.iter().cloned());
a
}
fn up_args(&self, plan: &Plan, name: &str, port: u16, pubkey: &str) -> Vec<String> {
let mut a: Vec<String> = vec!["run".into(), "-d".into(), "--name".into(), name.into()];
a.push("-p".into());
a.push(format!("127.0.0.1:{port}:22"));
for m in &plan.mounts {
a.push("-v".into());
a.push(format!(
"{}:{}{}",
m.host.display(),
m.guest.display(),
if m.read_only { ":ro" } else { "" }
));
}
for (k, v) in &plan.env {
a.push("-e".into());
a.push(format!("{k}={v}"));
}
a.push("-e".into());
a.push(format!("OMH_PUBKEY={pubkey}"));
a.extend(["--network".into(), plan.network.clone()]);
a.extend(["-w".into(), plan.workdir.clone()]);
a.push(plan.image.clone());
a.push("omh-session".into());
a
}
fn exec_detached_args(&self, name: &str, argv: &[String]) -> Vec<String> {
let mut a: Vec<String> = vec![
"exec".into(),
"-d".into(),
"-u".into(),
"agent".into(),
"-w".into(),
crate::container_workdir().into(),
];
a.push(name.into());
a.extend(argv.iter().cloned());
a
}
fn exec_args(&self, name: &str, argv: &[String], tty: bool) -> Vec<String> {
let mut a: Vec<String> = vec!["exec".into()];
if tty {
a.push("-it".into());
}
a.extend([
"-u".into(),
"agent".into(),
"-w".into(),
crate::container_workdir().into(),
]);
a.push(name.into());
a.extend(argv.iter().cloned());
a
}
}
impl Runtime for Sbx {
fn name(&self) -> &'static str {
"sbx"
}
fn program(&self) -> &'static str {
"sbx"
}
fn caps(&self) -> Caps {
Caps {
file_mounts: false,
free_guest_paths: false,
}
}
fn args(&self, plan: &Plan) -> Vec<String> {
let mut a: Vec<String> = vec!["run".into()];
for m in &plan.mounts {
a.push("--workspace".into());
a.push(format!(
"{}{}",
m.host.display(),
if m.read_only { ":ro" } else { "" }
));
}
for (k, v) in &plan.env {
a.push("--env".into());
a.push(format!("{k}={v}"));
}
a.push("--".into());
a.extend(plan.argv.iter().cloned());
a
}
fn up_args(&self, plan: &Plan, name: &str, port: u16, pubkey: &str) -> Vec<String> {
let mut a: Vec<String> = vec![
"run".into(),
"--detach".into(),
"--name".into(),
name.into(),
];
a.push("--publish".into());
a.push(format!("127.0.0.1:{port}:22"));
for m in &plan.mounts {
a.push("--workspace".into());
a.push(format!(
"{}{}",
m.host.display(),
if m.read_only { ":ro" } else { "" }
));
}
a.push("--env".into());
a.push(format!("OMH_PUBKEY={pubkey}"));
a.push("--".into());
a.push("omh-session".into());
a
}
fn exec_detached_args(&self, name: &str, argv: &[String]) -> Vec<String> {
let mut a: Vec<String> = vec!["exec".into(), "--detach".into(), name.into(), "--".into()];
a.extend(argv.iter().cloned());
a
}
fn exec_args(&self, name: &str, argv: &[String], tty: bool) -> Vec<String> {
let mut a: Vec<String> = vec!["exec".into()];
if tty {
a.push("-it".into());
}
a.push(name.into());
a.push("--".into());
a.extend(argv.iter().cloned());
a
}
}
pub fn select(preference: &str, available: &dyn Fn(&str) -> bool) -> Result<Box<dyn Runtime>> {
let build = |name: &str| -> Option<Box<dyn Runtime>> {
match name {
"docker" => Some(Box::new(Docker)),
"sbx" => Some(Box::new(Sbx)),
_ => None,
}
};
if preference == "auto" {
for name in ["sbx", "docker"] {
if available(name) {
return Ok(build(name).expect("name from the known list"));
}
}
anyhow::bail!(
"no container runtime found — install one of: {}",
NAMES.join(", ")
);
}
let Some(runtime) = build(preference) else {
anyhow::bail!(
"unknown runtime `{preference}` — expected one of: {}",
NAMES.join(", ")
);
};
if !available(preference) {
anyhow::bail!("runtime `{preference}` is not installed");
}
Ok(runtime)
}
pub fn installed(program: &str) -> bool {
std::process::Command::new("sh")
.args(["-c", &format!("command -v {program}")])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::Mount;
use std::path::PathBuf;
fn plan_with(mounts: Vec<Mount>) -> Plan {
Plan {
image: "omh/claude:latest".into(),
mounts,
env: vec![("OMH_SESSION".into(), "s01".into())],
network: "omh-repo".into(),
workdir: "/work".into(),
argv: vec!["claude".into()],
dropped: vec![],
dropped_hooks: vec![],
rules: Default::default(),
tty: true,
}
}
fn dir_mount(host: &str, guest: &str, read_only: bool) -> Mount {
Mount {
host: PathBuf::from(host),
guest: PathBuf::from(guest),
read_only,
file: false,
}
}
fn sample_plan() -> Plan {
plan_with(vec![
dir_mount("/host/worktree", "/work", false),
dir_mount("/host/skills", "/home/agent/.claude/skills", true),
])
}
#[test]
fn docker_can_do_everything_the_current_design_assumes() {
let c = Docker.caps();
assert!(c.file_mounts);
assert!(c.free_guest_paths);
}
#[test]
fn sbx_capabilities_stay_conservative_until_verified() {
let c = Sbx.caps();
assert!(!c.file_mounts, "unverified: assume no");
assert!(
!c.free_guest_paths,
"sbx mounts workspaces at their host path"
);
}
fn only(present: &'static str) -> impl Fn(&str) -> bool {
move |p: &str| p == present
}
#[test]
fn auto_prefers_the_stronger_isolation() {
let r = select("auto", &|_| true).unwrap();
assert_eq!(r.name(), "sbx");
}
#[test]
fn auto_falls_back_when_sbx_is_absent() {
let r = select("auto", &only("docker")).unwrap();
assert_eq!(r.name(), "docker");
}
#[test]
fn auto_fails_clearly_when_nothing_is_installed() {
let err = select("auto", &|_| false).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("docker") && msg.contains("sbx"), "got: {msg}");
}
#[test]
fn an_explicit_choice_overrides_detection() {
let r = select("docker", &|_| true).unwrap();
assert_eq!(r.name(), "docker");
}
#[test]
fn an_explicit_choice_that_is_not_installed_is_an_error() {
let err = select("sbx", &only("docker")).unwrap_err();
assert!(err.to_string().contains("sbx"), "got: {err}");
}
#[test]
fn an_unknown_runtime_lists_the_real_ones() {
let err = select("podman", &|_| true).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("docker") && msg.contains("sbx"), "got: {msg}");
}
#[test]
fn a_directory_only_plan_passes_on_any_backend() {
for caps in [
Docker.caps(),
Caps {
file_mounts: false,
free_guest_paths: true,
},
] {
sample_plan().validate(&caps).unwrap();
}
}
#[test]
fn a_plan_needing_file_mounts_is_refused_by_a_backend_without_them() {
let plan = plan_with(vec![
dir_mount("/host/worktree", "/work", false),
Mount {
host: PathBuf::from("/host/run/mcp.rendered"),
guest: PathBuf::from("/home/agent/.mcp.json"),
read_only: true,
file: true,
},
]);
plan.validate(&Docker.caps())
.expect("docker supports file mounts");
let err = plan.validate(&Sbx.caps()).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains(".mcp.json"),
"must name the offending mount: {msg}"
);
}
#[test]
fn a_plan_relocating_guest_paths_is_refused_when_paths_are_not_free() {
let caps = Caps {
file_mounts: true,
free_guest_paths: false,
};
let err = sample_plan().validate(&caps).unwrap_err();
assert!(format!("{err:#}").contains("/work"), "got: {err:#}");
}
#[test]
fn a_plan_without_a_tty_does_not_ask_for_one() {
let mut plan = sample_plan();
plan.tty = false;
let args = Docker.args(&plan);
assert!(!args.contains(&"-it".to_string()), "got: {args:?}");
plan.tty = true;
assert!(Docker.args(&plan).contains(&"-it".to_string()));
}
#[test]
fn docker_passes_every_mount_and_preserves_read_only() {
let plan = sample_plan();
let args = Docker.args(&plan).join(" ");
for m in &plan.mounts {
assert!(
args.contains(&m.host.display().to_string()),
"missing {m:?}"
);
}
assert_eq!(args.matches(":ro").count(), 1);
assert!(args.contains("-w /work"));
assert!(args.ends_with("claude"), "harness argv comes last: {args}");
}
#[test]
fn sbx_carries_the_same_information_as_docker() {
let plan = sample_plan();
let args = Sbx.args(&plan).join(" ");
for m in &plan.mounts {
assert!(
args.contains(&m.host.display().to_string()),
"missing {m:?}"
);
}
assert!(args.contains(":ro"), "read-only mounts must stay read-only");
assert!(args.ends_with("claude"), "harness argv comes last: {args}");
}
#[test]
fn no_backend_may_widen_write_access() {
let plan = sample_plan();
let writable: Vec<_> = plan.mounts.iter().filter(|m| !m.read_only).collect();
assert_eq!(writable.len(), 1);
for backend in [&Docker as &dyn Runtime, &Sbx as &dyn Runtime] {
let args = backend.args(&plan).join(" ");
let ro = args.matches(":ro").count();
assert_eq!(
ro,
plan.mounts.len() - 1,
"{} dropped a read-only marker",
backend.name()
);
}
}
#[test]
fn the_session_publishes_ssh_on_loopback_only() {
let joined = Docker
.up_args(&sample_plan(), "omh-repo-s01", 49200, "ssh-ed25519 AAA")
.join(" ");
assert!(joined.contains("127.0.0.1:49200:22"), "got: {joined}");
assert!(!joined.contains("0.0.0.0"));
}
#[test]
fn published_ports_are_loopback_only() {
let joined = Docker.up_args(&sample_plan(), "n", 49200, "k").join(" ");
assert!(!joined.contains("0.0.0.0"), "got: {joined}");
assert_eq!(
joined.matches("127.0.0.1:").count(),
1,
"ssh only: {joined}"
);
}
#[test]
fn the_session_runs_detached_and_named() {
let args = Docker.up_args(&sample_plan(), "omh-repo-s01", 49200, "k");
assert!(
args.contains(&"-d".to_string()),
"must outlive the terminal: {args:?}"
);
assert!(args
.windows(2)
.any(|w| w[0] == "--name" && w[1] == "omh-repo-s01"));
assert!(
!args.contains(&"--rm".to_string()),
"a session must survive its launch"
);
}
#[test]
fn the_session_carries_every_mount() {
let plan = sample_plan();
let joined = Docker.up_args(&plan, "n", 1, "k").join(" ");
for m in &plan.mounts {
assert!(
joined.contains(&m.host.display().to_string()),
"missing {m:?}"
);
}
assert_eq!(joined.matches(":ro").count(), plan.mounts.len() - 1);
}
#[test]
fn the_public_key_reaches_the_session() {
let joined = Docker
.up_args(&sample_plan(), "n", 1, "ssh-ed25519 AAAkey")
.join(" ");
assert!(joined.contains("ssh-ed25519 AAAkey"), "got: {joined}");
}
#[test]
fn exec_targets_the_named_session_and_runs_unprivileged() {
let args = Docker.exec_args("omh-repo-s01", &["claude".into()], true);
assert_eq!(args[0], "exec");
assert!(args.contains(&"omh-repo-s01".to_string()));
assert!(
args.windows(2).any(|w| w[0] == "-u" && w[1] == "agent"),
"got: {args:?}"
);
assert_eq!(args.last().unwrap(), "claude");
}
#[test]
fn a_detached_exec_outlives_the_caller() {
let args = Docker.exec_detached_args("omh-repo-s01", &["sh".into()]);
assert!(args.contains(&"-d".to_string()), "got: {args:?}");
assert!(args.contains(&"omh-repo-s01".to_string()));
}
#[test]
fn exec_asks_for_a_terminal_only_when_there_is_one() {
assert!(Docker
.exec_args("n", &["x".into()], true)
.contains(&"-it".to_string()));
assert!(!Docker
.exec_args("n", &["x".into()], false)
.contains(&"-it".to_string()));
}
}
#[cfg(test)]
mod workdir_tests {
#[test]
fn only_one_place_spells_the_container_workdir() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut offenders = Vec::new();
let mut stack = vec![root];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).unwrap().flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().is_none_or(|e| e != "rs") {
continue;
}
let body = std::fs::read_to_string(&path).unwrap();
let production = body.split("#[cfg(test)]").next().unwrap_or("");
for (i, line) in production.lines().enumerate() {
if line.contains("\"/work\"") {
let name = path.file_name().unwrap().to_string_lossy().to_string();
offenders.push(format!("{name}:{}", i + 1));
}
}
}
}
assert_eq!(
offenders.len(),
1,
"exactly one place may spell it — `container_workdir` itself. Found: {offenders:?}"
);
}
}