Custom container images
mj can run a session in any container image that meets a small contract.
containers/Containerfile.agent-dev is the reference image and satisfies all
of it; start there if you’re building your own. CI publishes this image as
ghcr.io/brokkai/mjolnir/agent-dev, multi-arch for linux/amd64 and
linux/arm64: :latest tracks master, and each release is also published
under its own immutable version tag.
The entrypoint
Section titled “The entrypoint”mj starts a session’s container detached, running sh -c 'trap "" ...; exec sleep infinity'
as its command (the trap makes signals sent to the container’s main process do nothing), and runs every later command — the worker upload, Git, the harness,
and the ACP bridge — with exec against that running container. Your image
needs a POSIX shell and a sleep binary on PATH. It does not need any
particular process supervisor or init system beyond that.
Git and GitHub CLI
Section titled “Git and GitHub CLI”If git or gh is missing, Mjolnir installs both itself the first time a
session needs them, using whichever of apt-get, dnf, yum, or apk it
finds, and configures gh auth git-credential as the HTTPS credential helper
for github.com and gist.github.com. That auto-install needs root or
passwordless sudo inside the container, plus one of those package managers.
If your image runs as a non-root user with no sudo, or uses a different
package manager, bake git and gh into the image yourself. Either way,
gh is what lets HTTPS Git pushes work using the GitHub token Mjolnir syncs into
the session (see Container targets).
Mjolnir gives the harness its Git settings through a worker-owned configuration
file, named by GIT_CONFIG_GLOBAL. That file includes the session user’s own
global configuration first, so image and user settings still apply, and then
points the github.com and gist.github.com credential helpers at the
worker’s gh wrapper. A file is used because some harnesses drop
credential-shaped variable names from the processes they start, which would
deliver only part of an equivalent environment setting.
ACP bridges
Section titled “ACP bridges”For each harness, Mjolnir first looks for an image-baked bridge binary on
PATH: codex-acp, claude-agent-acp, kimi, grok, opencode, mimo, or dsh-acp. If it doesn’t find
one, Codex and Claude Code install Mjolnir’s pinned packages into a leased cache
using npm ci. This requires Node.js 22+ and npm on the target PATH. Codex also
uses this installation when the image’s bridge version differs from Mjolnir’s
pin; Claude accepts the image’s installed bridge. Selection and installation
finish before the worker starts an ACP session. The worker executes the resolved
bridge and provider.
Kimi Code, Grok Build, and OpenCode have no npm bridge:
Mjolnir runs their official installer with curl piped to Bash instead, which
needs both tools in the image. An image-baked opencode must be OpenCode 2,
because each session imports its logins with opencode auth import before it
starts.
MiMo Code can fall back to npx -y @mimo-ai/cli@0.1.15 acp, so an unbaked
image needs Node.js 22, npm, and npx. Bake @mimo-ai/cli@0.1.15 and set
MIMOCODE_DISABLE_AUTOUPDATE=1 for a stable managed runtime.
DSH speaks ACP itself, so dsh-acp is both its bridge and its agent. Without
dsh-acp on PATH, a session falls back to npx -y @brokkai/dsh-acp@0.2.0,
which needs Node.js 22 or newer, npm, and npx; the npm package bundles the
release binaries. To bake it in, run npm install -g @brokkai/dsh-acp@0.2.0
(Node.js 22 or newer), or put the dsh-acp binary from a release archive at
https://github.com/BrokkAi/dsh-acp/releases on PATH, as the reference image
does. The Linux binaries need glibc 2.34 or newer, so musl images such as
Alpine cannot run DSH.
Baking the bridges in, the way the reference image does, avoids that
installation cost. Codex images should install the bridge version pinned by
Mjolnir and set CODEX_PATH to the installed Codex executable so the bridge
runs the selected provider.
Mjolnir-owned worker and bridge commands are started with the environment the
worker discovered, not under a login shell of their own. Images must therefore
expose required tools on their ordinary process PATH; PATH setup that exists
only in a user dotfile is visible only if the image user’s own shell reads it
during the worker’s startup probe. The documented !command surface always runs
the command through bash -lc, so a custom image must also provide bash on
PATH for that surface to work. Those commands intentionally use the session
user’s shell environment.
Muse Code requires both muse-acp on PATH and the native muse executable.
Set MUSE_CLI to the native binary’s absolute path. Install the pinned
@brokkai/muse-acp adapter from npm, or let npx fetch it at launch, and use
scripts/install-muse.py with mj-worker/assets/muse/runtime.json to install
the matching checksum-verified native binary as managed workers do. Copy the
manifest and script into your image build and run the installer with the
manifest path and destination bin directory. It needs Python 3.11 or newer and
curl.
Workspace and Mjolnir’s own files
Section titled “Workspace and Mjolnir’s own files”Sessions work under /workspace. Separately, Mjolnir writes its own session
relay binary and a staged, allowlisted copy of the harness profile
(credentials, config, skills, and similar) under /var/lib/hel/ inside the
container — for example /var/lib/hel/workers/<session-id> and
/var/lib/hel/profiles/<session-id>.
New Podman session containers run as uid and gid 0:0 in the default rootless
user namespace. Container uid 0 maps to the host account running Mjolnir, and
Mjolnir sets HOME=/home/hel when it creates the container to keep Git and
home-based tools on the reference image’s existing home path. Harness profiles
remain staged under /var/lib/hel/profiles/<session>. Uploaded workers and
profiles are owned by the container’s configured user before their permissions
are restricted. Existing containers keep their recorded image user and HOME
when resumed; Podman exec inherits both from the container configuration.
Docker and Apple Container continue to run as the image’s configured user; on
those engines, that user needs write access to /workspace and /var/lib/hel.
Images need chown and stat -c '%u:%g' (GNU coreutils and BusyBox both
provide these). The reference image creates /workspace and /var/lib/hel
before switching to its hel user; rootless Podman’s uid-0 mapping makes these
directories writable to new Podman sessions without changing image-layer
owners.
Browser tests and profiling tools
Section titled “Browser tests and profiling tools”Nothing in the container contract requires either, but the reference image bakes both in for consistent availability. This avoids depending on runtime package installation, which may be unavailable with Docker or Apple Container’s configured image user.
Playwright’s Chromium needs a set of X11, GTK, and font shared libraries that
most base images omit. The reference image installs them with
npx playwright@<version> install-deps chromium, pinned to the same Playwright
version tests/e2e/web/package.json uses, and pre-installs the Chromium
headless shell into PLAYWRIGHT_BROWSERS_PATH=/ms-playwright so a test run
needs no network. Only the shell is baked in, which is what a headless: true
config launches; add npx playwright install chromium yourself if you need
headed Chromium. If your image skips all of this, run
npx playwright install --with-deps chromium inside the session before browser
tests, which needs root or passwordless sudo.
For optional local coverage analysis, the reference image carries the
llvm-tools-preview rustup component, pinned cargo-llvm-cov, and lcov so
genhtml can render lcov output.
The reference image also carries perf (Debian’s linux-perf),
cargo-flamegraph, samply, and heaptrack. perf depends on the
host kernel and runtime policy as well as the image: it needs
kernel.perf_event_paranoid set to 1 or lower on the host, or an environment
that already provides the required capability. Mjolnir has no configuration
key for adding container capabilities or arbitrary runtime arguments.
Resource metrics
Section titled “Resource metrics”mj samples /sys/fs/cgroup (memory.current, memory.max,
memory.swap.current, memory.swap.max, cpu.stat, cpu.max) inside the
container to drive the CPU and memory numbers in the resource pane. This
needs cgroup v2. If those files aren’t readable, the sampling command
fails, and Mjolnir silently drops that failed sample instead of raising an
error — the session keeps running normally, but the resource pane won’t show
current numbers for it. Nothing about running the container or the session
itself depends on cgroup v2 being present.
What you can’t override
Section titled “What you can’t override”The container-template configuration exposes exactly seven keys: image,
platform, cpus, memory, environment, pull_policy, and
workspace_storage. Pull policy applies to Podman, Docker, and Apple container
targets. All three participate in background image downloads and eligible
moving-tag refreshes, including SSH Podman and Docker targets; launch also
evaluates the configured policy. Non-default workspace
storage is Podman-only; Docker and Apple targets reject it.
There’s no configuration key
for arbitrary extra container-runtime arguments. Mjolnir derives the
rest of the run command itself — the generated container name and the
dev.mj.session, dev.mj.managed, and dev.mj.instance ownership labels it uses
to find and recover its own containers later — and validates that nothing can override
those before starting a container.
Example target
Section titled “Example target”[targets.podman]kind = "podman"image = "localhost/mjolnir/agent-dev:latest"# Selects the image platform and the matching mj worker architecture.platform = "linux/amd64"cpus = "8"memory = "32g"
[targets.podman.environment]# Extra container environment variables, merged in at container start.RUSTFLAGS = "-D warnings"Use kind = "docker" under a [targets.docker] table for the same image
contract on Docker.