---
title: "OCI sandbox"
description: "Use mikan-managed per-conversation OCI containers on nerdctl, Podman, or Docker, with vault isolation."
url: "https://geminixiang.github.io/sandbox/oci/"
---

# OCI sandbox

```bash
# 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:

```bash
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](/sandbox/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

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

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.

## 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

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

In `settings.json`, you can configure CPU and memory limits for each managed container:

```json
{
  "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 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
