-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile.agents
More file actions
292 lines (273 loc) · 15.2 KB
/
Copy pathDockerfile.agents
File metadata and controls
292 lines (273 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# syntax=docker/dockerfile:1.7
###############################################################################
# Dockerfile.agents — middle layer of the NeoLabHQ sandbox image chain.
#
# Chain: base -> agents -> final (-> universal).
# - Inherits everything from Dockerfile.base (mise + nix + devbox + Homebrew,
# apt top-up list, gh CLI, dvc/yq, vscode user as UID/GID 1000, mise shims
# at /usr/local/share/mise/shims already on PATH).
# - Adds AI coding agents (Claude Code, OpenCode, Gemini CLI, Codex, plus
# hedged placeholders for pi and oh-my-pi), code-intelligence dependencies
# (codemap, gopls, pyright, typescript-language-server), npm-global
# tooling (rust-just, bun), and the docker-mcp CLI plugin baked into the
# vscode user's ~/.docker/cli-plugins so plain `docker run` consumers do
# not pay the install cost on every container start.
#
#
# Architectural note: codemap, gopls, pyright, and typescript-language-server
# are installed HERE (not in the final Dockerfile) because they are
# code-intelligence dependencies of the AI agents themselves — codemap feeds
# agent context; the LSPs are backends the agents call via MCP. Installing
# them at the agents layer keeps that layer self-sufficient for any consumer
# and avoids re-installing heavy Go/npm toolchains in the final image. The
# final Dockerfile only verifies their presence; it does NOT re-install them.
#
# This layer does NOT install configure-claude.sh / statusline.sh /
# install-mcps.sh — those are owned by the final Dockerfile (Step 3).
###############################################################################
ARG BASE_IMAGE=neolabhq/sandbox:base
FROM ${BASE_IMAGE}
###############################################################################
# Re-declare bash+pipefail SHELL.
#
# Per /workspaces/sandbox/.claude/rules/dockerfile-curl-pipe-pipefail.md
# (hadolint DL4006), every Dockerfile that contains a `curl ... | bash`
# pipeline MUST switch the RUN shell to bash with `-o pipefail` BEFORE any
# such line. The SHELL directive does NOT carry across `FROM`, so it must be
# re-declared here even though Dockerfile.base already set it. The default
# `/bin/sh` is dash on Debian, which does not support pipefail and silently
# produces a successful-but-empty layer when curl fails mid-stream.
###############################################################################
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
###############################################################################
# OCI image annotations. Consumers (GHCR UI, `docker inspect`, Renovate,
# Dependabot, supply-chain scanners) read these labels to surface source,
# description, and license. Declared once at the top of the file so they
# stick to the final image even if intermediate layers shuffle around.
###############################################################################
LABEL org.opencontainers.image.source="https://github.com/NeoLabHQ/sandbox"
LABEL org.opencontainers.image.description="NeoLabHQ sandbox: AI coding agents (Claude, OpenCode, Gemini, Codex, pi, oh-my-pi) + LSPs (gopls, pyright, typescript-language-server) + codemap + docker-mcp on top of :base (jdtls ships in :universal alongside the Java runtime)"
LABEL org.opencontainers.image.licenses="MIT"
###############################################################################
# PATH layout for the agents layer.
#
# Why set PATH this early, before any RUN?
# - The Claude Code installer drops binaries into ~/.local/bin (vscode).
# - The OpenCode installer drops the binary under ~/.opencode/bin; we
# symlink it into ~/.local/bin so a single PATH entry covers it too.
# - `go install` puts binaries in $GOBIN — we set it to ~/.local/bin so
# go-installed code-intelligence binaries (gopls, and anything else added
# later via `go install`) land on PATH for the vscode user with no extra
# wiring. Note: codemap is NOT installed via `go install`; it is built
# from a git clone and copied to /usr/local/bin/codemap (system bin,
# already on the inherited PATH) so it is available to every user.
# - docker-mcp lives in the CLI-plugins dir; the Docker CLI discovers it
# via ~/.docker/cli-plugins/, but having the dir on PATH lets users
# invoke the binary directly too.
#
# NPM global prefix is intentionally NOT overridden. The mise-managed Node
# from Dockerfile.base lays out its own per-user prefix under the mise data
# dir (owned by vscode); `npm install -g` writes there and a subsequent
# `mise reshim` exposes the new binaries via the shim directory at
# /usr/local/share/mise/shims (already on the inherited PATH). Overriding
# NPM_CONFIG_PREFIX to a hand-rolled path would bypass mise's reshim
# bookkeeping and leave npm-installed CLIs invisible to non-interactive
# shells that resolve through the shim directory.
#
# Inherited from Dockerfile.base:
# - /usr/local/share/mise/shims (mise-managed runtimes + shims)
# - /home/vscode/.nix-profile/bin (nix-installed user CLIs)
# - /home/linuxbrew/.linuxbrew/bin (Homebrew)
# - /usr/local/bin:/usr/bin:/bin (apt-installed tools)
###############################################################################
ENV GOBIN=/home/vscode/.local/bin \
PATH=/home/vscode/.local/bin:/home/vscode/.docker/cli-plugins:${PATH}
###############################################################################
# Prepare user-writable directories as root so chown is consistent.
#
# ~/.local/bin: lands binaries from Claude Code installer + GOBIN.
# ~/.docker/cli-plugins: lands docker-mcp.
# ~/.cache: scratch dir used by curl-fed installers (Claude Code, OpenCode)
# and `go install` (module cache). Pre-creating with vscode ownership avoids
# root-owned cache files left behind by RUN steps that happen to run as root.
###############################################################################
USER root
RUN mkdir -p \
/home/vscode/.local/bin \
/home/vscode/.docker/cli-plugins \
/home/vscode/.cache \
&& chown -R vscode:vscode \
/home/vscode/.local \
/home/vscode/.docker \
/home/vscode/.cache
###############################################################################
# Build-time apt deps used only for installs in this layer.
#
# - `make` is required to build docker-mcp from source.
# - Everything else (curl, git, ca-certificates, tar, gzip, build-essential,
# go, node, npm) is already provided by Dockerfile.base.
###############################################################################
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends \
make \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
###############################################################################
# AI coding agents (installed as vscode).
#
# Versions are NOT pinned in this Dockerfile per
# /workspaces/sandbox/.claude/rules/research-version-claims.md — each
# installer below resolves the current upstream release at build time, and
# the actually-resolved versions are recorded by the Step 7 verification
# commands ("claude --version" etc.) and surfaced in the CI build summary.
#
# - Claude Code: official curl installer (the legacy
# `npm install -g @anthropic-ai/claude-code` is deprecated). Installs to
# ~/.local/bin/claude.
# - OpenCode: official curl installer. Installs to ~/.opencode/bin; we
# symlink the binary into ~/.local/bin so a single PATH entry covers all
# four agents.
# - Gemini CLI: npm global. Requires Node 20+ (satisfied by mise's node@lts
# from Dockerfile.base).
# - Codex CLI: npm global.
#
# Both curl|bash lines below rely on the bash+pipefail SHELL declared at the
# top of this file — without it, a partial-body curl would silently produce
# an empty install.
###############################################################################
USER vscode
RUN curl -fsSL https://claude.ai/install.sh | bash
# Set color mode for claude code, resolve issues caused by opening container terminal
ENV COLORTERM=truecolor
RUN curl -fsSL https://opencode.ai/install | bash \
&& if [ -x /home/vscode/.opencode/bin/opencode ]; then \
ln -sf /home/vscode/.opencode/bin/opencode /home/vscode/.local/bin/opencode; \
fi
###############################################################################
# npm globals (run as vscode — the mise-managed npm prefix is per-user; if
# this ran as root the binaries would land in root's prefix and be invisible
# to the vscode user at runtime, per the spec's explicit warning).
#
# Bundled here:
# - @google/gemini-cli, @openai/codex — required AI agents.
# - typescript-language-server, typescript — TypeScript LSP (used by agents
# via MCP / editor integrations).
# - pyright — Python LSP.
# - rust-just — `just` task runner (preserved
# from legacy .devcontainer/
# Dockerfile line 103).
# - bun — alternative JS runtime /
# package manager (preserved
# from legacy .devcontainer/
# Dockerfile line 103).
#
# `mise reshim` is invoked after the install so the new npm-global bin/* land
# as shims under /usr/local/share/mise/shims (which is on the inherited PATH).
# Without reshim, the binaries exist under the mise data dir but the shim
# directory does not see them and non-interactive shells cannot resolve them.
###############################################################################
RUN npm install -g \
@google/gemini-cli \
@openai/codex \
typescript-language-server \
typescript \
pyright \
rust-just \
bun \
&& npm cache clean --force \
&& mise reshim
###############################################################################
# codemap — Go binary that feeds project structure context to AI agents.
#
# Built as the vscode user using the mise-managed Go from Dockerfile.base.
# The spec (Step 2) requires the explicit shim path /usr/local/share/mise/
# shims/go rather than a bare `go` invocation, because the bare invocation
# depends on PATH ordering (we want to anchor on the mise toolchain even if
# something later prepends a different Go to PATH).
#
# Building as root risks mise refusing to operate when the data dir is owned
# by a different user, so the clone + build run under vscode. We avoid `cd`
# (DL3003) by passing `go build -C <dir>` (Go 1.21+ requires `-C` to be the
# first flag) which builds inside the clone without changing the RUN's working
# directory.
#
# Placing the binary system-wide into /usr/local/bin (matches the legacy
# .devcontainer/Dockerfile layout — codemap is available to every user)
# requires root. Rather than `sudo install` (DL3004 — sudo in RUN is an
# error-severity rule), we split into two RUNs: the vscode build above, then a
# `USER root` step that `install`s the binary and removes the /tmp build dir.
# Tradeoff: the intermediate vscode layer still contains /tmp/codemap, so the
# clone/build artifacts live in an earlier layer even though the final
# filesystem is clean — acceptable here (small Go module, not secret-bearing).
###############################################################################
RUN git clone --depth 1 https://github.com/JordanCoin/codemap.git /tmp/codemap \
&& /usr/local/share/mise/shims/go build -C /tmp/codemap -o /tmp/codemap/codemap .
USER root
RUN install -m 0755 /tmp/codemap/codemap /usr/local/bin/codemap \
&& rm -rf /tmp/codemap
USER vscode
###############################################################################
# gopls — Go language server.
#
# Installed via `go install` as vscode; GOBIN (set via ENV above) routes the
# resulting binary into /home/vscode/.local/bin so it is on PATH for the
# vscode user without any extra wiring. Using `go install` (rather than apt
# or brew) is the upstream-recommended install for gopls.
#
# Version is NOT pinned — `@latest` resolves at build time per the version-
# claims rule.
###############################################################################
RUN /usr/local/share/mise/shims/go install golang.org/x/tools/gopls@latest
###############################################################################
# docker-mcp CLI plugin — baked into the published image so plain `docker run`
# consumers do not pay the ~30-60s install cost on every container start.
#
# Built as vscode (matching the upstream Makefile's expectations and so that
# the mise-managed `go` toolchain — whose data dir is owned by vscode under
# /usr/local/share/mise — runs as its owner; building as root would trigger
# mise's "wrong owner" refusal).
#
# IMPORTANT — registration is intentionally deferred to runtime. The spec
# explicitly prohibits running `docker mcp server add` at build time because
# the registration requires the host docker socket and runtime environment
# variables (DOCKER_MCP_SERVER, DOCKER_MCP_CATALOG_DIR) that are unavailable
# during image build. The final Dockerfile's entrypoint.sh (Step 3) reads
# DOCKER_MCP_SERVER at container start and performs the registration there.
# This layer only bakes the binary into /home/vscode/.docker/cli-plugins/
# docker-mcp so the Docker CLI plugin discovery mechanism finds it
# immediately on first `docker mcp ...` invocation.
#
# The /tmp/mcp-gateway scratch dir is cloned as vscode (so subsequent rm does
# not need sudo) and removed at the end of the same RUN to keep the layer
# small.
###############################################################################
USER vscode
RUN set -eux; \
git clone --depth 1 https://github.com/docker/mcp-gateway.git /tmp/mcp-gateway; \
HOME=/home/vscode DOCKER_MCP_CLI_PLUGIN_DST=/home/vscode/.docker/cli-plugins/docker-mcp \
make -C /tmp/mcp-gateway docker-mcp; \
rm -rf /tmp/mcp-gateway
###############################################################################
# Belt-and-braces ownership fix for ~/.docker.
#
# The Makefile above lands the plugin in $HOME/.docker/cli-plugins/docker-mcp
# while running as vscode, so ownership is *already* correct in practice (and
# the pre-chown at the top of this file pre-creates the dir). We still
# perform an explicit `chown -R vscode:vscode /home/vscode/.docker` here
# because:
# 1. The task spec (Step 2) requires it as the documented ownership
# guarantee for this layer — downstream consumers should not have to
# inspect the Makefile to know who owns ~/.docker.
# 2. If `make docker-mcp` ever changes to emit files under a sub-path with
# different ownership (e.g. a vendored cache), this catches it.
###############################################################################
USER root
RUN chown -R vscode:vscode /home/vscode/.docker
###############################################################################
# Final user. The agents image hands off to downstream consumers (the final
# Dockerfile, devcontainer, plain `docker run`) as the non-root vscode user
# — matching the base image's contract.
###############################################################################
USER vscode