FROM coder-devbox-base:latest AS base-default USER root # socat: bridges kilo acp's stdio-only Agent Client Protocol onto a plain TCP # socket for peer-workspace collaboration (see peer-mcp-bridge.py / README's # "Peer workspaces" section) — kilo serve has no MCP server endpoint of its # own, ACP is the only real path to another workspace's live agent session. RUN apt-get update -qq \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends socat \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # ENV NVM_DIR=/home/coder/.nvm ENV NVM_DIR=/usr/local/nvm # Exposes every global npm install (not just a hardcoded allowlist) without # per-binary symlinks — same pattern as dotnet/ruby's flavor Dockerfiles. ENV PATH="$NVM_DIR/versions/node/active/bin:${PATH}" RUN mkdir -p "$NVM_DIR" \ && chown -R coder:coder "$NVM_DIR" USER coder # Install NVM RUN curl -o- "https://raw.githubusercontent.com/nvm-sh/nvm/refs/heads/master/install.sh" | bash # Sanity check that it actually landed where expected RUN test -s "$NVM_DIR/nvm.sh" && echo "nvm.sh found at $NVM_DIR" || (echo "nvm install failed" && exit 1) RUN bash -c "source $NVM_DIR/nvm.sh \ && nvm install 20 \ && nvm alias default 20 \ && nvm use default \ && npm install -g @kilocode/cli \ && npm install -g @openai/codex \ && ln -sfn \$NVM_DIR/versions/node/\$(nvm version default) \$NVM_DIR/versions/node/active" RUN echo "[ Installing Claude Code (native installer) ]" \ && curl -fsSL https://claude.ai/install.sh | bash \ && "$HOME/.local/bin/claude" --version # uv/uvx (Astral's Python package manager) — needed for MiniMax's own MCP # servers (minimax-mcp, minimax-coding-plan-mcp; see the "MiniMax multimodal # MCP servers" section of devenv/README.md for why these Python packages were # picked over the community JS port). Installed here as `coder` (same as the # Claude Code native installer just above) so the binaries land in # $HOME/.local/bin, then copied out to /usr/local/bin below — same # outside-/home/coder reasoning as the claude binary copy further down. RUN echo "[ Installing uv (for MiniMax's official Python MCP servers) ]" \ && curl -LsSf https://astral.sh/uv/install.sh | sh \ && "$HOME/.local/bin/uv" --version # Bake plugins + skills into the image rather than installing them from every # workspace's startup_script: they land under /home/coder/.claude, and since # /home/coder is a per-workspace *named* Docker volume (see devenv/main.tf's # docker_volume.home_volume) that's empty the first time any container mounts # it, Docker copies this image layer's content into it on that first mount — # so every freshly created workspace gets these pre-installed for free, with # zero network calls or plugin-CLI invocations at startup. (Pre-existing # workspaces' volumes are already non-empty and won't pick this up # automatically; those need a one-time manual install.) # # `claude plugin marketplace update` first: the marketplace ships with a # baked-in snapshot that's stale relative to GitHub by the time this image # builds (frontend-design/superpowers/code-review 404'd against it in a real # workspace — the CLI's own error suggested this exact fix), so an explicit # refresh is required before install, `marketplace add` alone isn't enough. # typescript-lsp installed here too (plugin registration only — it needs # typescript-language-server on PATH to actually do anything, installed a # few steps down as root; the plugin CLI doesn't touch/require the LSP # binary at install time, only at use time). Installed here rather than # alongside it because this whole block runs as `coder`, so it lands # correctly under /home/coder/.claude with the right ownership — the # typescript-language-server RUN step below runs as root, and this same # `claude plugin install` there would write root-owned files into a # directory the `coder` user needs to write to at runtime (marketplace # cache updates, enabling/disabling plugins). `superpowers` is left enabled # (no `plugin disable` call) — on by default for every workspace. # # `impeccable` (pbakaus/impeccable) is installed from its own marketplace # here too, not via its CLI installer: as of v4.0.2 it ships a real # `.claude-plugin/marketplace.json` + `plugin/.claude-plugin/plugin.json` # (confirmed against the live repo — this didn't exist when the CLI # installer was first wired up), so it's a genuine first-party-shaped # plugin for Claude Code now, same install shape as the official plugins # above. Marketplace name and plugin name are both `impeccable` (per its # own marketplace.json), hence `impeccable@impeccable`. Codex still gets it # via the separate CLI-installer step further down — it has no # `.codex-plugin/` manifest, so this marketplace only covers Claude Code. # # `context7` (upstash/context7) similarly ships a real # `.claude-plugin/marketplace.json` (marketplace name `context7-marketplace`, # confirmed via GitHub API), bundling a `context7-mcp` skill and a `/docs` # command on top of the context7 MCP server registration devenv/main.tf # already wires up by hand — installing it here is a strict addition, not a # replacement (Claude Code's MCP precedence is CLI-flag config > plugin # config, so the plugin's own duplicate "context7" MCP entry is silently # ignored, no conflict). Purpose: push the agent to actually pull current # library docs before guessing/trial-and-error-scripting an API surface, # rather than relying on the bare MCP entry alone. Note its repo layout # splits Claude's plugin (`plugins/claude/context7`) and Codex's # (`plugins/codex/context7`) into separate directories neither one's # `marketplace.json` cross-references, unlike the shared same-directory # dual-manifest shape claude-plugins-official uses above — so unlike that # marketplace, `codex plugin add` can't discover this one; Codex keeps using # the hand-wired MCP entry only, same as before this change. # # `playwright` needs no separate `marketplace add` — it's published straight # from the same `claude-plugins-official` marketplace already added above. # Confirmed against that marketplace's live marketplace.json: entry `"name": # "playwright", "source": "./external_plugins/playwright"`, whose # `.claude-plugin/plugin.json` + `.mcp.json` (`npx @playwright/mcp@latest`) # duplicate the hand-wired `playwright`/`playwright-visible` MCP entries # devenv/main.tf already renders. CORRECTION (found live on # chris/portal-rework, see devenv/README.md's playwright section): "CLI-flag # config wins over plugin config" only holds when a session is actually # invoked with `--mcp-config` — any invocation that skips it (a headless/ # background job, an IDE-launched session) falls through to this plugin's # broken un-flagged default (real Chrome, never installed) instead. Fixed via # a project-root `.mcp.json` render in main.tf (`claude_project_mcp_config`), # not anything in this Dockerfile. No `.codex-plugin` manifest exists for # playwright (confirmed against the repo, same situation as impeccable), so # Codex keeps using only its own hand-wired entry. # # `ralph-loop`, `playground`, `feature-dev`, `serena`, `figma` — all 5 # confirmed real via a direct fetch of claude-plugins-official's own # marketplace.json (not just trusting search results/subagent summaries, # same verification standard as every plugin above). `ralph-loop`/ # `playground`/`feature-dev` are pure skill/command/agent plugins with no # `.mcp.json` at all (confirmed via each plugin's own directory listing). # Unlike impeccable (whose OWN standalone CLI installer drops plain # SKILL.md content into ~/.agents/skills/, a directory Kilo also scans by # default per its own docs — see devenv/README.md's impeccable section for # the live-confirmed correction), a Claude Code *marketplace plugin*'s # content stays nested under ~/.claude/plugins/cache/..., never flattened # into ~/.claude/skills/ or ~/.agents/skills/ — so there's genuinely nothing # for Kilo's compatibility scan to find here. Claude Code only, no Kilo/ # Codex equivalent exists to wire up, same conclusion as superpowers. # `serena` (semantic code # analysis, `uvx --from git+https://github.com/oraios/serena serena # start-mcp-server` — `uvx` already installed below) and `figma` (Figma's # own remote MCP, `https://mcp.figma.com/mcp`) both do bundle a real MCP # server, hand-wired separately into kilo.jsonc.tftpl and # codex_serena_mcp_toml/codex_figma_mcp_toml in devenv/main.tf for # cross-harness parity — see those files' comments. Figma's remote MCP # authenticates via an interactive OAuth browser flow with no documented # headless alternative; installed anyway for config parity, but not yet # confirmed to actually complete that handshake inside a display-less # container. None of these 5 ship a `.codex-plugin` manifest either # (confirmed against each plugin's directory listing), so `codex plugin add` # can't discover any of them regardless. # # `code-simplifier`, `github`, `claude-security`, `security-guidance` — same # marketplace, same verification standard as above. Two of these ship hooks # worth knowing about before debugging surprising behavior: `claude-security`'s # hook only fires on its own `/claude-security` slash-command menu (a banner, # no side effects), but `security-guidance` genuinely runs on every session # unconditionally (SessionStart/UserPromptSubmit/PostToolUse/Stop) — pattern- # based warnings on every Edit/Write and an LLM-based diff review on Stop and # on `git commit`/`git push`, which can force Claude to keep going (Stop hook # exit code 2) until it addresses findings. `github` bundles its own MCP # server against GitHub's Copilot MCP endpoint, needs the # `github-personal-access-token` Coder secret before it actually authenticates # (see devenv/README.md's "Secrets" section) — installed and enabled either # way, just 401s until that secret exists. None of these 4 ship a # `.codex-plugin` manifest either, Claude Code only. RUN CLAUDE="$HOME/.local/bin/claude" \ && echo "[ Installing Claude Code plugins: frontend-design, superpowers, code-review, typescript-lsp, impeccable, context7, playwright, ralph-loop, playground, feature-dev, serena, figma, code-simplifier, github, claude-security, security-guidance ]" \ && ( "$CLAUDE" plugin marketplace update claude-plugins-official \ || "$CLAUDE" plugin marketplace add anthropics/claude-plugins-official ) \ && "$CLAUDE" plugin install frontend-design@claude-plugins-official \ && "$CLAUDE" plugin install superpowers@claude-plugins-official \ && "$CLAUDE" plugin install code-review@claude-plugins-official \ && "$CLAUDE" plugin install typescript-lsp@claude-plugins-official \ && "$CLAUDE" plugin install playwright@claude-plugins-official \ && "$CLAUDE" plugin install ralph-loop@claude-plugins-official \ && "$CLAUDE" plugin install playground@claude-plugins-official \ && "$CLAUDE" plugin install feature-dev@claude-plugins-official \ && "$CLAUDE" plugin install serena@claude-plugins-official \ && "$CLAUDE" plugin install figma@claude-plugins-official \ && "$CLAUDE" plugin install code-simplifier@claude-plugins-official \ && "$CLAUDE" plugin install github@claude-plugins-official \ && "$CLAUDE" plugin install claude-security@claude-plugins-official \ && "$CLAUDE" plugin install security-guidance@claude-plugins-official \ && "$CLAUDE" plugin marketplace add pbakaus/impeccable \ && "$CLAUDE" plugin install impeccable@impeccable \ && "$CLAUDE" plugin marketplace add upstash/context7 \ && "$CLAUDE" plugin install context7@context7-marketplace # Codex CLI plugins — same claude-plugins-official marketplace as the Claude # Code block above. Its plugins ship a `.codex-plugin/plugin.json` manifest # alongside `.claude-plugin/plugin.json` — confirmed live: `codex plugin add # frontend-design@claude-plugins-official` installs cleanly, and # typescript-lsp's `.codex-plugin/plugin.json` declares a native # `lspServers` entry pointing at the same typescript-language-server this # image installs below — this is a real first-party cross-agent # marketplace, not something scraped or repurposed. Lands under # /home/coder/.codex, the same per-workspace named volume as Claude's # ~/.claude — same bake-into-the-image reasoning as the Claude block above. # `codex` resolves via $NVM_DIR/versions/node/active/bin already on PATH # (see ENV PATH at the top of this file), no full path needed the way # Claude's native installer requires "$HOME/.local/bin/claude". # # No `codex plugin disable` subcommand exists (confirmed against `codex # plugin --help`/`add --help`/`remove --help`) — a plugin's enabled state # lives entirely in config.toml's `[plugins."x@y"].enabled` key, which # devenv/main.tf's codex_config re-renders on every codex_config_version # bump anyway (same enabledPlugins-style fix already applied for Claude # Code), so superpowers' enabled state is set there instead of here (on by # default, same as Claude Code). RUN echo "[ Installing Codex CLI plugins: frontend-design, superpowers, code-review, typescript-lsp ]" \ && ( codex plugin marketplace upgrade claude-plugins-official \ || codex plugin marketplace add anthropics/claude-plugins-official ) \ && codex plugin add frontend-design@claude-plugins-official \ && codex plugin add superpowers@claude-plugins-official \ && codex plugin add code-review@claude-plugins-official \ && codex plugin add typescript-lsp@claude-plugins-official # mcp-builder / web-artifacts-builder: plain SKILL.md directories from the # public anthropics/skills repo, not plugins, so fetched via tarball straight # into ~/.claude/skills/ instead of through the plugin CLI. Same # baked-into-the-image reasoning as the plugins above. RUN mkdir -p "$HOME/.claude/skills" \ && curl -sL "https://github.com/anthropics/skills/archive/refs/heads/main.tar.gz" -o /tmp/anthropic-skills.tar.gz \ && tar -xzf /tmp/anthropic-skills.tar.gz -C "$HOME/.claude/skills" --strip-components=2 \ skills-main/skills/mcp-builder skills-main/skills/web-artifacts-builder \ && rm -f /tmp/anthropic-skills.tar.gz # impeccable (pbakaus/impeccable) for Codex only — its own official CLI # installer, not a plugin marketplace: unlike Claude Code (installed as a # real marketplace plugin above), it still ships no `.codex-plugin/` # manifest (confirmed against the repo), so `codex plugin add` can't see it # the way frontend-design/superpowers/etc. above can; the CLI installer is # the only path for Codex. `--scope=global` writes plain SKILL.md content to # ~/.agents/skills/impeccable (confirmed live — Codex CLI reads skills from # $HOME/.agents/skills globally, per its own docs), landing in the same # per-workspace home volume as everything else in this block, so it's free # on first mount. `--providers=codex` only: Claude Code would also accept # `claude` here, but that'd double-install the skill content the # `impeccable@impeccable` marketplace plugin above already provides # (its plugin.json bundles its own `./.claude/skills/`) — no need for both. # `--no-hooks` skips the optional per-project PostToolUse/Stop design-check # hook (`.codex/hooks.json`) — it's scoped to whatever directory the # installer runs in, which here is the image build context, not any real # future project, and Codex's hook additionally needs an interactive # per-project `/hooks` trust approval that doesn't fit a headless build # step; the skill itself (the `/impeccable` commands) works fully without # it. Package declares `engines.node >= 22.12.0`; this image's default Node # is 20 (see `nvm install 20` above) — confirmed live that this is only an # npm engine warning, not a hard failure, so no separate Node version is # provisioned just for this one build-time install. RUN bash -c "source $NVM_DIR/nvm.sh \ && npx --yes impeccable@latest install --providers=codex --scope=global --yes --no-hooks" USER root # Copy (not symlink — the home copy may get overwritten by auto-update, # self-update, or a stale/older volume) into image-owned territory so a # fresh or stale workspace always has a working claude binary immediately RUN cp "/home/coder/.local/bin/claude" /usr/local/bin/claude \ && chmod +x /usr/local/bin/claude \ && /usr/local/bin/claude --version # Same copy-out reasoning as the claude binary above, for uv/uvx. RUN cp "/home/coder/.local/bin/uv" /usr/local/bin/uv \ && cp "/home/coder/.local/bin/uvx" /usr/local/bin/uvx \ && chmod +x /usr/local/bin/uv /usr/local/bin/uvx \ && /usr/local/bin/uv --version # LSP servers for Kilo's "lsp": true config — bash and typescript are common # enough across every devbox flavor to belong here rather than per-flavor. # (typescript-lsp@claude-plugins-official, Claude Code's equivalent, is # installed above in the USER-coder block, not here — see that comment. # Language-specific "-lsp" plugins for ruby/php/csharp are installed the # same way in their own flavor Dockerfiles instead, since those languages # aren't in every image.) RUN bash -c "source $NVM_DIR/nvm.sh \ && npm install -g bash-language-server typescript-language-server typescript" # Playwright MCP server (for kilo.jsonc's "playwright" MCP entry). Browsers go to # /opt/ms-playwright (not ~/.cache/ms-playwright under /home/coder) since that # home directory is a per-workspace persistent volume that shadows anything # baked into the image after its first-ever creation — see kilo.jsonc.tftpl notes. # # Only @playwright/mcp is installed globally — NOT a separate top-level # `playwright` package. @playwright/mcp pins its own playwright-core version # (currently a pre-release, ahead of whatever `npm install -g playwright` # resolves to "latest"), and the two disagree on browser revision numbers: a # separately-installed playwright downloads a revision @playwright/mcp doesn't # recognize, so the MCP server errors "Browser is not installed" even though a # browser is very much on disk. Installing/installing-deps through # @playwright/mcp's own bundled playwright-core CLI keeps both in lockstep. ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright RUN bash -c "source $NVM_DIR/nvm.sh \ && npm install -g @playwright/mcp \ && PW_CORE_CLI=\$(find \$NVM_DIR/versions/node/active/lib/node_modules/@playwright/mcp -path '*playwright-core/cli.js' | head -1) \ && node \"\$PW_CORE_CLI\" install-deps chromium \ && node \"\$PW_CORE_CLI\" install chromium" \ && chmod -R a+rX /opt/ms-playwright # Chrome DevTools MCP server (Google's official chrome-devtools-mcp, for # kilo.jsonc/claude-mcp.json's "chrome-devtools" MCP entry — DevTools # protocol access: console, network, performance traces, not just # navigate/click/screenshot like Playwright). Same non-home-directory # reasoning as Playwright above: Chrome for Testing goes to # /opt/puppeteer-cache (not ~/.cache/puppeteer under /home/coder), which # chrome-devtools-mcp picks up automatically via PUPPETEER_CACHE_DIR — no # --executablePath needed, confirmed live (a real MCP `initialize` handshake # over stdio against a container built from this exact image, headless # Chrome launched and responded, no --no-sandbox required since it runs as # the unprivileged `coder` user, not root). ENV PUPPETEER_CACHE_DIR=/opt/puppeteer-cache RUN bash -c "source $NVM_DIR/nvm.sh \ && npm install -g chrome-devtools-mcp \ && npx --yes puppeteer browsers install chrome --path /opt/puppeteer-cache" \ && chmod -R a+rX /opt/puppeteer-cache # mcp-telegram (MTProto-based Telegram MCP server) — installed so the binary # is available on PATH, deliberately NOT wired into kilo.jsonc/mcp.json: # using it for real needs a Telegram API ID/hash and an interactive # device-login flow, which doesn't belong baked into a shared image or # secrets file the way an API key does. Whoever wants it configures and adds # the MCP entry themselves, per-workspace, when they actually need it. # Caveat: pulls in `telegram` (gramjs), which upstream has archived in favor # of the `teleproto` fork — fine for "available but unused," worth # re-evaluating if this actually gets configured for real use. RUN bash -c "source $NVM_DIR/nvm.sh && npm install -g mcp-telegram" # MiniMax's official multimodal MCP servers — minimax-mcp (image/video/tts/ # voice-clone/voice-design/music generation) and minimax-coding-plan-mcp # (Token-Plan-exclusive web_search + image understanding), both authoritative # Python packages per MiniMax's own docs (see devenv/README.md's "MiniMax # multimodal MCP servers" section for why these were picked over the # community JS port). UV_TOOL_DIR/UV_TOOL_BIN_DIR keep the installed venvs # and shims outside /home/coder — same reasoning as PLAYWRIGHT_BROWSERS_PATH/ # PUPPETEER_CACHE_DIR above: it's the only way a rebuilt image actually # reaches workspaces whose /home/coder volume already exists. ENV UV_TOOL_DIR=/opt/uv-tools \ UV_TOOL_BIN_DIR=/opt/uv-tools/bin ENV PATH="/opt/uv-tools/bin:${PATH}" RUN uv tool install minimax-mcp \ && uv tool install minimax-coding-plan-mcp \ && chmod -R a+rX /opt/uv-tools # Dokku CLI — a small custom wrapper (not the official dokku/dokku # contrib/dokku_client.sh) that lets any workspace run # `dokku --app ` directly against the Dokku PaaS on this # same host (stacks/harry/dokku/), not just `git push dokku-demo`. The # official client was tried first and rejected: it always shells out to a # literal `ssh` binary, but this environment has no live ssh-agent socket # and no on-disk SSH identity file for it to authenticate with — confirmed # live in a real workspace (`ssh-add -l` and a bare `ssh -T git@github.com` # both fail with no agent/no identity). The one thing that DOES # authenticate here is Coder's own GIT_SSH_COMMAND proxy # (` gitssh --`, injected as an agent-wide env var by # Coder itself, fetching the real per-user key from the Coder control plane # on demand) — that's what `git clone`/`git push dokku-demo` actually use # under the hood, not a plain ssh-agent identity, contrary to what was # originally assumed here. Vendoring the upstream script and patching its # one `ssh` call was considered and rejected too: it's re-fetched fresh from # `master` on every image rebuild, so a sed-based patch could silently stop # matching upstream content on some future rebuild. dokku-cli.sh is a # handful of lines, fully owned, and calls `coder gitssh` directly instead — # confirmed live end-to-end against the real dokku container (`dokku # apps:list` returned real app names through this exact path). # # App auto-detection from a git remote (the official client's main value # beyond plain ssh) wouldn't have worked here regardless: this template's # dokku-demo remote is deliberately written as "dokku-demo:" with no # literal "dokku@" prefix (auth comes from the `User dokku` directive on the # dokku-demo SSH alias instead — see main.tf), which never matches a # `dokku@host:app`-shaped parser. So every invocation needs an explicit # --app either way (the app name is always the workspace's own # lowercased name) — documented in devbox-agents.md. ENV DOKKU_HOST=dokku COPY ./dokku-cli.sh /usr/local/bin/dokku RUN chmod +x /usr/local/bin/dokku # Lives outside /home/coder (like vnc-startup.sh) so it always reflects the # current image, not whatever was baked into a volume on its first-ever # creation. COPY ./kilo/acp-bridge-listen.sh /usr/local/bin/acp-bridge-listen.sh COPY ./kilo/acp-bridge-auth.py /usr/local/bin/acp-bridge-auth.py COPY ./kilo/peer-mcp-bridge.py /usr/local/bin/peer-mcp-bridge.py RUN chmod +x /usr/local/bin/acp-bridge-listen.sh /usr/local/bin/acp-bridge-auth.py /usr/local/bin/peer-mcp-bridge.py # Claude Code's side of the peer bridge — see claude/claude-peer-listen.sh # for why this is a headless `claude -p` runner rather than an ACP dance # (Claude Code has no stdio agent-protocol server the way `kilo acp` does). # Reuses acp-bridge-auth.py unchanged as the token gate for both bridges. COPY ./claude/claude-peer-listen.sh /usr/local/bin/claude-peer-listen.sh COPY ./claude/claude-peer-runner.py /usr/local/bin/claude-peer-runner.py COPY ./claude/peer-mcp-bridge-claude.py /usr/local/bin/peer-mcp-bridge-claude.py RUN chmod +x /usr/local/bin/claude-peer-listen.sh /usr/local/bin/claude-peer-runner.py /usr/local/bin/peer-mcp-bridge-claude.py # Codex CLI's side of the peer bridge — see codex/codex-peer-listen.sh for # why this one EXECs straight into `codex mcp-server` (no runner script # needed, unlike Claude Code's): `codex mcp-server` already speaks # newline-delimited JSON-RPC on stdio, the same shape `kilo acp` uses. # Reuses acp-bridge-auth.py unchanged as the token gate for all three bridges. COPY ./codex/codex-peer-listen.sh /usr/local/bin/codex-peer-listen.sh COPY ./codex/peer-mcp-bridge-codex.py /usr/local/bin/peer-mcp-bridge-codex.py RUN chmod +x /usr/local/bin/codex-peer-listen.sh /usr/local/bin/peer-mcp-bridge-codex.py USER coder