Files
opencode-devbox/Dockerfile.variant
T
pi 37960186c6
Lint / docs-check (push) Successful in 5s
Lint / hadolint (push) Successful in 12s
Lint / actionlint (push) Has been cancelled
release: v2.9.0 — agent-browser, manifest reader, opencode 1.18.13
Not tagged yet; this is the v2.9.0 changeset landing on main.

Added
- agent-browser + a Playwright-managed headless Chromium in the base (~625 MB
  after deleting the redundant chromium_headless_shell build), so an agent can
  drive a real browser and VERIFY front-end work instead of assuming it renders.
  Ported from pi-devbox. AGENT_BROWSER_EXECUTABLE_PATH points at the stable
  symlink /usr/local/bin/agent-chrome, which the Dockerfile resolves with `find`
  rather than hardcoding: Playwright's browser dir is per-version AND per-arch
  (chrome-linux on arm64, chrome-linux64 on amd64), and the headless shell binary
  is named chrome-headless-shell so `-name chrome` skips it.
- opencode-devbox-version: a reader for the build manifest. The image has baked
  ground truth to /etc/opencode-devbox/build-manifest.json for several releases,
  but nothing read it and nothing printed it — so "which image am I running?"
  meant knowing the path by heart. Three modes (--json/--quiet/human), plus a
  live-vs-baked drift check, because NPM_CONFIG_PREFIX points at the persistent
  config volume and a user `npm install -g opencode` can shadow the baked binary.
  entrypoint-user.sh prints it as its first output.
- ENV COLORTERM=truecolor, completing a true-colour story the image already
  half-shipped (terminfo entries + Neovim termguicolors, but no capability
  advertisement, so bat/delta fell back to 256 colours).
- Smoke assertions for agent-browser, that agent-chrome resolves to an
  executable (catches a Playwright layout change, not just a dangling symlink),
  COLORTERM, the manifest's release_tag, and all three version-command modes.

Changed
- opencode 1.17.20 -> 1.18.13. Verified by diffing upstream source, not release
  notes: core config.ts, config/provider.ts and schema.json are byte-identical,
  so generate-config.py needs no change. 1.18.13 (published mid-audit) was
  re-verified separately — 249 files in the compare payload, under GitHub's
  300-file cap, so the list is complete rather than truncated; content is the
  Electron app plus localisation; the five contract-surface files hash identical
  at both tags. The bg-subagents removal trigger has NOT fired: runtime-flags.ts
  still gates the flag behind OPENCODE_EXPERIMENTAL at all three tags.
- yq: dropped Debian's apt package (the unrelated Python kislyuk/yq — jq syntax,
  3.x line) for mikefarah's Go yq v4 from GitHub. The cloud-init repo's
  provision.sh/deploy.sh need v4 syntax, and THIRD_PARTY.md already credited
  "yq (mikefarah)" while the image shipped the Python one, so this also closes a
  documented-vs-shipped mismatch. Smoke pins the contract to mikefarah v4.
  BEHAVIOUR CHANGE for any in-image script calling yq with jq-style syntax.
- mempalace pin 3.5.0 -> 3.6.0, in lockstep with pi-devbox (5724302). Reviewed
  for MCP tool-schema changes before bumping — none, and nothing touches
  diary_write.
- Default models -> claude-opus-5 (anthropic, and bedrock's
  global.anthropic.claude-opus-5) and openai/gpt-5.6. gpt-5.4 had gone stale:
  gpt-5.6 shipped four days before the v2.8.0 cut. Affects only new containers
  with no OPENCODE_MODEL and no existing config.
- Smoke size thresholds +650 MB (base 2950->3600, omos 3650->4300), sized to
  keep the same ~250 MB headroom so the guardrail still catches runaway growth
  rather than routine apt drift. Do NOT copy pi-devbox's number: it sums
  `docker history`, this repo uses `docker image inspect .Size`.

Documentation
- New README section "Choosing a provider and model", making explicit that the
  baked defaults are only defaults and nobody is locked to Anthropic/Bedrock,
  including the three real gotchas: defaults seed only a NEW config, an existing
  opencode.jsonc on the persistent volume is never rewritten, and switching
  model needs no rebuild.
