micke — a desk for AI agents.
micke is a local-first platform that provides a full computer desktop environment for AI agents. It is named after the IKEA desk brand — a simple, sturdy desk on which your agent can work.
When an AI agent creates a new session via MCP (Model Context Protocol), micke provisions a Linux desktop environment inside a gvx sandbox. The agent can then interact with that desktop: move the mouse, type on the keyboard, launch apps, browse the web, and see the screen.
AI agent ──MCP──> micke daemon ──> gvx sandbox (Xvfb :99 + openbox + AT-SPI
(4 tools) + chromium + x11vnc/noVNC), one per session
│
agent-desktop (AT-SPI a11y tree: snapshot →
act on @ref → wait), driven through
session_shell (a fresh bash per call)
x11vnc/noVNC mirror of the display for humans
- Session create — the agent calls the
session_createMCP tool. - Sandbox spawn — micke runs one foreground
rungv runchild: a real Linux desktop (Xvfb + openbox + AT-SPI, chromium preinstalled). - Interaction — the agent drives everything through
session_shell(a fresh bash per call; cwd/env do not persist):agent-desktopsnapshot → act on@ref→ wait loops, plus any CLI work. gvx has noexec, so the guest runs a tiny TCP shell driver at:7801; the host opens one connection per command throughrungv -p. - Session close — the rungv process tree is killed, leaving nothing behind.
Sessions are not owned by the daemon: if micke crashes or restarts, the sandboxes stay alive (desktops, chromium windows, and human noVNC logins included). The next daemon start adopts them from their per-session pidfiles and a fresh session_shell re-attaches.
- Local-first: everything runs on your machine. No cloud, no telemetry.
- One session, one desk: every
session_createcall gets a fresh, disposable gvx sandbox (MCP transport itself is stateless). - Agent-friendly: tools designed for screen understanding and precise input control.
- Reproducible: the same registry image gives agents the same desk every time.
- One runtime: gvx (
rungv), unprivileged and daemonless. Docker is not a runtime dependency.
Requirements: Linux x86_64 or arm64 (the published image is multi-arch: linux/amd64 + linux/arm64) with gvx (rungv v0.4.x) on PATH, Bun, and the micke image in gvx's cache.
# 1. get the desktop image (or let the daemon pull it on the first create
# while no sessions are live)
rungv pull -cache-dir ~/.cache/micke/gvx ghcr.io/gosuda/micke:latest
# 2. start the daemon (MCP at http://127.0.0.1:7800/mcp + dashboard at /)
bun install
bun startThen create a session through any MCP client. Every created session reports a
novnc_url; when noVNC is up, open it in a browser to watch (or drive) the
live desktop.
# maintainer smoke test: boot the image directly, watch the publish lines,
# open the port forwarded for 6080 (the shell listener is 7801)
rungv run -cache-dir ~/.cache/micke/gvx -pull=never -m 2g -p 7801 -p 6080 ghcr.io/gosuda/micke:latest
# rungv: publishing 127.0.0.1:38271 -> 6080/tcp
# open http://127.0.0.1:38271/vnc.htmlThe desktop inside the sandbox is driven by agent-desktop: it reads and operates apps through the AT-SPI accessibility tree with stable refs, no pixel matching.
When noVNC is up, each micke session mirrors its Xvfb display through
x11vnc on guest port 5900 and serves the noVNC web client through
websockify on guest port 6080. micke publishes 6080 with rungv -p
(kernel-picked host port, loopback-only) and puts the URL in session_create,
ready rows of session_list, and the dashboard. noVNC startup is best-effort:
a session can be ready with a novnc_url whose viewer is down.
- Ports are fixed inside the guest (
5900VNC,6080noVNC); the host port is picked by gvx at launch and recorded in the session pidfile, so it survives a daemon restart. MICKE_VNC_PASSWORD(optional): if set, x11vnc requires classic VNC auth with this password; if unset, it accepts viewers with no password (-nopw) — safe only because the forward is bound to loopback.- noVNC connects back through
ws://127.0.0.1:<host-port>/websockify; gvx forwards plain TCP, so nothing runtime-specific is needed.
Early stage. The runtime is gvx-only (owner decision, 2026-11-02): the
Docker runtime path was removed. Docker survives only as the maintainer/CI
image build tool and the docker save → rungv load ingest fallback —
never as a session runtime.
A supervisor + MCP daemon lives in src/ (Bun + TypeScript; MCP over
Streamable HTTP; exactly four tools: session_create, session_list,
session_close, session_shell — a stateless shell (one fresh bash per call:
cd/export do not persist; pass cwd: for a directory), which is how agents
drive the desktop through agent-desktop).
bun install
bun start # daemon on http://127.0.0.1:7800/ (MCP + dashboard; MICKE_PORT to change)The daemon must be running before any MCP client connects (it does not
auto-restart — run it under a service manager such as systemd or launchd).
Every created session reports a
novnc_url (gvx-picked host port, loopback-only) for live view/intervention,
and the daemon serves a static, no-JS session table at
http://127.0.0.1:7800/ (auto-refreshing; guards: instance lock, free-RAM
floor, serialized image pulls).
A host workspace is always mounted at /workspace: pass an absolute
directory to session_create, or omit it and micke creates
$MICKE_STATE_DIR/workspaces/<session_id> and reports it as workspace.
gvx has no uid mapping, so an explicit workspace must be writable by the guest
(uid 1000); otherwise session_create fails fast with
WORKSPACE_NOT_WRITABLE and the exact chmod -R a+w <path> remedy. The
auto-created workspace is chmod 0777 for that reason. Artifacts (downloads,
dumps, the occasional screenshot) written there are already on the host — no
base64 through the shell socket. Files are kept after session_close; delete
the directory when you are done with it.
Five additions make the shell agent-friendly:
agent-desktopexits non-zero when its JSON envelope saysok:false(shimmed in the image), soset -eand&&are trustworthy.micke-page-text <url>reads a page as small JSON ({title,text,links}) via headless chromium. Use it for content; use the AT-SPI tree for interaction — snapshots of heavy pages truncate (truncated:true) andfindtimes out.micke-stateanswers "is chromium alive / did the click land?" as ONE line of JSON ({alive,title,url,refs}) from the a11y tree, in about two seconds and without writing a file — verify with that instead of a screenshot. Exit 1 = no browser;readydescribes the sandbox, not the browser;screenshotstill succeeds after a browser death.micke-key <combo>delivers one key combo to the chromium window (xdotool key --clearmodifiers, after activating it) and prints one line of JSON. Use it instead ofagent-desktop press, which can SIGSEGV chromium on this image; it proves delivery, not effect — verify withmicke-state.micke-find <substring> [--role <role>]finds a clickable@refon a heavy page with one filtered snapshot (occasionally two or three) and printsref<TAB>role<TAB>name, ready forclick. Prefer it toagent-desktop find, which needs--firstand then just spends its budget.
See docs/server-internals.md for configuration, the shell-channel protocol, and adoption semantics. Tests are described under Testing.
- Driving chromium in a session — required flags,
--arg=passthrough, /dev/shm, and the snapshot → click → verify loop: docs/chromium-flags.md - Pausing so a human can log in over noVNC (the credential goes straight into the desktop, not the transcript): docs/manual-intervention.md
bun test tests/ # fast tier: no sandbox, no rungv, no image (<10 s)
MICKE_E2E=1 bun test tests/ # E2E tier: real image (bun run test:e2e)The fast tier replaces the guest and rungv with tests/fake/ (a real TCP
guest and a real fake-rungv child process), so the wire protocol, supervisor
lifecycle, adoption and dashboard run in seconds. The E2E tier is mainly the
residue that needs the real image: boot smoke, desktop smoke (chromium →
micke-find → click → micke-state), the stateless-shell contract, and the
/workspace writability contract. The instance-lock check also sits in the
gated tier because its 1.5 s retry would dominate the fast budget.
E2E is not part of the edit loop — run it once before pushing, or not at all for docs/test-only changes. If an assertion fails twice, stop and report instead of re-running. A failure must name its step; never add a multi-step mega-test. See AGENTS.md for the full rules.
| Variable | Default | Meaning |
|---|---|---|
MICKE_PORT |
7800 |
Daemon port on 127.0.0.1 — MCP + dashboard (0 = kernel-picked) |
MICKE_IMAGE |
ghcr.io/gosuda/micke:latest |
Default OCI image for session_create |
MICKE_GVX_CACHE |
$XDG_CACHE_HOME/micke/gvx (~/.cache/micke/gvx) |
EROFS layer cache (-cache-dir on rungv image/run calls) |
MICKE_RUNGV |
rungv |
rungv binary path |
MICKE_STATE_DIR |
$XDG_STATE_HOME/micke (~/.local/state/micke) |
Pidfiles + logs + instance lock |
MICKE_SESSION_MEMORY |
2g |
Per-sandbox rungv -m guard |
MICKE_BOOT_TIMEOUT_MS |
30000 |
Wait for the driver's micke: desktop ready line |
MICKE_MIN_FREE_MB |
1024 |
Free-RAM floor for session_create (0 disables) |
MICKE_GUEST_UID |
1000 |
Guest uid used for the /workspace writability check |
MICKE_GUEST_GID |
1000 |
Guest gid used for the /workspace writability check |
MICKE_VNC_PASSWORD |
unset | Optional classic-VNC password passed into the sandbox |
Any MCP client that speaks Streamable HTTP can use micke. For example, in
OpenCode's config (opencode.json):
{
"mcp": {
"servers": {
"micke": {
"type": "remote",
"url": "http://127.0.0.1:7800/mcp",
"oauth": false
}
}
}
}(oauth: false matters for OpenCode: remote servers default to OAuth, and
against a no-auth loopback server the probe fails with a misleading
ECONNREFUSED instead of just connecting — verified 2026-11-02. micke never
emits 401, so there is nothing to authenticate.)
(The daemon binds loopback only; clients and the daemon must share the host.
Adjust the port if you set MICKE_PORT.)
Users never build images. CI builds and pushes
ghcr.io/gosuda/micke on every image/** change to main
(.github/workflows/image.yml) for linux/amd64 and linux/arm64 on native
GitHub runners (no QEMU); that is the distribution leg. For local
development, Docker can still be used as the build tool:
docker build -t micke:dev -f image/Dockerfile .
docker save micke:dev -o micke.tar
rungv load -cache-dir ~/.cache/micke/gvx -i micke.tarDocker is not involved in running micke.
TBD