diff --git a/.agents/skills/helm-dev-environment/SKILL.md b/.agents/skills/helm-dev-environment/SKILL.md index 2a6b9ad38..fd9d0aba3 100644 --- a/.agents/skills/helm-dev-environment/SKILL.md +++ b/.agents/skills/helm-dev-environment/SKILL.md @@ -34,7 +34,7 @@ Also applies the upstream agent-sandbox CRDs/controller (pinned via `AGENT_SANDB in `tasks/scripts/helm-k3s-local.sh`, fetched from `github.com/kubernetes-sigs/agent-sandbox` releases), enables its OTLP tracing on v0.5 and later, installs an OTLP trace collector and UI in the `observability` namespace, -and preloads the default community sandbox image into k3d so the first sandbox create +and preloads the default sandbox image into k3d so the first sandbox create does not wait on a large registry pull. Traefik is disabled at cluster creation time. **Multi-worktree support:** the cluster name is derived from the last component of the @@ -52,7 +52,7 @@ Port mappings created at cluster time (cannot be changed without recreating): Override with env vars before running `helm:k3s:create`: - `HELM_K3S_LB_HOST_PORT` (default: `8080`) - `HELM_K3S_PRELOAD_SANDBOX_IMAGE` (default: - `ghcr.io/nvidia/openshell-community/sandboxes/base:latest`; set to an empty value to skip) + `nvcr.io/nvidia/base/ubuntu:24.04`; set to an empty value to skip) - `HELM_K3S_COLLECTOR_IMAGE` (default: `mcr.microsoft.com/dotnet/aspire-dashboard:latest`) - `HELM_K3S_COLLECTOR_HEALTH_TIMEOUT` (default: `120` seconds) diff --git a/.agents/skills/update-docs/SKILL.md b/.agents/skills/update-docs/SKILL.md index aa2db8cd5..86edcd273 100644 --- a/.agents/skills/update-docs/SKILL.md +++ b/.agents/skills/update-docs/SKILL.md @@ -63,7 +63,7 @@ For each relevant commit, determine which doc page(s) it affects. Use this mappi | `python/` (SDK changes) | `docs/reference/` or `docs/get-started/quickstart.mdx` | | `proto/` (API changes) | `docs/reference/` | | `deploy/` (Dockerfile, Helm) | `docs/sandboxes/manage-gateways.mdx`, `docs/about/architecture.mdx` | -| Community sandbox definitions | `docs/sandboxes/community-sandboxes.mdx` | +| Sandbox image behavior | `docs/sandboxes/manage-sandboxes.mdx` | If a commit does not map to any existing page but introduces a user-visible concept, flag it as needing a new page. diff --git a/README.md b/README.md index 3237c169f..12582793a 100644 --- a/README.md +++ b/README.md @@ -59,19 +59,18 @@ For deploying OpenShell on OpenShift, see [`deploy/helm/openshell/README.md#inst ### Create a sandbox ```bash -openshell sandbox create -- claude # or opencode, codex, copilot +openshell sandbox create --name demo ``` -The sandbox container includes the following tools by default: +The gateway defaults to `nvcr.io/nvidia/base/ubuntu:24.04`, a minimal Ubuntu +Noble workload. To run an agent, build or select an OCI image that contains the +agent and pass its explicit reference: -| Category | Tools | -| ---------- | -------------------------------------------------------- | -| Agent | `claude`, `opencode`, `codex`, `copilot` | -| Language | `python` (3.14), `node` (22) | -| Developer | `gh`, `git`, `vim`, `nano` | -| Networking | `ping`, `dig`, `nslookup`, `nc`, `traceroute`, `netstat` | +```bash +openshell sandbox create --from registry.example.com/agents/my-agent:1.0 -- my-agent +``` -For more details see https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base. +Attach the providers and policy required by that workload. ### See network policy in action @@ -147,25 +146,27 @@ Inference access uses the same provider workflow. Attach an inference-capable pr OpenShell can pass host GPUs into sandboxes for local inference, fine-tuning, or any GPU workload. Add `--gpu` when creating a sandbox: ```bash -openshell sandbox create --gpu --from [gpu-enabled-sandbox] -- claude +openshell sandbox create --gpu --from registry.example.com/your-org/gpu-agent:latest -- claude ``` Docker-backed GPU sandboxes auto-select CDI when available and otherwise fall back to Docker's NVIDIA GPU request path (`--gpus all`). -**Requirements:** NVIDIA drivers and the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) must be installed on the host. The sandbox image itself must include the appropriate GPU drivers and libraries for your workload — the default `base` image does not. See the [BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container) for building a custom sandbox image with GPU support. +**Requirements:** NVIDIA drivers and the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) must be installed on the host. The sandbox image itself must include the appropriate GPU drivers and libraries for your workload — the default Ubuntu image does not. See the [BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container) for building a custom sandbox image with GPU support. ## Supported Agents -| Agent | Source | Notes | -| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `ANTHROPIC_API_KEY`. | -| [OpenCode](https://opencode.ai/) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `OPENAI_API_KEY` or `OPENROUTER_API_KEY`. | -| [Codex](https://developers.openai.com/codex) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `OPENAI_API_KEY`. | -| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `GITHUB_TOKEN` or `COPILOT_GITHUB_TOKEN`. | -| [OpenClaw](https://openclaw.ai/) | [NemoClaw](https://github.com/NVIDIA/NemoClaw) | Run OpenClaw more securely inside NVIDIA OpenShell with the NemoClaw blueprint. | -| [Hermes Agent](https://github.com/NousResearch/hermes-agent) | [NemoClaw](https://github.com/NVIDIA/NemoClaw) | Run Hermes Agent more securely inside NVIDIA OpenShell with the NemoClaw blueprint. | -| [Ollama](https://ollama.com/) | [Community](https://github.com/NVIDIA/OpenShell-Community) | Launch with `openshell sandbox create --from ollama`. | -| [Pi](https://pi.dev/) | [Community](https://github.com/NVIDIA/OpenShell-Community) | Launch with `openshell sandbox create --from pi`. | +OpenShell can run Linux agents packaged in OCI images. The default Ubuntu +workload does not bundle agent CLIs. Build or select an image containing your +agent, then authorize its binary paths, service endpoints, and credentials. + +| Agent | Integration | +| ----- | ----------- | +| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | Package Claude Code in a workload image and attach a `claude-code` provider or another endpoint-bearing model profile. | +| [OpenCode](https://opencode.ai/) | Package OpenCode in a workload image and attach its model provider and policy. | +| [Codex](https://developers.openai.com/codex) | Package Codex in a workload image and attach an OpenAI provider and policy. | +| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | Package the CLI in a workload image and attach GitHub credentials and policy. | +| [OpenClaw](https://openclaw.ai/) | Use the [NemoClaw](https://github.com/NVIDIA/NemoClaw) blueprint. | +| [Hermes Agent](https://github.com/NousResearch/hermes-agent) | Use the [NemoClaw](https://github.com/NVIDIA/NemoClaw) blueprint. | ## Key Commands @@ -197,23 +198,23 @@ openshell term The TUI gives you a live, keyboard-driven view of your gateway and sandboxes. Navigate with `Tab` to switch panels, `j`/`k` to move through lists, `Enter` to select, and `:` for command mode. Gateway health and sandbox status auto-refresh every two seconds. -## Community Sandboxes and BYOC +## Workload Images and BYOC -Use `--from` to create sandboxes from the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) catalog or a container image: +Use `--from` with an explicit OCI image reference: ```bash -openshell sandbox create --from gemini # community catalog -docker build -t my-sandbox:latest ./my-sandbox-dir # Docker gateway -openshell sandbox create --from my-sandbox:latest # Docker built image -podman build -t localhost/my-sandbox:latest ./my-sandbox-dir # Podman gateway -openshell sandbox create --from localhost/my-sandbox:latest # Podman built image -openshell sandbox create --from registry.io/img:v1 # container image +docker build -t my-sandbox:latest ./my-sandbox-dir +openshell sandbox create --from my-sandbox:latest + +podman build -t localhost/my-sandbox:latest ./my-sandbox-dir +openshell sandbox create --from localhost/my-sandbox:latest + +openshell sandbox create --from registry.example.com/agents/my-agent:1.0 ``` Build with the container engine used by your local gateway. For a remote -gateway, push the image to a registry that the gateway can pull from. - -See the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) catalog and the [BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container) for details. +gateway, push the image to a registry that the gateway can pull from. See the +[BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container). ## Use OpenShell with Your Agent diff --git a/TESTING.md b/TESTING.md index 699f44a72..e0de6dd91 100644 --- a/TESTING.md +++ b/TESTING.md @@ -207,7 +207,7 @@ the current checkout. The phase-1 scenario verifies the complete CLI-to-gateway-to-driver path without depending on how the gateway was installed or which driver is configured. It requires machine-readable gRPC status, creates a uniquely named detached -sandbox with `--from base`, verifies the sandbox is `Ready` by finding its +sandbox with the configured default image, verifies the sandbox is `Ready` by finding its unique name in paginated JSON list output, executes `echo` with a run-specific marker, deletes the sandbox, and verifies that its name no longer appears. Driver suites enable the same profile diff --git a/architecture/build.md b/architecture/build.md index 90914fa4a..37b1b063a 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -22,7 +22,7 @@ OpenShell builds these main artifacts: | VM driver/runtime assets | `crates/openshell-driver-vm` | | Published docs site | `docs/` rendered by Fern config in `fern/` | -Sandbox community images are built outside this repository. +Workload images are standard OCI images supplied by operators or users. ## Build Features diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index a2bb25c31..f224d1867 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -385,9 +385,8 @@ readiness. ## Images -The gateway image and Helm chart are built from this repository. Sandbox images -are maintained separately in the OpenShell Community repository or supplied by -users. +The gateway image and Helm chart are built from this repository. Users supply +workload images as standard OCI images. Custom sandbox images must include the agent runtime and any system dependencies, but they should not need to include the gateway. GPU-capable diff --git a/crates/openshell-cli/src/completers.rs b/crates/openshell-cli/src/completers.rs index c5fec2063..a0ab9a9b5 100644 --- a/crates/openshell-cli/src/completers.rs +++ b/crates/openshell-cli/src/completers.rs @@ -217,6 +217,7 @@ mod tests { with_vars( [ ("XDG_CONFIG_HOME", Some(tmp.as_str())), + ("OPENSHELL_SYSTEM_GATEWAY_DIR", Some(tmp.as_str())), ("OPENSHELL_GATEWAY", None::<&str>), ], f, diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index fdec4ce7e..5d230e375 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -319,7 +319,7 @@ const SANDBOX_EXAMPLES: &str = "\x1b[1mALIAS\x1b[0m \x1b[1mEXAMPLES\x1b[0m $ openshell sandbox create - $ openshell sandbox create --from python + $ openshell sandbox create --from registry.example.com/agents/python:latest $ openshell sandbox connect my-sandbox $ openshell sandbox list $ openshell sandbox delete my-sandbox @@ -1275,13 +1275,8 @@ enum SandboxCommands { #[arg(long, conflicts_with_all = ["from", "gpu", "cpu", "memory", "driver_config_json", "envs"])] template: Option, - /// Sandbox source: a community sandbox name (e.g., `ollama`), a rootfs - /// tar archive (`.tar`, `.tar.gz`, or `.tgz`), or a full container - /// image reference (e.g., `myregistry.com/img:tag`). - /// - /// Community names are resolved to - /// `ghcr.io/nvidia/openshell-community/sandboxes/:latest` - /// (override the prefix with `OPENSHELL_COMMUNITY_REGISTRY`). + /// Sandbox source: a rootfs tar archive (`.tar`, `.tar.gz`, or `.tgz`) + /// or a container image reference (e.g., `myregistry.com/img:tag`). /// /// To use a local Dockerfile, build and tag it with the container /// engine used by your local gateway, then pass the resulting image diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 77784fed2..5ffa4f529 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -1210,7 +1210,7 @@ enum ResolvedSource { /// 1. Existing file with `.tar`, `.tar.gz`, or `.tgz` extension → rootfs tar archive. /// 2. Local Dockerfile and directory paths → an actionable build-and-tag error. /// 3. Other explicit local paths → an actionable error. -/// 4. Full image reference or community sandbox name → resolve as an image. +/// 4. Any other value is passed through as an explicit image reference. fn resolve_from(value: &str) -> Result { let path = Path::new(value); @@ -1251,11 +1251,7 @@ fn resolve_from(value: &str) -> Result { )); } - // Full image reference or community sandbox name — delegate to shared - // resolution in openshell-core. - Ok(ResolvedSource::Image( - openshell_core::image::resolve_community_image(value), - )) + Ok(ResolvedSource::Image(value.to_string())) } #[allow(clippy::case_sensitive_file_extension_comparisons)] // already lowercased @@ -6699,7 +6695,7 @@ mod tests { } #[test] - fn resolve_from_keeps_bare_community_name_when_local_directory_matches() { + fn resolve_from_keeps_bare_image_reference_when_local_directory_matches() { let _lock = TEST_ENV_LOCK .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); @@ -6711,11 +6707,8 @@ mod tests { let result = resolve_from("python"); std::env::set_current_dir(original_dir).expect("restore current directory"); - match result.expect("bare community name should not be a local path") { - super::ResolvedSource::Image(image) => assert_eq!( - image, - "ghcr.io/nvidia/openshell-community/sandboxes/python:latest" - ), + match result.expect("bare image reference should not be a local path") { + super::ResolvedSource::Image(image) => assert_eq!(image, "python"), other @ super::ResolvedSource::RootfsTar { .. } => { panic!("expected image source, got {other:?}"); } diff --git a/crates/openshell-core/src/image.rs b/crates/openshell-core/src/image.rs index e804afd60..e092b9afb 100644 --- a/crates/openshell-core/src/image.rs +++ b/crates/openshell-core/src/image.rs @@ -1,124 +1,29 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Shared image-name resolution for community sandbox images. -//! -//! Both the CLI and TUI need to expand bare sandbox names (e.g. `"base"`) into -//! fully-qualified container image references. This module centralises that -//! logic so every client resolves names identically. +//! Default workload image selection. -/// Default registry prefix for community sandbox images. +/// Default sandbox workload image. /// -/// Bare sandbox names are expanded to `{prefix}/{name}:latest`. -/// Override at runtime with the `OPENSHELL_COMMUNITY_REGISTRY` env var. -pub const DEFAULT_COMMUNITY_REGISTRY: &str = "ghcr.io/nvidia/openshell-community/sandboxes"; +/// `OpenShell` uses a version-qualified NVIDIA Ubuntu Noble image so a fresh +/// installation does not depend on a separately maintained image catalog. +pub const DEFAULT_SANDBOX_BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; -/// Return the default sandbox image reference (`{registry}/base:latest`). +/// Return the default sandbox image reference. /// /// Used by all compute drivers as the fallback image when none is specified in /// the sandbox spec. #[must_use] pub fn default_sandbox_image() -> String { - format!("{DEFAULT_COMMUNITY_REGISTRY}/base:latest") -} - -/// Resolve a user-supplied image string into a fully-qualified reference. -/// -/// Resolution rules (applied in order): -/// 1. If the value contains `/`, `:`, or `.` it is treated as a complete image -/// reference and returned as-is. -/// 2. Otherwise it is treated as a community sandbox name and expanded to -/// `{registry}/{value}:latest` where `{registry}` defaults to -/// [`DEFAULT_COMMUNITY_REGISTRY`] but can be overridden via the -/// `OPENSHELL_COMMUNITY_REGISTRY` environment variable. -/// -/// This function only handles image-name resolution. Dockerfile detection is -/// the responsibility of the caller (e.g. the CLI's `resolve_from()`). -pub fn resolve_community_image(value: &str) -> String { - // Already a fully-qualified reference. - if value.contains('/') || value.contains(':') || value.contains('.') { - return value.to_string(); - } - - // Community sandbox shorthand → expand with registry prefix. - let prefix = std::env::var("OPENSHELL_COMMUNITY_REGISTRY") - .unwrap_or_else(|_| DEFAULT_COMMUNITY_REGISTRY.to_string()); - let prefix = prefix.trim_end_matches('/'); - format!("{prefix}/{value}:latest") + DEFAULT_SANDBOX_BASE_IMAGE.to_string() } #[cfg(test)] -#[allow(unsafe_code)] mod tests { use super::*; - use std::sync::{Mutex, OnceLock}; - - fn env_lock() -> &'static Mutex<()> { - static ENV_LOCK: OnceLock> = OnceLock::new(); - ENV_LOCK.get_or_init(|| Mutex::new(())) - } #[test] - fn bare_name_expands_to_community_registry() { - let _guard = env_lock().lock().unwrap(); - let result = resolve_community_image("base"); - assert_eq!( - result, - "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" - ); - } - - #[test] - fn bare_name_with_env_override() { - let _guard = env_lock().lock().unwrap(); - // Use a temp env override. Safety: test-only, and these env-var tests - // are not run concurrently with other tests reading the same var. - let key = "OPENSHELL_COMMUNITY_REGISTRY"; - let prev = std::env::var(key).ok(); - // SAFETY: single-threaded test context; no other thread reads this var. - unsafe { std::env::set_var(key, "my-registry.example.com/sandboxes") }; - let result = resolve_community_image("python"); - assert_eq!(result, "my-registry.example.com/sandboxes/python:latest"); - // Restore. - match prev { - Some(v) => unsafe { std::env::set_var(key, v) }, - None => unsafe { std::env::remove_var(key) }, - } - } - - #[test] - fn full_reference_with_slash_passes_through() { - let _guard = env_lock().lock().unwrap(); - let input = "ghcr.io/myorg/myimage:v1"; - assert_eq!(resolve_community_image(input), input); - } - - #[test] - fn reference_with_colon_passes_through() { - let _guard = env_lock().lock().unwrap(); - let input = "myimage:latest"; - assert_eq!(resolve_community_image(input), input); - } - - #[test] - fn reference_with_dot_passes_through() { - let _guard = env_lock().lock().unwrap(); - let input = "registry.example.com"; - assert_eq!(resolve_community_image(input), input); - } - - #[test] - fn trailing_slash_in_env_is_trimmed() { - let _guard = env_lock().lock().unwrap(); - let key = "OPENSHELL_COMMUNITY_REGISTRY"; - let prev = std::env::var(key).ok(); - // SAFETY: single-threaded test context; no other thread reads this var. - unsafe { std::env::set_var(key, "my-registry.example.com/sandboxes/") }; - let result = resolve_community_image("base"); - assert_eq!(result, "my-registry.example.com/sandboxes/base:latest"); - match prev { - Some(v) => unsafe { std::env::set_var(key, v) }, - None => unsafe { std::env::remove_var(key) }, - } + fn default_image_is_version_qualified_nvidia_ubuntu() { + assert_eq!(default_sandbox_image(), "nvcr.io/nvidia/base/ubuntu:24.04"); } } diff --git a/crates/openshell-core/src/sandbox_env.rs b/crates/openshell-core/src/sandbox_env.rs index c1c91822b..24640c790 100644 --- a/crates/openshell-core/src/sandbox_env.rs +++ b/crates/openshell-core/src/sandbox_env.rs @@ -243,6 +243,18 @@ pub const SANDBOX_UID: &str = "OPENSHELL_SANDBOX_UID"; /// supervisor drops privileges to a group other than the UID's primary group. pub const SANDBOX_GID: &str = "OPENSHELL_SANDBOX_GID"; +/// Default numeric UID assigned to a sandbox when the image declares no OCI +/// `USER` (for example, a minimal base image). +/// +/// Local container drivers (Docker, Podman) supply this in place of an empty +/// OCI declaration so the supervisor runs the sandbox as a synthesized non-root +/// account instead of rejecting the image, matching the numeric-identity +/// behavior of the Kubernetes and VM drivers. +pub const DEFAULT_SANDBOX_UID: u32 = 1000; + +/// Default numeric GID paired with [`DEFAULT_SANDBOX_UID`]. +pub const DEFAULT_SANDBOX_GID: u32 = 1000; + /// Raw OCI `Config.User` declaration from the immutable image selected by a /// local container driver. /// diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 1bdfac927..0d7936565 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -654,9 +654,18 @@ fn resolve_docker_identity_from_accounts( requested_user }; if user_selector.is_empty() { - return Err(Status::failed_precondition( - "the pinned image defaults to root; configure a non-root process.run_as_user", - )); + // The image declares no USER (for example, a minimal base image) and the policy + // requested none. Synthesize a numeric non-root identity instead of + // rejecting, matching the Podman driver's USER-less default and the + // numeric-identity behavior of the Kubernetes and VM drivers. + return ResolvedWorkloadIdentity::new( + openshell_core::sandbox_env::DEFAULT_SANDBOX_UID, + openshell_core::sandbox_env::DEFAULT_SANDBOX_GID, + Vec::new(), + "default".to_string(), + image.id.clone(), + ) + .map_err(|error| Status::failed_precondition(error.to_string())); } let (uid, passwd_entry) = resolve_numeric_or_named_user(user_selector, &passwd)?; let username = passwd_entry.map(|entry| entry.name.as_str()); diff --git a/crates/openshell-driver-docker/src/tests.rs b/crates/openshell-driver-docker/src/tests.rs index a0f45f31b..9c20f1671 100644 --- a/crates/openshell-driver-docker/src/tests.rs +++ b/crates/openshell-driver-docker/src/tests.rs @@ -62,7 +62,7 @@ fn test_sandbox() -> DriverSandbox { log_level: "debug".to_string(), environment: HashMap::from([("SPEC_ENV".to_string(), "spec".to_string())]), template: Some(DriverSandboxTemplate { - image: "ghcr.io/nvidia/openshell-community/sandboxes/base:latest".to_string(), + image: "nvcr.io/nvidia/base/ubuntu:24.04".to_string(), agent_socket_path: String::new(), labels: HashMap::new(), environment: HashMap::from([("TEMPLATE_ENV".to_string(), "template".to_string())]), @@ -1675,6 +1675,33 @@ fn docker_identity_resolution_uses_pinned_image_accounts_and_exact_groups() { assert_eq!(resolved.resource_digest, "sha256:image"); } +#[test] +fn docker_identity_resolution_uses_numeric_default_for_userless_image() { + let image = DockerImageMetadata { + id: "sha256:image".to_string(), + user: String::new(), + working_dir: "/".to_string(), + volumes: Vec::new(), + }; + let resolved = resolve_docker_identity_from_accounts( + &test_sandbox(), + &image, + b"root:x:0:0:root:/root:/bin/sh\n", + b"root:x:0:\n", + ) + .unwrap(); + + assert_eq!( + (resolved.uid, resolved.gid), + ( + openshell_core::sandbox_env::DEFAULT_SANDBOX_UID, + openshell_core::sandbox_env::DEFAULT_SANDBOX_GID, + ) + ); + assert_eq!(resolved.source, "default"); + assert_eq!(resolved.resource_digest, "sha256:image"); +} + #[test] fn docker_identity_resolution_honors_policy_selectors_and_rejects_root() { let mut sandbox = test_sandbox(); diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index 4378d0a6b..fd5dd4a5a 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -593,18 +593,37 @@ fn build_env( // hostname could otherwise present a certificate for a name they control // and intercept the sandbox JWT. env.remove(openshell_core::sandbox_env::GATEWAY_TLS_SERVER_NAME); - env.insert( - openshell_core::sandbox_env::OCI_IMAGE_USER.into(), - oci_user.to_string(), - ); - env.insert( - openshell_core::sandbox_env::SANDBOX_UID.into(), - String::new(), - ); - env.insert( - openshell_core::sandbox_env::SANDBOX_GID.into(), - String::new(), - ); + if oci_user.is_empty() { + // The image declares no OCI USER (for example, a minimal base image). Assign a + // numeric non-root identity like the Kubernetes and VM drivers so the + // supervisor synthesizes the account instead of rejecting the image. + env.insert( + openshell_core::sandbox_env::OCI_IMAGE_USER.into(), + String::new(), + ); + env.insert( + openshell_core::sandbox_env::SANDBOX_UID.into(), + openshell_core::sandbox_env::DEFAULT_SANDBOX_UID.to_string(), + ); + env.insert( + openshell_core::sandbox_env::SANDBOX_GID.into(), + openshell_core::sandbox_env::DEFAULT_SANDBOX_GID.to_string(), + ); + } else { + // The image declares a USER; preserve the OCI resolution path. + env.insert( + openshell_core::sandbox_env::OCI_IMAGE_USER.into(), + oci_user.to_string(), + ); + env.insert( + openshell_core::sandbox_env::SANDBOX_UID.into(), + String::new(), + ); + env.insert( + openshell_core::sandbox_env::SANDBOX_GID.into(), + String::new(), + ); + } // 4. Gateway-minted sandbox JWT. Keep the raw bearer out of container // metadata; the supervisor reads it from a driver-owned bind mount. @@ -1130,7 +1149,7 @@ fn build_base_spec( image_volumes, hostname: format!("sandbox-{}", sandbox.name), // Override the image's ENTRYPOINT so the supervisor binary runs - // directly. Sandbox images (e.g. the community base image) set + // directly. Workload images can set // ENTRYPOINT ["/bin/bash"], and Podman's `command` field only // overrides CMD — which gets appended as args to the entrypoint. // Without this, the container would run the entrypoint binary with diff --git a/crates/openshell-driver-podman/src/isolation.rs b/crates/openshell-driver-podman/src/isolation.rs index b616fa647..639ed35ad 100644 --- a/crates/openshell-driver-podman/src/isolation.rs +++ b/crates/openshell-driver-podman/src/isolation.rs @@ -87,6 +87,19 @@ pub fn resolve_identity( } else { requested_group }; + if user.is_empty() && group.is_empty() { + // The image declares no OCI USER (for example, a minimal base image) and the + // policy requested no identity. Synthesize a numeric non-root identity + // instead of rejecting the image, matching Docker, Kubernetes, and VM. + return ResolvedWorkloadIdentity::new( + openshell_core::sandbox_env::DEFAULT_SANDBOX_UID, + openshell_core::sandbox_env::DEFAULT_SANDBOX_GID, + Vec::new(), + "default".into(), + image_id.into(), + ) + .map_err(invalid); + } let account = accounts .iter() .find(|(name, uid, _)| *name == user || user.parse::().ok() == Some(*uid)); @@ -361,10 +374,30 @@ mod tests { assert_eq!(identity.supplementary_gids, vec![2000]); assert_eq!(identity.resource_digest, "sha256:pinned"); assert!(resolve_identity(&sandbox, "sha256:pinned", "root", passwd, groups).is_err()); - assert!(resolve_identity(&sandbox, "sha256:pinned", "", passwd, groups).is_err()); assert!(resolve_identity(&sandbox, "sha256:pinned", "2000", passwd, groups).is_err()); } + #[test] + fn identity_uses_numeric_default_for_userless_image() { + let identity = resolve_identity( + &DriverSandbox::default(), + "sha256:pinned", + "", + b"root:x:0:0:root:/root:/bin/sh\n", + b"root:x:0:\n", + ) + .unwrap(); + + assert_eq!( + (identity.uid, identity.gid), + ( + openshell_core::sandbox_env::DEFAULT_SANDBOX_UID, + openshell_core::sandbox_env::DEFAULT_SANDBOX_GID, + ) + ); + assert_eq!(identity.source, "default"); + } + fn files(bytes: &[u8]) -> BTreeMap> { tar::Archive::new(bytes) .entries() diff --git a/crates/openshell-driver-vm/runtime/pins.env b/crates/openshell-driver-vm/runtime/pins.env index 9977eddcc..e56b03834 100644 --- a/crates/openshell-driver-vm/runtime/pins.env +++ b/crates/openshell-driver-vm/runtime/pins.env @@ -22,8 +22,8 @@ VM_BASE_IMAGE="${VM_BASE_IMAGE:-nvcr.io/nvidia/base/ubuntu@sha256:43fa5063e80fbb # ── Container images for rootfs pre-loading (digest-pinned) ──────────── # Tag: registry.k8s.io/agent-sandbox/agent-sandbox-controller:v0.1.0 AGENT_SANDBOX_IMAGE="${AGENT_SANDBOX_IMAGE:-registry.k8s.io/agent-sandbox/agent-sandbox-controller@sha256:ba71ea40ae0872791197badf2ab84f3f482df3902f1fce7ca9e076b1de9b57f6}" -# Tag: ghcr.io/nvidia/openshell-community/sandboxes/base:latest -COMMUNITY_SANDBOX_IMAGE="${COMMUNITY_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:d446c17105e7448e602238a8a5a4ddd0233c071082406522f81c31f8b1309525}" +# Tag: nvcr.io/nvidia/base/ubuntu:24.04 +VM_SANDBOX_IMAGE="${VM_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c}" # SERVER_IMAGE is intentionally NOT pinned here — it changes frequently # during local development. Override via IMAGE_REPO_BASE and IMAGE_TAG diff --git a/crates/openshell-ocsf/tests/roundtrip.rs b/crates/openshell-ocsf/tests/roundtrip.rs index 42664400a..4617cbfe2 100644 --- a/crates/openshell-ocsf/tests/roundtrip.rs +++ b/crates/openshell-ocsf/tests/roundtrip.rs @@ -22,7 +22,7 @@ fn ctx() -> EventContext { EventContext { sandbox_id: "sb-7f3a9c2e14b8".to_string(), sandbox_name: "agent-workspace-01".to_string(), - container_image: "ghcr.io/nvidia/openshell-community/sandboxes/base:latest".to_string(), + container_image: "nvcr.io/nvidia/base/ubuntu:24.04".to_string(), hostname: "openshell-sb-7f3a9c2e14b8".to_string(), product_version: "0.42.1".to_string(), proxy_ip: IpAddr::V4(Ipv4Addr::LOCALHOST), diff --git a/crates/openshell-policy/src/lib.rs b/crates/openshell-policy/src/lib.rs index a9d2ab0bb..0f2d14521 100644 --- a/crates/openshell-policy/src/lib.rs +++ b/crates/openshell-policy/src/lib.rs @@ -923,7 +923,7 @@ pub use openshell_core::container_paths::CONTAINER_POLICY_PATH; /// Legacy path used before the navigator → openshell rename. /// -/// Existing community sandbox images still ship their policy at this path. +/// Older images may still ship their policy at this path. /// The sandbox supervisor tries [`CONTAINER_POLICY_PATH`] first, then falls /// back to this legacy path for backward compatibility. pub const LEGACY_CONTAINER_POLICY_PATH: &str = "/etc/navigator/policy.yaml"; @@ -945,7 +945,6 @@ pub fn restrictive_default_policy() -> SandboxPolicy { "/lib".into(), "/proc".into(), "/dev/urandom".into(), - "/app".into(), "/etc".into(), "/var/log".into(), ], diff --git a/crates/openshell-sandbox/src/file_transfer.rs b/crates/openshell-sandbox/src/file_transfer.rs index 00b23a0b4..2f49ddf6a 100644 --- a/crates/openshell-sandbox/src/file_transfer.rs +++ b/crates/openshell-sandbox/src/file_transfer.rs @@ -68,13 +68,15 @@ fn components(path: &Path) -> Result> { Ok(result) } -fn safe_symlink_target(path: &Path) -> Result<()> { +fn safe_symlink_target(path: &Path, mut parent_depth: usize) -> Result<()> { for component in path.components() { - if matches!( - component, - Component::ParentDir | Component::RootDir | Component::Prefix(_) - ) { - return Err(miette::miette!(SYMLINK_ESCAPE_ERROR)); + match component { + Component::CurDir => {} + Component::Normal(_) => parent_depth += 1, + Component::ParentDir if parent_depth > 0 => parent_depth -= 1, + Component::ParentDir | Component::RootDir | Component::Prefix(_) => { + return Err(miette::miette!(SYMLINK_ESCAPE_ERROR)); + } } } Ok(()) @@ -202,7 +204,7 @@ fn extract(reader: R, workload_root: &Path, destination: &Path) -> Resu .link_name() .into_diagnostic()? .ok_or_else(|| miette::miette!("symlink entry has no target"))?; - safe_symlink_target(&target)?; + safe_symlink_target(&target, parent_path.len())?; remove_non_directory(&parent, leaf)?; symlinkat(target.as_os_str(), &parent, leaf) .into_diagnostic() @@ -226,11 +228,17 @@ fn archive(writer: W, workload_root: &Path, source: &Path) -> Result<( let source = target_components(workload_root, source)?; let mut archive = tar::Builder::new(writer); if source.is_empty() { - append_directory_contents(&mut archive, &root, Path::new(""))?; + append_directory_contents(&mut archive, &root, Path::new(""), 0)?; } else { let (parent_path, leaf) = split_parent(&source)?; let parent = open_dir_path(&root, parent_path, false)?; - append_entry(&mut archive, &parent, leaf, Path::new(leaf))?; + append_entry( + &mut archive, + &parent, + leaf, + Path::new(leaf), + parent_path.len(), + )?; } archive .finish() @@ -253,6 +261,7 @@ fn append_entry( parent: impl AsFd, name: &OsStr, archive_path: &Path, + workspace_parent_depth: usize, ) -> Result<()> { let stat = statat(&parent, name, AtFlags::SYMLINK_NOFOLLOW) .into_diagnostic() @@ -286,11 +295,11 @@ fn append_entry( archive .append_data(&mut header, archive_path, std::io::empty()) .into_diagnostic()?; - append_directory_contents(archive, &fd, archive_path)?; + append_directory_contents(archive, &fd, archive_path, workspace_parent_depth + 1)?; } else if kind == libc::S_IFLNK { let target = readlinkat(&parent, name, Vec::new()).into_diagnostic()?; let target = PathBuf::from(OsString::from_vec(target.into_bytes())); - safe_symlink_target(&target)?; + safe_symlink_target(&target, workspace_parent_depth)?; let mut header = header(&stat, tar::EntryType::Symlink); header.set_size(0); header.set_cksum(); @@ -310,6 +319,7 @@ fn append_directory_contents( archive: &mut tar::Builder, directory: impl AsFd, archive_path: &Path, + workspace_directory_depth: usize, ) -> Result<()> { let mut names = Dir::read_from(&directory) .into_diagnostic()? @@ -320,7 +330,13 @@ fn append_directory_contents( names.sort(); for name in names { let name = OsString::from_vec(name); - append_entry(archive, &directory, &name, &archive_path.join(&name))?; + append_entry( + archive, + &directory, + &name, + &archive_path.join(&name), + workspace_directory_depth, + )?; } Ok(()) } @@ -336,6 +352,33 @@ mod tests { assert_eq!(components(Path::new("a/./b")).unwrap().len(), 2); } + #[test] + fn permits_parent_symlinks_that_stay_inside_the_workspace() { + assert!(safe_symlink_target(Path::new("../.agents/skills"), 1).is_ok()); + assert!(safe_symlink_target(Path::new("../escape"), 0).is_err()); + assert!(safe_symlink_target(Path::new("nested/../../escape"), 0).is_err()); + assert!(safe_symlink_target(Path::new("/etc/passwd"), 3).is_err()); + } + + #[test] + fn round_trips_a_parent_symlink_that_stays_inside_the_workspace() { + let source = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(source.path().join(".agents/skills")).unwrap(); + std::fs::create_dir(source.path().join(".claude")).unwrap(); + std::os::unix::fs::symlink("../.agents/skills", source.path().join(".claude/skills")) + .unwrap(); + + let mut bytes = Vec::new(); + archive(&mut bytes, source.path(), Path::new("")).unwrap(); + + let destination = tempfile::tempdir().unwrap(); + extract(bytes.as_slice(), destination.path(), Path::new(".")).unwrap(); + assert_eq!( + std::fs::read_link(destination.path().join(".claude/skills")).unwrap(), + Path::new("../.agents/skills") + ); + } + #[test] fn extraction_never_follows_an_archive_created_symlink() { let destination = tempfile::Builder::new() diff --git a/crates/openshell-sdk/README.md b/crates/openshell-sdk/README.md index 5f889f61f..b8817028a 100644 --- a/crates/openshell-sdk/README.md +++ b/crates/openshell-sdk/README.md @@ -108,7 +108,7 @@ client }), spec: Some(SandboxWorkloadTemplateSpec { workload: Some(SandboxWorkloadConfig { - image: "ghcr.io/nvidia/openshell-community/sandboxes/python:latest".to_string(), + image: "registry.example.com/agents/python:latest".to_string(), ..Default::default() }), ..Default::default() diff --git a/crates/openshell-sdk/src/types.rs b/crates/openshell-sdk/src/types.rs index 3937462d0..adf7f7d59 100644 --- a/crates/openshell-sdk/src/types.rs +++ b/crates/openshell-sdk/src/types.rs @@ -146,7 +146,7 @@ impl From for SandboxPhase { pub struct SandboxSpec { /// Optional user-supplied sandbox name. When empty the server generates one. pub name: Option, - /// Container image reference (e.g. `ghcr.io/nvidia/openshell-community/sandboxes/python:latest`). + /// Container image reference (e.g. `registry.example.com/agents/python:latest`). pub image: Option, /// Labels attached to the sandbox. pub labels: HashMap, diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 4cca02d6a..e3558ac73 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -11718,7 +11718,7 @@ mod tests { ..Default::default() }), template: Some(SandboxTemplate { - image: "ghcr.io/nvidia/openshell-community/sandboxes/base:latest".to_string(), + image: "nvcr.io/nvidia/base/ubuntu:24.04".to_string(), driver_config: Some(prost_types::Struct { fields: [ ( diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 23705a24e..70e0cf5f5 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1459,9 +1459,8 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { tokio::spawn(async move { let has_custom_image = !image.is_empty(); let template = if has_custom_image { - let resolved = openshell_core::image::resolve_community_image(&image); Some(openshell_core::proto::SandboxTemplate { - image: resolved, + image, ..Default::default() }) } else { diff --git a/deploy/docker/gateway.toml b/deploy/docker/gateway.toml index 1372bfc3f..c0cbda4ef 100644 --- a/deploy/docker/gateway.toml +++ b/deploy/docker/gateway.toml @@ -35,7 +35,7 @@ disable_tls = true [openshell.drivers.docker] # Default image pulled for `openshell sandbox create` without --from. -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" # Sandbox runtime image from which the openshell-sandbox binary is extracted. sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:latest" # Image containing the external supervisor process. diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 231983aca..f835d4d2b 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -288,7 +288,7 @@ discovery endpoint or its TLS CA. | server.policyValidationFailureMode | string | `"fail_closed"` | Posture when a candidate sandbox policy fails validation. `fail_closed` deactivates the previous policy; `retain_last_valid` keeps it active. | | server.providerTokenGrants.spiffe.enabled | bool | `false` | Mount the SPIFFE Workload API socket into gateway and sandbox pods for dynamic provider token grants. | | server.providerTokenGrants.spiffe.workloadApiSocketPath | string | `"/spiffe-workload-api/spire-agent.sock"` | Path to the SPIFFE Workload API socket mounted into gateway and sandbox pods. | -| server.sandboxImage | string | `"ghcr.io/nvidia/openshell-community/sandboxes/base:latest"` | Default sandbox image used when requests do not specify one. | +| server.sandboxImage | string | `"nvcr.io/nvidia/base/ubuntu:24.04"` | Default sandbox image used when requests do not specify one. | | server.sandboxImagePullPolicy | string | `nil` | Pull policy for sandbox pods. Leave unset to use the Kubernetes image default (Always for :latest, IfNotPresent otherwise). Prefer always, if_not_present, or never; the chart also accepts legacy Kubernetes spellings Always, IfNotPresent, and Never. | | server.sandboxImagePullSecrets | list | `[]` | Image pull secrets attached to sandbox pods. Referenced Secrets must exist in the sandbox namespace. | | server.sandboxJwt.gatewayId | string | `""` | Stable gateway identity embedded in iss/aud of every minted token. Defaults to the release name so HA replicas share identity. | diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 67d5587ec..f395b7fa5 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -214,7 +214,7 @@ server: # `uri` key, e.g. postgresql://user:pass@host:5432/dbname. externalDbSecret: "" # -- Default sandbox image used when requests do not specify one. - sandboxImage: "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" + sandboxImage: "nvcr.io/nvidia/base/ubuntu:24.04" # -- Pull policy for sandbox pods. Leave unset to use the Kubernetes image # default (Always for :latest, IfNotPresent otherwise). Prefer always, # if_not_present, or never; the chart also accepts legacy Kubernetes spellings diff --git a/deploy/kube/manifests/openshell-helmchart.yaml b/deploy/kube/manifests/openshell-helmchart.yaml index 3ca6e3b90..f2c754033 100644 --- a/deploy/kube/manifests/openshell-helmchart.yaml +++ b/deploy/kube/manifests/openshell-helmchart.yaml @@ -29,7 +29,7 @@ spec: tag: latest pullPolicy: __IMAGE_PULL_POLICY__ server: - sandboxImage: ghcr.io/nvidia/openshell-community/sandboxes/base:latest + sandboxImage: nvcr.io/nvidia/base/ubuntu:24.04 sandboxImagePullPolicy: __SANDBOX_IMAGE_PULL_POLICY__ supervisorImage: ghcr.io/nvidia/openshell/supervisor:latest dbUrl: __DB_URL__ diff --git a/deploy/rpm/CONFIGURATION.md b/deploy/rpm/CONFIGURATION.md index af2e97a94..b6a1303c9 100644 --- a/deploy/rpm/CONFIGURATION.md +++ b/deploy/rpm/CONFIGURATION.md @@ -221,7 +221,7 @@ overrides that persist across package upgrades. |-------------|---------|-------------| | `bind_address` | `127.0.0.1:17670` (gateway default) | Address for the primary gRPC/HTTP API listener. | | `compute_driver` | `"podman"` (RPM default) | When unset, the gateway auto-detects Kubernetes, then Podman, then Docker. The RPM default pins to Podman; legacy `compute_drivers` lists are rejected. | -| `[openshell.drivers.podman].default_image` | `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` | Default sandbox image. | +| `[openshell.drivers.podman].default_image` | `nvcr.io/nvidia/base/ubuntu:24.04` | Default sandbox image. | | `[openshell.drivers.podman].sandbox_runtime_image` | `ghcr.io/nvidia/openshell/sandbox:latest` | Static musl sandbox runtime image mounted into Podman workloads. | | `[openshell.drivers.podman].supervisor_image` | `ghcr.io/nvidia/openshell/supervisor:latest` | Dynamic glibc supervisor image used outside the workload. | | `[openshell.gateway].guest_tls_ca`, `guest_tls_cert`, `guest_tls_key` | auto-generated paths | Gateway-owned client TLS material injected into the selected local driver and mounted into sandbox containers. | @@ -244,7 +244,7 @@ version = 2 compute_driver = "podman" [openshell.drivers.podman] -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" health_check_interval_secs = 10 network_name = "openshell" @@ -261,7 +261,7 @@ To update cached images: ```shell podman pull ghcr.io/nvidia/openshell/supervisor:latest -podman pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest +podman pull nvcr.io/nvidia/base/ubuntu:24.04 ``` Or set `image_pull_policy = "always"` in @@ -273,7 +273,7 @@ To pin specific image versions instead of `:latest`, set these values in ```toml sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:v0.0.37" supervisor_image = "ghcr.io/nvidia/openshell/supervisor:v0.0.37" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:v0.0.37" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" ``` For air-gapped environments: @@ -282,9 +282,9 @@ For air-gapped environments: ```shell podman pull ghcr.io/nvidia/openshell/supervisor:latest - podman pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest + podman pull nvcr.io/nvidia/base/ubuntu:24.04 podman save -o supervisor.tar ghcr.io/nvidia/openshell/supervisor:latest - podman save -o sandbox.tar ghcr.io/nvidia/openshell-community/sandboxes/base:latest + podman save -o sandbox.tar nvcr.io/nvidia/base/ubuntu:24.04 ``` 1. Transfer the tarballs to the air-gapped host and load them: diff --git a/deploy/rpm/TROUBLESHOOTING.md b/deploy/rpm/TROUBLESHOOTING.md index 29b27ad20..b9b2fdb56 100644 --- a/deploy/rpm/TROUBLESHOOTING.md +++ b/deploy/rpm/TROUBLESHOOTING.md @@ -176,7 +176,7 @@ sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER **Image pull failure.** Verify ghcr.io is reachable: ```shell -podman pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest +podman pull nvcr.io/nvidia/base/ubuntu:24.04 ``` ### Images not updating @@ -185,7 +185,7 @@ The default image pull policy is `if_not_present` -- images are pulled once and cached. To update: ```shell -podman pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest +podman pull nvcr.io/nvidia/base/ubuntu:24.04 podman pull ghcr.io/nvidia/openshell/supervisor:latest ``` @@ -239,7 +239,7 @@ To pick up new container images after an upgrade: ```shell podman pull ghcr.io/nvidia/openshell/supervisor:latest -podman pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest +podman pull nvcr.io/nvidia/base/ubuntu:24.04 ``` ### Migrating a TLS-enabled local driver to schema version 2 diff --git a/docs/about/overview.mdx b/docs/about/overview.mdx index 90b565ae3..4c8a234e7 100644 --- a/docs/about/overview.mdx +++ b/docs/about/overview.mdx @@ -47,7 +47,7 @@ OpenShell supports a range of agent deployment patterns. | Secure coding agents | Run Claude Code, OpenCode, Codex, or GitHub Copilot CLI with constrained file and network access. | | Private enterprise development | Grant selected sandboxes access to self-hosted or private model endpoints while keeping sensitive context under your control. | | Compliance and audit | Treat policy YAML as version-controlled security controls that can be reviewed and audited. | -| Reusable environments | Use community sandbox images or bring your own containerized runtime. | +| Reusable environments | Use the default Ubuntu workload or bring your own containerized runtime. | ## Next Steps diff --git a/docs/about/release-notes.mdx b/docs/about/release-notes.mdx index c5f518fd7..9aad3f5d9 100644 --- a/docs/about/release-notes.mdx +++ b/docs/about/release-notes.mdx @@ -12,6 +12,23 @@ NVIDIA OpenShell follows a frequent release cadence. Use the following GitHub re ## 0.1.0 migration notes +### Workload images + +The default workload image is now `nvcr.io/nvidia/base/ubuntu:24.04`. It is a +minimal Ubuntu Noble environment and does not include agent CLIs or an +image-baked OpenShell policy. OpenShell applies its built-in restrictive policy +when no explicit policy is supplied. + +The CLI no longer expands bare catalog names or supports the former catalog +registry override. Build or select an OCI image and pass its explicit reference: + +```shell +openshell sandbox create --from registry.example.com/agents/my-agent:1.0 -- my-agent +``` + +Gateway operators can replace the default with the driver configuration or the +Helm `server.sandboxImage` value. + ### Network policy binaries OpenShell 0.1.0 removes the deprecated `NetworkBinary.harness` protobuf field and reserves its field number and name. Generated protobuf decoders ignore the unknown value. When the gateway loads policy history written by an older version, it migrates binaries marked by the former field into endpoint-provenance-marked rules so the upgrade cannot widen private-address access. diff --git a/docs/about/supported-agents.mdx b/docs/about/supported-agents.mdx index 13704aa94..601a91ebe 100644 --- a/docs/about/supported-agents.mdx +++ b/docs/about/supported-agents.mdx @@ -6,19 +6,17 @@ description: "AI agent frameworks and runtimes compatible with OpenShell sandbox keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Claude, Codex, Cursor" position: 5 --- -The following table summarizes the agents that run in OpenShell sandboxes. Most agent sandbox images are maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. Agents in the base image are auto-configured when passed as the trailing command to `openshell sandbox create`. -| Agent | Source | Default Policy | Notes | -|---|---|---|---| -| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Works out of the box with an attached `claude-code` provider. For another model platform, attach an endpoint-bearing profile and configure Claude Code for that platform's native API. | -| [OpenCode](https://opencode.ai/) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Partial coverage | Pre-installed. Attach the selected model provider, configure its native base URL and model, and ensure the imported profile names OpenCode's service endpoint and binary paths. | -| [Codex](https://developers.openai.com/codex) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | No coverage | Pre-installed. Requires a custom policy with OpenAI endpoints and Codex binary paths. Requires `OPENAI_API_KEY`. | -| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Pre-installed. Works out of the box. Requires `GITHUB_TOKEN` or `COPILOT_GITHUB_TOKEN`. | -| [OpenClaw](https://openclaw.ai/) | [NemoClaw](https://github.com/NVIDIA/NemoClaw) | Blueprint-managed | Run OpenClaw more securely inside NVIDIA OpenShell with the NemoClaw blueprint. | -| [Hermes Agent](https://github.com/NousResearch/hermes-agent) | [NemoClaw](https://github.com/NVIDIA/NemoClaw) | Blueprint-managed | Run Hermes Agent more securely inside NVIDIA OpenShell with the NemoClaw blueprint. | -| [Ollama](https://ollama.com/) | [`ollama`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/ollama) | Bundled | Run cloud and local models. Includes Claude Code, Codex, and OpenCode. Launch with `openshell sandbox create --from ollama`. | -| [Pi](https://pi.dev/) | [`pi`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/pi) | Bundled | Comes with Pi pre-installed. Launch with `openshell sandbox create --from pi`. | +OpenShell can run containerized agents that support Linux. The default Ubuntu workload image does not bundle agent CLIs. Build or select an OCI image containing the agent, pass its explicit image reference to `--from`, and attach the providers and policy it needs. -For base image details and `--from` usage, refer to [Sandboxes](/sandboxes/manage-sandboxes#base-sandbox-container). +| Agent | Image workflow | Provider and policy requirements | +|---|---|---| +| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | Add Claude Code to your workload image. | Attach a `claude-code` provider or an endpoint-bearing profile for the selected model platform. Authorize the agent binary and service endpoints. | +| [OpenCode](https://opencode.ai/) | Add OpenCode to your workload image. | Attach the selected model provider and authorize OpenCode's binary paths and service endpoints. | +| [Codex](https://developers.openai.com/codex) | Add Codex to your workload image. | Attach an OpenAI provider and authorize the Codex binary paths and OpenAI endpoints. | +| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | Add GitHub Copilot CLI to your workload image. | Attach GitHub credentials and authorize the CLI binary and required GitHub endpoints. | +| [OpenClaw](https://openclaw.ai/) | Use the [NemoClaw](https://github.com/NVIDIA/NemoClaw) blueprint. | The blueprint manages the workload and its policy. | +| [Hermes Agent](https://github.com/NousResearch/hermes-agent) | Use the [NemoClaw](https://github.com/NVIDIA/NemoClaw) blueprint. | The blueprint manages the workload and its policy. | +| [Ollama](https://ollama.com/) | Build an image containing Ollama, or connect a sandbox to a separately managed Ollama service. | Authorize the client binary and the Ollama endpoint. Refer to [Inference with Ollama](/get-started/tutorials/inference-ollama). | -For a complete support matrix, refer to the [Support Matrix](/reference/support-matrix) page. +For image selection and `--from` usage, refer to [Manage Sandboxes](/sandboxes/manage-sandboxes#sandbox-images). For a complete platform matrix, refer to [Support Matrix](/reference/support-matrix). diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 4d7a3e2de..92edecbbc 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Quickstart" -description: "Install the OpenShell CLI, connect to a gateway, and create your first sandboxed AI agent." +description: "Install the OpenShell CLI, connect to a gateway, and create your first policy-enforced sandbox." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Installation, Quickstart, Gateway, Docker, Kubernetes, Podman" position: 1 --- @@ -36,66 +36,37 @@ After installing the CLI, run `openshell --help` in your terminal to view the fu Install the public OpenShell agent skills with `npx skills add NVIDIA/OpenShell`. The `openshell-cli` skill guides your agent through common workflows and uses the installed CLI help as the command reference; no OpenShell source checkout is required. -## Create Your First OpenShell Sandbox +## Create Your First Sandbox -Create a sandbox and launch an agent inside it. -Choose the tab that matches your agent: - - - - -Run the following command to create a sandbox with Claude Code: +Create a sandbox with the gateway's default workload image: ```shell -openshell sandbox create -- claude +openshell sandbox create --name quickstart ``` -The CLI prompts you to create a provider from local credentials. -Type `yes` to continue. -If `ANTHROPIC_API_KEY` is set in your environment, the CLI picks it up automatically. -If not, you can configure it from inside the sandbox after it launches. +The default is `nvcr.io/nvidia/base/ubuntu:24.04`. It provides a minimal Ubuntu Noble environment; install workload-specific tools in your own image. - -`ANTHROPIC_API_KEY` is an API key from [console.anthropic.com](https://console.anthropic.com), not a subscription token. Subscription users must generate a separate API key. - - - - - -Run the following command to create a sandbox with OpenCode: +Connect to the sandbox: ```shell -openshell sandbox create -- opencode +openshell sandbox connect quickstart ``` -The CLI prompts you to create a provider from local credentials. -Type `yes` to continue. -If `OPENAI_API_KEY` or `OPENROUTER_API_KEY` is set in your environment, the CLI picks it up automatically. -If not, you can configure it from inside the sandbox after it launches. - - - - -Run the following command to create a sandbox with Codex: +Run a command without opening an interactive session: ```shell -openshell sandbox create -- codex +openshell sandbox exec -n quickstart -- cat /etc/os-release ``` -The CLI prompts you to create a provider from local credentials. -Type `yes` to continue. -If `OPENAI_API_KEY` is set in your environment, the CLI picks it up automatically. -If not, you can configure it from inside the sandbox after it launches. - +## Run an Agent Image - - -Use the `--from` flag to create a sandbox from the base container: +Build an OCI image containing your agent and its dependencies, then pass its full image reference: ```shell -openshell sandbox create --from base +docker build -t my-agent:latest ./my-agent +openshell sandbox create --from my-agent:latest -- my-agent ``` - +For a Podman gateway, build and use a Podman-visible name such as `localhost/my-agent:latest`. For a remote gateway, push the image to a registry that the gateway can pull from. - +Attach a provider and a policy that authorize the agent's credentials, endpoints, and executable paths. Refer to [Bring Your Own Container](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container), [Providers](/providers), and [Customize Sandbox Policies](/sandboxes/policies). diff --git a/docs/get-started/tutorials/docker-compose.mdx b/docs/get-started/tutorials/docker-compose.mdx index fd8c13f05..678e3a95b 100644 --- a/docs/get-started/tutorials/docker-compose.mdx +++ b/docs/get-started/tutorials/docker-compose.mdx @@ -123,40 +123,38 @@ Confirm the provider was stored: openshell provider list ``` -## Pre-pull sandbox images (optional) +## Pre-pull workload images (optional) -Sandbox images are pulled automatically on first use, but the initial pull can take several minutes for large images. Pre-pull to avoid long waits at sandbox creation time: +Workload images are pulled automatically on first use. Pre-pull the default +Ubuntu workload or your own agent image to avoid waiting during sandbox +creation: ```shell -# Base image — includes Claude Code, OpenCode, Codex, and Copilot -docker pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest +docker pull nvcr.io/nvidia/base/ubuntu:24.04 +docker pull registry.example.com/your-org/agent:latest ``` -## Create a sandbox +## Create a Sandbox - - - -OpenClaw runs inside OpenShell through [NemoClaw](https://github.com/NVIDIA/NemoClaw), which manages the sandbox image, model-provider setup, and security policies. - -Follow the [NemoClaw Quickstart](https://docs.nvidia.com/nemoclaw/latest/get-started/quickstart/) to set up an OpenClaw sandbox. - - - +Create a minimal Ubuntu sandbox: ```shell -openshell sandbox create -- claude +openshell sandbox create --name compose-demo ``` - - +To run an agent, use an explicit OCI image that contains it: ```shell -openshell sandbox create -- opencode +openshell sandbox create \ + --from registry.example.com/your-org/agent:latest \ + --provider \ + -- my-agent ``` - - +OpenClaw runs through [NemoClaw](https://github.com/NVIDIA/NemoClaw), which +manages its sandbox image, model provider, and security policies. Follow the +[NemoClaw Quickstart](https://docs.nvidia.com/nemoclaw/latest/get-started/quickstart/) +for that workflow. Wait for the phase to change from `Provisioning` to `Ready`: diff --git a/docs/get-started/tutorials/github-sandbox.mdx b/docs/get-started/tutorials/github-sandbox.mdx index 89b6c0885..5b4cc1b7b 100644 --- a/docs/get-started/tutorials/github-sandbox.mdx +++ b/docs/get-started/tutorials/github-sandbox.mdx @@ -54,7 +54,7 @@ Create a [credential provider](/sandboxes/manage-providers) that injects your Gi ```shell GITHUB_TOKEN= openshell provider create --name my-github --type github --from-existing -openshell sandbox create --provider my-github -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --provider my-github -- claude ``` `openshell sandbox create` keeps the sandbox running after Claude Code exits, so you can apply policy updates later without recreating the environment. Add `--no-keep` if you want the sandbox deleted automatically instead. diff --git a/docs/get-started/tutorials/index.mdx b/docs/get-started/tutorials/index.mdx index 2050480b9..cda35bfff 100644 --- a/docs/get-started/tutorials/index.mdx +++ b/docs/get-started/tutorials/index.mdx @@ -29,7 +29,7 @@ Configure a Microsoft Graph provider profile with gateway-managed OAuth2 refresh -Run Ollama models in a community sandbox or attach a profile for a host-level service. +Attach a profile that authorizes a sandbox to use a host-level Ollama service. diff --git a/docs/get-started/tutorials/inference-ollama.mdx b/docs/get-started/tutorials/inference-ollama.mdx index c23e6eaf6..017e8ec88 100644 --- a/docs/get-started/tutorials/inference-ollama.mdx +++ b/docs/get-started/tutorials/inference-ollama.mdx @@ -4,65 +4,19 @@ title: "Run Local Inference with Ollama" sidebar-title: "Inference with Ollama" slug: "get-started/tutorials/inference-ollama" -description: "Run Ollama inside a community sandbox or attach a profile for a host-level Ollama server." +description: "Authorize an OpenShell sandbox to use an Ollama service running on the gateway host." keywords: "Generative AI, Cybersecurity, Tutorial, Ollama, Local Inference, Sandbox, Provider Profile" --- -This tutorial covers two Ollama deployments: - -1. The self-contained Ollama community sandbox. This is the recommended path. -2. A host-level Ollama service shared by explicitly authorized sandboxes. +This tutorial connects an explicitly authorized sandbox to an Ollama service running on the same machine as the OpenShell gateway. ## Prerequisites -Complete the [Quickstart](/get-started/quickstart) before proceeding. +Complete the [Quickstart](/get-started/quickstart) before proceeding. Your workload image must contain the client or agent you plan to use. -## Option A: Ollama Community Sandbox +## Start Ollama on a Reachable Address -The community image bundles Ollama and supported coding agents: - -```shell -openshell sandbox create --from ollama -``` - -Inside the sandbox, run a local or cloud model: - -```shell -ollama run qwen3.5 -ollama run kimi-k2.5:cloud -``` - -Or launch a coding agent against Ollama: - -```shell -ollama launch claude -ollama launch codex -ollama launch opencode -``` - -For an automated workflow: - -```shell -ollama launch claude --yes --model qwen3.5 -``` - -Useful starter models include `qwen3.5:0.8b` for smoke tests and `qwen3.5` -for coding and tool use. Check the -[Ollama model library](https://ollama.com/library) for current model details. - -Update the bundled Ollama installation with `update-ollama`, or request an -update at sandbox start: - -```shell -openshell sandbox create --from ollama -e OLLAMA_UPDATE=1 -``` - -## Option B: Host-level Ollama - -Use this path when Ollama runs on the same machine as the OpenShell gateway and -selected sandboxes should share it. - -### Start Ollama on a Reachable Address +Install Ollama on the gateway host and listen on an address reachable from sandboxes: ```shell curl -fsSL https://ollama.com/install.sh | sh @@ -75,7 +29,7 @@ In another terminal, pull a model: ollama pull qwen3.5:0.8b ``` -### Import an Endpoint-bearing Profile +## Import an Endpoint-bearing Profile Save this as `ollama-openai.yaml`: @@ -107,14 +61,16 @@ openshell provider profile import -f ollama-openai.yaml openshell provider create --name ollama --type ollama-openai ``` -The provider has no secret because this Ollama server does not authenticate. -Its attachment still carries the endpoint and binary policy. +The provider has no secret because this Ollama server does not authenticate. Its attachment still carries the endpoint and binary policy. -### Attach and Verify +## Attach and Verify + +Use an explicit workload image containing `curl`: ```shell openshell sandbox create \ --name ollama-client \ + --from registry.example.com/tools/curl:latest \ --provider ollama \ -- \ curl http://host.openshell.internal:11434/v1/chat/completions \ @@ -125,9 +81,7 @@ openshell sandbox create \ }' ``` -For an OpenAI SDK client, set its base URL to -`http://host.openshell.internal:11434/v1`, use any non-empty API key value the -SDK accepts, and select the real Ollama model in the request. +For an OpenAI SDK client, set its base URL to `http://host.openshell.internal:11434/v1`, use any non-empty API key value the SDK accepts, and select the real Ollama model in the request. ## Troubleshooting @@ -136,8 +90,7 @@ SDK accepts, and select the real Ollama model in the request. - Confirm attachment with `openshell sandbox provider list ollama-client`. - Inspect the effective policy with `openshell policy get ollama-client --full`. - Run `ollama ps` and `ollama pull ` when the model is unavailable. -- If the gateway is remote, the hostname refers to the remote gateway host, - not your laptop. Use a shared service address or tunnel instead. +- If the gateway is remote, the hostname refers to the remote gateway host, not your laptop. Use a shared service address or tunnel instead. ## Next Steps diff --git a/docs/index.mdx b/docs/index.mdx index b7623332e..9a40bca09 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -49,7 +49,7 @@ Install OpenShell and create your first sandbox in two commands. ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh -openshell sandbox create -- claude +openshell sandbox create --name quickstart ``` @@ -86,7 +86,7 @@ Hands-on walkthroughs from first sandbox to custom policies. -Deploy gateways, create sandboxes, configure policies, providers, and community images for your AI agents. +Deploy gateways, create sandboxes, configure policies, providers, and workload images for your AI agents. Concept diff --git a/docs/reference/default-policy.mdx b/docs/reference/default-policy.mdx index bd5c01fac..fd564ed61 100644 --- a/docs/reference/default-policy.mdx +++ b/docs/reference/default-policy.mdx @@ -7,23 +7,28 @@ description: "Breakdown of the built-in default policy applied when you create a keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Policy" position: 2 --- -The default policy is the policy applied when you create an OpenShell sandbox without `--policy`. It is baked into the community base image ([`ghcr.io/nvidia/openshell-community/sandboxes/base`](https://github.com/NVIDIA/OpenShell-Community)) and defined in the community repo's `dev-sandbox-policy.yaml`. -## Agent Compatibility +When you create a sandbox without `--policy`, OpenShell applies a restrictive built-in fallback. The policy comes from the OpenShell runtime and does not depend on the selected workload image. -The following table shows the coverage of the default policy for common agents. +## Filesystem Access -| Agent | Coverage | Action Required | -|---|---|---| -| Claude Code | Full | None. Works out of the box. | -| OpenCode | Partial | Add `opencode.ai` endpoint and OpenCode binary paths. | -| Codex | None | Provide a complete custom policy with OpenAI endpoints and Codex binary paths. | +The fallback includes the sandbox working directory and grants read-only access to standard runtime paths: - -If you run a non-Claude agent without a custom policy, the agent's API calls are denied by the proxy. You must provide a policy that declares the agent's endpoints and binaries. +- `/usr` +- `/lib` +- `/proc` +- `/dev/urandom` +- `/etc` +- `/var/log` - +It grants read-write access to `/tmp` and `/dev/null`. Landlock enforcement uses `best_effort` compatibility so OpenShell can use the strongest ABI available on the host while retaining its mandatory baseline protections. -## Default Policy Blocks +## Network Access -The default policy blocks are defined in the community base image. Refer to the [OpenShell Community repository](https://github.com/NVIDIA/OpenShell-Community) for the full `dev-sandbox-policy.yaml` source. +The fallback defines no network policies or provider-derived endpoints, so outbound network access is denied. Attach a provider or apply a custom policy that names the required endpoints and executable paths before running a networked agent. + +## Process Identity + +The fallback leaves process identity selection to the compute driver. Docker and Podman honor a non-root OCI `USER`; when an image declares no user, they use numeric UID and GID `1000`. Kubernetes and MicroVM drivers apply their configured non-root identities. + +Use `openshell policy get --full` to inspect the effective policy. Refer to [Customize Sandbox Policies](/sandboxes/policies) to replace the fallback. diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 9940f2ee2..8105330a0 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -548,7 +548,7 @@ workspace_mode = "shared" # gateway_id = "openshell" namespace = "agents" service_account_name = "openshell-sandbox" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" image_pull_secrets = ["regcred"] # Defaults to the gateway version; override to pin a specific build. @@ -698,7 +698,7 @@ guest_tls_key = "/etc/openshell/certs/client-key.pem" [openshell.drivers.docker] socket_path = "/var/run/docker.sock" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" # Canonical values: always | if_not_present | never. `newer` is Podman-only. image_pull_policy = "if_not_present" # Value assigned to the openshell.sandbox_namespace label on sandbox containers. @@ -759,7 +759,7 @@ guest_tls_key = "/etc/openshell/certs/client-key.pem" # asks the podman CLI where its socket is, and fails to start if neither finds # one. Set this to pin a specific Podman machine instead. socket_path = "/run/user/1000/podman/podman.sock" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" # always | if_not_present | never | newer # Optional override. When omitted, the gateway derives this endpoint. grpc_endpoint = "https://host.containers.internal:17670" @@ -910,12 +910,12 @@ guest_tls_key = "/var/lib/openshell/guest-tls/client-key.pem" state_dir = "/var/lib/openshell/vm" # Where the gateway looks for the openshell-driver-vm subprocess binary. driver_dir = "/usr/local/libexec/openshell" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" # Optional override. When omitted, the gateway derives # https://host.openshell.internal: for the VM driver. grpc_endpoint = "https://host.openshell.internal:17670" # Empty falls back to default_image. -bootstrap_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +bootstrap_image = "nvcr.io/nvidia/base/ubuntu:24.04" krun_log_level = 1 vcpus = 2 mem_mib = 2048 diff --git a/docs/reference/support-matrix.mdx b/docs/reference/support-matrix.mdx index 7671866de..80f818734 100644 --- a/docs/reference/support-matrix.mdx +++ b/docs/reference/support-matrix.mdx @@ -59,22 +59,22 @@ Install the software for the compute driver you use: | kubectl | Compatible with your cluster | Required for Kubernetes operational inspection and secret creation. | | Host virtualization | Host dependent | Required for MicroVM-backed gateways. MicroVM uses Hypervisor.framework on macOS and KVM on Linux. | -## Sandbox Runtime Versions +## Workload Images -Sandbox container images are maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. Refer to that repository for the current list of installed components and their versions. +OpenShell accepts standard Linux OCI images. The built-in default workload is +`nvcr.io/nvidia/base/ubuntu:24.04` for `linux/amd64` and `linux/arm64`. It is a +minimal Ubuntu Noble userspace and does not bundle agent CLIs. Operators can +change the default, and users can select an explicit image with `--from`. ## Container Images -OpenShell publishes the gateway image for `linux/amd64` and `linux/arm64`. - | Image | Reference | Pulled When | |---|---|---| | Gateway | `ghcr.io/nvidia/openshell/gateway:latest` | Helm chart install or upgrade, or standalone container deployment | +| Default workload | `nvcr.io/nvidia/base/ubuntu:24.04` | First sandbox creation unless preloaded or overridden | The Helm chart in `deploy/helm/openshell` deploys the gateway workload, service account, service, optional persistent storage, and network policy for Kubernetes. It defaults to a StatefulSet for SQLite-backed installs and can render a Deployment for external database-backed installs. -Sandbox images are maintained separately in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. - To override the default image references, use Helm values: | Helm value | Purpose | diff --git a/docs/sandboxes/manage-providers.mdx b/docs/sandboxes/manage-providers.mdx index 93f5b004b..0072176be 100644 --- a/docs/sandboxes/manage-providers.mdx +++ b/docs/sandboxes/manage-providers.mdx @@ -309,7 +309,7 @@ with an error after it attempts every requested deletion if any entry failed. Pass one or more `--provider` flags when creating a sandbox: ```shell -openshell sandbox create --provider my-claude --provider my-github -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --provider my-claude --provider my-github -- claude ``` Each `--provider` flag attaches one provider. The sandbox receives eligible @@ -336,7 +336,7 @@ discovery if one does not already exist. You do not need to create the provider separately: ```shell -openshell sandbox create -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest -- claude ``` This detects `claude` as a known tool, finds your `ANTHROPIC_API_KEY`, creates @@ -476,5 +476,5 @@ Explore related topics: - To manage workspace access for providers, refer to [Manage Workspaces and Access](/sandboxes/manage-workspaces). - To control what the agent can access, refer to [Policies](/sandboxes/policies). -- To use the base sandbox container, refer to [Sandboxes](/sandboxes/manage-sandboxes#base-sandbox-container). +- To use the default workload image, refer to [Sandboxes](/sandboxes/manage-sandboxes#default-workload-image). - To view the complete field reference for the policy YAML, refer to the [Policy Schema Reference](/reference/policy-schema). diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index 86968a991..630203130 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -17,7 +17,7 @@ A sandbox is the OpenShell data plane: a safe, private execution environment whe Create a sandbox with a single command. For example, to create a sandbox with Claude, run: ```shell -openshell sandbox create -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest -- claude ``` The trailing command is the sandbox's canonical main process. OpenShell starts @@ -80,7 +80,7 @@ openshell gateway select local Set per-sandbox CPU and memory amounts with `--cpu` and `--memory`: ```shell -openshell sandbox create --cpu 2 --memory 4Gi -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --cpu 2 --memory 4Gi -- claude ``` CPU values use Kubernetes-style quantities such as `500m`, `1`, or `2.5`. @@ -115,13 +115,13 @@ the same behavior. To request GPU resources, add `--gpu`: ```shell -openshell sandbox create --gpu -- claude +openshell sandbox create --gpu --from registry.example.com/your-org/gpu-agent:latest -- claude ``` Request a specific number of GPUs by passing a count to `--gpu`: ```shell -openshell sandbox create --gpu 2 -- claude +openshell sandbox create --gpu 2 --from registry.example.com/your-org/gpu-agent:latest -- claude ``` When you omit the count, OpenShell treats the request as `--gpu 1`. @@ -153,22 +153,22 @@ openshell sandbox create \ -- claude ``` -### Custom Containers +### Sandbox Images -Use `--from` to create a sandbox from the base image, another pre-built sandbox name, a rootfs tar archive, or a container image: +Without `--from`, the gateway uses its configured default workload image. The +built-in default is `nvcr.io/nvidia/base/ubuntu:24.04`. + +Use `--from` with an explicit OCI image reference or a rootfs tar archive: ```shell -openshell sandbox create --from base -openshell sandbox create --from ollama -openshell sandbox create --from ./rootfs.tar +openshell sandbox create --from nvcr.io/nvidia/base/ubuntu:24.04 openshell sandbox create --from my-registry.example.com/my-image:latest +openshell sandbox create --from ./rootfs.tar ``` -Bare names such as `base` and `ollama` resolve to images under `ghcr.io/nvidia/openshell-community/sandboxes`. Set `OPENSHELL_COMMUNITY_REGISTRY` when you need to use an internal mirror. - -`--from` does not build local Dockerfiles or directories. Build and tag the -image with the container engine used by your local gateway, then pass the -resulting image reference: +`--from` does not expand catalog aliases and does not build local Dockerfiles or +directories. Build and tag the image with the container engine used by your +local gateway, then pass the resulting image reference: ```shell # Docker gateway @@ -185,8 +185,8 @@ from and use that registry image reference. **Pre-0.1.0 breaking change:** `openshell sandbox create --from ./Dockerfile` -and directory sources no longer build images. Build and tag the image before -you create the sandbox. +and directory sources no longer build images. Bare catalog names are no longer +expanded. Build or select an image and pass its explicit reference. #### Rootfs Tar Archives @@ -277,17 +277,27 @@ For JSON or YAML, template list output contains `templates` and `next_page_token` fields. Pass the returned token to `--page-token` to continue. -## Base Sandbox Container +## Default Workload Image -The `base` sandbox container is the default runtime image for standard OpenShell sandboxes unless the gateway overrides its default sandbox image. It is published as `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` and maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) repository. +OpenShell defaults to `nvcr.io/nvidia/base/ubuntu:24.04` unless the gateway +operator configures another image. The image provides a minimal Ubuntu Noble +userspace. It does not include agent CLIs or an image-baked OpenShell policy. +OpenShell applies its built-in restrictive policy when no explicit policy is +provided. -The base container includes common development tooling, supported agent CLIs, and the default sandbox policy. Use it when you want a general-purpose agent environment without a workflow-specific image: +Create a sandbox with the default image: ```shell -openshell sandbox create --from base +openshell sandbox create ``` -For default policy coverage by agent, refer to [Default Policy](/reference/default-policy). For the supported agent list, refer to [Supported Agents](/about/supported-agents). +Override it with any image visible to the active compute driver: + +```shell +openshell sandbox create --from registry.example.com/agents/my-agent:1.0 +``` + +Refer to [Default Policy](/reference/default-policy), [Supported Agents](/about/supported-agents), and the [BYOC example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container). ## Connect to a Sandbox @@ -390,7 +400,7 @@ Environment variable names starting with `OPENSHELL_` are reserved. Keys must ma Attach labels when you create a sandbox to track ownership, environment, or workflow grouping: ```shell -openshell sandbox create --label env=dev --label team=platform -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --label env=dev --label team=platform -- claude ``` List only the sandboxes that match a label selector: @@ -431,7 +441,7 @@ with SandboxClient.from_active_cluster() as client: templates.create( workspace="default", name="python", - image="ghcr.io/nvidia/openshell-community/sandboxes/python:latest", + image="registry.example.com/agents/python:latest", ) sandbox = client.create_from_template(workspace="default", template_name="python") @@ -645,7 +655,7 @@ The default table colorizes the `STATUS` column only when both standard output a You can also forward a port at creation time with `--forward`: ```shell -openshell sandbox create --forward 8000 -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --forward 8000 -- claude ``` @@ -697,7 +707,7 @@ You can also upload files at creation time with the `--upload` flag on several paths in a single command: ```shell -openshell sandbox create --upload ./src:/workspace/src --upload ./config:/workspace/config -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --upload ./src:/workspace/src --upload ./config:/workspace/config -- claude ``` @@ -796,4 +806,4 @@ For Docker, Podman, MicroVM, and Kubernetes behavior, refer to [Sandbox Compute - To select a workspace or understand access roles, refer to [Manage Workspaces and Access](/sandboxes/manage-workspaces). - To supply API keys or tokens, refer to [Manage Providers](/sandboxes/manage-providers). - To control what the agent can access, refer to [Policies](/sandboxes/policies). -- To use the default runtime image, refer to [Base Sandbox Container](#base-sandbox-container). +- To use the default runtime image, refer to [Default Workload Image](#default-workload-image). diff --git a/docs/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx index f4202319a..2d7a326a2 100644 --- a/docs/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -172,7 +172,7 @@ The first TCP endpoint is a deliberate exception to ordinary dynamic network pol Pass a policy YAML file when creating the sandbox: ```shell -openshell sandbox create --policy ./my-policy.yaml -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --policy ./my-policy.yaml -- claude ``` The trailing command is the sandbox's canonical main process. If it exits, the @@ -183,7 +183,7 @@ To avoid passing `--policy` every time, set a default policy with an environment ```shell export OPENSHELL_SANDBOX_POLICY=./my-policy.yaml -openshell sandbox create -- claude +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest -- claude ``` The CLI uses the policy from `OPENSHELL_SANDBOX_POLICY` whenever `--policy` is not explicitly provided. diff --git a/e2e/configs/gateway/docker.toml b/e2e/configs/gateway/docker.toml index 3e872885e..e186004cd 100644 --- a/e2e/configs/gateway/docker.toml +++ b/e2e/configs/gateway/docker.toml @@ -20,7 +20,7 @@ kid_path = ".cache/openshell-e2e/gateway-jwt/kid" gateway_id = "openshell-e2e" [openshell.drivers.docker] -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" sandbox_label = "openshell-e2e" sandbox_runtime_image = "localhost/openshell/sandbox:e2e-vm" diff --git a/e2e/configs/gateway/podman.toml b/e2e/configs/gateway/podman.toml index a53708c61..332accc98 100644 --- a/e2e/configs/gateway/podman.toml +++ b/e2e/configs/gateway/podman.toml @@ -20,7 +20,7 @@ kid_path = ".cache/openshell-e2e/gateway-jwt/kid" gateway_id = "openshell-e2e" [openshell.drivers.podman] -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" health_check_interval_secs = 10 network_name = "openshell-e2e" diff --git a/e2e/configs/gateway/schema-v2-compute-boundary-comparison.json b/e2e/configs/gateway/schema-v2-compute-boundary-comparison.json index 783bb0093..b29829a78 100644 --- a/e2e/configs/gateway/schema-v2-compute-boundary-comparison.json +++ b/e2e/configs/gateway/schema-v2-compute-boundary-comparison.json @@ -53,11 +53,11 @@ "supervisor_base_image_digest": "sha256:7c8cb692ae09657cbc4a3f3cbd0e8d5a2690ba38386aaaf252dbb060bf5eb2e6", "supervisor_base_runtime_image": "docker.io/library/alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce", "supervisor_package_manifest_sha256": "6966057ffaf1ef0d4617d413dad53646bcd049a4b08f572796392fb0ffbb01b9", - "sandbox_image_request": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", + "sandbox_image_request": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", "sandbox_image_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", - "sandbox_image_digest": "sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_runtime_image": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_client_image_alias": "ghcr.io/nvidia/openshell-community/sandboxes/base:latest", + "sandbox_image_digest": "sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_runtime_image": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_client_image_alias": "nvcr.io/nvidia/base/ubuntu:24.04", "sandbox_client_image_alias_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", "gateway_sha256_before_execution": "264cf3d809bd1d54633bce9251ec1a5540b567b8150640091df85bdfbe61797a", "cli_sha256_before_execution": "3ad0ec143bf6850af780bf643c303242956d6ef1066d4a9372bc62fef33ada20", @@ -121,11 +121,11 @@ "supervisor_base_image_digest": "sha256:7c8cb692ae09657cbc4a3f3cbd0e8d5a2690ba38386aaaf252dbb060bf5eb2e6", "supervisor_base_runtime_image": "docker.io/library/alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce", "supervisor_package_manifest_sha256": "6966057ffaf1ef0d4617d413dad53646bcd049a4b08f572796392fb0ffbb01b9", - "sandbox_image_request": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", + "sandbox_image_request": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", "sandbox_image_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", - "sandbox_image_digest": "sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_runtime_image": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_client_image_alias": "ghcr.io/nvidia/openshell-community/sandboxes/base:latest", + "sandbox_image_digest": "sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_runtime_image": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_client_image_alias": "nvcr.io/nvidia/base/ubuntu:24.04", "sandbox_client_image_alias_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", "gateway_sha256_before_execution": "2bd42b145f0438f48a579b010537f501e036035b0e22a4ff70e37db733a6e6ff", "cli_sha256_before_execution": "82827d9068f3e9c75681a804f8fb48274ecf8acb179a46f179fb9fbfe828f69a", @@ -196,11 +196,11 @@ "supervisor_base_image_digest": "sha256:7c8cb692ae09657cbc4a3f3cbd0e8d5a2690ba38386aaaf252dbb060bf5eb2e6", "supervisor_base_runtime_image": "docker.io/library/alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce", "supervisor_package_manifest_sha256": "6966057ffaf1ef0d4617d413dad53646bcd049a4b08f572796392fb0ffbb01b9", - "sandbox_image_request": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", + "sandbox_image_request": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", "sandbox_image_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", - "sandbox_image_digest": "sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_runtime_image": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_client_image_alias": "ghcr.io/nvidia/openshell-community/sandboxes/base:latest", + "sandbox_image_digest": "sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_runtime_image": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_client_image_alias": "nvcr.io/nvidia/base/ubuntu:24.04", "sandbox_client_image_alias_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", "gateway_sha256_before_execution": "5cc2f700d93dde5dd09060d9c9190ae44a99440b1399530a2b23941ec4e88f85", "cli_sha256_before_execution": "3ad0ec143bf6850af780bf643c303242956d6ef1066d4a9372bc62fef33ada20", @@ -218,7 +218,7 @@ "external_driver_environment": { "OPENSHELL_COMPUTE_DRIVER_SOCKET": "/tmp/openshell-e2e-podman.H8oMaO/compute-driver.sock", "OPENSHELL_PODMAN_SOCKET": "/tmp/openshell-e2e-podman.H8oMaO/podman/podman.sock", - "OPENSHELL_SANDBOX_IMAGE": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", + "OPENSHELL_SANDBOX_IMAGE": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY": "missing", "OPENSHELL_HEALTH_CHECK_INTERVAL_SECS": 10, "OPENSHELL_GRPC_ENDPOINT": "https://host.containers.internal:50871", @@ -290,11 +290,11 @@ "supervisor_base_image_digest": "sha256:7c8cb692ae09657cbc4a3f3cbd0e8d5a2690ba38386aaaf252dbb060bf5eb2e6", "supervisor_base_runtime_image": "docker.io/library/alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce", "supervisor_package_manifest_sha256": "6966057ffaf1ef0d4617d413dad53646bcd049a4b08f572796392fb0ffbb01b9", - "sandbox_image_request": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", + "sandbox_image_request": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", "sandbox_image_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", - "sandbox_image_digest": "sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_runtime_image": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", - "sandbox_client_image_alias": "ghcr.io/nvidia/openshell-community/sandboxes/base:latest", + "sandbox_image_digest": "sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_runtime_image": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", + "sandbox_client_image_alias": "nvcr.io/nvidia/base/ubuntu:24.04", "sandbox_client_image_alias_id": "65fa5d3d598a07d385ddbea41bf593be15af306337b76255b40705287cebcbbf", "gateway_sha256_before_execution": "fdefc047c1b675c311b1796db9f1b1a944456fc34b76547efc0352c6a75a7707", "cli_sha256_before_execution": "82827d9068f3e9c75681a804f8fb48274ecf8acb179a46f179fb9fbfe828f69a", @@ -312,7 +312,7 @@ "external_driver_environment": { "OPENSHELL_COMPUTE_DRIVER_SOCKET": "/tmp/openshell-e2e-podman.M8xsgQ/compute-driver.sock", "OPENSHELL_PODMAN_SOCKET": "/tmp/openshell-e2e-podman.M8xsgQ/podman/podman.sock", - "OPENSHELL_SANDBOX_IMAGE": "ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:c2a43bb0d765774e2790b3babfb20997bb2eac7b4bf4c6d7d8661e99817bf904", + "OPENSHELL_SANDBOX_IMAGE": "nvcr.io/nvidia/base/ubuntu@sha256:c280ee89f8bfcbdaba6179ad4347f60c509e841cbe63eb93002f01bf70e0819c", "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY": "if_not_present", "OPENSHELL_HEALTH_CHECK_INTERVAL_SECS": 10, "OPENSHELL_GRPC_ENDPOINT": "https://host.containers.internal:32901", diff --git a/e2e/docker/Dockerfile.external-kubernetes-gateway b/e2e/docker/Dockerfile.external-kubernetes-gateway index 20738a512..3b860dacc 100644 --- a/e2e/docker/Dockerfile.external-kubernetes-gateway +++ b/e2e/docker/Dockerfile.external-kubernetes-gateway @@ -17,7 +17,7 @@ ENV OPENSHELL_COMPUTE_DRIVER=kubernetes \ OPENSHELL_GATEWAY_ID=openshell \ OPENSHELL_SANDBOX_NAMESPACE=openshell \ OPENSHELL_K8S_SANDBOX_SERVICE_ACCOUNT=openshell-sandbox \ - OPENSHELL_SANDBOX_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest \ + OPENSHELL_SANDBOX_IMAGE=nvcr.io/nvidia/base/ubuntu:24.04 \ OPENSHELL_SANDBOX_IMAGE_PULL_POLICY=if_not_present \ OPENSHELL_GRPC_ENDPOINT=http://openshell.openshell.svc.cluster.local:8080 \ OPENSHELL_SUPERVISOR_IMAGE=${SUPERVISOR_IMAGE} \ diff --git a/e2e/gpu/images/cuda-basic/Dockerfile b/e2e/gpu/images/cuda-basic/Dockerfile index a7dde7422..972103888 100644 --- a/e2e/gpu/images/cuda-basic/Dockerfile +++ b/e2e/gpu/images/cuda-basic/Dockerfile @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 ARG CUDA_BUILD_IMAGE=nvcr.io/nvidia/cuda:12.8.1-base-ubuntu22.04 -ARG OPENSHELL_SANDBOX_BASE_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest +ARG OPENSHELL_SANDBOX_BASE_IMAGE=nvcr.io/nvidia/base/ubuntu:24.04 FROM ${CUDA_BUILD_IMAGE} AS builder @@ -68,5 +68,5 @@ RUN chmod 0755 /usr/local/bin/openshell-gpu-workload \ /usr/local/lib/openshell-gpu-workload/deviceQuery \ /usr/local/lib/openshell-gpu-workload/vectorAdd -USER sandbox +USER 1000:1000 ENTRYPOINT ["/usr/local/bin/openshell-gpu-workload"] diff --git a/e2e/gpu/images/cuda-basic/README.md b/e2e/gpu/images/cuda-basic/README.md index deb0b5c5a..096d51302 100644 --- a/e2e/gpu/images/cuda-basic/README.md +++ b/e2e/gpu/images/cuda-basic/README.md @@ -12,7 +12,7 @@ runtime workload. It is a single image that runs two validation steps: The image builds the samples from `NVIDIA/cuda-samples` tag `v12.8` with a CUDA 12.8 builder image, then copies only the compiled binaries into the OpenShell -community base final image. +default workload final image. The workload prints `OPENSHELL_GPU_WORKLOAD_SUCCESS` only after both samples pass. On failure it prints `OPENSHELL_GPU_WORKLOAD_FAILURE` and exits non-zero. diff --git a/e2e/gpu/images/smoke-fail/Dockerfile b/e2e/gpu/images/smoke-fail/Dockerfile index f74aa3c5e..9e288d886 100644 --- a/e2e/gpu/images/smoke-fail/Dockerfile +++ b/e2e/gpu/images/smoke-fail/Dockerfile @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -ARG OPENSHELL_SANDBOX_BASE_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest +ARG OPENSHELL_SANDBOX_BASE_IMAGE=nvcr.io/nvidia/base/ubuntu:24.04 FROM ${OPENSHELL_SANDBOX_BASE_IMAGE} @@ -11,5 +11,5 @@ USER root COPY workload.sh /usr/local/bin/openshell-gpu-workload RUN chmod 0755 /usr/local/bin/openshell-gpu-workload -USER sandbox +USER 1000:1000 ENTRYPOINT ["/usr/local/bin/openshell-gpu-workload"] diff --git a/e2e/gpu/images/smoke-pass/Dockerfile b/e2e/gpu/images/smoke-pass/Dockerfile index f74aa3c5e..9e288d886 100644 --- a/e2e/gpu/images/smoke-pass/Dockerfile +++ b/e2e/gpu/images/smoke-pass/Dockerfile @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -ARG OPENSHELL_SANDBOX_BASE_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest +ARG OPENSHELL_SANDBOX_BASE_IMAGE=nvcr.io/nvidia/base/ubuntu:24.04 FROM ${OPENSHELL_SANDBOX_BASE_IMAGE} @@ -11,5 +11,5 @@ USER root COPY workload.sh /usr/local/bin/openshell-gpu-workload RUN chmod 0755 /usr/local/bin/openshell-gpu-workload -USER sandbox +USER 1000:1000 ENTRYPOINT ["/usr/local/bin/openshell-gpu-workload"] diff --git a/e2e/parity/kubernetes-options.sh b/e2e/parity/kubernetes-options.sh index 658e751c3..3ddca0355 100644 --- a/e2e/parity/kubernetes-options.sh +++ b/e2e/parity/kubernetes-options.sh @@ -22,7 +22,7 @@ KUBE_CONTEXT="${OPENSHELL_PARITY_KUBE_CONTEXT:-}" HOST_GATEWAY_IP="${OPENSHELL_PARITY_HOST_GATEWAY_IP:-}" RUN_ID="${OPENSHELL_PARITY_RUN_ID:-$(date +%s)-$$}" OUT="${OPENSHELL_PARITY_OUTPUT_DIR:-${ROOT}/target/parity/step8-kubernetes-${CANDIDATE_SHA:0:8}}" -SANDBOX_IMAGE="${OPENSHELL_PARITY_KUBERNETES_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}" +SANDBOX_IMAGE="${OPENSHELL_PARITY_KUBERNETES_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}" SUPERVISOR_IMAGE="${OPENSHELL_PARITY_KUBERNETES_SUPERVISOR_IMAGE:-ghcr.io/nvidia/openshell/supervisor:latest}" RUNTIME_CLASS="openshell-parity-runc-${RUN_ID}" diff --git a/e2e/parity/podman-options.sh b/e2e/parity/podman-options.sh index f1894b587..ae24ec036 100755 --- a/e2e/parity/podman-options.sh +++ b/e2e/parity/podman-options.sh @@ -10,7 +10,7 @@ set -euo pipefail CLI="${OPENSHELL_BIN:?OPENSHELL_BIN is required}" RESULT="${OPENSHELL_PARITY_ORACLE_RESULT:?OPENSHELL_PARITY_ORACLE_RESULT is required}" VARIANT="${OPENSHELL_PARITY_VARIANT:?OPENSHELL_PARITY_VARIANT is required}" -IMAGE="${OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE:-${OPENSHELL_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}}" +IMAGE="${OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE:-${OPENSHELL_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}}" GATEWAY_LOG="${OPENSHELL_E2E_GATEWAY_LOG:?OPENSHELL_E2E_GATEWAY_LOG is required}" NAME="po-${VARIANT:0:1}-${RANDOM}" WORKDIR="${TMPDIR:-/tmp}/openshell-parity-options-${NAME}" diff --git a/e2e/parity/run.sh b/e2e/parity/run.sh index e6217df0e..87da1c939 100755 --- a/e2e/parity/run.sh +++ b/e2e/parity/run.sh @@ -21,10 +21,9 @@ PODMAN_OPTIONS_ORACLE="${OPENSHELL_PARITY_PODMAN_OPTIONS_ORACLE:-${ROOT}/e2e/par CONFORMANCE_TRACE_WRAPPER="${ROOT}/e2e/parity/trace-conformance.sh" RESULTS_VERIFIER="${ROOT}/e2e/parity/verify-results.py" PODMAN_BIN="${OPENSHELL_PARITY_PODMAN_BIN:-podman}" -DEFAULT_SANDBOX_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +DEFAULT_SANDBOX_IMAGE="nvcr.io/nvidia/base/ubuntu:24.04" SANDBOX_IMAGE_REQUEST="${OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE:-${DEFAULT_SANDBOX_IMAGE}}" PARITY_SANDBOX_RUNTIME_IMAGE="" -PARITY_SANDBOX_COMMUNITY_REGISTRY="" PARITY_SUPERVISOR_BASE_IMAGE="" PARITY_SUPERVISOR_BASE_RUNTIME_IMAGE="" TEMP_WORKTREE="" @@ -404,11 +403,6 @@ resolve_parity_sandbox_image() { echo "ERROR: could not resolve one immutable parity sandbox image from ${SANDBOX_IMAGE_REQUEST}." >&2 exit 2 fi - PARITY_SANDBOX_COMMUNITY_REGISTRY="${repository%/base}" - if [ "${PARITY_SANDBOX_COMMUNITY_REGISTRY}" = "${repository}" ]; then - echo "ERROR: parity sandbox repository must end in /base for the conformance alias: ${repository}." >&2 - exit 2 - fi echo "Using one immutable parity sandbox image for both variants: ${PARITY_SANDBOX_RUNTIME_IMAGE} (ID ${image_id})" } @@ -551,7 +545,6 @@ run_variant() { -u OPENSHELL_SANDBOX_PROXY_AUTH_FILE -u OPENSHELL_SANDBOX_PROXY_AUTH_ALLOW_INSECURE \ -u OPENSHELL_SANDBOX_PROXY_CONNECT_BY_HOSTNAME -u OPENSHELL_SANDBOX_PROXY_CA_BUNDLE \ -u OPENSHELL_OTLP_ENDPOINT -u OPENSHELL_GATEWAY_NAME -u OPENSHELL_COMPUTE_DRIVER_BIND \ - -u OPENSHELL_COMMUNITY_REGISTRY \ OPENSHELL_PARITY_VARIANT="${variant}" \ OPENSHELL_E2E_CONFIG_SCHEMA_VERSION="${schema}" \ OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER="$([ "${SCENARIO}" = external-driver ] && printf 1 || printf 0)" \ @@ -561,7 +554,6 @@ run_variant() { OPENSHELL_E2E_FORCE_TEMP_PODMAN_SERVICE=1 \ OPENSHELL_E2E_REQUIRE_DIGEST_PINNED_SANDBOX_IMAGE=1 \ OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE="${PARITY_SANDBOX_RUNTIME_IMAGE}" \ - OPENSHELL_COMMUNITY_REGISTRY="${PARITY_SANDBOX_COMMUNITY_REGISTRY}" \ OPENSHELL_E2E_SUPERVISOR_BASE_IMAGE="${PARITY_SUPERVISOR_BASE_IMAGE}" \ OPENSHELL_E2E_SUPERVISOR_BASE_RUNTIME_IMAGE="${PARITY_SUPERVISOR_BASE_RUNTIME_IMAGE}" \ OPENSHELL_E2E_EXPECTED_GATEWAY_SHA256="${gateway_digest}" \ diff --git a/e2e/parity/test.sh b/e2e/parity/test.sh index f3052b7a7..ea1c93399 100755 --- a/e2e/parity/test.sh +++ b/e2e/parity/test.sh @@ -138,10 +138,9 @@ for variable in \ OPENSHELL_OTLP_ENDPOINT OPENSHELL_GATEWAY_NAME OPENSHELL_COMPUTE_DRIVER_BIND; do [ -z "${!variable:-}" ] || exit 23 done -expected_sandbox="ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:$(printf '%064d' 0)" +expected_sandbox="nvcr.io/nvidia/base/ubuntu@sha256:$(printf '%064d' 0)" [ "${OPENSHELL_E2E_REQUIRE_DIGEST_PINNED_SANDBOX_IMAGE:-0}" = 1 ] || exit 24 [ "${OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE:-}" = "${expected_sandbox}" ] || exit 25 -[ "${OPENSHELL_COMMUNITY_REGISTRY:-}" = "ghcr.io/nvidia/openshell-community/sandboxes" ] || exit 28 expected_base="docker.io/library/debian@sha256:$(printf '%064d' 0)" [ "${OPENSHELL_E2E_SUPERVISOR_BASE_IMAGE:-}" = "${OPENSHELL_PARITY_TEST_SUPERVISOR_BASE}" ] || exit 26 [ "${OPENSHELL_E2E_SUPERVISOR_BASE_RUNTIME_IMAGE:-}" = "${expected_base}" ] || exit 27 @@ -159,7 +158,7 @@ schema = int(os.environ["OPENSHELL_E2E_CONFIG_SCHEMA_VERSION"]) external = os.environ.get("OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER") == "1" zero = "0" * 64 image_digest = f"sha256:{zero}" -sandbox_runtime = f"ghcr.io/nvidia/openshell-community/sandboxes/base@{image_digest}" +sandbox_runtime = f"nvcr.io/nvidia/base/ubuntu@{image_digest}" sandbox_boundary = "localhost/openshell/sandbox:dev" supervisor_runtime = f"localhost/openshell/supervisor@{image_digest}" base_runtime = f"docker.io/library/debian@{image_digest}" @@ -218,7 +217,7 @@ launch = { "sandbox_image_digest": image_digest, "sandbox_runtime_image": sandbox_runtime, "sandbox_boundary_image": sandbox_boundary, - "sandbox_client_image_alias": "ghcr.io/nvidia/openshell-community/sandboxes/base:latest", + "sandbox_client_image_alias": "nvcr.io/nvidia/base/ubuntu:24.04", "sandbox_client_image_alias_id": zero, "gateway_sha256_before_execution": os.environ[ "OPENSHELL_E2E_EXPECTED_GATEWAY_SHA256" @@ -292,7 +291,7 @@ done printf '%s %064d sha256:%064d %s %s %s %s\n' \ "${expected_sandbox}" 0 0 "${expected_base}" \ "localhost/openshell/supervisor@sha256:$(printf '%064d' 0)" \ - "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" \ + "nvcr.io/nvidia/base/ubuntu:24.04" \ "${package_hash}" >&2 if [ "${OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER:-0}" = 1 ]; then printf 'fixture external driver log\n' >"${OPENSHELL_PARITY_EXTERNAL_DRIVER_LOG_CAPTURE}" @@ -422,7 +421,6 @@ OPENSHELL_SANDBOX_PROXY_CA_BUNDLE=/tmp/untrusted-proxy-ca \ OPENSHELL_OTLP_ENDPOINT=http://untrusted.invalid:4317 \ OPENSHELL_GATEWAY_NAME=untrusted \ OPENSHELL_COMPUTE_DRIVER_BIND=192.0.2.2:50061 \ -OPENSHELL_COMMUNITY_REGISTRY=untrusted.invalid/community \ run_harness assert_contains "${WORKDIR}/calls" "baseline|1|${WORKDIR}/results/artifacts/baseline/gateway|${WORKDIR}/results/artifacts/baseline/cli|${WORKDIR}/results/artifacts/baseline/conformance" assert_contains "${WORKDIR}/calls" "candidate|2|${WORKDIR}/results/artifacts/candidate/gateway|${WORKDIR}/results/artifacts/candidate/cli|${WORKDIR}/results/artifacts/candidate/conformance" @@ -439,7 +437,7 @@ assert_contains "${WORKDIR}/results/semantic-verification.json" '"accepted": tru assert_not_contains "${WORKDIR}/results/baseline.json" '"scenarios"' assert_contains "${WORKDIR}/results/baseline.log" '"scenarios"' assert_contains "${WORKDIR}/results/baseline.conformance.json" '"passed":true' -assert_contains "${WORKDIR}/podman-calls" 'pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest' +assert_contains "${WORKDIR}/podman-calls" 'pull nvcr.io/nvidia/base/ubuntu:24.04' assert_contains "${WORKDIR}/podman-calls" "pull ${TEST_SUPERVISOR_BASE}" assert_contains "${WORKDIR}/podman-calls" 'unshare rm -rf -- ' assert_contains "${WORKDIR}/podman-calls" 'openshell-parity-run.' diff --git a/e2e/parity/verify-results.py b/e2e/parity/verify-results.py index 4e8a4619b..ccaf3fbb1 100644 --- a/e2e/parity/verify-results.py +++ b/e2e/parity/verify-results.py @@ -56,6 +56,16 @@ def require(condition: bool, message: str) -> None: raise ValueError(message) +def image_repository(reference: str) -> str: + """Return an OCI image repository without a tag or digest.""" + repository = reference.split("@", 1)[0] + last_slash = repository.rfind("/") + last_colon = repository.rfind(":") + if last_colon > last_slash: + repository = repository[:last_colon] + return repository + + def verify_conformance_report(path: Path) -> None: report = load_json(path) require(report.get("passed") is True, f"{path}: conformance report did not pass") @@ -330,7 +340,8 @@ def verify_variant( sandbox_alias = launch.get("sandbox_client_image_alias") require( isinstance(sandbox_alias, str) - and sandbox_alias == sandbox_runtime.rsplit("@", 1)[0] + ":latest" + and "@" not in sandbox_alias + and image_repository(sandbox_alias) == image_repository(sandbox_runtime) and launch.get("sandbox_client_image_alias_id") == sandbox_id, f"{launch_path}: sandbox client alias is not bound to the pinned image", ) diff --git a/e2e/rust/e2e-vm.sh b/e2e/rust/e2e-vm.sh index 5528c0087..5d7d15029 100755 --- a/e2e/rust/e2e-vm.sh +++ b/e2e/rust/e2e-vm.sh @@ -51,7 +51,7 @@ DRIVER_BIN="${OPENSHELL_VM_DRIVER_BIN:-${ROOT}/target/debug/openshell-driver-vm} CLI_BIN="${OPENSHELL_BIN:-${ROOT}/target/debug/openshell}" E2E_TEST_OVERRIDE="${OPENSHELL_E2E_VM_TEST:-}" E2E_FEATURES="${OPENSHELL_E2E_VM_FEATURES-e2e-vm}" -SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-${COMMUNITY_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}}" +SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-${VM_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}}" # The VM driver places `compute-driver.sock` under `[openshell.drivers.vm].state_dir`. # AF_UNIX SUN_LEN is 104 bytes on macOS (108 on Linux), so paths anchored diff --git a/e2e/rust/src/harness/container.rs b/e2e/rust/src/harness/container.rs index 542b9db8e..b28ebc3f6 100644 --- a/e2e/rust/src/harness/container.rs +++ b/e2e/rust/src/harness/container.rs @@ -16,7 +16,7 @@ use tokio::time::{interval, timeout}; use super::port::find_free_port; -const DEFAULT_TEST_SERVER_IMAGE: &str = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"; +const DEFAULT_TEST_SERVER_IMAGE: &str = super::sandbox::E2E_WORKLOAD_IMAGE; #[must_use] pub fn e2e_driver() -> Option { diff --git a/e2e/rust/src/harness/sandbox.rs b/e2e/rust/src/harness/sandbox.rs index e0a1f3589..15586c265 100644 --- a/e2e/rust/src/harness/sandbox.rs +++ b/e2e/rust/src/harness/sandbox.rs @@ -17,6 +17,12 @@ use tokio::time::timeout; use super::binary::openshell_cmd; use super::output::{extract_field, strip_ansi}; +/// Tool-capable workload image used by the E2E harness. +/// +/// Product defaults remain on the minimal NVIDIA Ubuntu image. Tests that +/// explicitly pass `--from` continue to exercise their requested image. +pub const E2E_WORKLOAD_IMAGE: &str = "ghcr.io/astral-sh/uv:0.12.17-python3.12-trixie-slim@sha256:9a59bb7206905ccaae4f7dab222fbac47c125a21e5fc16f43f427cd6c940ade3"; + /// Extract the sandbox name from CLI create output. /// /// The CLI prints `Created sandbox: ` (current format). Falls back to @@ -26,10 +32,9 @@ fn extract_sandbox_name(output: &str) -> Option { } /// Default timeout for waiting for a sandbox to become ready. -/// In VM mode, the overlayfs snapshotter re-extracts all image layers -/// from the content store on every boot (~250s for the 1GB sandbox -/// base image), so 600s accommodates extraction + workspace-init + pod -/// startup. +/// In VM mode, the overlayfs snapshotter re-extracts image layers from the +/// content store on every boot, so 600s accommodates cold image preparation, +/// workspace initialization, and sandbox startup. const SANDBOX_READY_TIMEOUT: Duration = Duration::from_secs(600); static NEXT_SANDBOX_NAME: AtomicU64 = AtomicU64::new(1); @@ -39,6 +44,17 @@ fn has_explicit_sandbox_name(args: &[&str]) -> bool { .any(|arg| *arg == "--name" || arg.starts_with("--name=")) } +fn has_explicit_sandbox_image(args: &[&str]) -> bool { + args.iter() + .any(|arg| *arg == "--from" || arg.starts_with("--from=")) +} + +fn add_test_image_if_missing(command: &mut tokio::process::Command, args: &[&str]) { + if !has_explicit_sandbox_image(args) { + command.arg("--from").arg(E2E_WORKLOAD_IMAGE); + } +} + fn add_unique_name_if_missing(command: &mut tokio::process::Command, args: &[&str]) { if !has_explicit_sandbox_name(args) { command.arg("--name").arg(format!( @@ -96,6 +112,18 @@ impl SandboxGuard { /// Returns an error if the CLI exits with a non-zero status or the sandbox /// name cannot be parsed from the output. pub async fn create(args: &[&str]) -> Result { + Self::create_inner(args, true).await + } + + /// Create a sandbox using the gateway's configured default image. + /// + /// Most E2E tests use [`Self::create`], which supplies the tool-capable E2E + /// image. This variant is reserved for coverage of the product default. + pub async fn create_with_gateway_default(args: &[&str]) -> Result { + Self::create_inner(args, false).await + } + + async fn create_inner(args: &[&str], use_test_image: bool) -> Result { let separator = args.iter().position(|arg| *arg == "--"); let (create_args, command) = separator.map_or((args, &[][..]), |index| { (&args[..index], &args[index + 1..]) @@ -111,6 +139,9 @@ impl SandboxGuard { let mut cmd = openshell_cmd(); cmd.arg("sandbox").arg("create").arg("--detach"); add_unique_name_if_missing(&mut cmd, create_args); + if use_test_image { + add_test_image_if_missing(&mut cmd, create_args); + } for arg in create_args { cmd.arg(arg); } @@ -187,6 +218,7 @@ impl SandboxGuard { let mut cmd = openshell_cmd(); cmd.arg("sandbox").arg("create").arg("--detach"); add_unique_name_if_missing(&mut cmd, &[]); + add_test_image_if_missing(&mut cmd, &[]); cmd.arg("--") .args(command) .stdout(Stdio::piped()) @@ -235,6 +267,7 @@ impl SandboxGuard { let mut create_cmd = openshell_cmd(); create_cmd.arg("sandbox").arg("create").arg("--detach"); add_unique_name_if_missing(&mut create_cmd, create_args); + add_test_image_if_missing(&mut create_cmd, create_args); for arg in create_args { create_cmd.arg(arg); } @@ -374,6 +407,7 @@ impl SandboxGuard { let mut cmd = openshell_cmd(); cmd.arg("sandbox").arg("create").arg("--detach"); add_unique_name_if_missing(&mut cmd, &[]); + add_test_image_if_missing(&mut cmd, &[]); for (local, dest) in uploads { cmd.arg("--upload").arg(format!("{local}:{dest}")); } @@ -703,7 +737,7 @@ impl Drop for SandboxGuard { #[cfg(test)] mod tests { - use super::has_explicit_sandbox_name; + use super::{has_explicit_sandbox_image, has_explicit_sandbox_name}; #[test] fn detects_explicit_sandbox_names() { @@ -711,4 +745,11 @@ mod tests { assert!(has_explicit_sandbox_name(&["--name=example"])); assert!(!has_explicit_sandbox_name(&["--policy", "policy.yaml"])); } + + #[test] + fn detects_explicit_sandbox_images() { + assert!(has_explicit_sandbox_image(&["--from", "example:latest"])); + assert!(has_explicit_sandbox_image(&["--from=example:latest"])); + assert!(!has_explicit_sandbox_image(&["--policy", "policy.yaml"])); + } } diff --git a/e2e/rust/tests/community_image.rs b/e2e/rust/tests/community_image.rs deleted file mode 100644 index 2fab46385..000000000 --- a/e2e/rust/tests/community_image.rs +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#![cfg(feature = "e2e")] - -//! E2E test: pull and launch a community sandbox image from GHCR. -//! -//! This test verifies that: -//! 1. The `base` community sandbox image can be pulled from GHCR -//! 2. A sandbox can be created and run using the community image -//! 3. Basic command execution works inside the community sandbox -//! -//! Prerequisites: -//! - A running openshell gateway (`mise run gateway:docker`) -//! - Network access to ghcr.io/nvidia/openshell-community/sandboxes/ - -use openshell_e2e::harness::output::strip_ansi; -use openshell_e2e::harness::sandbox::SandboxGuard; - -/// Create a sandbox using the community `base` image and verify it works. -/// -/// The `--from base` argument should resolve to: -/// `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` -#[tokio::test] -async fn sandbox_from_community_base_image() { - // Create a sandbox using the community "base" image. - // The CLI should expand "base" to the full GHCR path. - let mut guard = SandboxGuard::create(&["--from", "base", "--", "echo", "community-image-ok"]) - .await - .expect("sandbox create from community base image"); - - // Verify the command output contains our marker. - let clean_output = strip_ansi(&guard.create_output); - assert!( - clean_output.contains("community-image-ok"), - "expected 'community-image-ok' in sandbox output:\n{clean_output}" - ); - - guard.cleanup().await; -} - -/// Create a sandbox using the full GHCR image path explicitly. -/// -/// This tests that explicit image references work correctly. -#[tokio::test] -async fn sandbox_from_explicit_ghcr_image() { - let image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"; - - let mut guard = SandboxGuard::create(&["--from", image, "--", "cat", "/etc/os-release"]) - .await - .expect("sandbox create from explicit GHCR image"); - - // The base image should have an /etc/os-release file. - let clean_output = strip_ansi(&guard.create_output); - assert!( - clean_output.contains("ID=") || clean_output.contains("NAME="), - "expected OS release info in sandbox output:\n{clean_output}" - ); - - guard.cleanup().await; -} diff --git a/e2e/rust/tests/default_image.rs b/e2e/rust/tests/default_image.rs new file mode 100644 index 000000000..6a1dbb3cd --- /dev/null +++ b/e2e/rust/tests/default_image.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "e2e")] + +//! E2E coverage for the default NVIDIA Ubuntu workload image. + +use openshell_e2e::harness::output::strip_ansi; +use openshell_e2e::harness::sandbox::SandboxGuard; + +#[tokio::test] +async fn sandbox_from_default_image() { + let mut guard = SandboxGuard::create_with_gateway_default(&["--", "cat", "/etc/os-release"]) + .await + .expect("sandbox create from default image"); + + let clean_output = strip_ansi(&guard.create_output); + assert!( + clean_output.contains("ID=ubuntu"), + "expected Ubuntu OS release info in sandbox output:\n{clean_output}" + ); + + guard.cleanup().await; +} + +#[tokio::test] +async fn sandbox_from_explicit_nvidia_ubuntu_image() { + let image = "nvcr.io/nvidia/base/ubuntu:24.04"; + let mut guard = SandboxGuard::create(&["--from", image, "--", "cat", "/etc/os-release"]) + .await + .expect("sandbox create from explicit NVIDIA Ubuntu image"); + + let clean_output = strip_ansi(&guard.create_output); + assert!( + clean_output.contains("ID=ubuntu"), + "expected Ubuntu OS release info in sandbox output:\n{clean_output}" + ); + + guard.cleanup().await; +} diff --git a/e2e/rust/tests/driver_config_volume.rs b/e2e/rust/tests/driver_config_volume.rs index 8aaf1e3cb..054ba3bab 100644 --- a/e2e/rust/tests/driver_config_volume.rs +++ b/e2e/rust/tests/driver_config_volume.rs @@ -21,7 +21,7 @@ use openshell_e2e::harness::container::{ImageGuard, e2e_driver}; use openshell_e2e::harness::sandbox::SandboxGuard; use serde_json::{Map, Value}; -const TEST_IMAGE: &str = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"; +const TEST_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; const VOLUME_TARGET: &str = "/sandbox/e2e-volume"; const BIND_TARGET: &str = "/sandbox/e2e-bind"; #[cfg(feature = "e2e-docker")] diff --git a/e2e/rust/tests/host_gateway_alias.rs b/e2e/rust/tests/host_gateway_alias.rs index cf7b9e657..95d0494d6 100644 --- a/e2e/rust/tests/host_gateway_alias.rs +++ b/e2e/rust/tests/host_gateway_alias.rs @@ -158,7 +158,7 @@ fn write_binding_profile( .tempfile() .map_err(|e| format!("create provider profile: {e}"))?; let profile = format!( - r#"id: {id} + r"id: {id} display_name: {display_name} category: other credentials: @@ -174,8 +174,8 @@ endpoints: protocol: rest access: full enforcement: enforce -binaries: [/usr/bin/curl] -"# +binaries: [/usr/bin/bash] +" ); file.write_all(profile.as_bytes()) .map_err(|e| format!("write provider profile: {e}"))?; @@ -187,11 +187,11 @@ binaries: [/usr/bin/curl] fn write_binding_policy(port: u16) -> Result { let mut file = NamedTempFile::new().map_err(|e| format!("create binding policy: {e}"))?; let policy = format!( - r#"version: 1 + r"version: 1 filesystem_policy: include_workdir: true - read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_only: [/bin, /usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] read_write: [/sandbox, /tmp, /dev/null] landlock: @@ -218,8 +218,8 @@ network_policies: access: full enforcement: enforce binaries: - - path: /usr/bin/curl -"# + - path: /usr/bin/bash +" ); file.write_all(policy.as_bytes()) .map_err(|e| format!("write binding policy: {e}"))?; @@ -255,6 +255,65 @@ async fn delete_provider_profile(id: &str) { let _ = cmd.status().await; } +fn binding_probe_command(port: u16) -> String { + format!( + r#" +http_request() {{ + local host="$1" path="$2" status_line line + HTTP_STATUS= HTTP_BODY= + exec 3<>"/dev/tcp/$host/{port}" || return 1 + printf 'GET %s HTTP/1.1\r\nHost: %s:{port}\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' "$path" "$host" "$BOUND_TOKEN_A" >&3 + IFS= read -r status_line <&3 || return 1 + status_line="${{status_line%$'\r'}}" + HTTP_STATUS="${{status_line#* }}" + HTTP_STATUS="${{HTTP_STATUS%% *}}" + while IFS= read -r line <&3; do + line="${{line%$'\r'}}" + [[ -z "$line" ]] && break + done + while IFS= read -r line <&3 || [[ -n "$line" ]]; do HTTP_BODY+="$line"; done + exec 3>&- 3<&- +}} +http_request host.openshell.internal /allowed/check; allowed="$HTTP_BODY" +http_request host.docker.internal /allowed/check; host_denied="$HTTP_STATUS" +http_request host.openshell.internal /other/check; path_denied="$HTTP_STATUS" +printf 'ALLOWED=%s HOST_DENIED=%s PATH_DENIED=%s\n' "$allowed" "$host_denied" "$path_denied" +"# + ) +} + +fn assert_binding_results(guard: &SandboxGuard, logs: &str) { + assert!( + guard + .create_output + .contains(r#"ALLOWED={"authorized":true}"#), + "credential should resolve at the bound endpoint:\n{}\nlogs:\n{logs}", + guard.create_output, + ); + assert!( + guard.create_output.contains("HOST_DENIED=403"), + "same placeholder must be denied at an unbound host:\n{}", + guard.create_output + ); + assert!( + guard.create_output.contains("PATH_DENIED=403"), + "same placeholder must be denied at an unbound path on its bound host:\n{}", + guard.create_output + ); + assert!( + logs.contains("openshell.provider_credential.endpoint_mismatch") + && logs.contains("credential_endpoint_mismatch"), + "OCSF logs should explain the endpoint-binding denial without secret material:\n{logs}" + ); + assert!( + !logs.contains("e2e-bound-secret") + && !logs.contains("e2e-provider-b-secret") + && !logs.contains("BOUND_TOKEN_A") + && !logs.contains("BOUND_TOKEN_B"), + "OCSF logs must not contain credential values or environment keys:\n{logs}" + ); +} + fn write_policy(port: u16) -> Result { let mut file = NamedTempFile::new().map_err(|e| format!("create temp policy file: {e}"))?; let policy = format!( @@ -264,6 +323,7 @@ filesystem_policy: include_workdir: true read_only: - /usr + - /bin - /lib - /proc - /dev/urandom @@ -294,7 +354,7 @@ network_policies: - "192.168.0.0/16" - "fc00::/7" binaries: - - path: /usr/bin/curl + - path: /usr/bin/bash "# ); file.write_all(policy.as_bytes()) @@ -316,16 +376,17 @@ async fn sandbox_reaches_host_openshell_internal_via_host_gateway_alias() { .expect("temp policy path should be utf-8") .to_string(); + let command = format!( + r#"exec 3<>/dev/tcp/host.openshell.internal/{0}; printf 'GET / HTTP/1.1\r\nHost: host.openshell.internal:{0}\r\nConnection: close\r\n\r\n' >&3; while IFS= read -r line <&3 || [[ -n $line ]]; do printf '%s\n' "$line"; done"#, + server.port + ); let guard = SandboxGuard::create(&[ "--policy", &policy_path, "--", - "curl", - "--silent", - "--show-error", - "--max-time", - "15", - &format!("http://host.openshell.internal:{}/", server.port), + "/usr/bin/bash", + "-c", + &command, ]) .await .expect("sandbox create with host.openshell.internal echo request"); @@ -400,10 +461,7 @@ async fn static_provider_credentials_are_bound_to_profile_endpoints() { .await .expect("create endpoint-bound provider B"); - let command = format!( - r#"allowed=$(curl --silent --show-error --max-time 15 -H "Authorization: Bearer $BOUND_TOKEN_A" http://host.openshell.internal:{}/allowed/check); host_denied=$(curl --silent --show-error --max-time 15 -o /tmp/host-denied-body -w "%{{http_code}}" -H "Authorization: Bearer $BOUND_TOKEN_A" http://host.docker.internal:{}/allowed/check); path_denied=$(curl --silent --show-error --max-time 15 -o /tmp/path-denied-body -w "%{{http_code}}" -H "Authorization: Bearer $BOUND_TOKEN_A" http://host.openshell.internal:{}/other/check); printf 'ALLOWED=%s HOST_DENIED=%s PATH_DENIED=%s\n' "$allowed" "$host_denied" "$path_denied""#, - server.port, server.port, server.port - ); + let command = binding_probe_command(server.port); let mut guard = SandboxGuard::create(&[ "--policy", &policy_path, @@ -413,7 +471,7 @@ async fn static_provider_credentials_are_bound_to_profile_endpoints() { BINDING_PROVIDER_B_NAME, "--no-auto-providers", "--", - "sh", + "/usr/bin/bash", "-c", &command, ]) @@ -426,36 +484,7 @@ async fn static_provider_credentials_are_bound_to_profile_endpoints() { }) .await .expect("fetch endpoint mismatch logs"); - assert!( - guard - .create_output - .contains(r#"ALLOWED={"authorized":true}"#), - "credential should resolve at the bound endpoint:\n{}\nlogs:\n{logs}", - guard.create_output, - ); - assert!( - guard.create_output.contains("HOST_DENIED=403"), - "same placeholder must be denied at an unbound host:\n{}", - guard.create_output - ); - assert!( - guard.create_output.contains("PATH_DENIED=403"), - "same placeholder must be denied at an unbound path on its bound host:\n{}", - guard.create_output - ); - - assert!( - logs.contains("openshell.provider_credential.endpoint_mismatch") - && logs.contains("credential_endpoint_mismatch"), - "OCSF logs should explain the endpoint-binding denial without secret material:\n{logs}" - ); - assert!( - !logs.contains("e2e-bound-secret") - && !logs.contains("e2e-provider-b-secret") - && !logs.contains("BOUND_TOKEN_A") - && !logs.contains("BOUND_TOKEN_B"), - "OCSF logs must not contain credential values or environment keys:\n{logs}" - ); + assert_binding_results(&guard, &logs); guard.cleanup().await; delete_provider(BINDING_PROVIDER_A_NAME).await; diff --git a/e2e/rust/tests/podman_oci_identity.rs b/e2e/rust/tests/podman_oci_identity.rs index e74888c84..c56c700d0 100644 --- a/e2e/rust/tests/podman_oci_identity.rs +++ b/e2e/rust/tests/podman_oci_identity.rs @@ -18,7 +18,7 @@ use openshell_e2e::harness::container::{ContainerEngine, is_e2e_driver}; use openshell_e2e::harness::output::strip_ansi; use openshell_e2e::harness::sandbox::SandboxGuard; -const BASE_IMAGE: &str = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"; +const BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; const READY_MARKER: &str = "podman-oci-identity-ready"; const OCI_UID: &str = "2345"; const OCI_GID: &str = "2346"; @@ -182,7 +182,7 @@ async fn podman_uses_oci_identity_and_inspected_image_id() { } let image = ImageGuard::build().expect("build Podman OCI identity image"); - // The community base image contains a baked default policy with an + // The fixture image contains a policy with an // explicit `sandbox` process identity. Supply a complete policy that // intentionally omits `process` so this test exercises OCI fallback. let policy = tempfile::NamedTempFile::new().expect("create OCI fallback policy"); diff --git a/e2e/rust/tests/provider_refresh_handles.rs b/e2e/rust/tests/provider_refresh_handles.rs index a12281f3e..2d94d3af7 100644 --- a/e2e/rust/tests/provider_refresh_handles.rs +++ b/e2e/rust/tests/provider_refresh_handles.rs @@ -106,7 +106,7 @@ fn write_profile(resource_port: u16, token_port: u16) -> Result Result { .tempfile() .map_err(|error| format!("create policy: {error}"))?; let policy = format!( - r#"version: 1 + r"version: 1 filesystem_policy: include_workdir: true read_only: [/usr, /lib, /proc, /etc, /dev/urandom] @@ -181,8 +181,8 @@ network_policies: - 172.0.0.0/8 - 192.168.0.0/16 binaries: - - path: /usr/bin/curl -"# + - path: /usr/local/bin/python3 +" ); file.write_all(policy.as_bytes()) .map_err(|error| format!("write policy: {error}"))?; @@ -311,9 +311,7 @@ echo {READY_MARKER} while true; do if [ -f /sandbox/probe-trigger ]; then rm -f /sandbox/probe-trigger - if curl --fail --silent --output /dev/null \ - --header "Authorization: Bearer $REFRESH_E2E_ACCESS_TOKEN" \ - {resource_url}; then + if python3 -c 'import os, urllib.request; request = urllib.request.Request("{resource_url}", headers=dict(Authorization="Bearer " + os.environ["REFRESH_E2E_ACCESS_TOKEN"])); urllib.request.urlopen(request, timeout=5).read()'; then echo ok > /sandbox/probe-result else echo failed > /sandbox/probe-result @@ -343,7 +341,7 @@ done"# wait_for_probe_failure(&sandbox).await?; let fresh_probe = format!( - "curl --fail --silent --output /dev/null --header \"Authorization: Bearer $REFRESH_E2E_ACCESS_TOKEN\" {resource_url}" + r#"python3 -c 'import os, urllib.request; request = urllib.request.Request("{resource_url}", headers=dict(Authorization="Bearer " + os.environ["REFRESH_E2E_ACCESS_TOKEN"])); urllib.request.urlopen(request, timeout=5).read()'"# ); sandbox.exec(&["sh", "-c", &fresh_probe]).await?; Ok(()) diff --git a/e2e/rust/tests/provider_token_exchange.rs b/e2e/rust/tests/provider_token_exchange.rs index c4e06f029..2f3600c9c 100644 --- a/e2e/rust/tests/provider_token_exchange.rs +++ b/e2e/rust/tests/provider_token_exchange.rs @@ -554,7 +554,7 @@ fn write_profile(profile_type: &str, token_port: u16, target_port: u16) -> Named .tempfile() .expect("create provider profile temp file"); let profile = format!( - r#"id: {profile_type} + r"id: {profile_type} display_name: Podman token exchange e2e description: Podman e2e provider profile for two-stage token exchange category: other @@ -592,9 +592,8 @@ endpoints: - 172.0.0.0/8 - 192.168.0.0/16 binaries: - - /usr/bin/curl - - /usr/local/bin/curl -"# + - /usr/local/bin/python3 +" ); file.write_all(profile.as_bytes()) .expect("write provider profile"); @@ -604,10 +603,10 @@ binaries: fn sandbox_script(token_port: u16) -> String { let _ = token_port; - r#"set -eu + r"set -eu echo token-server-ready while true; do sleep 60; done -"# +" .to_string() } @@ -773,8 +772,11 @@ async fn container_loopback_port_ready(container_name: &str, token_port: u16) -> .unwrap_or(false) } -async fn sandbox_exec_curl(sandbox_name: &str, target_port: u16) -> Result { +async fn sandbox_exec_http(sandbox_name: &str, target_port: u16) -> Result { let url = format!("http://host.openshell.internal:{target_port}/resource"); + let script = format!( + "import urllib.request; print(urllib.request.urlopen({url:?}, timeout=5).read().decode())" + ); let mut last_output = String::new(); for _ in 0..20 { let output = openshell_cmd() @@ -785,11 +787,9 @@ async fn sandbox_exec_curl(sandbox_name: &str, target_port: u16) -> Result Result output, Err(err) => { let debug = provider_token_debug(&sandbox.name, token_port, target_port).await; - panic!("curl protected target from kept sandbox: {err}\n{debug}"); + panic!("request protected target from kept sandbox: {err}\n{debug}"); } }; @@ -887,8 +887,8 @@ async fn podman_provider_token_exchange_injects_bearer_header() { sandbox.cleanup().await; assert!( - curl_output.contains("token-exchange-ok"), + request_output.contains("token-exchange-ok"), "protected target should receive the final exchanged bearer token:\n{}", - curl_output + request_output ); } diff --git a/e2e/with-docker-gateway.sh b/e2e/with-docker-gateway.sh index 95c6d6e3c..97249d912 100755 --- a/e2e/with-docker-gateway.sh +++ b/e2e/with-docker-gateway.sh @@ -18,7 +18,7 @@ # OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE=... # OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE_PULL_POLICY=always|if_not_present|never # -# The default community sandbox image uses :latest. This wrapper refreshes it +# The default sandbox image uses a mutable tag. This wrapper refreshes it # before starting the gateway, while the Docker driver defaults to # if_not_present so local Dockerfile-built images remain usable. # @@ -516,7 +516,7 @@ build_local_docker_sandbox_runtime_image_if_required "${SANDBOX_RUNTIME_IMAGE}" ensure_docker_sandbox_runtime_image "${SANDBOX_RUNTIME_IMAGE}" echo "Using Docker sandbox runtime image: ${SANDBOX_RUNTIME_IMAGE}" -DEFAULT_SANDBOX_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +DEFAULT_SANDBOX_IMAGE="nvcr.io/nvidia/base/ubuntu:24.04" SANDBOX_IMAGE="${OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE:-${OPENSHELL_SANDBOX_IMAGE:-${DEFAULT_SANDBOX_IMAGE}}}" SANDBOX_IMAGE_PULL_POLICY="${OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE_PULL_POLICY:-${OPENSHELL_SANDBOX_IMAGE_PULL_POLICY:-if_not_present}}" if ! ensure_sandbox_image_available "${SANDBOX_IMAGE}"; then diff --git a/e2e/with-podman-gateway.sh b/e2e/with-podman-gateway.sh index 058b389af..044f74a5a 100755 --- a/e2e/with-podman-gateway.sh +++ b/e2e/with-podman-gateway.sh @@ -613,7 +613,7 @@ SANDBOX_BOUNDARY_IMAGE="$(resolve_podman_sandbox_runtime_image)" ensure_podman_sandbox_runtime_image "${SANDBOX_BOUNDARY_IMAGE}" echo "Using Podman sandbox runtime image: ${SANDBOX_BOUNDARY_IMAGE}" -DEFAULT_SANDBOX_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +DEFAULT_SANDBOX_IMAGE="nvcr.io/nvidia/base/ubuntu:24.04" SANDBOX_IMAGE_REQUEST="${OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE:-${OPENSHELL_SANDBOX_IMAGE:-${DEFAULT_SANDBOX_IMAGE}}}" if [ "${OPENSHELL_E2E_REQUIRE_DIGEST_PINNED_SANDBOX_IMAGE:-0}" = "1" ] \ && ! [[ "${SANDBOX_IMAGE_REQUEST}" =~ ^[^@]+@sha256:[0-9a-f]{64}$ ]]; then diff --git a/examples/agent-driven-policy-management/demo.sh b/examples/agent-driven-policy-management/demo.sh index b123c66ad..2cb10dedb 100755 --- a/examples/agent-driven-policy-management/demo.sh +++ b/examples/agent-driven-policy-management/demo.sh @@ -362,7 +362,6 @@ start_agent_sandbox() { ( "$OPENSHELL_BIN" sandbox create \ --name "$DEMO_SANDBOX_NAME" \ - --from base \ --provider "$DEMO_CODEX_PROVIDER_NAME" \ --provider "$DEMO_GITHUB_PROVIDER_NAME" \ --policy "$POLICY_FILE" \ diff --git a/examples/aws-s3-sts.md b/examples/aws-s3-sts.md index 94b73c73c..76bace9f3 100644 --- a/examples/aws-s3-sts.md +++ b/examples/aws-s3-sts.md @@ -104,7 +104,7 @@ gateway_id = "podman-dev" ttl_secs = 3600 [openshell.drivers.podman] -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" supervisor_image = "localhost/openshell/supervisor:dev" image_pull_policy = "if_not_present" health_check_interval_secs = 10 diff --git a/examples/multi-agent-notepad/README.md b/examples/multi-agent-notepad/README.md index 2bff46cd3..910128316 100644 --- a/examples/multi-agent-notepad/README.md +++ b/examples/multi-agent-notepad/README.md @@ -85,6 +85,7 @@ for this demo, or one you're comfortable with the demo creating files in. export DEMO_GITHUB_OWNER= export DEMO_GITHUB_REPO= export DEMO_GITHUB_TOKEN="$(gh auth token)" +export DEMO_AGENT_IMAGE="registry.example.com/your-org/codex-agent:latest" bash examples/multi-agent-notepad/demo.sh ``` @@ -93,9 +94,9 @@ bash examples/multi-agent-notepad/demo.sh usually broader than `contents:write`. If you'd rather use a scope-limited PAT, set `DEMO_GITHUB_TOKEN` to that value instead. -By default the script launches five worker agents and one synthesis agent in -the OpenShell `base` image, where Codex is preinstalled. To run a faster -smoke test: +The selected image must contain Codex, `curl`, and Bash. The script launches +five worker agents and one synthesis agent by default. To run a faster smoke +test: ```bash export DEMO_AGENT_COUNT=2 @@ -139,7 +140,7 @@ values never sit in the sandbox filesystem. The script renders `policy.template.yaml` for the configured GitHub repository and run id. The policy allows: -- Codex traffic to OpenAI and ChatGPT endpoints used by the community base +- Codex traffic to OpenAI and ChatGPT endpoints used by the selected workload image - Limited Codex plugin metadata reads from `github.com/openai/plugins.git` - GitHub REST `GET` and `PUT` calls scoped to diff --git a/examples/multi-agent-notepad/demo.sh b/examples/multi-agent-notepad/demo.sh index 0d68b3cdb..53535a644 100755 --- a/examples/multi-agent-notepad/demo.sh +++ b/examples/multi-agent-notepad/demo.sh @@ -12,6 +12,7 @@ PROMPTS_DIR="${SCRIPT_DIR}/prompts" OPENSHELL_BIN="${OPENSHELL_BIN:-openshell}" DEMO_TOPIC="${DEMO_TOPIC:-How should teams evaluate sandboxed coding agents?}" DEMO_AGENT_COUNT="${DEMO_AGENT_COUNT:-5}" +DEMO_AGENT_IMAGE="${DEMO_AGENT_IMAGE:-}" DEMO_BRANCH="${DEMO_BRANCH:-main}" DEMO_RUN_ID="${DEMO_RUN_ID:-$(date +%Y%m%d-%H%M%S)}" # Sandbox names are capped at 19 characters. Derive a short tag from the @@ -157,9 +158,10 @@ create_providers() { run_sandbox() { local name="$1" shift + [[ -n "$DEMO_AGENT_IMAGE" ]] || fail "set DEMO_AGENT_IMAGE to an OCI image containing Codex, curl, and bash" "$OPENSHELL_BIN" sandbox create \ --name "$name" \ - --from base \ + --from "$DEMO_AGENT_IMAGE" \ --provider "$DEMO_CODEX_PROVIDER_NAME" \ --provider "$DEMO_GITHUB_PROVIDER_NAME" \ --policy "$POLICY_FILE" \ diff --git a/examples/private-ip-routing/README.md b/examples/private-ip-routing/README.md index aae18d7ae..148871c2e 100644 --- a/examples/private-ip-routing/README.md +++ b/examples/private-ip-routing/README.md @@ -11,8 +11,8 @@ When an endpoint in the sandbox policy includes an `allowed_ips` field, the proxy validates the resolved IP against that CIDR allowlist instead of blanket-blocking. Loopback and link-local remain always-blocked regardless. -The default sandbox policy (baked into the community base image) includes a `cluster_pods` -entry that allows any binary to reach port 8080 on the configured pod network: +Save an explicit policy as `private-policy.yaml` that allows the client binary +to reach port 8080 on the configured pod network: ```yaml cluster_pods: @@ -62,7 +62,10 @@ Create a sandbox and curl the private API through the proxy. Replace the IP with whatever `kubectl get pod` showed above: ```bash -openshell sandbox create -- bash -c \ +openshell sandbox create \ + --from registry.example.com/tools/curl:latest \ + --policy ./private-policy.yaml \ + -- bash -c \ 'curl -s --proxytunnel -x http://10.200.0.1:3128 http://10.42.0.128:8080/' ``` diff --git a/examples/spiffe-token-exchange-demo/podman/README.md b/examples/spiffe-token-exchange-demo/podman/README.md index 197fe70cd..2fc379fa4 100644 --- a/examples/spiffe-token-exchange-demo/podman/README.md +++ b/examples/spiffe-token-exchange-demo/podman/README.md @@ -109,7 +109,7 @@ TOKEN_ISSUER_PORT=18080 MANAGED_GATEWAY_PORT=18082 MANAGED_GATEWAY_HEALTH_PORT=18083 GATEWAY_IMAGE=ghcr.io/nvidia/openshell/gateway:latest -SANDBOX_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest +SANDBOX_IMAGE=nvcr.io/nvidia/base/ubuntu:24.04 SUPERVISOR_IMAGE=ghcr.io/nvidia/openshell/supervisor:latest SANDBOX_IMAGE_PULL_POLICY=if_not_present PODMAN_STOP_TIMEOUT_SECS=3 diff --git a/examples/sync-files.md b/examples/sync-files.md index 2047df2ce..d50e4065e 100644 --- a/examples/sync-files.md +++ b/examples/sync-files.md @@ -66,9 +66,8 @@ openshell sandbox download dev /sandbox/coverage ./coverage ## How it works -File sync uses **tar-over-SSH**. The CLI streams a tar archive through the -existing SSH proxy tunnel -- no `rsync` or other external tools required on -your machine. The sandbox base image provides GNU `tar` for extraction. - -- **Push**: `tar::Builder` (Rust) -> stdin | `ssh sandbox "tar xf - -C "` -- **Pull**: `ssh sandbox "tar cf - -C "` | stdout -> `tar::Archive` (Rust) +File sync uses the native OpenShell streaming file-transfer protocol. The CLI +creates and extracts tar streams in Rust, and the sandbox performs the matching +operation under the workload identity. Transfers use bounded frames with +backpressure, cancellation, and explicit completion; they do not require +`ssh`, `scp`, `rsync`, or a `tar` executable in the workload image. diff --git a/nix/test-guest/provisioners/roles/gateway-podman/tasks/development-gateway.yml b/nix/test-guest/provisioners/roles/gateway-podman/tasks/development-gateway.yml index 939400f5f..d1959ec51 100644 --- a/nix/test-guest/provisioners/roles/gateway-podman/tasks/development-gateway.yml +++ b/nix/test-guest/provisioners/roles/gateway-podman/tasks/development-gateway.yml @@ -56,7 +56,7 @@ gateway_id = "openshell-test-guest" [openshell.drivers.podman] - default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" + default_image = "nvcr.io/nvidia/base/ubuntu:24.04" supervisor_image = "{{ openshell_supervisor_image }}" image_pull_policy = "always" network_name = "openshell-test-guest" diff --git a/python/openshell/gateway_schema_v2_live_results_test.py b/python/openshell/gateway_schema_v2_live_results_test.py index 3cb52c5d6..cfd0a0151 100644 --- a/python/openshell/gateway_schema_v2_live_results_test.py +++ b/python/openshell/gateway_schema_v2_live_results_test.py @@ -85,7 +85,7 @@ STEP_10_CANDIDATE_COMMIT = EXPECTED_EXECUTED_CANDIDATE_COMMITS[ "compute-driver-boundary-parity" ] STEP_10_REPORT_SHA256 = ( - "65541eec5f642461a88b04b5459474fd7a475adeb7071a65a53fe183caad6a01" + "3c1297eef6c0a3b22530b980b571bda2d967a39a79a3e3286bcc09e30df84bf3" ) STEP_10_EVIDENCE_BUNDLES = { "in_tree": "target/parity/step10-intree-4a39da51", diff --git a/rfc/0003-gateway-configuration/README.md b/rfc/0003-gateway-configuration/README.md index 8007236d7..4527c85d0 100644 --- a/rfc/0003-gateway-configuration/README.md +++ b/rfc/0003-gateway-configuration/README.md @@ -122,7 +122,7 @@ scopes_claim = "" # empty disables scope enforcement [openshell.drivers.kubernetes] namespace = "openshell" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" supervisor_image_pull_policy = "if_not_present" @@ -132,7 +132,7 @@ host_gateway_ip = "10.0.0.1" ssh_socket_path = "/run/openshell/ssh.sock" [openshell.drivers.docker] -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" sandbox_label = "docker-dev" grpc_endpoint = "https://host.openshell.internal:8080" @@ -142,7 +142,7 @@ supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" # used to [openshell.drivers.podman] socket_path = "/run/podman/podman.sock" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" # always | if_not_present | never | newer supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" network_name = "openshell" @@ -225,7 +225,7 @@ disable_tls = true [openshell.drivers.kubernetes] namespace = "agents" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" supervisor_image = "ghcr.io/nvidia/openshell/supervisor:0.9.0" grpc_endpoint = "https://openshell-gateway.agents.svc:8080" ``` @@ -245,7 +245,7 @@ gateway: drivers: kubernetes: namespace: agents - default_image: ghcr.io/nvidia/openshell-community/sandboxes/base:latest + default_image: nvcr.io/nvidia/base/ubuntu:24.04 supervisor_image: ghcr.io/nvidia/openshell/supervisor:0.9.0 ``` diff --git a/scripts/agents/gator/Dockerfile b/scripts/agents/gator/Dockerfile index 6484746db..db1f5218a 100644 --- a/scripts/agents/gator/Dockerfile +++ b/scripts/agents/gator/Dockerfile @@ -5,7 +5,7 @@ # Gator sandbox image. # -# This mirrors the OpenShell Community base image's core system and developer +# This installs the core system and developer # tooling, but keeps the initial agent surface focused on Codex + GitHub tooling # for the gator-gate workflow. @@ -17,7 +17,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ WORKDIR /sandbox -# Core system dependencies copied from the community base sandbox image. +# Core system dependencies required by the gator workload. # iproute2: network namespace management (ip netns, veth pairs) # iptables: legacy bypass detection (kept for transition) # nftables: bypass detection; log + reject rules for direct connection diagnostics @@ -43,7 +43,7 @@ RUN groupadd -r supervisor && useradd -r -g supervisor -s /usr/sbin/nologin supe FROM system AS devtools # Node.js 22 + build toolchain. Keep the default apt installs aligned with the -# community base image, then add the small CLI tools gator commonly needs. +# workload, then add the small CLI tools gator commonly needs. RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y --no-install-recommends \ build-essential \ diff --git a/scripts/agents/gator/README.md b/scripts/agents/gator/README.md index b1b821963..a683a2030 100644 --- a/scripts/agents/gator/README.md +++ b/scripts/agents/gator/README.md @@ -4,7 +4,7 @@ Launch a headless sandbox agent that runs the `gator-gate` skill against OpenShe ## Prerequisites -- `gh` is authenticated on the host and has access to `NVIDIA/OpenShell` and `NVIDIA/OpenShell-Community`. +- `gh` is authenticated on the host and has access to `NVIDIA/OpenShell`. - For `--harness codex`, `codex login` has created `$HOME/.codex/auth.json`. - For `--harness codex`, local Codex auth must include an access token, refresh token, and account ID. - A local gateway and either Docker or Podman are available to build the diff --git a/scripts/agents/gator/prompts/gator.md b/scripts/agents/gator/prompts/gator.md index fee69f38d..27cba2a0c 100644 --- a/scripts/agents/gator/prompts/gator.md +++ b/scripts/agents/gator/prompts/gator.md @@ -10,7 +10,7 @@ Load and follow this skill exactly: Important sandbox constraints: -- GitHub REST write access is scoped to NVIDIA/OpenShell and NVIDIA/OpenShell-Community. +- GitHub REST write access is scoped to NVIDIA/OpenShell. - GitHub GraphQL access is read-only except for the named `ResolveGatorReviewThread` mutation used by `resolve-gator-review-threads`. Prefer REST endpoints for all other write diff --git a/scripts/agents/gator/providers/github-gator.yaml b/scripts/agents/gator/providers/github-gator.yaml index d0c1f32d4..5d8e30238 100644 --- a/scripts/agents/gator/providers/github-gator.yaml +++ b/scripts/agents/gator/providers/github-gator.yaml @@ -28,7 +28,6 @@ endpoints: q: any: - "*repo:NVIDIA/OpenShell*" - - "*repo:NVIDIA/OpenShell-Community*" - allow: { method: GET, path: /repos/NVIDIA/OpenShell } - allow: { method: GET, path: /repos/NVIDIA/OpenShell/** } - allow: { method: POST, path: /repos/NVIDIA/OpenShell/issues/*/comments } @@ -43,20 +42,6 @@ endpoints: - allow: { method: POST, path: /repos/NVIDIA/OpenShell/pulls/*/reviews } - allow: { method: POST, path: /repos/NVIDIA/OpenShell/pulls/*/reviews/*/comments } - allow: { method: POST, path: /repos/NVIDIA/OpenShell/statuses/* } - - allow: { method: GET, path: /repos/NVIDIA/OpenShell-Community } - - allow: { method: GET, path: /repos/NVIDIA/OpenShell-Community/** } - - allow: { method: POST, path: /repos/NVIDIA/OpenShell-Community/issues/*/comments } - - allow: { method: PATCH, path: /repos/NVIDIA/OpenShell-Community/issues/comments/* } - - allow: { method: POST, path: /repos/NVIDIA/OpenShell-Community/issues/*/labels } - - allow: { method: PUT, path: /repos/NVIDIA/OpenShell-Community/issues/*/labels } - - allow: { method: DELETE, path: /repos/NVIDIA/OpenShell-Community/issues/*/labels/* } - - allow: { method: PATCH, path: /repos/NVIDIA/OpenShell-Community/issues/* } - - allow: { method: POST, path: /repos/NVIDIA/OpenShell-Community/labels } - - allow: { method: PATCH, path: /repos/NVIDIA/OpenShell-Community/labels/* } - - allow: { method: POST, path: /repos/NVIDIA/OpenShell-Community/pulls/*/comments } - - allow: { method: POST, path: /repos/NVIDIA/OpenShell-Community/pulls/*/reviews } - - allow: { method: POST, path: /repos/NVIDIA/OpenShell-Community/pulls/*/reviews/*/comments } - - allow: { method: POST, path: /repos/NVIDIA/OpenShell-Community/statuses/* } - host: api.github.com port: 443 path: /graphql @@ -79,18 +64,12 @@ endpoints: - allow: { method: GET, path: /NVIDIA/OpenShell.git/** } - allow: { method: POST, path: /NVIDIA/OpenShell/**/git-upload-pack } - allow: { method: POST, path: /NVIDIA/OpenShell.git/**/git-upload-pack } - - allow: { method: GET, path: /NVIDIA/OpenShell-Community } - - allow: { method: GET, path: /NVIDIA/OpenShell-Community/** } - - allow: { method: GET, path: /NVIDIA/OpenShell-Community.git/** } - - allow: { method: POST, path: /NVIDIA/OpenShell-Community/**/git-upload-pack } - - allow: { method: POST, path: /NVIDIA/OpenShell-Community.git/**/git-upload-pack } - host: codeload.github.com port: 443 protocol: rest enforcement: enforce rules: - allow: { method: GET, path: /NVIDIA/OpenShell/** } - - allow: { method: GET, path: /NVIDIA/OpenShell-Community/** } - host: results-receiver.actions.githubusercontent.com port: 443 protocol: rest diff --git a/sdk/go/openshell/v1/integration_test.go b/sdk/go/openshell/v1/integration_test.go index d4c0beeaf..d8e70daf9 100644 --- a/sdk/go/openshell/v1/integration_test.go +++ b/sdk/go/openshell/v1/integration_test.go @@ -44,7 +44,7 @@ func TestIntegration_SandboxExecSmoke(t *testing.T) { image := os.Getenv("OPENSHELL_GO_SDK_TEST_IMAGE") if image == "" { - image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" + image = "nvcr.io/nvidia/base/ubuntu:24.04" } name := fmt.Sprintf("go-smoke-%09d", time.Now().UnixNano()%1_000_000_000) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 52122ccfb..ca7b4020f 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -34,7 +34,7 @@ const client = await OpenShellClient.connect({ }) const sandbox = await client.sandbox.create({ - image: 'ghcr.io/nvidia/openshell-community/sandboxes/python:latest', + image: 'registry.example.com/agents/python:latest', }) await client.sandbox.waitReady(sandbox.name, 120) @@ -186,7 +186,7 @@ const template: SandboxWorkloadTemplate = await client.sandboxTemplates.create( metadata: { name: 'python', labels: { team: 'runtime' } }, spec: { workload: { - image: 'ghcr.io/nvidia/openshell-community/sandboxes/python:latest', + image: 'registry.example.com/agents/python:latest', environment: { FEATURE_FLAG: 'on' }, resources: { cpu: '1', memory: '512Mi' }, }, diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index efdd6aabc..58c56d0c4 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -661,7 +661,7 @@ describe('sandbox templates', () => { metadata: { name: 'python', labels: { team: 'runtime' } }, spec: { workload: { - image: 'ghcr.io/nvidia/openshell-community/sandboxes/python:latest', + image: 'registry.example.com/agents/python:latest', environment: { FEATURE_FLAG: 'on' }, resources: { cpu: '1', memory: '512Mi', gpu: { count: 1 } }, }, diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index 73c0863df..230c443d4 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -171,7 +171,7 @@ Key sections to reference: When middleware is requested, also read the published [supervisor middleware guide](https://docs.nvidia.com/openshell/latest/extensibility/supervisor-middleware.md). -For enforcement concepts and the shipped baseline, read [sandbox policies](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md) and the [default policy reference](https://docs.nvidia.com/openshell/latest/reference/default-policy.md). The default policy is baked into the community base image (`ghcr.io/nvidia/openshell-community/sandboxes/base:latest`). +For enforcement concepts and the shipped baseline, read [sandbox policies](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md) and the [default policy reference](https://docs.nvidia.com/openshell/latest/reference/default-policy.md). The default policy is built into the OpenShell runtime and applies when no explicit policy is supplied. ## Step 4: Choose Policy Shape @@ -636,5 +636,5 @@ private_services: - [Sandbox policies](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md) - [Default policy](https://docs.nvidia.com/openshell/latest/reference/default-policy.md) - [Supervisor middleware](https://docs.nvidia.com/openshell/latest/extensibility/supervisor-middleware.md) -- Default policy: baked into the community base image (`ghcr.io/nvidia/openshell-community/sandboxes/base:latest`) +- Default policy: built into the OpenShell runtime - For translation examples from real API docs, see [examples.md](examples.md) diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 2e4c9c3bd..25095c1e2 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -93,8 +93,8 @@ When supplying `--name`, use a portable DNS-1123 label: at most 63 lowercase alp **Shortcut for known tools**: When the trailing command is a recognized tool, the CLI auto-creates the required provider from local credentials: ```bash -openshell sandbox create -- claude # Auto-creates claude provider -openshell sandbox create -- codex # Auto-creates codex provider +openshell sandbox create --from registry.example.com/your-org/claude-agent:latest -- claude +openshell sandbox create --from registry.example.com/your-org/codex-agent:latest -- codex ``` The agent will be prompted interactively if credentials are missing. @@ -278,7 +278,7 @@ image, environment, sizing, or driver-specific configuration: ```bash openshell sandbox template create gpu-kata \ - --image ghcr.io/nvidia/openshell-community/sandboxes/python:latest \ + --image registry.example.com/agents/python:latest \ --cpu 2 \ --memory 4Gi \ --gpu 1 \ @@ -294,7 +294,7 @@ creates. Put driver config on a template only when it should be reused. ```bash openshell sandbox template create gpu-kata \ - --image ghcr.io/nvidia/openshell-community/sandboxes/python:latest \ + --image registry.example.com/agents/python:latest \ --cpu 2 \ --memory 4Gi \ --gpu 1 \ @@ -472,7 +472,7 @@ Create sandbox with initial policy ### Step 1: Create sandbox with initial policy ```bash -openshell sandbox create --name dev --policy ./initial-policy.yaml -- claude +openshell sandbox create --name dev --from registry.example.com/your-org/claude-agent:latest --policy ./initial-policy.yaml -- claude ``` Sandboxes stay alive by default for iteration. Add `--no-keep` only when the sandbox should be deleted automatically after the initial session. @@ -598,15 +598,14 @@ docker build -t my-app:latest . openshell sandbox create --from my-app:latest --name my-app ``` -The `--from` flag accepts an existing full image reference such as `myregistry.com/img:tag`, or a community sandbox name such as `ollama`. Build local Dockerfiles first with the same container engine as the local gateway, then pass the image tag. +The `--from` flag accepts an explicit OCI image reference such as `myregistry.com/img:tag`. It does not expand catalog aliases. Build local Dockerfiles first with the same container engine as the local gateway, then pass the image tag. -Use `docker build -t my-app:latest` for Docker gateways. For Podman gateways, use `podman build -t localhost/my-app:latest` and pass `localhost/my-app:latest` to `--from`. For remote gateways, push the image to a registry reachable by the gateway. Bare community names resolve under `ghcr.io/nvidia/openshell-community/sandboxes` unless `OPENSHELL_COMMUNITY_REGISTRY` overrides the prefix. +Use `docker build -t my-app:latest` for Docker gateways. For Podman gateways, use `podman build -t localhost/my-app:latest` and pass `localhost/my-app:latest` to `--from`. For remote gateways, push the image to a registry reachable by the gateway. For Docker and Podman gateways, custom images should declare a non-root OCI -`USER`. Each explicit `process.run_as_user` or `process.run_as_group` policy +`USER`. Images without one run as numeric UID and GID `1000`. Each explicit `process.run_as_user` or `process.run_as_group` policy field wins independently; omitted fields fall back to the image declaration. -An image with no `USER` fails before readiness unless policy supplies both -fields. Explicit numeric fields may use any UID/GID from `1` through +Explicit numeric fields may use any UID/GID from `1` through `4294967294`; `0` is root and `4294967295` is the invalid identity sentinel. Warn users that low IDs can inherit permissions from matching accounts, image files, mounted volumes, or devices. diff --git a/tasks/scripts/e2e-gpu-build-images.sh b/tasks/scripts/e2e-gpu-build-images.sh index efe3d8378..d7bebcbc8 100644 --- a/tasks/scripts/e2e-gpu-build-images.sh +++ b/tasks/scripts/e2e-gpu-build-images.sh @@ -12,7 +12,7 @@ source "${SCRIPT_DIR}/container-engine.sh" IMAGES_ROOT="${ROOT}/e2e/gpu/images" BUILD_DIR="${IMAGES_ROOT}/.build" -BASE_IMAGE="${OPENSHELL_SANDBOX_BASE_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}" +BASE_IMAGE="${OPENSHELL_SANDBOX_BASE_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}" CUDA_BUILD_IMAGE="${CUDA_BUILD_IMAGE:-nvcr.io/nvidia/cuda:12.8.1-base-ubuntu22.04}" CUDA_SAMPLES_REPO="${CUDA_SAMPLES_REPO:-https://github.com/NVIDIA/cuda-samples}" CUDA_SAMPLES_REF="${CUDA_SAMPLES_REF:-v12.8}" diff --git a/tasks/scripts/gateway-docker.sh b/tasks/scripts/gateway-docker.sh index 0b3edca72..44bdbf827 100644 --- a/tasks/scripts/gateway-docker.sh +++ b/tasks/scripts/gateway-docker.sh @@ -33,7 +33,7 @@ PORT="${OPENSHELL_SERVER_PORT:-18080}" GATEWAY_NAME="${OPENSHELL_DOCKER_GATEWAY_NAME:-docker-dev}" STATE_DIR="${OPENSHELL_DOCKER_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-docker}" SANDBOX_NAMESPACE="${OPENSHELL_SANDBOX_NAMESPACE:-docker-dev}" -SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}" +SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}" SUPERVISOR_IMAGE="${OPENSHELL_SUPERVISOR_IMAGE:-openshell/supervisor:dev}" SANDBOX_IMAGE_PULL_POLICY="$(normalize_image_pull_policy "${OPENSHELL_SANDBOX_IMAGE_PULL_POLICY:-if_not_present}")" LOG_LEVEL="${OPENSHELL_LOG_LEVEL:-info}" diff --git a/tasks/scripts/gateway-podman.sh b/tasks/scripts/gateway-podman.sh index a45c1d35d..eedd43b15 100644 --- a/tasks/scripts/gateway-podman.sh +++ b/tasks/scripts/gateway-podman.sh @@ -31,7 +31,7 @@ PORT="${OPENSHELL_SERVER_PORT:-18080}" GATEWAY_NAME="${OPENSHELL_PODMAN_GATEWAY_NAME:-podman-dev}" STATE_DIR="${OPENSHELL_PODMAN_GATEWAY_STATE_DIR:-${OPENSHELL_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-podman}}" SANDBOX_NAMESPACE="${OPENSHELL_SANDBOX_NAMESPACE:-podman-dev}" -SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}" +SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}" SANDBOX_IMAGE_PULL_POLICY="$(normalize_image_pull_policy "${OPENSHELL_SANDBOX_IMAGE_PULL_POLICY:-if_not_present}")" GRPC_ENDPOINT="${OPENSHELL_GRPC_ENDPOINT:-}" LOG_LEVEL="${OPENSHELL_LOG_LEVEL:-info}" diff --git a/tasks/scripts/gateway-vm.sh b/tasks/scripts/gateway-vm.sh index ff3f1e7df..d95ba4f2a 100755 --- a/tasks/scripts/gateway-vm.sh +++ b/tasks/scripts/gateway-vm.sh @@ -39,7 +39,7 @@ PORT="${OPENSHELL_SERVER_PORT:-18081}" GATEWAY_NAME="${OPENSHELL_VM_GATEWAY_NAME:-vm-dev}" STATE_DIR="${OPENSHELL_VM_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-vm}" SANDBOX_NAMESPACE="${OPENSHELL_SANDBOX_NAMESPACE:-vm-dev}" -SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-${COMMUNITY_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}}" +SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-${VM_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}}" VM_BOOTSTRAP_IMAGE="${OPENSHELL_VM_BOOTSTRAP_IMAGE:-}" SANDBOX_IMAGE_PULL_POLICY="${OPENSHELL_SANDBOX_IMAGE_PULL_POLICY:-if_not_present}" # VM currently has no image-pull-policy setting in its driver configuration; unlike diff --git a/tasks/scripts/gateway.sh b/tasks/scripts/gateway.sh index 34bc143fb..8df3dace9 100644 --- a/tasks/scripts/gateway.sh +++ b/tasks/scripts/gateway.sh @@ -207,7 +207,7 @@ PORT="${OPENSHELL_SERVER_PORT:-8080}" GATEWAY_NAME="${OPENSHELL_GATEWAY_NAME:-${DRIVER}-dev}" STATE_DIR="${OPENSHELL_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-${DRIVER}}" SANDBOX_NAMESPACE="${OPENSHELL_SANDBOX_NAMESPACE:-${DRIVER}-dev}" -SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}" +SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-nvcr.io/nvidia/base/ubuntu:24.04}" SANDBOX_IMAGE_PULL_POLICY="$(normalize_image_pull_policy "${OPENSHELL_SANDBOX_IMAGE_PULL_POLICY:-if_not_present}")" GRPC_ENDPOINT="${OPENSHELL_GRPC_ENDPOINT:-}" LOG_LEVEL="${OPENSHELL_LOG_LEVEL:-info}" diff --git a/tasks/scripts/helm-k3s-local.sh b/tasks/scripts/helm-k3s-local.sh index dc8adb9bd..e24893e7d 100755 --- a/tasks/scripts/helm-k3s-local.sh +++ b/tasks/scripts/helm-k3s-local.sh @@ -27,9 +27,9 @@ K3D_CLUSTER_NAME_MAX=32 # Host port forwarded to port 80 via the k3d load balancer. # Used by Envoy Gateway's LoadBalancer service (values-gateway.yaml). HOST_LB_PORT="${HELM_K3S_LB_HOST_PORT:-8080}" -# Preload the default community sandbox image so the first sandbox create does +# Preload the default sandbox image so the first sandbox create does # not pay the full registry pull cost inside the cluster. -DEFAULT_SANDBOX_PRELOAD_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +DEFAULT_SANDBOX_PRELOAD_IMAGE="nvcr.io/nvidia/base/ubuntu:24.04" PRELOAD_SANDBOX_IMAGE="${HELM_K3S_PRELOAD_SANDBOX_IMAGE-${DEFAULT_SANDBOX_PRELOAD_IMAGE}}" # Upstream agent-sandbox release pinned for both CRDs/controller and extensions. diff --git a/tests/ansible/roles/openshell_gateway/templates/gateway-docker.toml.j2 b/tests/ansible/roles/openshell_gateway/templates/gateway-docker.toml.j2 index 3e01d1562..eeda19a5d 100644 --- a/tests/ansible/roles/openshell_gateway/templates/gateway-docker.toml.j2 +++ b/tests/ansible/roles/openshell_gateway/templates/gateway-docker.toml.j2 @@ -4,7 +4,7 @@ {% block driver %} [openshell.drivers.docker] socket_path = "{{ openshell_runtime_socket }}" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" sandbox_label = "tmachine" grpc_endpoint = "http://host.openshell.internal:17670" diff --git a/tests/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 b/tests/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 index 61658c1af..61e7c6754 100644 --- a/tests/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 +++ b/tests/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 @@ -4,7 +4,7 @@ {% block driver %} [openshell.drivers.podman] socket_path = "{{ openshell_runtime_socket }}" -default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +default_image = "nvcr.io/nvidia/base/ubuntu:24.04" image_pull_policy = "if_not_present" sandbox_runtime_image = "docker.io/openshell/sandbox:tmachine" supervisor_image = "docker.io/openshell/supervisor:tmachine"