From 1c38a3582665600ab928b4ec2425a55878e63896 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Tue, 2 Jun 2026 16:19:09 +0000 Subject: [PATCH 1/4] feat(containerfile): add Hummingbird base image support Adds "hummingbird" as a supported base image backed by registry.access.redhat.com/hi/core-runtime with a default tag of "latest-builder". Includes unit tests, integration test fixtures, and README updates. Co-authored-by: Claude Sonnet 4.6 Signed-off-by: Philippe Martin --- README.md | 10 ++-- src/containerfile.rs | 67 +++++++++++++++++++++++++- tests/integration_test.rs | 98 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7c0f18a..33ac1a9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ OpenShell ships a set of [pre-built sandbox images](https://github.com/NVIDIA/OpenShell-Community), but they are general-purpose. `openshell-image-builder` lets you build your own: lightweight, workspace-specific images that contain only what you need — without writing a Containerfile by hand. -- **Base image selection** — Ubuntu, Fedora, or Red Hat UBI, any tag. +- **Base image selection** — Ubuntu, Fedora, Red Hat UBI, or Red Hat Hardened Images (HummingBird), any tag. - **Agent installation and configuration** — pre-installed in `PATH` with scoped network access to agent-specific endpoints. Settings files can be embedded into the image from a local directory. - **Inference configuration** — scoped network access to LLM backends. - **Dev Container Features** — install toolchains and utilities declared in your Kaiden workspace configuration. @@ -61,15 +61,15 @@ If a directory is given explicitly (via `--config` or the environment variable) version = 1 [openshell_image_builder.base_image] -image = "ubuntu" # "ubuntu", "fedora", or "ubi" -tag = "24.04" # ubuntu: "24.04", "22.04", … — fedora: "latest", "43", "42", … — ubi: "10.2-1780377767", … +image = "ubuntu" # "ubuntu", "fedora", "ubi", or "hummingbird" +tag = "24.04" # ubuntu: "24.04", "22.04", … — fedora: "latest", "43", "42", … — ubi: "10.2-1780377767", … — hummingbird: "latest-builder", … ``` | Field | Default | Description | | ------------------------------------------ | -------- | ---------------------------- | | `openshell_image_builder.version` | `1` | Configuration schema version | -| `openshell_image_builder.base_image.image` | `ubuntu` | Base image name (`ubuntu`, `fedora`, or `ubi`) | -| `openshell_image_builder.base_image.tag` | `24.04` | Base image tag — Ubuntu: `24.04`, `22.04`, …; Fedora: `latest`, `43`, `42`, …; UBI: `10.2-1780377767`, … | +| `openshell_image_builder.base_image.image` | `ubuntu` | Base image name (`ubuntu`, `fedora`, `ubi`, or `hummingbird`) | +| `openshell_image_builder.base_image.tag` | `24.04` | Base image tag — Ubuntu: `24.04`, `22.04`, …; Fedora: `latest`, `43`, `42`, …; UBI: `10.2-1780377767`, …; Hummingbird: `latest-builder`, … | ### Loading from a specific config directory diff --git a/src/containerfile.rs b/src/containerfile.rs index 3dad36a..63877b4 100644 --- a/src/containerfile.rs +++ b/src/containerfile.rs @@ -78,6 +78,11 @@ pub fn generate( "which", ], ), + "hummingbird" => dnf_system_stage( + "registry.access.redhat.com/hi/core-runtime", + tag, + &["bind-utils", "openssh-server", "procps-ng", "which"], + ), "ubuntu" => ubuntu_system_stage(tag), image => { return Err(ContainerfileError::NotSupported { @@ -208,10 +213,10 @@ fn dnf_system_stage(base_image: &str, tag: &str, packages: &[&str]) -> String { format!( r#"# System base FROM {base_image}:{tag} AS system - WORKDIR /sandbox # Core system dependencies +USER 0 RUN dnf install -y --setopt=install_weak_deps=False \ {pkg_lines} && dnf clean all @@ -293,6 +298,16 @@ mod tests { } } + fn hummingbird_config() -> Config { + Config { + version: 1, + base_image: BaseImageConfig { + image: "hummingbird".to_string(), + tag: "latest-builder".to_string(), + }, + } + } + struct MockAgent; impl Agent for MockAgent { @@ -467,6 +482,56 @@ mod tests { assert!(content.contains("COPY policy.yaml /etc/openshell/policy.yaml")); } + #[test] + fn hummingbird_generates_successfully() { + assert!(generate(&hummingbird_config(), None, &[], false, &[]).is_ok()); + } + + #[test] + fn hummingbird_containerfile_contains_tag() { + let content = generate(&hummingbird_config(), None, &[], false, &[]).unwrap(); + assert!( + content.contains( + "FROM registry.access.redhat.com/hi/core-runtime:latest-builder AS system" + ) + ); + } + + #[test] + fn hummingbird_containerfile_tag_is_substituted() { + let content = generate(&hummingbird_config(), None, &[], false, &[]).unwrap(); + assert!(!content.contains("{tag}")); + } + + #[test] + fn hummingbird_with_agent_includes_install() { + let content = generate(&hummingbird_config(), Some(&MockAgent), &[], false, &[]).unwrap(); + assert!(content.contains("RUN echo mock-agent")); + } + + #[test] + fn hummingbird_agent_install_runs_as_sandbox_user() { + let content = generate(&hummingbird_config(), Some(&MockAgent), &[], false, &[]).unwrap(); + let user_pos = content.find("USER sandbox").unwrap(); + let install_pos = content.find("RUN echo mock-agent").unwrap(); + assert!( + install_pos > user_pos, + "agent install must appear after USER sandbox" + ); + } + + #[test] + fn hummingbird_without_agent_omits_install() { + let content = generate(&hummingbird_config(), None, &[], false, &[]).unwrap(); + assert!(!content.contains("RUN echo mock-agent")); + } + + #[test] + fn hummingbird_copies_policy_yaml() { + let content = generate(&hummingbird_config(), None, &[], false, &[]).unwrap(); + assert!(content.contains("COPY policy.yaml /etc/openshell/policy.yaml")); + } + #[test] fn not_supported_error_message() { let err = ContainerfileError::NotSupported { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 6b2ab27..b7faad0 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -41,6 +41,16 @@ fn ubi_config_dir() -> tempfile::TempDir { dir } +fn hummingbird_config_dir() -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("config.toml"), + "[openshell_image_builder.base_image]\nimage = \"hummingbird\"\ntag = \"latest-builder\"\n", + ) + .unwrap(); + dir +} + fn build_image(tag: &str, extra_args: &[&str]) -> String { let binary = env!("CARGO_BIN_EXE_openshell-image-builder"); let status = Command::new(binary) @@ -81,6 +91,11 @@ static UBI_CLAUDE_IMAGE: OnceLock = OnceLock::new(); static UBI_OPENCODE_IMAGE: OnceLock = OnceLock::new(); static UBI_CLAUDE_VERTEXAI_IMAGE: OnceLock = OnceLock::new(); static UBI_OPENCODE_VERTEXAI_IMAGE: OnceLock = OnceLock::new(); +static HUMMINGBIRD_IMAGE: OnceLock = OnceLock::new(); +static HUMMINGBIRD_CLAUDE_IMAGE: OnceLock = OnceLock::new(); +static HUMMINGBIRD_OPENCODE_IMAGE: OnceLock = OnceLock::new(); +static HUMMINGBIRD_CLAUDE_VERTEXAI_IMAGE: OnceLock = OnceLock::new(); +static HUMMINGBIRD_OPENCODE_VERTEXAI_IMAGE: OnceLock = OnceLock::new(); static UBUNTU_CLAUDE_SKILLS_IMAGE: OnceLock = OnceLock::new(); static UBUNTU_OPENCODE_SKILLS_IMAGE: OnceLock = OnceLock::new(); @@ -338,6 +353,84 @@ fn ubi_opencode_vertexai_image() -> &'static str { }) } +fn hummingbird_image() -> &'static str { + HUMMINGBIRD_IMAGE.get_or_init(|| { + let config = hummingbird_config_dir(); + build_image( + "openshell-test-hummingbird:integration", + &["--config", config.path().to_str().unwrap()], + ) + }) +} + +fn hummingbird_claude_image() -> &'static str { + HUMMINGBIRD_CLAUDE_IMAGE.get_or_init(|| { + let config = hummingbird_config_dir(); + build_image( + "openshell-test-hummingbird-claude:integration", + &[ + "--config", + config.path().to_str().unwrap(), + "--agent", + "claude", + "--inference", + "anthropic", + ], + ) + }) +} + +fn hummingbird_opencode_image() -> &'static str { + HUMMINGBIRD_OPENCODE_IMAGE.get_or_init(|| { + let config = hummingbird_config_dir(); + build_image( + "openshell-test-hummingbird-opencode:integration", + &[ + "--config", + config.path().to_str().unwrap(), + "--agent", + "opencode", + "--inference", + "anthropic", + ], + ) + }) +} + +fn hummingbird_claude_vertexai_image() -> &'static str { + HUMMINGBIRD_CLAUDE_VERTEXAI_IMAGE.get_or_init(|| { + let config = hummingbird_config_dir(); + build_image( + "openshell-test-hummingbird-claude-vertexai:integration", + &[ + "--config", + config.path().to_str().unwrap(), + "--agent", + "claude", + "--inference", + "vertexai", + ], + ) + }) +} + +fn hummingbird_opencode_vertexai_image() -> &'static str { + HUMMINGBIRD_OPENCODE_VERTEXAI_IMAGE.get_or_init(|| { + let config = hummingbird_config_dir(); + build_image( + "openshell-test-hummingbird-opencode-vertexai:integration", + &[ + "--config", + config.path().to_str().unwrap(), + "--agent", + "opencode", + "--inference", + "vertexai", + ], + ) + }) +} + // --------------------------------------------------------------------------- // Shared assertion helpers // --------------------------------------------------------------------------- @@ -570,6 +663,11 @@ image_tests!(ubi_claude, ubi_claude_image, has_claude: image_tests!(ubi_opencode, ubi_opencode_image, has_claude: false, has_opencode: true, has_anthropic: true, has_vertexai: false); image_tests!(ubi_claude_vertexai, ubi_claude_vertexai_image, has_claude: true, has_opencode: false, has_anthropic: false, has_vertexai: true); image_tests!(ubi_opencode_vertexai, ubi_opencode_vertexai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: true); +image_tests!(hummingbird, hummingbird_image, has_claude: false, has_opencode: false, has_anthropic: false, has_vertexai: false); +image_tests!(hummingbird_claude, hummingbird_claude_image, has_claude: true, has_opencode: false, has_anthropic: true, has_vertexai: false); +image_tests!(hummingbird_opencode, hummingbird_opencode_image, has_claude: false, has_opencode: true, has_anthropic: true, has_vertexai: false); +image_tests!(hummingbird_claude_vertexai, hummingbird_claude_vertexai_image, has_claude: true, has_opencode: false, has_anthropic: false, has_vertexai: true); +image_tests!(hummingbird_opencode_vertexai, hummingbird_opencode_vertexai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: true); // --------------------------------------------------------------------------- // Workspace helpers for feature-based builds From 6123eb7010dfad5728ae490b30315eb9d569183e Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Tue, 2 Jun 2026 18:31:42 +0200 Subject: [PATCH 2/4] fix: document base images Signed-off-by: Philippe Martin --- README.md | 40 ++++++++++++++++++++++++++++++++++++--- src/containerfile.rs | 6 ++---- tests/integration_test.rs | 2 +- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 33ac1a9..c4b0242 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,41 @@ If no `config.toml` is found in the resolved directory, or the file is empty, bu If a directory is given explicitly (via `--config` or the environment variable) but it does not exist, the command fails immediately. -### Schema +### Base images + +**Ubuntu** (default) + +```toml +[openshell_image_builder.base_image] +image = "ubuntu" +tag = "24.04" +``` + +**Fedora** + +```toml +[openshell_image_builder.base_image] +image = "fedora" +tag = "latest" +``` + +**Red Hat UBI** + +```toml +[openshell_image_builder.base_image] +image = "ubi" +tag = "latest" +``` + +**Red Hat Hardened Images (Hummingbird)** + +```toml +[openshell_image_builder.base_image] +image = "hummingbird" +tag = "latest-builder" +``` + +### Full schema reference ```toml [openshell_image_builder] @@ -62,14 +96,14 @@ version = 1 [openshell_image_builder.base_image] image = "ubuntu" # "ubuntu", "fedora", "ubi", or "hummingbird" -tag = "24.04" # ubuntu: "24.04", "22.04", … — fedora: "latest", "43", "42", … — ubi: "10.2-1780377767", … — hummingbird: "latest-builder", … +tag = "24.04" ``` | Field | Default | Description | | ------------------------------------------ | -------- | ---------------------------- | | `openshell_image_builder.version` | `1` | Configuration schema version | | `openshell_image_builder.base_image.image` | `ubuntu` | Base image name (`ubuntu`, `fedora`, `ubi`, or `hummingbird`) | -| `openshell_image_builder.base_image.tag` | `24.04` | Base image tag — Ubuntu: `24.04`, `22.04`, …; Fedora: `latest`, `43`, `42`, …; UBI: `10.2-1780377767`, …; Hummingbird: `latest-builder`, … | +| `openshell_image_builder.base_image.tag` | `24.04` | Base image tag — Ubuntu: `24.04`, `22.04`, …; Fedora: `latest`, `43`, `42`, …; UBI: `latest`, `10.2-1780377767`, …; Hummingbird: `latest-builder`, … | ### Loading from a specific config directory diff --git a/src/containerfile.rs b/src/containerfile.rs index 63877b4..a8d264f 100644 --- a/src/containerfile.rs +++ b/src/containerfile.rs @@ -293,7 +293,7 @@ mod tests { version: 1, base_image: BaseImageConfig { image: "ubi".to_string(), - tag: "10.2-1780377767".to_string(), + tag: "latest".to_string(), }, } } @@ -442,9 +442,7 @@ mod tests { #[test] fn ubi_containerfile_contains_tag() { let content = generate(&ubi_config(), None, &[], false, &[]).unwrap(); - assert!( - content.contains("FROM registry.access.redhat.com/ubi10/ubi:10.2-1780377767 AS system") - ); + assert!(content.contains("FROM registry.access.redhat.com/ubi10/ubi:latest AS system")); } #[test] diff --git a/tests/integration_test.rs b/tests/integration_test.rs index b7faad0..b4908f4 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -35,7 +35,7 @@ fn ubi_config_dir() -> tempfile::TempDir { let dir = tempfile::tempdir().unwrap(); std::fs::write( dir.path().join("config.toml"), - "[openshell_image_builder.base_image]\nimage = \"ubi\"\ntag = \"10.2-1780377767\"\n", + "[openshell_image_builder.base_image]\nimage = \"ubi\"\ntag = \"latest\"\n", ) .unwrap(); dir From 410b49c667fcd7f6c9807a7927b7f46828e36faf Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Tue, 2 Jun 2026 20:51:51 +0200 Subject: [PATCH 3/4] feat(containerfile): explicitely set HOME to /sandbox Signed-off-by: Philippe Martin --- src/containerfile.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/containerfile.rs b/src/containerfile.rs index a8d264f..2c03145 100644 --- a/src/containerfile.rs +++ b/src/containerfile.rs @@ -255,6 +255,7 @@ RUN printf 'export PS1="\\u@\\h:\\w\\$ "\n' \ chown sandbox:sandbox /sandbox/.bashrc /sandbox/.profile && \ chown -R sandbox:sandbox /sandbox +ENV HOME=/sandbox USER sandbox {agent_settings_section}{skills_section}{agent_section}ENTRYPOINT ["/bin/bash"] @@ -530,6 +531,18 @@ mod tests { assert!(content.contains("COPY policy.yaml /etc/openshell/policy.yaml")); } + #[test] + fn home_env_set_to_sandbox() { + for content in [ + generate(&ubuntu_config("24.04"), None, &[], false, &[]).unwrap(), + generate(&fedora_config(), None, &[], false, &[]).unwrap(), + generate(&ubi_config(), None, &[], false, &[]).unwrap(), + generate(&hummingbird_config(), None, &[], false, &[]).unwrap(), + ] { + assert!(content.contains("ENV HOME=/sandbox")); + } + } + #[test] fn not_supported_error_message() { let err = ContainerfileError::NotSupported { From f94258f515f925058e692b3b33b528e599b46f0f Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Tue, 2 Jun 2026 21:19:39 +0200 Subject: [PATCH 4/4] fix(containerfile): add tar for hummingbird base image --- src/containerfile.rs | 2 +- tests/integration_test.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerfile.rs b/src/containerfile.rs index 2c03145..71d3fc4 100644 --- a/src/containerfile.rs +++ b/src/containerfile.rs @@ -81,7 +81,7 @@ pub fn generate( "hummingbird" => dnf_system_stage( "registry.access.redhat.com/hi/core-runtime", tag, - &["bind-utils", "openssh-server", "procps-ng", "which"], + &["bind-utils", "openssh-server", "procps-ng", "which", "tar"], ), "ubuntu" => ubuntu_system_stage(tag), image => { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index b4908f4..2697257 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -462,7 +462,7 @@ fn check_users_and_groups(image: &str) { } fn check_packages(image: &str) { - for pkg in ["curl", "ip", "ping"] { + for pkg in ["curl", "tar"] { let out = run_in_image(image, &format!("which {pkg}")); assert!(out.status.success(), "{pkg} not found in image"); }