Runner Image
agents-sandbox builds a Docker image for each sandbox. The image contains the selected coding agent, Node.js, and common CLI tools. Projects can extend the image with their own tooling.
The image is built to rebuild fast and stay lean. Its tools — the base CLI tools, Node, the selected agent, and the
optional Docker engine — live in separate build stages whose layers are reused unchanged, so editing your project’s
Dockerfile body does not re-run the tool installs, and the same tool layers are shared across every project on the machine. Node
is present only for the agent that needs it, and the image build’s downloads need no curl or tar from your base — they
run on the tool base.
Building and managing images
Build the image:
agents-sandbox build # builds if the image is missing or its inputs changed
agents-sandbox build -r # force rebuild, pulling the latest base image
Which builder the engine provides decides which Dockerfile features a project can use; see BuildKit vs the classic builder.
Preview the exact Dockerfile that would be built (without invoking docker):
agents-sandbox build dockerfile # default agent, no docker
agents-sandbox build dockerfile --docker # with the Docker engine block
agents-sandbox build dockerfile --agent-version 1.2.3 # pin the agent version in the preview
List cached images:
agents-sandbox image list
The image name includes the checkout slug and Docker image hash, so changes to the Dockerfile automatically trigger a rebuild. When the resulting image digest differs from the image the existing project VM was booted from, the VM is recreated on the next run so it uses the new image (the home volume is preserved).
Runner images are tagged per agent. The -latest tag is namespaced by the agent, so each agent’s image can be
identified and updated independently. The Docker tag keeps the agents-sandbox/runner- prefix so it stays
distinguishable in the system Docker daemon, while the microsandbox image reference is prefix-free and keyed by the
image content:
Docker tag: agents-sandbox/runner-<slug>:<agent>-latest
microsandbox name ref: <slug>:<agent>-latest
microsandbox content ref: <slug>:<agent>.<contentid>
The content reference is what keeps a project’s variants apart. Its <contentid> is the first 16 characters of the
image’s content ID, so each distinct image content is imported into microsandbox once, under its own reference. A
project’s variants — with and without the Docker engine, a different agent version, an edited project Dockerfile —
therefore coexist in the cache, and switching between them reimports nothing after the first load. The
<slug>:<agent>-latest name reference is the older prefix-free form (a 0.6.0 cache); it is still read, never produced.
For example, with a checkout slug of my-project, the opencode image is tagged
agents-sandbox/runner-my-project:opencode-latest and referenced in microsandbox as
my-project:opencode.9f2c1a7b4d6e0f31; the pi image is tagged agents-sandbox/runner-my-project:pi-latest and
referenced as my-project:pi.4b8d0e6f2a1c3b57.
An image loaded by an earlier version sits under the old prefixed microsandbox reference
(agents-sandbox/runner-<slug>:<agent>-latest). agents-sandbox reuses that reference whenever it still holds the
current content, so an upgrade never reimports an unchanged image; changed content is imported under a new content
reference, silently migrating the project.
Skip-build when unchanged
The Docker build is skipped when the baked org.agents-sandbox.dockerfile-id label matches the current content. This
label is a hash of the rendered Dockerfile, the build-context files it installs (the /etc/profile.d PATH merge
script and the CA-trust helper), and the agent version, so an image already built from the exact same inputs is reused
instead of being rebuilt.
Each tool stage pins its version as an ARG default in the rendered Dockerfile (NODE_VERSION, the agent’s
<AGENT>_VERSION, DOCKER_VERSION). The version is therefore part of the build content, so bumping it changes the
build identity and invalidates that stage’s install layer — the agent layer re-runs on an agent upgrade while the node
and Docker layers stay cached — regardless of how the builder tracks version build arguments.
With --log-level verbose, the launcher reports why a build happens on every rebuild path. Before building, each image
also records its individual inputs as org.agents-sandbox.identity.* labels (project Dockerfile hash, agent version, and
Docker-in-Docker), so the verbose line names the changed input — for example rebuilding runner image
…: project Dockerfile changed; agent version changed — or forced by --rebuild / runner image not present locally.
A reuse logs reusing runner image …: content identity unchanged; an image without these identity labels (built by an
older version) is reported as a generic content change.
The dev user is created with your host user’s UID and GID, so files it writes to /workspace stay owned by you. The
image records them in the org.agents-sandbox.identity.user label, and a run as a user with a different UID or GID
rebuilds the image (host user uid/gid changed). Images built by an older agents-sandbox version are rebuilt once
after the upgrade, since the rendered Dockerfile changed.
The base image is referenced by tag, so it can move. The image records the base it was built from in the
org.agents-sandbox.base label (<ref>@<image ID>), and every run compares it with the base cached in your local
Docker: when they differ, for example after docker pull debian:trixie-slim, the image is rebuilt
(base image updated). This check is local and never contacts a registry. To pick up a newer published base, run
agents-sandbox build -r (or --rebuild), which pulls the base before building. If that pull fails, for example
offline, the build warns and uses the cached base.
Reuse a stale image
Use --no-rebuild on run or shell when a rebuild would be slow or you are offline and the existing image is good
enough. The launcher then uses the project’s existing runner image even when an input changed, and prints the changes it
skipped — for example reusing runner image … despite changes (--no-rebuild): project Dockerfile changed. It also skips
the agent upgrade check, because accepting an upgrade would rebuild the image. A project without a runner image still
gets one built. --no-rebuild can’t be combined with --rebuild or --agent-version, since both only take effect
through a rebuild. The next run without the flag rebuilds as usual.
Content-verified load
Before skipping a load, EnsureLoaded verifies the microsandbox cache content by comparing its config digest against the
Docker image ID. If the cached content no longer matches, the image is reloaded so the VM always boots from the correct
image. A cached image that records no digest is treated as unknown content and reloaded rather than matched by the
dockerfile-id label, which is a source signature that can match while the bytes differ; the reload happens once,
because a load records a digest. At verbose level the load states whether the image was absent from the cache or its
cached content was stale.
The load tries three candidate references and reuses the first that already holds the current content: the new content
reference, the prefix-free name reference (<slug>:<agent>-latest, a 0.6.0 cache), and the legacy prefixed reference
(agents-sandbox/runner-<slug>:<agent>-latest, a pre-0.6 cache). When none matches, the Docker image is loaded under
its content reference. Every following VM creation, label read and log line uses the resolved reference.
Pruning images
Images can be pruned via the prune command or, more targeted, the image prune subcommand:
agents-sandbox image prune --dry-run # see what's stale
agents-sandbox image prune # actually remove them (prompts; or pass --yes)
See Pruning for the confirmation prompt, --yes, and --dry-run behavior.
Pruning retains any image a kept VM still references, the image the current run/shell is about to use, and any
project’s runner-image variants that were used within the prune age. It reclaims the rest — a pruned key’s refs,
aged-out variants, pre-redesign digest refs no sandbox uses, and orphaned surplus images. It also runs microsandbox’s
native image-data prune so manifests and layers orphaned by a runner-image refresh are reclaimed after no VM references
them; because that native prune reclaims any image no sandbox references, a run/shell’s startup auto-prune defers it
to keep the image it is about to load, and only an explicit image prune runs it. See
Commands for details on the prune command.
agents-sandbox also auto-prunes, at the start of run and shell, the resources that are ephemeral, unused or
haven’t been in use for more than 30 days by default (auto-prune-age). Cached runner images and dangling Docker
images are reclaimed only once they are older than the threshold, so a recently used project keeps its image across
restarts. Auto-prune is the safe tier: it never removes a home volume — a project’s home state survives pruning and is
deleted only by volume prune or prune --purge. The Docker-image pass honors the same --older-than threshold and is
previewed by --dry-run. See Sandboxes for the pruning model
and Configuration for how to configure auto-pruning.
Home directory
When rebuilding an image, all state in the VM’s home directory (/home/dev/) would get lost. Therefore, the home
directory is copied to a volume and mounted to the running VM, so it survives a rebuild.
When an image change actually leads to a rebuild, agents-sandbox prompts you to keep, migrate, or reset that volume;
see Home volumes for the choices and the non-interactive default. volume
migrate, volume reset, and volume edit remain available for manual management.
When a rebuilt image changes content, its new content reference does not collide with the previous one, so agents-sandbox refreshes the microsandbox cache by loading the new Docker image under the new reference. It does not remove the old cached manifest first, because a stopped project VM can still reference it. Unused image data is reclaimed later by image pruning.
One image per checkout
agents-sandbox builds a single runner image per checkout. The rendered Dockerfile is composed of per-tool multistage
stages with fixed parents, merged into the final stage with COPY after the user body:
agents-sandbox-base— the embeddeddebian:trixie-slimtools block for a managed base, or your whole custom base (FROMimage) for a custom base. For a managed base (FROM .../runner-base...), the final stage’sFROMis replaced in place with the embedded tools block; for a custom base, any earlier build stages in your project Dockerfile are preserved before it — so multi-stage project Dockerfiles are supported.agents-sandbox-toolbase— the internal stage the node, agent, and docker stages build on: a pinneddebian:trixie-slimpluscurl,ca-certificates, andtar. It is not part of the runner’s lineage, so your base needs none of those binaries and the tool layers cache across bases.agents-sandbox-node(only for agents that need node) — installs Node.js into/opt/agents-sandbox. It is emitted only when the selected agent requires node (today onlypi, an npm-installed node bundle);opencode,opencode2,claude-code,codex,hermes, andoh-my-piinstall native binaries or provision their own runtime, and their images carry no tool-installed node.agents-sandbox-agent(conditional) — installs the selected agent into/opt/agents-sandbox. It starts from the node stage when one was emitted, otherwise from the tool base, and creates/opt/agents-sandbox/binitself. npm agents (pi) install withnpm install -g --prefix /opt/agents-sandbox <pkg>@$VERSION; the rest copy the binary produced by their upstream installer into/opt/agents-sandbox/bin.agents-sandbox-docker(optional) — installs the Docker static binaries into/opt/agents-sandbox/bin.agents-sandbox-runner—FROM agents-sandbox-base, creates thedevuser, runs your Dockerfile body without the tool trees available, thenCOPYs the tool trees in.
Each tool layer caches independently of the user body, the base, and each other. Editing the user body no longer re-runs
the Node/Docker/agent installs, the same tool layers are reused across projects on one machine, and a base that already
provides a tool is left to that base (the runner’s PATH prefers base binaries).
Every tool lands under /opt/agents-sandbox; its binaries are exposed from /opt/agents-sandbox/bin, which the runner
appends to PATH (append, so a base-provided binary keeps precedence). The agent attach and the interactive shell run
/bin/bash -l, and /etc/profile on Debian and most distributions resets PATH, so the appended
/opt/agents-sandbox/bin (and any project ENV PATH entry) would not survive a login shell. To keep the composed PATH
effective there, the image records its composed PATH in the AGENTS_SANDBOX_IMAGE_PATH environment variable and installs
/etc/profile.d/agents-sandbox-path.sh. /etc/profile sources /etc/profile.d/*.sh after its PATH reset, so the script
re-appends any AGENTS_SANDBOX_IMAGE_PATH entry that is missing from the reset PATH (existing entries keep their
position; nothing is duplicated). mkdir -p /opt/agents-sandbox/bin always runs so the COPY source exists. The image
always ends with USER dev and WORKDIR /workspace.
The runner also installs microsandbox’s application certificate trust helper as /usr/local/bin/msb-trust. It is for
applications whose TLS trust is separate from the guest’s system store — a JDK’s cacerts, Chrome/Chromium’s NSS
store — which matters as soon as a secret turns on TLS interception. The helper is Apache-2.0 work, so its license is
installed at /usr/share/licenses/msb-trust/LICENSE. See
Secrets for which applications already work, and
the Java recipe for a worked example.
The runner installs an xdg-open replacement at /usr/local/bin/xdg-open, links x-www-browser and
sensible-browser to it, and sets BROWSER=xdg-open. It hands links to agents-sandbox on the host instead of opening
them in the sandbox. /usr/local/bin comes before /usr/bin on PATH, so it also replaces an xdg-open your base
image installs. See Opening links.
The base image contract
By default the base tools block starts from debian:trixie-slim and installs the recommended CLI tools: git,
ripgrep, jq, yq, curl, wget, xz-utils, file, gawk, less, lz4, moreutils, lsof, strace,
parallel, uuid-runtime, and iptables.
A project Dockerfile whose FROM is any other image is treated as a custom base: your final FROM becomes the base
stage the runner builds on, while the node, agent, and optional docker stages build on the tool base, independent of your
image. For a custom base, the earlier build stages in your project Dockerfile are kept before the base stage. The custom
base must meet these requirements:
- shadow-utils providing
groupaddanduseradd(used to create thedevuser). - A POSIX shell — the
devuser’s login shell is set to the first ofbash,zsh,shfound, falling back to/bin/sh. - A login profile that sources
/etc/profile.d/*.sh— the runner installs/etc/profile.d/agents-sandbox-path.shto restore the composedPATHin login shells, so a base whose profile does not source/etc/profile.dwill not get the mergedPATH. - For Docker only: the runtime prerequisites
iptables,git,ps, andxz, plus what the rootless engine needs:newuidmapandnewgidmap(Debian/Ubuntu packageuidmap, Fedorashadow-utils),slirp4netns, andip(iproute2). These are checked after your Dockerfile body runs, so they may be installed in the body or an earlier stage. If one is missing, the build fails and names the missing package. The engine download itself runs on the tool base, so docker adds nocurlortarrequirement of its own. - For the daemon agents (
opencode,opencode2): the in-VM tool the launcher uses —curl. It reaches the daemon (the health probe, session and worktree calls, and notifications); the daemon is launched and stopped as a runtime-owned job, so no process-control tool is needed.curlis checked after your Dockerfile body runs, so it may be installed in the body or an earlier stage; if it is missing, the build fails and names the tool.pi,claude-code,codex,hermes, andoh-my-pirun no daemon and need none. - For the host opener:
curl. The launcher replays a browser-login callback inside the VM with it, so every custom base needscurleven when the agent runs no daemon (the daemon agents’curlrequirement covers the same tool). It is checked after your Dockerfile body runs, and a missingcurlfails the build naming the tool. See Opening links. - For migrating the home volume:
tar. When the image changes and you choose migrate, the launcher copies your old home volume onto the new one withtarinside the VM, so every custom base needs it. It is checked after your Dockerfile body runs, and a missingtarfails the build naming the tool. - For
hermes: the shared librarylibatomic.so.1. Hermes’s provisioned Node.js links it on glibc, and the agent stage that installs it is discarded, so the base must provide it —libatomic1on Debian/Ubuntu,libatomicon Fedora/RHEL. It is resolved withldconfigafter your Dockerfile body runs, located fromPATHor the standard sbin directories, so it may be installed in the body and a narrowedPATHdoes not hide an installed library, and a missing library fails the build naming it. The check is skipped on musl bases. - The recommended CLI tools listed here are documented for your convenience, not as requirements — as a custom base you install your own.
- A base that already provides docker, node, or the agent is left alone (idempotency). A pre-created
devuser is tolerated, and thedevgroup is guaranteed: the runner creates it when the base or a pre-createddevuser lacks it, so the tool trees can always be handed todevat copy time.
Bases that do not provide these out of the box are not supported without a project Dockerfile that installs
them — for example Alpine (busybox adduser/addgroup, no shadow-utils or bash) and distroless/ scratch
(no shell or package manager).
When a base image already contains the selected agent, the image records the agent as user-provided and
agents-sandbox does not manage its upgrades. This works by PATH precedence, so a base-provided tool must live
outside /opt/agents-sandbox: that prefix is the tool-owned one, and the runner’s tool COPY overwrites any
same-named file a base placed inside it. If that provenance has no recorded version and a later rebuild no longer
contains the agent, the image builder resolves a real release version before installing it; user-provided is
provenance
metadata, not an agent release version. A matching user-provided image is reused without resolving a release first.
User context
The dev user (host UID/GID) is created as the first instruction of the final stage, and the image always ends with
USER dev active. The dev group is created if no base already provides it (including a base that pre-creates a dev
user without a group), and if the host GID is already taken by another group in the base image (e.g. macOS staff/20 vs
dialout), the group is created with the next free GID. Your Dockerfile body runs in the final stage, so it may
switch to USER dev — but earlier build stages in a multi-stage Dockerfile run as root:
FROM debian:trixie-slim
USER root
# Install your project's toolchain as root, e.g. via apt install
RUN apt-get update && apt-get install -y python3 && rm -rf /var/lib/apt/lists/*
ENV configuration
ENV definitions in Dockerfiles are applied to running sandboxes. If you need to configure e.g. PATH, just set
ENV PATH=...: in your Dockerfile. The image’s composed PATH is restored into login shells by the
/etc/profile.d/agents-sandbox-path.sh merge, so a project ENV PATH entry stays effective in shell and in !
commands, not only in non-login agent-executed commands.
Docker-in-Docker
Your project Dockerfile declares the Docker engine on its own. The engine is enabled when the final stage starts FROM
agents-sandbox/runner-base-docker:latest (or the deprecated runner-base-dind) or when the final-stage body carries a
# agents-sandbox:docker comment line. This holds for a custom base and a managed base alike, so a Dockerfile that asks
for Docker needs no launcher config.
Explicit config overrides that declaration in both directions. The --docker flag (on build, run, shell, or
build dockerfile), the docker config key, and the AGENTS_SANDBOX_DOCKER environment variable — or a dind
spelling — turn the engine off even when the Dockerfile asks for it, and on with no Dockerfile signal. The precedence is
explicit config > Dockerfile content > off. A present-but-false value counts as explicit, so docker: false
suppresses a Dockerfile signal rather than letting it re-enable the engine.
Only a marker in the final stage (after the last FROM) enables Docker and places the injection block. A marker in an
earlier stage is ignored, with a warning telling you to move it into the final stage. Enablement is case-sensitive:
# agents-sandbox:docker enables and a case variant such as # Agents-Sandbox:docker does not, though it is reported as
a near-miss warning so a case variant of the marker is caught. A typo inside the token (# agents-sandbox:dockr) is not
caught and leaves the engine off without a diagnostic.
The deprecated aliases --dind, the dind config key, and AGENTS_SANDBOX_DIND still work with lower precedence and
print a one-time warning. A project Dockerfile using the legacy runner-base-dind base or the # agents-sandbox:dind
marker keeps working and enables the engine, but warns on every run that ensures the runner image (including cached
reuse).
The Docker stage installs the engine from a docker static binary tarball; the embedded managed base image installs
fuse-overlayfs and its libfuse3 runtime (the runner copies only /opt/agents-sandbox from the Docker stage, so the
base is where a system package has to live — a custom base must provide both itself, otherwise the driver falls back to
vfs). The storage driver is selected inside the VM at start-up: fuse-overlayfs when the image provides the
binary and /dev/fuse is usable, falling back to vfs otherwise (the microsandbox filesystem cannot back the kernel
overlay2 driver, which is why vfs was the only option before). fuse-overlayfs merges in userspace and exposes its
own FUSE mount, so it copies layers on write instead of copying each one whole, which cuts build time and data-root disk
use. The image never writes /etc/docker/daemon.json, so your edits to it are preserved: if your daemon.json already
sets "storage-driver" to vfs or fuse-overlayfs, agents-sandbox leaves the file alone and starts the engine without
the flag (passing both makes the engine refuse to start); if it sets any other driver, start-up fails with an error
naming the two accepted drivers. The launcher starts the engine (and the agent’s serve daemon) as a microsandbox runtime-owned
detached job inside the VM: startup is confirmed by health probe, and if the job fails the launcher reports its exit code
and a log tail (there is no dockerd.log). buildx and docker compose are not installed — install them in your
project Dockerfile if you need them. The static tarball’s platform is selected by uname -m (x86_64/aarch64).
The engine runs rootless
The Docker engine in the sandbox runs as the dev user, the same user as the agent, not as root. This is called
rootless mode. A container started through it gets no more rights in the sandbox than the agent already has: its
root user is dev, so mounting / into a container does not let it read files only root may read, and
--privileged grants privileges only inside the engine’s own user namespace. Rootless mode keeps the agent from
turning access to Docker into root access inside the sandbox.
The docker command and tools that talk to the Docker API, such as Testcontainers, work unchanged:
/var/run/docker.sock points to the engine’s socket, so no DOCKER_HOST is needed. Some things work differently from a regular Docker host:
- Resource limits are not enforced. Options such as
--memoryand--cpusare accepted but have no effect, because the sandbox has no systemd to hand the engine a cgroup. - Privileged tooling does not work. Anything that needs real root, such as running Docker inside a container or loading kernel modules, fails.
- Container networking runs in user space (
slirp4netns). Published ports (-p 8080:80) are reachable from the sandbox as usual; the engine has nodocker0bridge. - Images and containers live in
/var/lib/agents-sandbox/dockeron the sandbox disk. They are lost when the sandbox is recreated, like before. Images, containers, and volumes created by an earlier agents-sandbox version, which ran the engine as root, are not carried over.
The engine reads your image’s /etc/docker/daemon.json. If it sets "data-root", that directory must be writable by
dev, or the engine fails to start and the launcher shows its log.
Placing the Docker install
With a custom base, a thin COPY --from=agents-sandbox-docker plus an adopt step is appended after your Dockerfile body
by default, so steps after it see docker (the heavy download stays in the cacheable Docker stage). To run your own steps
immediately after the engine is installed, place a # agents-sandbox:docker comment line in the final stage where the
block should be injected (the legacy # agents-sandbox:dind marker is still recognized, but warns on every ensure):
FROM ubuntu:24.04
# The Docker runtime prerequisites are checked after this body runs, so install
# them here. The engine download runs on the tool base, so curl/tar are not
# needed in your image.
RUN apt-get update && apt-get install -y iptables git procps xz-utils uidmap slirp4netns iproute2 && \
rm -rf /var/lib/apt/lists/*
# agents-sandbox:docker
# Steps that need docker, e.g. buildx and docker compose
RUN install -Dm755 /usr/local/bin/docker-buildx /usr/libexec/docker/cli-plugins/docker-buildx
Node and the agent
The agents-sandbox-node stage installs Node.js (v24.22.3, official tarball) into /opt/agents-sandbox, and it is
emitted only when the selected agent requires node — today only pi. The agents-sandbox-agent stage installs the
selected agent into /opt/agents-sandbox. Both build on the tool base, so they do not depend on your base; whether the
base already provides node, the agent, or dockerd is resolved after the user body by the /etc/agents-sandbox/agent-source
and /etc/agents-sandbox/docker-source provenance files (tool | user), and a base-provided binary wins over the tool’s
copy (the runner’s PATH prefers base binaries). The pinned node stays on the v24 line:
the v26 tarballs gained a hard libatomic.so.1 runtime dependency, while v24 has no such dependency on either target
architecture, so a custom base needs no extra library to run the tool-installed node. (The managed base carries
libatomic1 anyway, for hermes.)
Seven agents are built in: opencode, opencode2 (default, the opencode 2 beta), pi, claude-code, codex,
hermes, and oh-my-pi. Only pi needs node: its published package is an npm-installed #!/usr/bin/env node bundle, so it installs
through npm install -g --prefix /opt/agents-sandbox @earendil-works/pi-coding-agent@$PI_VERSION. The other six ship
self-contained native binaries or provision their own runtime — opencode and opencode2 via their upstream
installers https://opencode.ai/install and https://opencode.ai/v2/install, claude-code via
https://claude.ai/install.sh, copying the binary into /opt/agents-sandbox/bin, and codex from the static
codex-package-<arch>-unknown-linux-musl asset of its openai/codex GitHub release, unpacked into
/opt/agents-sandbox/codex (codex runs commands through helpers in that package) and linked into
/opt/agents-sandbox/bin, and oh-my-pi from the standalone omp-linux-<arch> binary of its can1357/oh-my-pi
GitHub release (about 275 MB, Bun and the native addon embedded), checked against the release’s SHA256SUMS.txt
before it is installed as /opt/agents-sandbox/bin/omp; their images contain no node/npm. hermes is the one whose installer needs more than a
binary copy: it clones the pinned release tag into /opt/agents-sandbox/hermes and provisions its own Python (and
Node, for the TUI/web product build) into /opt/agents-sandbox/hermes-home, so the tool stage needs no node of its
own, publishes its launcher into /opt/agents-sandbox/bin, and keeps its tree outside /home/dev — which the sandbox
home volume covers at runtime — handed to the runner’s dev user as it is copied in (ImageSpec.OwnsToolTree:
a COPY --chown=dev:dev emitted for agents that write into their own installation), because Hermes keeps its
dependency ledger and bytecode markers there. The launcher the stage publishes is therefore a small shim: it keeps user state in
$HERMES_HOME (/home/dev/.hermes, so config, sessions, and memories persist on the home volume) and links the tool
store, uv cache, and dependency environments from the image into it, so a fresh home volume starts Hermes without
reinstalling its dependencies. All seven resolve their latest version for an unpinned build — opencode, codex, hermes,
and oh-my-pi via the GitHub API, opencode2 via the opencode update API (/update/api/latest/cli/npm, the installer’s
own source), pi via pi.dev, and claude-code via the npm registry’s latest dist-tag — as a host-side HTTP query,
never inside the image. The GitHub queries authenticate with a GH_TOKEN or GITHUB_TOKEN from the launcher’s
environment when one is set (GH_TOKEN wins), which lifts the anonymous API rate limit; a token GitHub rejects is
retried once without it, so an expired or under-scoped token falls back to the anonymous lookup instead of failing the
build. The token is sent only to api.github.com. JS-based LSP servers are expected to run on the agent’s
embedded JS runtime (Bun) or on node that a project installs in its own Dockerfile.
Labels and provenance
Each runner image carries these labels:
org.agents-sandbox.managed=trueorg.agents-sandbox.agent=<name>org.agents-sandbox.base=<ref>@sha256:<digest>org.agents-sandbox.identity.agent-version=<release>
The agent-version label records the agent release baked into the image. The Upgrades section reads it to compare that
release against the new releases it finds.
Upgrades
The agent version is pinned at image build time and the agent’s runtime autoupdate is disabled per agent, so the agent
binary in a sandbox is stable across runs. The release baked into an image is recorded as the project’s upgrade baseline
on every run and build, whether or not the image was rebuilt. Later runs therefore compare new releases against the
version your image actually holds: an accepted upgrade sticks instead of reverting to the previous release, and a
--agent-version pin survives unpinned runs. The recorded version comes from the image’s agent-version label, which is
read locally and adds no network call to the prompt path.
By default the latest release available at build time is used for agents installed by agents-sandbox. Pin an explicit
version on the build command with --agent-version:
agents-sandbox build --agent-version 0.5.0
agents-sandbox build # uses the latest release
How to upgrade: Rebuild the runner image with agents-sandbox build, or pass --agent-version <version> to pin a
release. On run/shell, when a newer agent release exists than the version baked into the image, the launcher offers to
rebuild the image (interactive) or prints a notice advising agents-sandbox build (non-interactive). The prompt names both
the available release and the version currently baked into this project’s image, so you can tell exactly what a rebuild
would change. The recorded version is only a cache of that label: whenever the image is present, the launcher reconciles
it with the label before deciding whether to build, so a recorded version never pulls the image backwards and an offline
check keeps the existing image instead of rebuilding. Only when the image itself is missing (for example after pruning)
does the recorded version stand on its own, so an unpinned run still sees the version last used and asks before changing
the image. --rebuild forces a clean rebuild but still offers the upgrade; it bypasses the build cache and pulls the base image,
but does not pin the version. When agent-source=user, the tool never checks for upgrades.
--agent-version applies when agents-sandbox installs the selected agent. It does not replace an agent already supplied
by a custom base or project Dockerfile — for a user-provided agent the pin is ignored. If a user-provided agent has no recorded
version, a rebuild that needs to install the missing binary resolves a normal release version rather than passing the internal
provenance value to the installer.
--agent-version is available on build, run, and shell. On run/shell it pins the version baked into the image and
skips the upgrade check.