37960186c6
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.
654 lines
36 KiB
Docker
654 lines
36 KiB
Docker
# opencode-devbox — base image (variant-independent layers)
|
|
#
|
|
# This Dockerfile produces an image tagged base-<hash>, used as the parent
|
|
# for all published variants (base, omos). It contains everything that
|
|
# does not depend on variant-specific build-args (INSTALL_OPENCODE,
|
|
# INSTALL_OMOS). The variant Dockerfile (Dockerfile.variant) FROMs the
|
|
# base and adds only those deltas.
|
|
#
|
|
# The base is rebuilt only when this file or anything it COPYs in
|
|
# changes (rootfs/, entrypoint*.sh). Version bumps to OPENCODE_VERSION,
|
|
# OMOS_VERSION, etc. do NOT trigger a base rebuild.
|
|
#
|
|
# To force a base rebuild for fresh apt packages without other code
|
|
# changes, bump the BASE_REBUILD_DATE comment below. The hash is
|
|
# content-addressed over this file, so any byte change invalidates the
|
|
# cache. Recommended cadence: once per release for security updates.
|
|
#
|
|
# BASE_REBUILD_DATE: 2026-07-13 (v2.7.0 — typst PDF engine + terminal terminfo (ncurses-term/kitty/xterm-ghostty) + nvim true-colour; pi-devbox parity)
|
|
#
|
|
# See the project README's "Build pipeline" section for the rationale.
|
|
|
|
ARG DEBIAN_VERSION=trixie-slim
|
|
FROM debian:${DEBIAN_VERSION} AS base
|
|
|
|
ARG TARGETARCH
|
|
|
|
LABEL maintainer="joakimp"
|
|
LABEL description="opencode-devbox — base image (variant-independent)"
|
|
LABEL org.opencontainers.image.source="https://gitea.jordbo.se/joakimp/opencode-devbox"
|
|
|
|
# Avoid interactive prompts during build
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
# ── Core system packages ─────────────────────────────────────────────
|
|
# apt-get upgrade picks up any security/CVE fixes published between
|
|
# debian:trixie-slim base-image rebuilds. Paired with the index update
|
|
# and the install in the same layer so we don't bloat image history.
|
|
# `nano` is included as a small, non-modal terminal editor for users who
|
|
# don't want vi-style modal editing — a companion to nvim and the `micro`
|
|
# binary installed further down. ~2.8 MB; its deps (libc6, libncursesw6,
|
|
# libtinfo6) are already pulled in by nvim/less/htop/tmux, so it adds no
|
|
# extra packages. EDITOR stays nvim; opt in via `export EDITOR=nano`.
|
|
RUN apt-get update && \
|
|
apt-get upgrade -y --no-install-recommends && \
|
|
apt-get install -y --no-install-recommends \
|
|
ca-certificates \
|
|
curl \
|
|
wget \
|
|
git \
|
|
openssh-client \
|
|
gnupg \
|
|
jq \
|
|
ripgrep \
|
|
fd-find \
|
|
tree \
|
|
less \
|
|
htop \
|
|
tmux \
|
|
make \
|
|
patch \
|
|
diffutils \
|
|
git-crypt \
|
|
age \
|
|
file \
|
|
sudo \
|
|
locales \
|
|
procps \
|
|
unzip \
|
|
gcc \
|
|
g++ \
|
|
rsync \
|
|
python3-pip \
|
|
python3-venv \
|
|
nano \
|
|
pandoc \
|
|
xz-utils \
|
|
graphviz \
|
|
kitty-terminfo \
|
|
ncurses-term \
|
|
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
|
&& apt-get clean \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# ── SSH client defaults: ControlMaster on a writable socket path ──────
|
|
# Why this exists: the devbox typically mounts ~/.ssh from the host as
|
|
# read-only (security: keys are readable, but agents can't tamper with
|
|
# config / known_hosts / authorized_keys / plant a malicious ProxyCommand).
|
|
# OpenSSH's default ControlPath is ~/.ssh/cm/... which is unwritable on
|
|
# such mounts, so any attempt to use ControlMaster fails. Symptoms:
|
|
# unix_listener: cannot bind to path /home/.../.ssh/cm/...: Read-only file system
|
|
# kex_exchange_identification: Connection closed by remote host
|
|
# The latter manifests downstream of CGNAT per-destination flow caps
|
|
# (~4 concurrent flows on most European residential ISPs) which silently
|
|
# drop further SYNs once exceeded — making fresh ssh attempts fail with
|
|
# banner-exchange timeouts that look like a remote problem.
|
|
#
|
|
# Fix: set a system-wide default ControlPath in /tmp (per-container,
|
|
# tmpfs-friendly, always writable) so multiplexing Just Works without
|
|
# touching the read-only ~/.ssh mount. Per-host overrides in user's
|
|
# ~/.ssh/config still win — Debian's default /etc/ssh/ssh_config has
|
|
# `Include /etc/ssh/ssh_config.d/*.conf` *before* the `Host *` block,
|
|
# so user config can override these defaults if desired.
|
|
#
|
|
# ControlPersist=10m means the master socket sticks around 10 min after
|
|
# the last session closes, so consecutive ssh calls in a workflow reuse
|
|
# the same TCP flow. Companion entrypoint-user.sh creates /tmp/sshcm
|
|
# (mode 700) on each container start.
|
|
#
|
|
# CAVEAT (and why dssh/dscp are handled elsewhere): a user per-host override
|
|
# that points ControlPath BACK under the read-only ~/.ssh (e.g. the common
|
|
# CGNAT idiom `ControlPath ~/.ssh/cm/%r@%h:%p`) re-introduces the
|
|
# unwritable-socket failure for a plain `ssh <host>` — a system drop-in here
|
|
# can never override a user's per-host value. For `ssh -F ~/.ssh-local/config`
|
|
# (the dssh/dscp aliases), setup-lan-access.sh redirects ControlPath into the
|
|
# writable ~/.ssh-local sidecar, so those paths are unaffected. See CHANGELOG
|
|
# "Unreleased".
|
|
RUN mkdir -p /etc/ssh/ssh_config.d && \
|
|
printf '%s\n' \
|
|
'# Devbox-baked default. See Dockerfile.base "SSH client defaults".' \
|
|
'# Override per-host in ~/.ssh/config if the master socket location' \
|
|
'# needs to differ.' \
|
|
'Host *' \
|
|
' ControlMaster auto' \
|
|
' ControlPath /tmp/sshcm/%r@%h:%p' \
|
|
' ControlPersist 10m' \
|
|
' ServerAliveInterval 30' \
|
|
' ServerAliveCountMax 6' \
|
|
> /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf && \
|
|
chmod 644 /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf
|
|
|
|
# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
|
|
#
|
|
# Version policy for the binaries below:
|
|
# • Default is `latest` — resolved at build time by following the
|
|
# /releases/latest redirect on GitHub and reading the tag from the
|
|
# Location header. This means every base rebuild picks up the newest
|
|
# upstream release, with no risk of running months-old CVE-affected
|
|
# binaries.
|
|
# • Explicit pins still work: pass `--build-arg GOSU_VERSION=1.19` etc.
|
|
# • Resolved versions are printed during build and re-checked by the
|
|
# smoke test (scripts/smoke-test.sh), so drift is visible in CI logs.
|
|
|
|
# gosu — privilege de-escalation
|
|
ARG GOSU_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${GOSU_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing gosu ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \
|
|
chmod +x /usr/local/bin/gosu && \
|
|
gosu --version
|
|
|
|
# fzf — fuzzy finder
|
|
ARG FZF_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${FZF_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing fzf ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \
|
|
fzf --version
|
|
|
|
# git-lfs — Git Large File Storage
|
|
ARG GIT_LFS_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${GIT_LFS_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing git-lfs ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \
|
|
install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \
|
|
rm -rf /tmp/git-lfs-${V} && \
|
|
git lfs install --system && \
|
|
git-lfs --version
|
|
|
|
# gitleaks — secret scanner (used as a pre-commit hook in several of the
|
|
# repos this devbox is meant to operate on; pairs with git-crypt below).
|
|
# Distributed as a Go-compiled tarball; arch suffix is `x64` (not `x86_64`
|
|
# or `amd64`) on this project — mind the deviation from the surrounding
|
|
# tools' naming.
|
|
ARG GITLEAKS_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x64" ;; arm64) echo "arm64" ;; *) echo "x64" ;; esac) && \
|
|
V="${GITLEAKS_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/gitleaks/gitleaks/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing gitleaks ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/gitleaks/gitleaks/releases/download/v${V}/gitleaks_${V}_linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin gitleaks && \
|
|
chmod +x /usr/local/bin/gitleaks && \
|
|
gitleaks version
|
|
|
|
# neovim — modern text editor
|
|
ARG NVIM_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${NVIM_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing neovim ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \
|
|
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
|
|
nvim --version | head -1
|
|
|
|
# micro — modern, non-modal terminal editor. Ships alongside nvim so users
|
|
# who aren't comfortable with vi-style modal editing have a friendly option:
|
|
# desktop-style keybindings (Ctrl+S save, Ctrl+Q quit, Ctrl+C/V/X, Ctrl+Z
|
|
# undo), mouse support, and syntax highlighting out of the box. A single
|
|
# static Go binary (~12 MB) installed from GitHub releases, exactly like
|
|
# bat/eza/zoxide below. EDITOR stays nvim (see below); users opt in with
|
|
# `export EDITOR=micro` or `git config --global core.editor micro`.
|
|
#
|
|
# NOTE: upstream moved zyedidia/micro -> micro-editor/micro. The old org URL
|
|
# still 302s, but its /releases/latest redirect lands on ANOTHER /latest URL
|
|
# (the org rename), so the tag-parsing idiom below would resolve "latest"
|
|
# instead of a version. Use the canonical micro-editor/micro URL.
|
|
# Arch asset naming differs from the others: amd64 -> linux64, arm64 ->
|
|
# linux-arm64. The tarball extracts to micro-<version>/micro.
|
|
ARG MICRO_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "linux64" ;; arm64) echo "linux-arm64" ;; *) echo "linux64" ;; esac) && \
|
|
V="${MICRO_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing micro ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/download/v${V}/micro-${V}-${ARCH}.tar.gz" | tar -xz -C /tmp && \
|
|
install /tmp/micro-${V}/micro /usr/local/bin/micro && \
|
|
rm -rf /tmp/micro-${V} && \
|
|
micro --version
|
|
|
|
# bat — syntax-highlighted cat replacement
|
|
ARG BAT_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${BAT_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing bat ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
|
install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
|
|
rm -rf /tmp/bat-v${V}-* && \
|
|
bat --version
|
|
|
|
# eza — modern ls replacement
|
|
ARG EZA_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${EZA_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing eza ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \
|
|
eza --version | head -1
|
|
|
|
# zoxide — smarter cd command
|
|
ARG ZOXIDE_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${ZOXIDE_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing zoxide ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \
|
|
zoxide --version
|
|
|
|
# uv — fast Python package manager (replaces pip, venv, pyenv)
|
|
# Note: uv releases don't prefix tags with "v" (e.g. tag is "0.11.8").
|
|
ARG UV_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${UV_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing uv ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
|
install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
|
|
install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \
|
|
rm -rf /tmp/uv-* && \
|
|
uv --version
|
|
|
|
# tealdeer — Rust port of tldr (community-maintained command examples)
|
|
# Provides the `tldr` command; ~5 MB static binary, ~135 MB smaller than
|
|
# the Node tldr global. Same UX as the Node version.
|
|
ARG TEALDEER_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${TEALDEER_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing tealdeer ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/download/v${V}/tealdeer-linux-${ARCH}-musl" -o /usr/local/bin/tldr && \
|
|
chmod +x /usr/local/bin/tldr && \
|
|
tldr --version
|
|
|
|
# ── typst — lightweight PDF engine for pandoc (Markdown→PDF) ─────────
|
|
# pandoc (apt-installed above) is only a front-end; rendering PDF needs a
|
|
# back-end engine. Rather than a ~600 MB TeX Live install, we ship typst:
|
|
# a single ~30 MB static Rust binary with no LaTeX dependency, used via
|
|
# `pandoc --pdf-engine=typst`. A fuller TeX Live remains the higher-fidelity
|
|
# fallback for anyone who needs LaTeX-exact output (install on demand).
|
|
# Ported from pi-devbox (its v1.4.0 + v1.5.0 font-default fix).
|
|
#
|
|
# Follows the `latest` GitHub-release convention (like tealdeer/uv/bat).
|
|
# typst ships a `.tar.xz` asset (hence xz-utils in the apt layer above)
|
|
# that extracts to typst-<arch>-unknown-linux-musl/typst. Pin a specific
|
|
# tag with --build-arg TYPST_VERSION=vX.Y.Z.
|
|
#
|
|
# We also patch pandoc's bundled typst template
|
|
# (/usr/share/pandoc/data/templates/template.typst): its conf() defaults the
|
|
# document font to an empty tuple (`font: ()`), so a naked
|
|
# `pandoc --pdf-engine=typst` fails with "font fallback list must not be empty"
|
|
# unless the caller passes `-V mainfont=...`. We default it to Libertinus Serif
|
|
# (typst's own bundled default font) so PDF export works out-of-the-box.
|
|
ARG TYPST_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${TYPST_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing typst ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/download/v${V}/typst-${ARCH}-unknown-linux-musl.tar.xz" | tar -xJ -C /tmp && \
|
|
install /tmp/typst-${ARCH}-unknown-linux-musl/typst /usr/local/bin/typst && \
|
|
rm -rf /tmp/typst-${ARCH}-unknown-linux-musl && \
|
|
typst --version && \
|
|
sed -i 's/^ font: (),$/ font: ("Libertinus Serif",),/' /usr/share/pandoc/data/templates/template.typst && \
|
|
grep -q 'font: ("Libertinus Serif",),' /usr/share/pandoc/data/templates/template.typst
|
|
|
|
# ── yq (mikefarah) — YAML processor, jq's companion for YAML ─────────
|
|
# Installed as the mikefarah Go binary — NOT Debian's `yq` apt package, which
|
|
# is the unrelated Python kislyuk/yq (a jq wrapper with different syntax and a
|
|
# different version line, 3.x). THIRD_PARTY.md already credited "yq
|
|
# (mikefarah)" while the image actually shipped the Python one, so this closes
|
|
# a documented-vs-shipped mismatch as well as bringing parity with pi-devbox
|
|
# (its v1.2.3). The cloud-init repo's deploy.sh/provision.sh require mikefarah
|
|
# v4 syntax. Follows the repo's `latest` convention (like tealdeer/uv/typst);
|
|
# the smoke test pins the contract to major v4, so both a regression to the
|
|
# Python package and a surprise future yq v5 fail CI loudly instead of
|
|
# silently breaking those scripts. Pin a tag with --build-arg YQ_VERSION=vX.Y.Z.
|
|
ARG YQ_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${YQ_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/mikefarah/yq/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing mikefarah yq ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/mikefarah/yq/releases/download/${V}/yq_linux_${ARCH}" -o /usr/local/bin/yq && \
|
|
chmod +x /usr/local/bin/yq && \
|
|
yq --version
|
|
|
|
# ── MemPalace — local-first AI memory system ─────────────────────────
|
|
# Provides semantic search over conversation history via 29 MCP tools.
|
|
# Always installed in the base (variant-independent). Set
|
|
# INSTALL_MEMPALACE=false at base-build time to shave ~300 MB.
|
|
ARG INSTALL_MEMPALACE=true
|
|
# Pin mempalace explicitly (mirrors pi-devbox). An unpinned
|
|
# `uv tool install mempalace` is what silently swept in the broken
|
|
# diary_write top-level-anyOf schema (3.3.x/3.4.0) that breaks the
|
|
# Anthropic tools API; pinning makes every bump a deliberate, reviewable
|
|
# diff. Bump this in lockstep with pi-devbox's MEMPALACE_VERSION.
|
|
#
|
|
# 3.5.0 (2026-06) ships the upstream fix for that top-level-anyOf schema
|
|
# (issue #1728 / PR #1717, merged 2026-06-14): diary_write now advertises
|
|
# `"required": ["agent_name"]` with entry/content enforced at dispatch, which
|
|
# the Anthropic tools API accepts — so the perl mcp_server.py workaround that
|
|
# used to live below is gone. (pi-devbox dropped it in its v1.2.2.)
|
|
#
|
|
# 3.6.0 (2026-07-17) is an additive/reliability release — secure `mempalace
|
|
# serve` remote mode, optional Milvus backend, atomic KG supersede(),
|
|
# conversation chronology, mining exclusions, plus recovery/locking fixes.
|
|
# Reviewed for MCP tool-schema changes before bumping: there are NONE, and
|
|
# nothing touches diary_write — so the 3.3.x/3.4.0 regression class does not
|
|
# recur. Two fixes are directly relevant to how this image uses mempalace:
|
|
# read-only mode now covers checkpoint + delete_by_source in _MUTATING_TOOLS
|
|
# (#1930), and agent attribution is preserved in mempalace_checkpoint
|
|
# (#2023/#2034).
|
|
ARG MEMPALACE_VERSION=3.6.0
|
|
ENV UV_TOOL_DIR=/opt/uv-tools
|
|
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|
mkdir -p /opt/uv-tools && \
|
|
uv tool install --no-cache "mempalace==${MEMPALACE_VERSION}" && \
|
|
/opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
|
|
fi
|
|
|
|
# (The mempalace diary_write top-level-anyOf workaround that patched
|
|
# mcp_server.py here was removed when MEMPALACE_VERSION moved to 3.5.0 —
|
|
# fixed upstream via issue #1728 / PR #1717 (merged 2026-06-14). Mirrors
|
|
# pi-devbox v1.2.2. See CHANGELOG.md.)
|
|
|
|
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
|
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
|
ARG MEMPALACE_TOOLKIT_REF=main
|
|
# MEMPALACE_TOOLKIT_REPO is overridable so a relocated/forked build can repoint
|
|
# the clone without editing this Dockerfile (matches the *_REPO pattern used by
|
|
# other companions). Defaults to the canonical gitea origin; the default CI
|
|
# build is byte-identical.
|
|
ARG MEMPALACE_TOOLKIT_REPO=https://gitea.jordbo.se/joakimp/mempalace-toolkit.git
|
|
# MEMPALACE_TOOLKIT_REF accepts EITHER a branch name OR a commit SHA. CI
|
|
# resolves it to a SHA (resolve-versions job) and folds that SHA into the
|
|
# base-decide hash so the base rebuilds when the toolkit moves. `git clone
|
|
# --branch <40-char-SHA>` fails ("Remote branch not found"), so use
|
|
# `git fetch <ref> + checkout FETCH_HEAD`, which works for name and SHA.
|
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
|
rm -rf /opt/mempalace-toolkit && mkdir -p /opt/mempalace-toolkit && \
|
|
git -C /opt/mempalace-toolkit init -q && \
|
|
git -C /opt/mempalace-toolkit remote add origin "${MEMPALACE_TOOLKIT_REPO}" && \
|
|
ok=0; for i in 1 2 3 4 5; do \
|
|
if git -C /opt/mempalace-toolkit fetch --depth 1 origin "${MEMPALACE_TOOLKIT_REF}" && \
|
|
git -C /opt/mempalace-toolkit checkout -q FETCH_HEAD; then ok=1; break; fi; \
|
|
echo "git fetch mempalace-toolkit@${MEMPALACE_TOOLKIT_REF} failed (attempt $i/5), retrying in $((i*5))s..."; \
|
|
sleep $((i*5)); \
|
|
done; \
|
|
[ "$ok" = "1" ] && \
|
|
ln -sf /opt/mempalace-toolkit/bin/mempalace-session /usr/local/bin/mempalace-session && \
|
|
ln -sf /opt/mempalace-toolkit/bin/mempalace-docs /usr/local/bin/mempalace-docs && \
|
|
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs && \
|
|
mempalace-session --help >/dev/null && \
|
|
mempalace-docs --help >/dev/null && \
|
|
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
|
|
fi
|
|
|
|
# rustup — Rust toolchain manager (init binary only; toolchains installed at runtime)
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \
|
|
chmod +x /usr/local/bin/rustup-init
|
|
|
|
# gitea-mcp — MCP server for Gitea API
|
|
ARG GITEA_MCP_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${GITEA_MCP_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing gitea-mcp ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \
|
|
| tar -xz -C /usr/local/bin/ gitea-mcp && \
|
|
chmod +x /usr/local/bin/gitea-mcp && \
|
|
gitea-mcp --version
|
|
|
|
# Set locale — generate common UTF-8 locales (override via LANG/LC_ALL env vars)
|
|
RUN sed -i -E '/(en_US|en_GB|sv_SE|da_DK|nb_NO|fi_FI|de_DE|fr_FR|es_ES|it_IT|pt_BR|nl_NL|pl_PL|ja_JP|ko_KR|zh_CN)\.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
|
ENV LANG=en_US.UTF-8
|
|
ENV LANGUAGE=en_US:en
|
|
ENV LC_ALL=en_US.UTF-8
|
|
ENV EDITOR=nvim
|
|
# Advertise 24-bit colour so colour-aware tools (Neovim's own auto-detect, bat,
|
|
# delta, ...) use true colour instead of a 256-colour fallback. Completes the
|
|
# true-colour story the terminfo + sysinit.vim layers below already start.
|
|
# Safe for the modern terminals this devbox targets; override by exporting
|
|
# `COLORTERM=` (empty) from a terminal that lacks true-colour support.
|
|
ENV COLORTERM=truecolor
|
|
ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
|
|
# Enable opencode's native background subagents. opencode gates this behind an
|
|
# experimental flag (packages/opencode/src/tool/task.ts errors with
|
|
# "Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true"
|
|
# when unset). oh-my-opencode-slim V2+ makes background orchestration its DEFAULT
|
|
# workflow, so the omos variant is effectively broken without this. Baked here as
|
|
# a base ENV (applies to both variants; harmless for plain opencode — it only
|
|
# *enables* a capability). Overridable at runtime: -e OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false.
|
|
# REMOVAL TRIGGER: when opencode promotes background subagents out of experimental
|
|
# (flag becomes default / renamed), drop this ENV. No upstream roadmap date as of
|
|
# opencode 1.18.13 / omos 2.2.9 (2026-08). Re-verified against upstream source
|
|
# at all three tags (1.17.20, 1.18.12, 1.18.13): packages/opencode/src/effect/runtime-flags.ts
|
|
# is unchanged and still gates the flag behind OPENCODE_EXPERIMENTAL — trigger has NOT fired.
|
|
ENV OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
|
|
|
|
# ── Node.js (required for opencode/pi/omos at variant build + MCP servers) ──
|
|
ARG NODE_VERSION=22
|
|
RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
|
|
apt-get install -y --no-install-recommends nodejs && \
|
|
rm -rf /var/lib/apt/lists/*
|
|
|
|
# ── agent-browser + Playwright Chromium — real-browser verification ──
|
|
# Lets the agent drive an actual browser (open pages, click/fill/eval, snapshot
|
|
# the DOM, screenshot) to VERIFY front-end work — live DOM, WebGL, layout,
|
|
# popup positioning — instead of guessing. Ported from pi-devbox.
|
|
#
|
|
# We resolve the Chrome binary through a stable symlink (/usr/local/bin/
|
|
# agent-chrome) exposed via AGENT_BROWSER_EXECUTABLE_PATH — the symlink
|
|
# insulates the ENV from Playwright's per-version, per-ARCH browser directory
|
|
# (`chrome-linux` on arm64, `chrome-linux64` on amd64 — Chrome-for-Testing), so
|
|
# we `find` the `chrome` binary rather than hardcode the path; the headless-shell
|
|
# binary is named `chrome-headless-shell`, so `-name chrome` skips it.
|
|
#
|
|
# `playwright install --with-deps chromium` also apt-installs Chromium's runtime
|
|
# libs; verified to resolve correctly on Debian trixie (the t64 library renames
|
|
# are handled by Playwright's dep list). The build runs as root, so the apt step
|
|
# works. NPM_CONFIG_PREFIX=/usr keeps both CLIs on /usr so they survive the
|
|
# ~/.config/opencode/npm-global volume mount (the same trick the variant uses
|
|
# for opencode). After fetching, we DROP Playwright's `chromium_headless_shell-*`
|
|
# build — agent-browser drives the full chrome, so the headless shell is dead
|
|
# weight — and clean the apt/npm caches, trimming the layer to ~625 MB from
|
|
# ~960 MB. This is the bulk of the base's size and the one real tradeoff of
|
|
# shipping it to every variant; the smoke-test size thresholds were lifted in
|
|
# lockstep (see scripts/smoke-test.sh).
|
|
ARG AGENT_BROWSER_VERSION=latest
|
|
ARG PLAYWRIGHT_VERSION=latest
|
|
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/ms-playwright
|
|
RUN NPM_CONFIG_PREFIX=/usr npm install -g \
|
|
"agent-browser@${AGENT_BROWSER_VERSION}" \
|
|
"playwright@${PLAYWRIGHT_VERSION}" && \
|
|
playwright install --with-deps chromium && \
|
|
CHROME="$(find "${PLAYWRIGHT_BROWSERS_PATH}" -type f -name chrome -path '*/chromium-*/*' | head -n1)" && \
|
|
[ -n "$CHROME" ] && ln -sf "$CHROME" /usr/local/bin/agent-chrome && \
|
|
agent-browser --version && \
|
|
test -x "$(readlink -f /usr/local/bin/agent-chrome)" && \
|
|
rm -rf "${PLAYWRIGHT_BROWSERS_PATH}"/chromium_headless_shell-* && \
|
|
npm cache clean --force && \
|
|
rm -rf /var/lib/apt/lists/* /root/.npm /tmp/*
|
|
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/local/bin/agent-chrome
|
|
|
|
# ── AWS CLI v2 (for SSO/Bedrock authentication) ─────────────────────
|
|
RUN ARCH=$(case "${TARGETARCH}" in \
|
|
amd64) echo "x86_64" ;; \
|
|
arm64) echo "aarch64" ;; \
|
|
*) echo "x86_64" ;; \
|
|
esac) && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \
|
|
unzip -q /tmp/awscli.zip -d /tmp && \
|
|
/tmp/aws/install && \
|
|
rm -rf /tmp/aws /tmp/awscli.zip && \
|
|
aws --version
|
|
|
|
# ── Non-root user ────────────────────────────────────────────────────
|
|
ARG USER_NAME=developer
|
|
ARG USER_UID=1000
|
|
ARG USER_GID=1000
|
|
|
|
RUN groupadd --gid ${USER_GID} ${USER_NAME} && \
|
|
useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USER_NAME} && \
|
|
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${USER_NAME}
|
|
|
|
# Create standard directories
|
|
RUN mkdir -p /workspace \
|
|
/home/${USER_NAME}/.config/opencode/skills \
|
|
/home/${USER_NAME}/.config/opencode/npm-global \
|
|
/home/${USER_NAME}/.agents/skills \
|
|
/home/${USER_NAME}/.local/share/opencode \
|
|
/home/${USER_NAME}/.cache/bash \
|
|
/home/${USER_NAME}/.ssh && \
|
|
chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}
|
|
|
|
# ── Pre-warm chromadb embedding model ──────────────────────────────
|
|
# Runs as gosu developer so Path.home() resolves correctly. Uses
|
|
# the mempalace venv's python, which is the only one that has
|
|
# chromadb importable (system python3 cannot reach the isolated venv).
|
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|
gosu ${USER_NAME} /opt/uv-tools/mempalace/bin/python -c "\
|
|
from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2; \
|
|
ef = ONNXMiniLM_L6_V2(); \
|
|
_ = ef(['warmup']); \
|
|
print('chromadb embedding model warmed: all-MiniLM-L6-v2')" && \
|
|
ls -lh /home/${USER_NAME}/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ ; \
|
|
fi
|
|
|
|
# ── User-writable npm global prefix on the devbox-opencode-config volume ──
|
|
# By default npm's global prefix is /usr (writable only by root) so any
|
|
# `npm install -g <pkg>` invoked by the developer user would EACCES.
|
|
# Pointing the prefix into ~/.config/opencode places user-installed
|
|
# packages on the devbox-opencode-config named volume, which means they
|
|
# survive container recreation AND image rebuilds.
|
|
#
|
|
# NOTE (v2.0.0): this prefix previously lived at ~/.pi/npm-global — a
|
|
# pi-specific path. With pi removed (see docs/CLEANUP-v2.0.0.md) it now
|
|
# lives under ~/.config/opencode, which is a persistent named volume in
|
|
# BOTH docker-compose.yml and docker-compose.shared.yml (the old ~/.pi
|
|
# volume was only in the former). A one-time migration shim in
|
|
# entrypoint-user.sh copies any existing ~/.pi/npm-global contents to the
|
|
# new prefix on first start so user-installed globals are not lost.
|
|
#
|
|
# IMPORTANT: in this split-build layout the variant Dockerfile inherits
|
|
# this prefix at build time. To keep the baked binaries on /usr (so the
|
|
# volume mount doesn't shadow them), the variant Dockerfile MUST run each
|
|
# `npm install -g` with NPM_CONFIG_PREFIX=/usr in the per-RUN
|
|
# environment. See Dockerfile.variant.
|
|
ENV NPM_CONFIG_PREFIX=/home/${USER_NAME}/.config/opencode/npm-global
|
|
ENV PATH="/home/${USER_NAME}/.config/opencode/npm-global/bin:${PATH}"
|
|
|
|
# ── Shell defaults (bash history, aliases, readline) ─────────────────
|
|
RUN mkdir -p /etc/skel-devbox
|
|
COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
|
|
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
|
COPY rootfs/home/developer/.gitignore_global /etc/skel-devbox/.gitignore_global
|
|
|
|
# ── Editor defaults: system-wide Neovim true-colour ──────────────────
|
|
# /etc/xdg/nvim/sysinit.vim is Neovim's system vimrc: it loads for every user
|
|
# (before any personal ~/.config/nvim) and can still be overridden per-user.
|
|
# Enables termguicolors so the default theme renders in 24-bit colour instead
|
|
# of a muddy 256-colour fallback. Pairs with kitty-terminfo (installed above).
|
|
COPY rootfs/etc/xdg/nvim/sysinit.vim /etc/xdg/nvim/sysinit.vim
|
|
|
|
# ── Terminal support: xterm-ghostty terminfo alias ──────────────────
|
|
# ncurses-term (installed above) covers wezterm/alacritty/foot/st and the base
|
|
# `ghostty` entry, but Ghostty connects with TERM=xterm-ghostty, for which no
|
|
# distro packages an entry. Ship a thin alias (use=ghostty) and compile it into
|
|
# the system terminfo db with `tic -x`, so it inherits the maintained ghostty
|
|
# capability set. The `infocmp` check fails the build if the entry didn't land.
|
|
COPY rootfs/usr/local/share/terminfo-src/ghostty.terminfo /usr/local/share/terminfo-src/ghostty.terminfo
|
|
RUN tic -x -o /usr/share/terminfo /usr/local/share/terminfo-src/ghostty.terminfo && \
|
|
infocmp -x xterm-ghostty >/dev/null
|
|
|
|
# ── Entrypoint ────────────────────────────────────────────────────────
|
|
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
|
|
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
|
# Reader for the build manifest baked in Dockerfile.variant. Printed at
|
|
# container start by entrypoint-user.sh; also available on demand.
|
|
COPY rootfs/usr/local/bin/opencode-devbox-version /usr/local/bin/opencode-devbox-version
|
|
# Image-baked skills + harness instruction. Under /usr/local so a named volume
|
|
# over a home dir (e.g. devbox-opencode-config on ~/.config/opencode) can't
|
|
# shadow them; entrypoint-user.sh links them into ~/.agents/skills/ and
|
|
# ~/.config/opencode/instructions/ on every start. See
|
|
# rootfs/usr/local/share/opencode-devbox/skills/VENDORED.md.
|
|
COPY rootfs/usr/local/share/opencode-devbox/ /usr/local/share/opencode-devbox/
|
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
|
/usr/local/bin/dot-watch \
|
|
/usr/local/bin/opencode-devbox-version \
|
|
/usr/local/lib/opencode-devbox/*.py
|
|
|
|
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
|
WORKDIR /workspace
|
|
|
|
ENTRYPOINT ["entrypoint.sh"]
|
|
CMD ["bash", "-l"]
|