Skip to content

OCI sandbox

Use mikan-managed per-conversation OCI containers on nerdctl, Podman, or Docker, with vault isolation.

Terminal window
# 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 start
mikan --sandbox=oci /path/to/workspace
# Or any other image
mikan --sandbox=oci:docker.io/library/debian:trixie-slim /path/to/workspace

If you want to customize the image yourself, you can also build locally:

Terminal window
docker build -f deploy/docker/mikan-sandbox.Dockerfile -t mikan-sandbox:latest .
mikan --sandbox=oci:mikan-sandbox:latest /path/to/workspace

This 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-browser 0.38.1 runtime used by jev_browser
  • the image installs its tools under /usr/local and /opt, never under /root; npm i -g and uv tool install from inside the sandbox go to /root/.local, which is on PATH; 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.md and skills/, 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

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.

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.

  1. 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; with oci:<image>, the same reference again, so a moved tag such as :latest is refreshed. A failed pull logs a warning and offices keep the local image.
  2. 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).
  3. 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.

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.

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

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"
}
}
}
FieldDescriptionExample values
sandbox.cpusCPU core limit (floating-point string)"0.5", "2"
sandbox.memoryMemory limit (Docker memory format)"512m", "2g"
sandbox.boost.cpusTemporary CPU limit applied by /pi-sandbox boost"2", "4"
sandbox.boost.memoryTemporary 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 update on the next provision, without recreation
  • /pi-sandbox shows the current conversation’s effective limits plus its office visibility
  • /pi-sandbox boost temporarily upgrades the current conversation to the sandbox.boost spec; 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 sandbox tool to inspect or temporarily set the current conversation’s CPU / memory limit; these overrides are also cleared when the container stops