- New README section "Browser automation (agent-browser)"; opencode-devbox-version
  documented under Build provenance; COLORTERM under Terminal compatibility.
- README Build Args table drift fixed — FOUR missing args added
  (AGENT_BROWSER_VERSION, PLAYWRIGHT_VERSION, YQ_VERSION and GITLEAKS_VERSION,
  the last of which had existed as an ARG but was never listed), plus rows for
  the two pinned args absent entirely (MEMPALACE_VERSION, DEBIAN_VERSION), plus
  a refreshed stale OPENCODE_VERSION example. Third consecutive release to find
  drift in this table.
- AGENTS.md: the stale MemPalace anyOf convention rewritten. It described a perl
  RUN block already DELETED at the 3.5.0 bump and asserted "PyPI latest is 3.4.0
  (== our pin), no release contains the fix yet, the workaround must stay" — all
  three false. Replaced with a pin-review rule. Two new conventions added: the
  agent-browser/Chromium size coupling, and the yq identity trap.
- THIRD_PARTY.md: agent-browser, Playwright, Chromium.

Verified locally with the CI-pinned hadolint 2.14.0 and actionlint 1.7.7, the
shell guard, DOCKER_HUB.md sync, bash -n, py_compile, and by generating the
config for all three providers.
2026-08-04 16:31:13 +02:00

149 lines
7.3 KiB
Docker

