use crate::adapter::Adapter;
use crate::base::GRAPH_CACHE;
use anyhow::Result;
use std::path::Path;
pub fn base_tag() -> String {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
base_dockerfile().hash(&mut h);
format!("omh/base:{:x}", h.finish())
}
pub fn tag_for(adapter: &Adapter) -> String {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
harness_dockerfile(adapter).hash(&mut h);
base_dockerfile().hash(&mut h);
format!("omh/{}:{:x}", adapter.name, h.finish())
}
pub const GUEST_HOME: &str = "/home/agent";
fn digest_command() -> std::process::Command {
let mut c = std::process::Command::new("git");
c.env("GIT_DIR", "/omh-recipe-digest-has-no-repository")
.args(["hash-object", "--stdin"]);
c
}
pub fn recipe_digest(recipe: &str) -> Result<String> {
use anyhow::Context;
use std::io::Write;
use std::process::Stdio;
let mut child = digest_command()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.context("running git hash-object")?;
child
.stdin
.take()
.context("no stdin")?
.write_all(recipe.as_bytes())?;
let out = child.wait_with_output()?;
anyhow::ensure!(out.status.success(), "git hash-object failed");
Ok(String::from_utf8(out.stdout)?.trim().to_string())
}
pub fn base_dockerfile() -> String {
let notes = crate::memory::GUEST_LOCAL_NOTES;
format!(
r#"FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git ripgrep dtach sudo curl less jq procps openssh-server socat \
&& rm -rf /var/lib/apt/lists/*
RUN usermod -l agent -d {GUEST_HOME} -m node \
&& groupmod -n agent node \
&& echo 'agent ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/agent \
&& chmod 0440 /etc/sudoers.d/agent
# Assert the sbx kit contract at build time rather than assuming it. If a future
# base image moves UID 1000, this fails here instead of failing mysteriously
# inside a sandbox.
RUN test "$(id -u agent)" = "1000" && test "$(getent passwd agent | cut -d: -f6)" = "{GUEST_HOME}"
# The base set lives here, not in a harness layer: a code graph is
# harness-agnostic and every session should get the same one.
ARG TARGETARCH
RUN __GRAPH_INSTALL__
# Mount points the launcher expects to exist, owned by the unprivileged user.
# The graph cache is a volume; the image only needs the directory to exist and
# be owned by the agent, or docker creates it as root.
RUN mkdir -p /work /omh/sock /omh/cache /omh/layers {notes} {GRAPH_CACHE} \
&& chown -R agent:agent /work /omh {GUEST_HOME}/.cache
# Session entrypoint: install the key, start sshd, then stay alive so the
# container outlives the command that created it. The key arrives as an env var
# because a bind-mounted authorized_keys lands with host ownership and sshd
# silently refuses to read one it does not trust.
RUN printf '%s\n' \
'#!/bin/sh' \
'set -e' \
'mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"' \
'if [ -n "$OMH_PUBKEY" ]; then' \
' printf "%s\\n" "$OMH_PUBKEY" > "$HOME/.ssh/authorized_keys"' \
' chmod 600 "$HOME/.ssh/authorized_keys"' \
'fi' \
'sudo ssh-keygen -A >/dev/null 2>&1 || true' \
'sudo mkdir -p /run/sshd' \
'sudo /usr/sbin/sshd' \
'exec sleep infinity' \
> /usr/local/bin/omh-session \
&& chmod 0755 /usr/local/bin/omh-session
# Proxy forwarding, for backends that filter egress through one.
ENV HTTP_PROXY="" HTTPS_PROXY="" NO_PROXY=""
USER agent
WORKDIR /work
"#
)
.replace("__GRAPH_INSTALL__", &crate::base::graph_install())
}
pub fn harness_dockerfile(adapter: &Adapter) -> String {
let mut df = format!("FROM {}\nUSER root\nRUN {}\n", base_tag(), adapter.install);
let dirs = mount_parents(adapter);
if !dirs.is_empty() {
df.push_str(&format!(
"RUN mkdir -p {0} && chown -R agent:agent {0}\n",
dirs.join(" ")
));
}
df.push_str("USER agent\nWORKDIR /work\n");
df
}
pub fn stack_dockerfile(adapter: &Adapter, installs: &[&str]) -> String {
let mut df = format!("FROM {}\nUSER root\n", tag_for(adapter));
for install in installs {
df.push_str(&format!("RUN {install}\n"));
}
df.push_str("USER agent\nWORKDIR /work\n");
df
}
pub fn stack_tag(adapter: &Adapter, installs: &[&str]) -> String {
if installs.is_empty() {
return tag_for(adapter);
}
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
stack_dockerfile(adapter, installs).hash(&mut h);
tag_for(adapter).hash(&mut h);
base_dockerfile().hash(&mut h);
format!("omh/{}:{:x}", adapter.name, h.finish())
}
pub fn ensure_stack(
program: &str,
adapter: &Adapter,
installs: &[&str],
repo: &Path,
) -> Result<String> {
ensure(program, adapter)?;
let tag = stack_tag(adapter, installs);
if tag != tag_for(adapter) && !exists(program, &tag) {
eprintln!("omh: building {tag} — this repo's toolchain, first run only");
build(
program,
&tag,
&stack_dockerfile(adapter, installs),
&Kind::Stack(adapter, repo),
)?;
}
Ok(tag)
}
fn mount_parents(adapter: &Adapter) -> Vec<String> {
let mut dirs: Vec<String> = adapter
.capabilities
.values()
.map(|b| b.path.clone())
.chain(adapter.creds.iter().cloned())
.chain(adapter.token.iter().cloned())
.filter_map(|template| {
let path = crate::adapter::expand(template.trim_end_matches('/'), GUEST_HOME);
if !path.starts_with(GUEST_HOME) {
return None;
}
path.parent().map(|p| p.display().to_string())
})
.filter(|d| d != GUEST_HOME)
.collect();
dirs.sort();
dirs.dedup();
dirs
}
pub fn build_args(tag: &str, context: &Path, kind: &Kind) -> Vec<String> {
let mut a: Vec<String> = vec!["build".into(), "-t".into(), tag.into()];
for (k, v) in kind.stamp() {
a.push("--label".into());
a.push(format!("{k}={v}"));
}
a.extend([
"-f".into(),
"-".into(),
context.to_string_lossy().into_owned(),
]);
a
}
pub enum Kind<'a> {
Base,
Harness(&'a Adapter),
Stack(&'a Adapter, &'a Path),
}
impl Kind<'_> {
pub fn stamp(&self) -> Vec<(String, String)> {
let mut s = vec![("omh.kind".to_string(), self.name().to_string())];
match self {
Kind::Base => {}
Kind::Harness(a) => s.push(("omh.adapter".into(), a.name.clone())),
Kind::Stack(a, repo) => {
s.push(("omh.adapter".into(), a.name.clone()));
s.push(("omh.repo".into(), repo.to_string_lossy().into_owned()));
}
}
s
}
fn name(&self) -> &'static str {
match self {
Kind::Base => "base",
Kind::Harness(_) => "harness",
Kind::Stack(..) => "stack",
}
}
pub fn list_args(&self) -> Vec<String> {
let mut a: Vec<String> = vec!["images".into()];
for (k, v) in self.stamp() {
a.push("--filter".into());
a.push(format!("label={k}={v}"));
}
a.extend(["--format".into(), "{{.Repository}}:{{.Tag}}".into()]);
a
}
}
pub fn probe_args(tag: &str, script: &str) -> Vec<String> {
vec![
"run".into(),
"--rm".into(),
"--pull=never".into(),
tag.into(),
"sh".into(),
"-c".into(),
script.into(),
]
}
pub fn ensure(program: &str, adapter: &Adapter) -> Result<()> {
let base = base_tag();
if !exists(program, &base) {
eprintln!("omh: building {base} (first run only)");
build(program, &base, &base_dockerfile(), &Kind::Base)?;
}
let t = tag_for(adapter);
if !exists(program, &t) {
eprintln!("omh: building {t}");
build(
program,
&t,
&harness_dockerfile(adapter),
&Kind::Harness(adapter),
)?;
}
Ok(())
}
fn build(program: &str, tag: &str, dockerfile: &str, kind: &Kind) -> Result<()> {
use anyhow::Context;
use std::io::Write;
use std::process::Stdio;
let context = std::env::temp_dir().join("omh-build-context");
std::fs::create_dir_all(&context)?;
let mut child = std::process::Command::new(program)
.args(build_args(tag, &context, kind))
.stdin(Stdio::piped())
.spawn()
.with_context(|| format!("running {program} build"))?;
child
.stdin
.as_mut()
.context("build stdin")?
.write_all(dockerfile.as_bytes())?;
let status = child.wait()?;
if !status.success() {
anyhow::bail!("failed to build {tag}");
}
reap(program, tag, kind);
Ok(())
}
fn reap(program: &str, built: &str, kind: &Kind) {
let tags = match list_tags(program, kind) {
Ok(t) => t,
Err(e) => {
eprintln!("omh: could not list images to reap: {e}");
return;
}
};
let in_use = images_in_use(program);
let mut gone = Vec::new();
for tag in superseded(built, &tags, &in_use) {
match remove_image(program, &tag) {
Removal::Deleted => gone.push(tag),
Removal::InUse => {}
Removal::Untagged => {}
Removal::Failed(why) => eprintln!("omh: could not remove {tag}: {why}"),
}
}
if !gone.is_empty() {
eprintln!("omh: removed {} this build replaced", gone.join(", "));
}
}
enum Removal {
Deleted,
Untagged,
InUse,
Failed(String),
}
fn remove_image(program: &str, tag: &str) -> Removal {
let out = match std::process::Command::new(program)
.args(["image", "rm", tag])
.output()
{
Ok(o) => o,
Err(e) => return Removal::Failed(e.to_string()),
};
if out.status.success() {
return classify_removal(&String::from_utf8_lossy(&out.stdout));
}
let err = String::from_utf8_lossy(&out.stderr);
if err.contains("is using its referenced image") {
Removal::InUse
} else {
Removal::Failed(err.trim().to_string())
}
}
fn classify_removal(stdout: &str) -> Removal {
if stdout
.lines()
.any(|l| l.trim_start().starts_with("Deleted:"))
{
Removal::Deleted
} else {
Removal::Untagged
}
}
fn list_tags(program: &str, kind: &Kind) -> Result<Vec<String>> {
let out = std::process::Command::new(program)
.args(kind.list_args())
.output()?;
anyhow::ensure!(out.status.success(), "listing images to reap");
Ok(String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect())
}
fn images_in_use(program: &str) -> Vec<String> {
let read = |args: &[&str]| -> Vec<String> {
std::process::Command::new(program)
.args(args)
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect()
})
.unwrap_or_default()
};
let mut v = read(&["ps", "-a", "--format", "{{.Image}}"]);
v.extend(read(&[
"ps",
"-a",
"--filter",
"label=omh.image",
"--format",
"{{.Label \"omh.image\"}}",
]));
v.sort();
v.dedup();
v
}
pub fn ensure_network(program: &str, name: &str) -> Result<()> {
let present = std::process::Command::new(program)
.args(["network", "inspect", name])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if present {
return Ok(());
}
let out = std::process::Command::new(program)
.args(["network", "create", name])
.output()?;
if !out.status.success() {
anyhow::bail!(
"creating network {name}: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Running {
Yes,
No,
Unknown(String),
}
pub fn running_from(name: &str, asked: std::io::Result<std::process::Output>) -> Running {
let out = match asked {
Ok(out) => out,
Err(e) => {
return Running::Unknown(crate::out::untrusted(&format!(
"could not run the container runtime: {e}"
)))
}
};
if !out.status.success() {
return Running::Unknown(unreadable(
&String::from_utf8_lossy(&out.stderr),
&out.status,
));
}
match String::from_utf8_lossy(&out.stdout)
.lines()
.any(|listed| listed.trim() == name)
{
true => Running::Yes,
false => Running::No,
}
}
pub fn container_running(backend: &dyn crate::runtime::Runtime, name: &str) -> Running {
running_from(
name,
std::process::Command::new(backend.program())
.args(backend.running_args())
.output(),
)
}
fn unreadable(said: &str, status: &std::process::ExitStatus) -> String {
let said = crate::out::untrusted(said.trim());
match said.is_empty() {
false => said,
true => match status.code() {
Some(code) => format!("the container runtime exited {code}"),
None => "the container runtime was killed by a signal".into(),
},
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Probe {
Listed(String),
NotEnterable,
Gone,
Unknown(String),
}
const BROKEN_MOUNT: &str = "container mount namespace root";
const RUNTIME_ERROR: &str = "OCI runtime exec failed";
const DAEMON_ANSWERED: &str = "Error response from daemon";
pub fn probe_command() -> Vec<String> {
vec![
"sh".into(),
"-c".into(),
format!(
"[ -d {dir} ] || exit 0; ls -1 {dir}",
dir = crate::persist::SOCKET_DIR
),
]
}
pub fn probe_from(asked: std::io::Result<std::process::Output>) -> Probe {
let out = match asked {
Ok(out) => out,
Err(e) => {
return Probe::Unknown(crate::out::untrusted(&format!(
"could not run the container runtime: {e}"
)))
}
};
if out.status.success() {
return Probe::Listed(crate::out::untrusted(&String::from_utf8_lossy(&out.stdout)));
}
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let runtime_said = match stdout.contains(RUNTIME_ERROR) {
true => format!("{stdout}{stderr}"),
false => stderr.into_owned(),
};
if runtime_said.contains(BROKEN_MOUNT) {
return Probe::NotEnterable;
}
if runtime_said.contains(DAEMON_ANSWERED) {
return Probe::Gone;
}
Probe::Unknown(unreadable(&runtime_said, &out.status))
}
pub fn container_probe(program: &str, args: &[String]) -> Probe {
probe_from(std::process::Command::new(program).args(args).output())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stamp {
Read(std::collections::BTreeMap<String, String>),
Unknown(String),
}
pub fn container_stamp(program: &str, name: &str) -> Stamp {
stamp_from(
std::process::Command::new(program)
.args(["inspect", "-f", "{{json .Config.Labels}}", name])
.output(),
)
}
pub fn stamp_from(asked: std::io::Result<std::process::Output>) -> Stamp {
let out = match asked {
Ok(out) => out,
Err(e) => {
return Stamp::Unknown(crate::out::untrusted(&format!(
"could not run the container runtime: {e}"
)))
}
};
if !out.status.success() {
return Stamp::Unknown(unreadable(
&String::from_utf8_lossy(&out.stderr),
&out.status,
));
}
let said = String::from_utf8_lossy(&out.stdout);
if said.trim() == "null" {
return Stamp::Read(Default::default());
}
match serde_json::from_str::<std::collections::BTreeMap<String, String>>(said.trim()) {
Ok(all) => Stamp::Read(
all.into_iter()
.filter(|(k, _)| k.starts_with("omh."))
.collect(),
),
Err(e) => Stamp::Unknown(crate::out::untrusted(&format!(
"the container runtime answered with something omh could not read: {e}"
))),
}
}
pub fn container_remove(program: &str, name: &str) -> Result<()> {
let out = std::process::Command::new(program)
.args(["rm", "-f", name])
.output()?;
if !out.status.success() {
anyhow::bail!("{}", String::from_utf8_lossy(&out.stderr).trim());
}
Ok(())
}
pub fn superseded(built: &str, tags: &[String], in_use: &[String]) -> Vec<String> {
tags.iter()
.filter(|t| t.as_str() != built)
.filter(|t| !t.ends_with(":latest"))
.filter(|t| !in_use.iter().any(|u| u == *t))
.cloned()
.collect()
}
pub fn exists(program: &str, tag: &str) -> bool {
std::process::Command::new(program)
.args(["image", "inspect", tag])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
fn output(code: i32, stdout: &str, stderr: &str) -> std::io::Result<std::process::Output> {
use std::os::unix::process::ExitStatusExt;
Ok(std::process::Output {
status: std::process::ExitStatus::from_raw(code << 8),
stdout: stdout.as_bytes().to_vec(),
stderr: stderr.as_bytes().to_vec(),
})
}
#[test]
fn a_probe_that_failed_some_other_way_does_not_read_as_a_broken_worktree() {
let namespace = "OCI runtime exec failed: exec failed: unable to start container \
process: current working directory is outside of container mount namespace \
root -- possible container breakout detected";
assert_eq!(
probe_from(output(0, "agent.sock\n", "")),
Probe::Listed("agent.sock\n".into()),
"a healthy exec answers with what it listed"
);
assert_eq!(
probe_from(output(0, "", "")),
Probe::Listed(String::new()),
"including an empty listing — the socket directory is not there yet"
);
let Probe::Listed(listed) = probe_from(output(0, "s01-\u{1b}[2Jclaude\n", "")) else {
panic!("a successful exec is a listing");
};
assert!(
!listed.contains('\u{1b}'),
"and nothing in it repaints the terminal: {listed:?}"
);
for (code, stdout, stderr, what) in [
(
128,
namespace,
"",
"the measured signature, on the stream docker used",
),
(
1,
"",
namespace,
"the same message on stderr, should docker move it",
),
(
127,
namespace,
"",
"and at a code omh has no reason to expect",
),
] {
assert_eq!(
probe_from(output(code, stdout, stderr)),
Probe::NotEnterable,
"{what}"
);
}
for (stderr, what) in [
(
"Error response from daemon: No such container: omh-repo-s01",
"a container removed between the two calls",
),
(
"Error response from daemon: container 1794fad25dec is not running",
"and one that exited between them",
),
] {
assert_eq!(probe_from(output(1, "", stderr)), Probe::Gone, "{what}");
}
for (code, stdout, stderr, what) in [
(
128,
"OCI runtime exec failed: exec failed: unable to start container process: \
chdir to cwd (\"/loop\") set in config.json failed: too many levels of \
symbolic links",
"",
"a second 128 — measured, so matching the code is provably not enough",
),
(
127,
"OCI runtime exec failed: exec failed: unable to start container process: \
exec: \"sh\": executable file not found in $PATH",
"",
"an image with no shell, sharing the prefix and nothing else",
),
(
1,
"",
"failed to connect to the docker API at unix:///var/run/docker.sock; check \
if the path is correct and if the daemon is running",
"a daemon that could not be reached at all",
),
(137, "", "", "an exec that exited 137 saying nothing"),
(
137,
"s01-current working directory is outside of container mount namespace root\n",
"",
"a filename the agent chose, which is not docker speaking",
),
] {
let answered = probe_from(output(code, stdout, stderr));
assert!(
matches!(&answered, Probe::Unknown(_)),
"{what}: {answered:?}"
);
}
let Probe::Unknown(why) = probe_from(output(1, "", "cannot \u{1b}[2J connect")) else {
panic!("an unrecognised failure has no answer");
};
assert!(!why.contains('\u{1b}'), "sanitised: {why:?}");
assert!(why.contains("cannot"), "and the words survive: {why:?}");
let Probe::Unknown(quiet) = probe_from(output(137, "", "")) else {
panic!("a silent failure still has no answer");
};
assert!(
quiet.contains("137"),
"a failure that said nothing still says something: {quiet:?}"
);
assert!(
!quiet.contains("exit status"),
"and not `exited exit status: 137`: {quiet:?}"
);
assert!(
matches!(
probe_from(Err(std::io::Error::other("fork failed"))),
Probe::Unknown(_)
),
"and neither does a probe that never ran"
);
}
#[test]
fn a_stamp_omh_could_not_read_is_not_a_container_that_predates_the_check() {
let labels = r#"{"omh.harness":"claude","maintainer":"alpine"}"#;
let Stamp::Read(read) = stamp_from(output(0, labels, "")) else {
panic!("a readable stamp is read");
};
assert_eq!(read.get("omh.harness").map(String::as_str), Some("claude"));
assert!(
!read.contains_key("maintainer"),
"and everybody else's labels stay out of it: {read:?}"
);
assert_eq!(
stamp_from(output(0, "null\n", "")),
Stamp::Read(Default::default()),
"a container with no labels is read, and read as empty"
);
for (code, stdout, stderr, what) in [
(
1,
"",
"failed to connect to the docker API",
"a daemon that would not answer",
),
(
0,
"not json at all",
"",
"an answer in a shape omh does not know",
),
] {
let answered = stamp_from(output(code, stdout, stderr));
assert!(
matches!(&answered, Stamp::Unknown(_)),
"{what} is not a container that predates the check: {answered:?}"
);
}
}
#[test]
fn a_runtime_that_will_not_answer_is_not_a_container_that_is_stopped() {
assert_eq!(
running_from(
"omh-repo-s01",
output(0, "omh-repo-s01\nomh-repo-s02\n", "")
),
Running::Yes,
"listed among the running ones"
);
assert_eq!(
running_from("omh-repo-s01", output(0, "omh-repo-s02\n", "")),
Running::No,
"a listing that does not name it is an answer, and the answer is no"
);
assert_eq!(
running_from("omh-repo-s01", output(0, "", "")),
Running::No,
"nothing running at all, asked and answered"
);
assert_eq!(
running_from("omh-repo-s1", output(0, "omh-repo-s10\n", "")),
Running::No,
"a longer name that starts the same way is a different container"
);
assert_eq!(
running_from("omh-omh.rs-s01", output(0, "omh-omhXrs-s01\n", "")),
Running::No,
"and a `.` in a repo name is a character, not a wildcard — which is \
what `--filter name=^…$` made it"
);
let daemon_down = "failed to connect to the docker API at unix:///var/run/docker.sock";
let unknown = running_from("omh-repo-s01", output(1, "", daemon_down));
assert_ne!(unknown, Running::No, "a failed question is not a `no`");
assert!(
matches!(&unknown, Running::Unknown(why) if why.contains("docker API")),
"and it carries the runtime's own words: {unknown:?}"
);
let quiet = running_from("omh-repo-s01", output(1, "", ""));
assert!(
matches!(&quiet, Running::Unknown(_)),
"silence on stderr does not make a failure into an answer"
);
assert!(
matches!(&quiet, Running::Unknown(why) if why.contains('1')),
"the exit status stands in for the words it did not say: {quiet:?}"
);
assert!(
!format!("{quiet:?}").contains("exit status"),
"and not `exited exit status: 1`: {quiet:?}"
);
assert!(
matches!(
running_from("omh-repo-s01", Err(std::io::Error::other("fork failed"))),
Running::Unknown(_)
),
"and neither does a spawn that never ran"
);
}
#[test]
fn the_runtimes_own_words_are_sanitised_before_they_are_repeated() {
let sneaky = "cannot connect\u{1b}[2J to the daemon";
let Running::Unknown(why) = running_from("omh-repo-s01", output(1, "", sneaky)) else {
panic!("a failed probe is Unknown");
};
assert!(
!why.contains('\u{1b}'),
"no escape reaches the terminal: {why:?}"
);
assert!(why.contains("cannot connect"), "the words survive: {why:?}");
}
#[test]
fn every_backend_asks_for_the_running_set_and_names_it() {
use crate::runtime::Runtime;
for backend in [
&crate::runtime::Docker as &dyn Runtime,
&crate::runtime::Sbx as &dyn Runtime,
] {
let args = backend.running_args();
assert!(
args.first().is_some_and(|a| a == "ps"),
"{}: the running set: {args:?}",
backend.name()
);
assert!(
!args.iter().any(|a| a == "-a"),
"{}: running, not every container ever created: {args:?}",
backend.name()
);
assert!(
!args.iter().any(|a| a.contains("filter") || a.contains('^')),
"{}: nothing the runtime will read as a pattern: {args:?}",
backend.name()
);
}
}
#[test]
fn a_new_build_supersedes_the_older_tags_of_its_class() {
let tags = vec![
"omh/claude:new".to_string(),
"omh/claude:old".to_string(),
"omh/claude:older".to_string(),
"omh/claude:latest".to_string(),
];
let gone = superseded("omh/claude:new", &tags, &[]);
assert!(
gone.contains(&"omh/claude:old".to_string())
&& gone.contains(&"omh/claude:older".to_string()),
"the tags this build replaces: {gone:?}"
);
assert!(
!gone.contains(&"omh/claude:new".to_string()),
"never the one just built: {gone:?}"
);
assert!(
!gone.contains(&"omh/claude:latest".to_string()),
"no recipe omh still has reproduces `latest`, so removing it is \
the one removal that cannot be undone: {gone:?}"
);
}
#[test]
fn a_tag_a_container_is_using_survives_a_newer_build() {
let tags = vec![
"omh/claude:new".to_string(),
"omh/claude:held".to_string(),
"omh/claude:stale".to_string(),
];
let gone = superseded("omh/claude:new", &tags, &["omh/claude:held".to_string()]);
assert_eq!(
gone,
vec!["omh/claude:stale".to_string()],
"the held tag stays and the stale one goes: {gone:?}"
);
}
#[test]
fn dropping_a_name_is_not_removing_an_image() {
let untagged = "Untagged: omh/claude:old\n";
let deleted = "Untagged: omh/claude:old\nDeleted: sha256:9aeb9a1e2d14\n";
assert!(
matches!(classify_removal(untagged), Removal::Untagged),
"the image survives under its other name"
);
assert!(matches!(classify_removal(deleted), Removal::Deleted));
}
#[test]
fn a_stack_build_and_a_harness_build_are_not_the_same_class() {
let adapter = claude();
let here = Path::new("/checkouts/api");
let there = Path::new("/checkouts/web");
let harness = Kind::Harness(&adapter).stamp();
let mine = Kind::Stack(&adapter, here).stamp();
let theirs = Kind::Stack(&adapter, there).stamp();
assert_ne!(
mine, harness,
"a stack layer is not the harness layer it was built FROM"
);
assert_ne!(
mine, theirs,
"another checkout's toolchain is not this build's to replace"
);
assert_ne!(
harness,
Kind::Base.stamp(),
"the harness layer is not the base it was built FROM"
);
}
#[test]
fn the_listing_asks_docker_for_exactly_one_class() {
let adapter = claude();
let args = Kind::Stack(&adapter, Path::new("/checkouts/api")).list_args();
for (k, v) in Kind::Stack(&adapter, Path::new("/checkouts/api")).stamp() {
assert!(
args.windows(2)
.any(|w| w[0] == "--filter" && w[1] == format!("label={k}={v}")),
"{k} is part of the class but not part of the query: {args:?}"
);
}
}
#[test]
fn an_unstamped_image_is_in_nobodys_class() {
let adapter = claude();
for kind in [
Kind::Base,
Kind::Harness(&adapter),
Kind::Stack(&adapter, Path::new("/checkouts/api")),
] {
assert!(
!kind.stamp().is_empty(),
"an empty stamp would match every unlabelled image"
);
}
}
fn adapters() -> &'static Path {
Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/adapters"))
}
fn claude() -> Adapter {
Adapter::find(adapters(), "claude").unwrap()
}
#[test]
fn tags_name_their_harness() {
assert!(tag_for(&claude()).starts_with("omh/claude:"));
}
#[test]
fn a_changed_base_recipe_is_a_different_base_tag() {
let before = base_tag();
assert!(before.starts_with("omh/base:"));
assert_ne!(before, "omh/base:latest", "a mutable tag never rebuilds");
}
#[test]
fn the_harness_layer_pins_an_exact_base() {
let df = harness_dockerfile(&claude());
assert!(df.contains(&base_tag()), "got: {df}");
assert!(!df.contains("omh/base:latest"), "got: {df}");
}
#[test]
fn the_stack_layer_pins_the_exact_harness_layer() {
let df = stack_dockerfile(&claude(), &["apt-get install -y gcc"]);
assert!(df.contains(&tag_for(&claude())), "got: {df}");
assert!(!df.contains(":latest"), "got: {df}");
}
#[test]
fn a_different_set_of_fired_installs_is_a_different_tag() {
let a = claude();
let pnpm = stack_tag(&a, &["corepack enable pnpm"]);
let yarn = stack_tag(&a, &["corepack enable yarn"]);
let both = stack_tag(&a, &["corepack enable pnpm", "corepack enable yarn"]);
assert_ne!(pnpm, yarn, "different provides, different image");
assert_ne!(pnpm, both, "a superset is a different image too");
assert_eq!(
pnpm,
stack_tag(&a, &["corepack enable pnpm"]),
"and an unchanged resolution must not rebuild"
);
assert_ne!(
both,
stack_tag(&a, &["corepack enable yarn", "corepack enable pnpm"]),
"a reordered recipe is a different image"
);
}
#[test]
fn nothing_to_install_is_the_harness_image_itself() {
assert_eq!(stack_tag(&claude(), &[]), tag_for(&claude()));
}
#[test]
fn installs_run_in_the_order_the_stack_file_gave() {
let df = stack_dockerfile(
&claude(),
&["zzz corepack enable pnpm", "aaa apt-get install nodejs"],
);
let at = |needle: &str| df.find(needle).unwrap_or_else(|| panic!("missing: {df}"));
assert!(
at("zzz corepack") < at("aaa apt-get"),
"the recipe was reordered — file order is install order, and \
`corepack enable pnpm` needs the node above it:\n{df}"
);
}
#[test]
fn a_changed_recipe_is_a_different_tag() {
let mut a = claude();
let before = tag_for(&a);
a.install = "npm install -g @anthropic-ai/claude-code@next".into();
assert_ne!(tag_for(&a), before, "a changed recipe must force a rebuild");
}
#[test]
fn an_unchanged_recipe_keeps_its_tag() {
assert_eq!(tag_for(&claude()), tag_for(&claude()));
}
#[test]
fn the_base_satisfies_the_sandbox_contract() {
let df = base_dockerfile();
assert!(
df.contains("-u 1000") || df.contains("1000"),
"UID 1000: {df}"
);
assert!(df.contains("agent"), "agent user");
assert!(df.contains(GUEST_HOME), "home directory");
assert!(df.contains("NOPASSWD"), "passwordless sudo");
}
#[test]
fn the_image_creates_the_home_the_code_mounts_into() {
let df = base_dockerfile();
assert!(
df.contains(&format!("usermod -l agent -d {GUEST_HOME}")),
"the image must create the home the launcher mounts into:\n{df}"
);
assert!(
df.contains(&format!("cut -d: -f6)\" = \"{GUEST_HOME}\"")),
"and assert it at build time, not trust it:\n{df}"
);
}
#[test]
fn the_graph_cache_lives_under_the_agents_home() {
assert!(
GRAPH_CACHE.starts_with(&format!("{GUEST_HOME}/")),
"graph cache {GRAPH_CACHE} is not under {GUEST_HOME}"
);
assert!(
base_dockerfile().contains(GRAPH_CACHE),
"the image must create the cache directory, or docker makes it root-owned"
);
}
#[test]
fn a_recipe_digest_does_not_inherit_the_repository_it_is_run_beside() {
let dir = tempfile::tempdir().unwrap();
let odd = dir.path().join("sha256");
let made = std::process::Command::new("git")
.args(["init", "-q", "--object-format=sha256"])
.arg(&odd)
.output()
.expect("git must be installed to run this test");
if !made.status.success() {
return; }
let run = |pinned: bool| {
let mut c = if pinned {
digest_command()
} else {
let mut c = std::process::Command::new("git");
c.args(["hash-object", "--stdin"]);
c
};
c.current_dir(&odd);
let mut ch = c
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
use std::io::Write;
ch.stdin.take().unwrap().write_all(b"a recipe").unwrap();
let out = ch.wait_with_output().unwrap();
String::from_utf8_lossy(&out.stdout).trim().to_string()
};
let inherited = run(false);
let pinned = run(true);
assert_eq!(
inherited.len(),
64,
"the premise: a sha256 repository answers in 64 hex"
);
assert_eq!(
pinned,
recipe_digest("a recipe").unwrap(),
"and omh's digest must be the same wherever it was run from"
);
}
#[test]
fn an_image_recipe_digest_is_stable_across_toolchains() {
assert_eq!(
recipe_digest("hello\n").unwrap(),
"ce013625030ba8dba906f756967f9e9ca394464a",
"this is git's SHA-1 of the blob and it is not allowed to change"
);
assert_eq!(recipe_digest("a").unwrap(), recipe_digest("a").unwrap());
assert_ne!(recipe_digest("a").unwrap(), recipe_digest("b").unwrap());
}
#[test]
fn the_image_creates_the_note_store_the_launcher_mounts_into() {
let df = base_dockerfile();
let notes = crate::memory::GUEST_LOCAL_NOTES;
assert!(
df.contains(notes),
"the image must create {notes}, or docker makes it root-owned"
);
let owned = df
.lines()
.find(|l| l.contains("chown -R agent:agent"))
.expect("the image must chown its mount points");
assert!(
owned
.split_whitespace()
.any(|dir| notes == dir || notes.starts_with(&format!("{dir}/"))),
"{notes} is not covered by: {owned}"
);
}
#[test]
fn the_guest_home_is_defined_once() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut declarations = Vec::new();
for entry in std::fs::read_dir(&src).unwrap().flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "rs") {
let body = std::fs::read_to_string(&path).unwrap();
for line in body.lines() {
let l = line.trim();
let is_const = l.starts_with("const ") || l.starts_with("pub const ");
if is_const && l.contains("= \"/home/agent\"") {
declarations.push(format!("{}: {l}", path.display()));
}
}
}
}
assert_eq!(
declarations.len(),
1,
"the agent's home should be declared once:\n {}",
declarations.join("\n ")
);
}
#[test]
fn the_base_provides_what_every_session_needs() {
let df = base_dockerfile();
for tool in ["git", "dtach", "ripgrep"] {
assert!(df.contains(tool), "missing {tool}: {df}");
}
}
#[test]
fn the_base_image_carries_the_base_set() {
let df = base_dockerfile();
assert!(df.contains(crate::base::GRAPH_BIN), "no code graph: {df}");
}
#[test]
fn the_base_owns_the_graph_cache_directory() {
let df = base_dockerfile();
assert!(df.contains(crate::base::GRAPH_CACHE), "got: {df}");
}
#[test]
fn the_base_can_serve_ssh() {
let df = base_dockerfile();
assert!(df.contains("openssh-server"), "got: {df}");
assert!(df.contains("omh-session"), "needs a session entrypoint");
}
#[test]
fn the_session_entrypoint_installs_the_key_with_permissions_sshd_accepts() {
let df = base_dockerfile();
assert!(
df.contains("OMH_PUBKEY"),
"key must come from the environment"
);
assert!(df.contains("chmod 700"), "~/.ssh perms");
assert!(df.contains("chmod 600"), "authorized_keys perms");
}
#[test]
fn the_session_entrypoint_outlives_the_command_that_started_it() {
let df = base_dockerfile();
assert!(df.contains("sshd"), "must start sshd");
assert!(df.contains("sleep infinity"), "PID 1 must not exit");
}
#[test]
fn the_base_creates_the_paths_the_launcher_mounts_into() {
let df = base_dockerfile();
for dir in ["/work", "/omh/sock", "/omh/cache"] {
assert!(df.contains(dir), "missing {dir}: {df}");
}
}
#[test]
fn the_harness_layer_extends_the_base_and_installs_the_harness() {
let df = harness_dockerfile(&claude());
assert!(df.contains(&format!("FROM {}", base_tag())), "got: {df}");
assert!(
df.contains("@anthropic-ai/claude-code"),
"install command: {df}"
);
}
#[test]
fn the_harness_layer_owns_the_directories_it_mounts_into() {
let df = harness_dockerfile(&claude());
assert!(df.contains("/home/agent/.claude"), "config dir: {df}");
assert!(
df.contains("chown"),
"must be owned by agent, not root: {df}"
);
}
#[test]
fn only_directories_under_the_agents_home_are_created() {
for name in ["claude", "opencode"] {
let a = Adapter::find(adapters(), name).unwrap();
for dir in mount_parents(&a) {
assert!(
dir.starts_with("/home/agent"),
"{name}: {dir} is not ours to create"
);
}
}
}
#[test]
fn credential_directories_are_created_too() {
let a = Adapter::find(adapters(), "opencode").unwrap();
let dirs = mount_parents(&a);
assert!(
dirs.iter().any(|d| d.contains("/.local/share")),
"the creds parent must be created: {dirs:?}"
);
}
#[test]
fn every_home_side_mount_has_its_parent_created() {
for name in ["claude", "opencode"] {
let a = Adapter::find(adapters(), name).unwrap();
let created = mount_parents(&a);
let wanted = a
.capabilities
.values()
.map(|b| b.path.clone())
.chain(a.creds.iter().cloned())
.chain(a.token.iter().cloned());
for template in wanted {
let p = crate::adapter::expand(template.trim_end_matches('/'), "/home/agent");
if !p.starts_with(GUEST_HOME) {
continue;
}
let Some(parent) = p.parent().map(|x| x.display().to_string()) else {
continue;
};
if parent == GUEST_HOME {
continue; }
assert!(
created
.iter()
.any(|d| parent == *d || parent.starts_with(&format!("{d}/"))),
"{name}: nothing creates {parent} for {template}"
);
}
}
}
#[test]
fn images_end_as_the_unprivileged_user() {
for df in [
base_dockerfile(),
harness_dockerfile(&claude()),
stack_dockerfile(&claude(), &["apt-get install -y gcc"]),
] {
let last_user = df
.lines()
.rfind(|l| l.trim_start().starts_with("USER "))
.unwrap_or("");
assert_eq!(last_user.trim(), "USER agent", "ended privileged:\n{df}");
}
}
#[test]
fn the_stack_layer_installs_as_root() {
let df = stack_dockerfile(&claude(), &["apt-get install -y gcc"]);
let line = |needle: &str| {
df.lines()
.position(|l| l.trim_start().starts_with(needle))
.unwrap_or_else(|| panic!("missing `{needle}`:\n{df}"))
};
assert!(
line("USER root") < line("RUN "),
"the recipe runs before the layer takes root:\n{df}"
);
}
#[test]
fn the_toolchain_probe_can_see_nothing_but_the_image() {
let args = probe_args("omh/x:latest", "#!/bin/sh\ntrue\n");
let tag_at = args
.iter()
.position(|a| a == "omh/x:latest")
.expect("the tag must be among the arguments");
for a in &args[1..tag_at] {
assert!(
a == "--rm" || a == "--pull=never",
"an unexpected argument reached the probe — it must see nothing \
but the image, or it answers about the wrong machine: {args:?}"
);
}
assert!(
args[1..tag_at].contains(&"--rm".to_string()),
"a diagnostic must leave no container behind: {args:?}"
);
assert!(
args.contains(&"omh/x:latest".into()),
"and it runs in the image the session will use: {args:?}"
);
assert_eq!(
args.last().map(String::as_str),
Some("#!/bin/sh\ntrue\n"),
"the script is what runs, whole and unedited: {args:?}"
);
}
#[test]
fn the_probe_never_fetches_the_image_it_asks_about() {
let args = probe_args("omh/x:latest", "#!/bin/sh\ntrue\n");
let tag_at = args.iter().position(|a| a == "omh/x:latest").unwrap();
assert!(
args[1..tag_at].iter().any(|a| a == "--pull=never"),
"a missing image must be an error, never a registry fetch: {args:?}"
);
}
#[test]
fn build_reads_the_dockerfile_from_stdin() {
let args = build_args("omh/x:latest", Path::new("/tmp/ctx"), &Kind::Base);
assert_eq!(args[0], "build");
assert!(args.contains(&"-t".into()) && args.contains(&"omh/x:latest".into()));
assert!(
args.windows(2).any(|w| w[0] == "-f" && w[1] == "-"),
"Dockerfile must come from stdin so nothing is written to disk: {args:?}"
);
}
#[test]
fn only_omhs_own_records_are_read_back() {
let Stamp::Read(got) = stamp_from(output(
0,
r#"{"omh.image":"omh/claude:ab","maintainer":"nodejs"}"#,
"",
)) else {
panic!("a readable stamp is read");
};
assert_eq!(got.len(), 1);
assert_eq!(got.get("omh.image").unwrap(), "omh/claude:ab");
}
#[test]
fn a_container_with_no_labels_reads_as_none_rather_than_an_error() {
for said in ["null", "{}"] {
assert_eq!(
stamp_from(output(0, said, "")),
Stamp::Read(Default::default()),
"`{said}` is a container with nothing stamped on it"
);
}
}
#[test]
fn an_unreadable_answer_is_not_mistaken_for_a_container_with_no_labels() {
for said in ["", "<html>error</html>"] {
let answered = stamp_from(output(0, said, ""));
assert!(
matches!(&answered, Stamp::Unknown(_)),
"`{said}` is not a container that predates the check: {answered:?}"
);
}
}
#[test]
fn a_multi_line_value_survives_the_round_trip() {
let Stamp::Read(got) = stamp_from(output(
0,
r#"{"omh.mounts":"ro /a -> /b\nrw /c -> /d"}"#,
"",
)) else {
panic!("a readable stamp is read");
};
assert_eq!(got.get("omh.mounts").unwrap().lines().count(), 2);
}
}