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 lets you build your own: lightweight, workspace-specific images that contain only what you need — without writing a Containerfile by hand.
The tool assembles the image from a base image and project-specific toolchains. 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.
- Project image — supplied with the required
--fromflag, with the tools your project needs already installed. The builder uses the packages already present in this image; it does not automatically install system tools. - Project-specific toolchains — toolchains and utilities declared as Dev Container Features in
.kaiden/workspace.jsonare installed when--with-workspace-configis used.
Built-in agent installation and configuration have been removed. OCI addon support is tracked in #178.
.kaiden/workspace.json is the per-workspace configuration file, read when --with-workspace-config is passed. The following fields are supported:
| Field | Description | Details |
|---|---|---|
features |
Dev Container Features to install in the image | Dev Container Features |
skills |
||
environment |
||
mcp |
||
mounts |
||
ports |
||
secrets |
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, and OPENSHELL_BUILD_IMAGE_CONFIG are no longer used. .kaiden/workspace.json remains available through --with-workspace-config. Feature install scripts must support the selected image and have the tools they need available in it.
The OpenShell E2E workflow builds an image with this tool, starts an isolated
OpenShell v0.1.2 gateway, and creates a sandbox from the result. It covers
Podman on Linux x86_64 and the VM driver on a self-hosted Apple Silicon Mac.
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 and use no Dev Container Features.
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 required checks
Podman 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 — useful for tracing feature staging and 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.
The tool supports Dev Container Features declared in .kaiden/workspace.json. Pass --with-workspace-config to enable this; without it the file is not read and no features are installed.
{
"features": {
"<feature-ref>": {
"<option>": "<value>"
}
}
}Each key in features is a feature reference; each value is a map of options passed to the feature's install.sh.
| Format | Example | Resolves to |
|---|---|---|
| OCI registry reference | ghcr.io/devcontainers/features/rust:1 |
downloaded from registry |
| Local path | ./my-feature |
.kaiden/my-feature/ |
Local paths are resolved relative to .kaiden/: ./my-feature points to .kaiden/my-feature/.
OCI references without an explicit registry default to ghcr.io. Tags and digests (@sha256:…) are both supported. Direct http:// / https:// tarball URLs are not supported.
{
"features": {
"ghcr.io/devcontainers/features/rust:1": {
"version": "stable",
"profile": "minimal"
},
"./my-feature": {}
}
}With the above, ./my-feature refers to a local feature at .kaiden/my-feature/.
Features are installed in the order defined by each feature's installsAfter field in its devcontainer-feature.json. Within the same dependency level, features are processed in alphabetical order by reference.
When --with-workspace-config is passed, the tool reads .kaiden/workspace.json and:
- Downloads and extracts each OCI feature into a temporary build context directory (
/tmp/openshell-build-image…). - Copies local feature directories into the same build context.
- Passes the build context to
podman build, where each feature is installed via:COPY features/<dir>/ /tmp/feature-install/<dir>/ RUN chmod +x /tmp/feature-install/<dir>/install.sh && \ OPTION="value" /tmp/feature-install/<dir>/install.sh
- Cleans up all feature files from the image with
RUN rm -rf /tmp/feature-installafter all features are installed.
Features run as root so install scripts can write to system paths.
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) |
--with-workspace-config |
Read .kaiden/workspace.json and apply its features |
--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.
With Dev Container Features declared in .kaiden/workspace.json, build the image:
openshell-build-image --runtime podman \
--from myproject:dev \
--with-workspace-config \
myproject:latest