# opencode-devbox — variant image
#
# FROMs a base-<hash> image produced by Dockerfile.base and adds only
# the variant-specific tools (opencode, pi, oh-my-opencode-slim, Go).
#
# The two published variants are produced from THIS Dockerfile by
# varying build args:
#
# variant INSTALL_OPENCODE INSTALL_OMOS
# ──────── ──────────────── ────────────
# base true false
# omos true true
#
# pi was removed in v2.0.0 (it had been deprecated since v1.17.2). It now
# ships from its own self-contained image: joakimp/pi-devbox:latest
# (https://gitea.jordbo.se/joakimp/pi-devbox). See docs/CLEANUP-v2.0.0.md
# for the removal history.
#
# Pass `--build-arg BASE_IMAGE=<repo>:base-<hash>` to select the base.
# The CI workflow computes the base hash from Dockerfile.base + rootfs/
# + entrypoint*.sh and feeds it in.
#
# IMPORTANT: the base image sets NPM_CONFIG_PREFIX to
# /home/developer/.config/opencode/npm-global so runtime `npm install -g`
# by the developer user lands on the named volume. At BUILD time we want
# the baked binaries on /usr so they survive the volume mount. Each
# `npm install -g` below therefore prefixes the command with
# `NPM_CONFIG_PREFIX=/usr`.
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
ARG TARGETARCH
ARG USER_NAME=developer
# ── Install opencode via npm ─────────────────────────────────────────
# OPENCODE_VERSION is intentionally pinned in this Dockerfile (not
# 'latest'). It drives the release tag and gets bumped via a source
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
# v0.75.5 cannot apply here.
ARG INSTALL_OPENCODE=true
ARG OPENCODE_VERSION=1.18.13
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
opencode --version ; \
fi
# ── Optional: Go ─────────────────────────────────────────────────────
ARG INSTALL_GO=false
ARG GO_VERSION=latest
RUN if [ "${INSTALL_GO}" = "true" ]; then \
GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
V="${GO_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \
awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \
fi && \
[ -n "$V" ] && \
echo "Installing Go ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \
ln -s /usr/local/go/bin/go /usr/local/bin/go && \
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
fi
# ── Optional: oh-my-opencode-slim (multi-agent orchestration) ────────
# Installs Bun runtime and the oh-my-opencode-slim npm package.
# OMOS_VERSION has a cache-hit footgun when left at the `latest` default
# in registry-cached CI builds: the resulting build-arg string is byte-
# identical across builds, so the layer-hash is identical, so the
# registry buildcache silently reuses the layer from whatever omos
# version was current when the cache was first populated. CI resolves it
# via `npm view oh-my-opencode-slim version` and passes the concrete
# value as a build-arg (see resolve-versions in docker-publish-split.yml).
ARG INSTALL_OMOS=false
ARG OMOS_VERSION=latest
RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
BUN_ARCH="x64-baseline"; \
elif [ "$ARCH" = "aarch64" ]; then \
BUN_ARCH="aarch64"; \
fi && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \
unzip -o /tmp/bun.zip -d /tmp/bun && \
mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \
chmod +x /usr/local/bin/bun && \
ln -sf bun /usr/local/bin/bunx && \
rm -rf /tmp/bun /tmp/bun.zip && \
bun --version && \
test -L /usr/local/bin/bunx && \
NPM_CONFIG_PREFIX=/usr npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
fi
# ── Build provenance: OCI labels + on-disk manifest ──────────────────
# These ARGs are declared LAST, immediately before the layer that uses
# them, so a changing BUILD_DATE / RELEASE_TAG / SOURCE_REVISION never
# invalidates the expensive npm-install layers above. OPENCODE_VERSION,
# OMOS_VERSION and INSTALL_OMOS are already in scope from earlier in this
# stage and need no re-declaration; MEMPALACE_TOOLKIT_REF is consumed in
# Dockerfile.base, so it is re-declared here only to land in the labels.
ARG RELEASE_TAG=dev
ARG BUILD_DATE=
ARG SOURCE_REVISION=
ARG MEMPALACE_TOOLKIT_REF=main
LABEL org.opencontainers.image.version="${RELEASE_TAG}" \
org.opencontainers.image.revision="${SOURCE_REVISION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
se.jordbo.opencode-devbox.opencode-version="${OPENCODE_VERSION}" \
se.jordbo.opencode-devbox.install-omos="${INSTALL_OMOS}" \
se.jordbo.opencode-devbox.omos-version="${OMOS_VERSION}" \
se.jordbo.opencode-devbox.mempalace-toolkit-ref="${MEMPALACE_TOOLKIT_REF}"
# The manifest is written from GROUND TRUTH — the live `opencode --version`,
# the omos package's installed version (when present), and the actual
# checked-out HEAD of /opt/mempalace-toolkit (cloned in the base) — not
# merely the intended build-args. That way it also exposes a dependency
# that silently resolved to something other than the requested value.
# oh-my-opencode-slim is present only in the omos variant (JSON null
# otherwise). NOTE: omos is installed under prefix /usr at build time, so
# we resolve its dir via `npm root -g` with that prefix rather than the
# runtime NPM_CONFIG_PREFIX the base sets for the developer volume.
RUN set -e; \
mkdir -p /etc/opencode-devbox; \
rev() { git -C "$1" rev-parse HEAD 2>/dev/null || echo "unknown"; }; \
OPENCODE_V="$(opencode --version 2>/dev/null | head -n1 | tr -d '\r\n')"; \
OMOS_REV='null'; \
if [ "${INSTALL_OMOS}" = "true" ]; then \
OMOS_DIR="$(NPM_CONFIG_PREFIX=/usr npm root -g 2>/dev/null)/oh-my-opencode-slim"; \
OMOS_V="$(node -e "process.stdout.write(require('${OMOS_DIR}/package.json').version)" 2>/dev/null || echo unknown)"; \
OMOS_REV="\"${OMOS_V}\""; \
fi; \
{ \
echo '{'; \
echo " \"release_tag\": \"${RELEASE_TAG}\","; \
echo " \"build_date\": \"${BUILD_DATE}\","; \
echo " \"source_revision\": \"${SOURCE_REVISION}\","; \
echo " \"opencode_version\": \"${OPENCODE_V}\","; \
echo " \"components\": {"; \
echo " \"opencode\": \"${OPENCODE_V}\","; \
echo " \"oh-my-opencode-slim\": ${OMOS_REV},"; \
echo " \"mempalace-toolkit\": \"$(rev /opt/mempalace-toolkit)\""; \
echo " }"; \
echo '}'; \
} > /etc/opencode-devbox/build-manifest.json; \
echo "── build manifest ──"; cat /etc/opencode-devbox/build-manifest.json
# WORKDIR / ENTRYPOINT / CMD inherited from base.