OCI sandbox
Use mikan-managed per-conversation OCI containers on nerdctl, Podman, or Docker, with vault isolation.
# Run mikan with managed per-conversation containers from the image# published for this mikan version (ghcr.io/geminixiang/mikan-sandbox:<version>);# mikan pulls it on startmikan --sandbox=oci /path/to/workspace
# Or any other imagemikan --sandbox=oci:docker.io/library/debian:trixie-slim /path/to/workspaceIf you want to customize the image yourself, you can also build locally:
docker build -f deploy/docker/mikan-sandbox.Dockerfile -t mikan-sandbox:latest .mikan --sandbox=oci:mikan-sandbox:latest /path/to/workspaceThis mode was called image:<image> before; that spelling still works and logs a deprecation warning until the next major release.
Features:
- the standard tool image includes Node.js 24, Chromium, CJK fonts, ffmpeg, and the pinned
agent-browser0.38.1 runtime used byjev_browser - the image installs its tools under
/usr/localand/opt, never under/root;npm i -ganduv tool installfrom inside the sandbox go to/root/.local, which is onPATH; Python packages go through uv (uv run --with <package>), because the system pip refuses installs - mikan creates an isolated vault and container for each conversation
- each container gets its own bridge network, separating direct container-to-container networking; outbound network access remains enabled
- managed containers are created with
--cap-drop=ALL,--security-opt=no-new-privileges, and--pids-limit=1024 - containers use the time zone of the host running mikan (
TZ); the image itself sets none, so it stays UTC elsewhere - inside the container, workspace mounts follow the office’s visibility (ADR 0008): public offices write the shared
MEMORY.mdandskills/, private offices read them, and no office reaches another private office - vault env is injected at execution time
- vault file credentials are automatically bind-mounted into the container, at a target inferred from each file’s name (see Vault)
- idle containers are checked every 10 minutes and stopped after at least 10 minutes of inactivity; depending on scan timing, stopping occurs roughly 10–20 minutes after last tracked use
Container engine
Section titled “Container engine”mikan drives containers through the docker command set, which nerdctl (containerd), Podman, and Docker all accept. You do not choose the engine: with container:* or oci:*, mikan uses the first of nerdctl, podman, and docker whose info succeeds, and logs the choice at startup. Replace docker with that engine in the commands on this page. Containers belong to the engine that created them, so a host that starts another engine later (for example a Podman machine at login) gets new containers, and the old engine’s containers stay behind; set CONTAINER_ENGINE in ~/.mikan/mikan.env to pin one. Rootless Podman works when the host delegates the cgroup CPU and memory controllers to the user, which Podman machine on macOS does.
On macOS every engine runs Linux in a VM, and a bind mount reaches the container only from a folder the VM shares; keep the workspace and ~/.mikan under your home directory.
Upgrading the sandbox image
Section titled “Upgrading the sandbox image”A managed container is disposable. Only its bind mounts (the conversation office, shared knowledge,
and vault files) outlive it; anything written elsewhere in the container, including /root, package
installs, and /etc edits, is discarded when the container is replaced. Keep anything durable in
the workspace.
- Upgrade mikan and restart it. On start mikan pulls its image in the background: with
--sandbox=oci, the image published for the new mikan version; withoci:<image>, the same reference again, so a moved tag such as:latestis refreshed. A failed pull logs a warning and offices keep the local image. - A running container is never interrupted. Once a container has been stopped for idleness, the
next message replaces it from the new image (
docker rm+docker run). - When the last container on an older published image is replaced, mikan removes that image. An image you built or tagged yourself is left alone.
Rollback: run the previous mikan version (or pass --sandbox=oci:<image> with the previous image)
and let containers be replaced again.
Mounts and the conversation office
Section titled “Mounts and the conversation office”The conversation’s office directory is bind-mounted read-write at /workspace/<office-key>, where
the office key is the v1-<platform>-<readable-id>-<hash> segment that also names the directory on
the host. Every office also gets the workspace-global MEMORY.md and skills/ (read-write for a
public office, read-only for a private one) and every other public office read-only under
/workspace/public/<office-key>. Nothing mounts the whole workspace root, and scheduled events stay
host-only.
When the mounts change, for example after a visibility change, the next message replaces the container from the current image.
Vault and container keys
Section titled “Vault and container keys”Credentials are keyed by office key: the vault directory for a conversation is
~/.mikan/vaults/<office-key>/. The key is derived by hashing the platform name together with the
platform’s raw conversation id, so two platforms that happen to use the same raw id can never
resolve each other’s credentials. mikan migrate renames 0.5.3 vault directories, which used the raw
conversation id, to office keys.
The managed container is named mikan-sandbox-<office-key>, and its network
mikan-sandbox-net-<office-key>.
Suitable for:
- multiple users sharing one mikan instance
- per-conversation env/file credential isolation
- managed filesystem projection and stronger isolation than a shared container
Container resource limits
Section titled “Container resource limits”In settings.json, you can configure CPU and memory limits for each managed container:
{ "sandbox": { "cpus": "0.5", "memory": "512m", "boost": { "cpus": "2", "memory": "4g" } }}| Field | Description | Example values |
|---|---|---|
sandbox.cpus | CPU core limit (floating-point string) | "0.5", "2" |
sandbox.memory | Memory limit (Docker memory format) | "512m", "2g" |
sandbox.boost.cpus | Temporary CPU limit applied by /pi-sandbox boost | "2", "4" |
sandbox.boost.memory | Temporary memory limit applied by /pi-sandbox boost | "4g", "8g" |
- when creating a new container, limits are added directly to
docker run - running containers receive new limits immediately through
docker updateon the next provision, without recreation /pi-sandboxshows the current conversation’s effective limits plus its office visibility/pi-sandbox boosttemporarily upgrades the current conversation to thesandbox.boostspec; boost state follows the container and ends when the container stops/pi-sandbox visibility <private|default>narrows a public channel to a private office or clears that override; the container is recreated with the new mounts on the next message and keeps the workspace and vault files- the agent can use the built-in
sandboxtool to inspect or temporarily set the current conversation’s CPU / memory limit; these overrides are also cleared when the container stops
