OpenShell is NVIDIA's runtime environment for autonomous AI agents. It provides isolated sandboxes where agents can safely run and iterate — without risk to the host system or your credentials.
OpenShell ships a set of pre-built sandbox images, but they are general-purpose. openshell-build-image builds from an image prepared for your project without requiring you to write a Containerfile by hand.
The tool builds on a project image that already has the tools your project needs. Use --runtime to select what drives the build: a container CLI on the host (podman, docker, or the macOS container CLI), or a microVM (vm) that needs no container runtime installed at all — see Building in a VM.
Built-in agent installation and configuration have been removed. OCI addon support is tracked in #178.
Build an image with a single command:
openshell-build-image --runtime podman --from myproject:dev myimage:latest--from, --runtime, and <TAG> are required. --from selects the project image to extend, --runtime selects the build backend (podman, docker, container, or vm), and <TAG> names the result. There is no default base image.
The three CLI runtimes hand the build to a container engine installed on your machine. --runtime vm instead boots a lightweight Linux microVM with libkrun and runs buildah inside it, so no container engine is needed on the host.
The VM runs its own kernel in its own process namespace and sees only three directories you share with it: the VM's root filesystem, the build context, and the output directory. Nothing else on the host is reachable from the build.
This is the one way --runtime vm differs from the others in its result. A container CLI leaves a tagged image in its local image store. The VM has no access to that store, so it writes a flattened rootfs tarball instead:
openshell-build-image --runtime vm --from ghcr.io/example/myproject:dev myimage:latest
# -> ./myimage-latest.tarThe default filename comes from <TAG>, with : and / replaced by -. Override it with --vm-output. Load the result into an image store yourself, or unpack it:
tar -xf myimage-latest.tar -C /path/to/rootfs--runtime vm works only on macOS with Apple Silicon, because libkrun builds on Apple's Hypervisor.framework. It also needs:
-
libkrun, which the binary links against rather than bundling:
brew tap libkrun/krun && brew trust libkrun/krun brew install libkrun/krun/libkrunIf a build fails with
Couldn't find or load libkrunfw, addexport DYLD_LIBRARY_PATH="$(brew --prefix)/lib". -
A binary built with the
vmfeature and signed for the hypervisor. Theaarch64-apple-darwindownload from a release has both. To build one yourself:make build-vm
The feature is off by default because it links against libkrun, and macOS refuses the hypervisor to an unsigned binary.
make build-vmdoes both, and re-signs on every build, since compiling clears the signature.
The VM's own root filesystem — a Linux tree with buildah in it — comes inside the binary and unpacks itself on first use, into ~/Library/Application Support/openshell-build-image/vm-rootfs.
Only needed to change what the build VM contains. It takes Podman on a linux/arm64 machine:
make build-vm-rootfs # writes vm-rootfs/ and vm-rootfs.tar
make build-vm # embeds the tarball in the binary
make run-vm FROM=ghcr.io/example/myproject:dev TAG=myimage:latestOr point at the directory without embedding anything: --vm-rootfs ./vm-rootfs. make help lists the rest.
buildah uses the vfs storage driver inside the VM — virtio-fs does not support overlayfs — which stores a full copy of every layer rather than a diff. Builds therefore need more memory than the same build under overlayfs. If one fails with an out-of-memory message, raise it:
openshell-build-image \
--runtime vm \
--from ghcr.io/example/myproject:dev \
--vm-cpus 4 \
--vm-memory 8192 \
myimage:latestThe VM has no virtual network card: libkrun forwards its connections to the host, which makes them for real. Nothing supplies the guest a resolv.conf, and a lookup still travels to whatever nameserver the guest is told to use — the host's resolver settings do not apply to it, only the host's routing and firewall rules.
So the build reads the host's own nameservers on each run and hands them to the VM. That is what makes a build work behind a firewall that only allows DNS to the company resolver, and what lets a FROM line reach a registry mirror that only internal DNS knows about.
Override it when the host's resolvers are not the ones the build should use:
openshell-build-image --runtime vm --from ghcr.io/example/myproject:dev --vm-dns 10.0.0.53 --vm-dns 10.0.0.54 myimage:latestA loopback address is rejected: inside the VM, loopback is the VM. If the host resolves through one — systemd-resolved, a VPN client's local stub — pass the address it forwards to instead. When the host has no usable nameserver at all, the VM falls back to 1.1.1.1.
Pass --from <IMAGE> to extend an image prepared for your project. Image names, tags, registry ports, and digests are passed directly to the build engine; there is no list of supported distributions.
openshell-build-image --runtime podman --from localhost:5000/myproject:dev myimage:latest
openshell-build-image --runtime podman --from registry.fedoraproject.org/fedora:latest fedora-project:latest
openshell-build-image --runtime podman --from docker.io/library/alpine:3.24 alpine-project:latestFor a pinned image, use --from registry.example.com/myproject@sha256:<digest> with its full digest. The VM backend pulls the image into its own Buildah store, so it needs a registry reference accessible from the VM rather than an image present only in the host's store.
Base-image configuration files, --config, OPENSHELL_BUILD_IMAGE_CONFIG, and .kaiden/workspace.json are not used by the image builder. Supply the required tools in the image passed to --from.
The OpenShell E2E workflow builds an image with this tool, starts an isolated
OpenShell gateway using the release configured in the
E2E release manifest, and
creates a sandbox from the result. It covers Podman and Docker on Linux x86_64
and the VM driver on a self-hosted Apple Silicon Mac. Each Linux job builds and
launches the image through the same container engine.
Each driver job uses an image matrix covering the digest-pinned Ubuntu-based
buildpack-deps:noble-curl and Alpine-based alpine/curl:8.22.0 images.
Both include curl, a shell, and CA certificates.
Each matrix job installs OpenShell and builds the image builder. Each image
gets a fresh gateway, sandbox and policy state, with separate diagnostics under
target/e2e/<driver>/{ubuntu,alpine}. The test checks
the Containerfile copied into the produced image without starting a container.
It creates a sandbox with an explicit non-root UID/GID of 10001 and checks the
IDs and group memberships, then requires a failed curl to https://example.com/ and a matching
OpenShell policy-denial log. It approves
that destination for curl, waits for the policy to load, and requires the same
request to succeed. DNS, TLS, and timeout failures alone cannot pass the test.
Setup and lifecycle commands live in named shell steps in
the E2E workflow and its shared composite actions: install and verify OpenShell,
build and inspect the image, generate certificates and keys, configure and start
the gateway, register its client certificates, and create the sandbox. The macOS
and Linux jobs select their own dependencies, release assets, builds, driver
settings, and cleanup. They share granular actions for installation, gateway
startup, sandbox creation, and common diagnostics and shutdown.
The VM job keeps the integration approval environment on its self-hosted runner.
E2E and VM Runtime also share the rootfs build workflow.
Diagnostics and cleanup run after each image even when an assertion fails.
The matrix uses fail-fast: false, so a failure on one image does not cancel the
other. VM variants run one at a time to limit resource use on the self-hosted Mac;
matrix scheduling does not guarantee an image order. The result checks
Podman on Linux, Docker on Linux, and VM on macOS each require both image
variants to pass.
See calling these workflows from another repository
for inputs and a complete caller example.
The Rust test connects to the already running sandbox. It checks the OpenShell
version and driver, workload identity, blocked-request evidence, and the approved
request. Ordinary cargo test runs its assertion guards and timeout test without
starting OpenShell. After following the workflow's setup steps locally, export
E2E_DIR, E2E_NAME, E2E_DRIVER, E2E_ARTIFACTS, and E2E_VERSION (the
OpenShell release version installed during setup) and run:
cargo test --locked --test e2e_test network_policy -- --ignored --nocapture --test-threads=1E2E_DIR is the private temporary directory created by the setup step. It contains
the OpenShell binaries, client registration, and osenv wrapper that isolates
OpenShell's configuration from the host. Run the workflow's diagnostic and
cleanup shell steps when finished.
For macOS, install the VM build prerequisites, plus e2fsprogs,
openssl@3, and jq. The self-hosted runner uses labels self-hosted, macos,
arm64 and the existing integration environment with required reviewers. It
must support Hypervisor.framework (sysctl -n kern.hv_support returns 1).
The workflow checks release archive SHA-256 digests and signs the VM driver with
the hypervisor entitlement. The image builder is signed by make build-vm.
make build-vm needs vm-rootfs.tar to embed the build root filesystem. Obtain
it from the CI artifact or build it on Linux arm64.
The workflow unpacks it into the run's temporary directory. The OpenShell VM
driver has its own runtime embedded in the downloaded release; it is separate
from the rootfs used by this image builder.
Diagnostics are written to target/e2e/<driver> and uploaded by CI even on
failure. Certificates and private keys remain outside that directory. Each run
cleans up its sandbox, gateway, images, temporary files, and network.
Use -v (info) or -vv (debug) to increase log verbosity during builds:
openshell-build-image --runtime podman --from myproject:dev -v myimage:latestOpenShell manages CA certificates for the sandbox. The image builder does not discover or copy the host's CA bundle into images.
Pass --copy-containerfile to include the exact Containerfile used for the build at $HOME/Containerfile inside the image, using the home directory and user inherited from the base image.
openshell-build-image --runtime podman --from myproject:dev --copy-containerfile myimage:latest
podman run --rm --entrypoint /bin/sh myimage:latest -c 'cat "$HOME/Containerfile"'openshell-build-image --runtime <RUNTIME> --from <IMAGE> [OPTIONS] <TAG>
| Argument / Option | Description |
|---|---|
<TAG> |
Tag for the built image (e.g. myimage:latest) |
--runtime <RUNTIME> |
Backend to build the image with (podman, docker, container, vm — see Building in a VM) |
--from <IMAGE> |
Required project image to build from (name, tag, or digest) |
--copy-containerfile |
Copy the build Containerfile to $HOME/Containerfile inside the image |
--vm-rootfs <DIR> |
Root filesystem the build VM boots from (--runtime vm only). Defaults to the one embedded in the binary. |
--vm-output <FILE> |
Path for the rootfs tarball produced by --runtime vm. Defaults to a name derived from <TAG> in the current directory. |
--vm-cpus <N> |
vCPUs given to the build VM (--runtime vm only). Default 2. |
--vm-memory <MIB> |
RAM in MiB given to the build VM (--runtime vm only). Default 4096. |
--vm-dns <ADDR> |
Nameserver the build VM resolves through (--runtime vm only). Repeatable. Defaults to the host's own nameservers. |
-v / -vv |
Increase log verbosity (info / debug) |
The five --vm-* options are rejected with any other --runtime, rather than silently ignored.