Files
opencode-devbox/AGENTS.md
T
joakimp 23bae2ab7d
Validate / docs-check (push) Successful in 20s
Validate / validate-base (push) Successful in 11m32s
Validate / validate-omos (push) Successful in 15m18s
Publish Docker Image / build-base (push) Successful in 53m5s
Publish Docker Image / build-omos (push) Successful in 1h11m3s
Publish Docker Image / update-description (push) Successful in 15s
Use mempalace-mcp entry point directly, drop redundant wrapper
The mempalace Python package ships a 'mempalace-mcp' console entry
point; 'uv tool install' places it on PATH as a shim whose shebang
points at the isolated venv's Python. Our hand-rolled wrapper at
/usr/local/bin/mempalace-mcp-server was duplicating what uv installs
for free — one less file to maintain.

Fixes the MCP error users saw after the v1.14.28b → v1.14.29 upgrade
path: custom opencode.json files typically had the pre-v1.14.29
command ['python3', '-m', 'mempalace.mcp_server'] which worked with
the old pip install but fails silently after the uv-tool migration
because system python3 cannot import from the venv. Opencode surfaced
this as 'MCP error -32000: connection closed'.

- generate-config.py now emits ['mempalace-mcp'] and keys its detect
  on shutil.which('mempalace-mcp').
- Dockerfile drops 'COPY rootfs/usr/local/bin/' and the chmod of the
  wrapper. Build shrinks from 30 to 29 stages.
- rootfs/usr/local/bin/ removed entirely.
- Smoke test asserts /usr/local/bin/mempalace-mcp is executable and
  prints its symlink target.
- README's MemPalace section shows ['mempalace-mcp'] and explicitly
  warns against the old pattern with the observed failure mode.
- CHANGELOG adds a v1.14.29c entry.
2026-04-29 15:27:30 +02:00

7.9 KiB

AGENTS.md

Project overview

Docker image packaging opencode into a production-ready dev container. Two image variants (base and omos) are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfile, entrypoint scripts, docker-compose, documentation).

File roles

  • Dockerfile — single multi-stage build for both variants. OMOS variant is controlled by INSTALL_OMOS=true build arg; mempalace is controlled by INSTALL_MEMPALACE (default true). All GitHub-sourced binaries are pinned with version ARGs.
  • entrypoint.sh — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via .devbox-owner sentinel when ownership is already correct). Then drops to developer via gosu.
  • entrypoint-user.sh — runs as developer: git config, opencode.json generation (delegated to generate-config.py), OMOS setup.
  • rootfs/usr/local/lib/opencode-devbox/generate-config.py — generates ~/.config/opencode/opencode.json from env vars. Never overwrites an existing config. Auto-registers MCP servers for detected tools (mempalace via the mempalace-mcp entry point, gitea-mcp).
  • scripts/smoke-test.sh — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
  • scripts/generate-dockerhub-md.py — generates DOCKER_HUB.md from README.md using explicit section rules. --check fails if the committed file is out of sync (enforced by the validate workflow).
  • DOCKER_HUB.mdauto-generated from README. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
  • README.md — authoritative source documentation. Sections are selected/dropped/replaced for DOCKER_HUB.md per SECTION_RULES in scripts/generate-dockerhub-md.py.
  • .gitea/workflows/validate.yml — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
  • .gitea/workflows/docker-publish.yml — CI pipeline on tag push: smoke-test each variant on amd64, then full multi-arch (amd64 + arm64) build-and-push, then update Docker Hub description.

Versioning scheme

Tags follow v{opencode_version}[letter] — e.g. v1.14.20 for the first build on a new opencode release, and v1.14.20b, v1.14.20c, … for subsequent rebuilds on the same opencode version.

  • The number tracks the opencode npm version (see OPENCODE_VERSION ARG in Dockerfile).
  • No letter suffix on the first build of a new opencode version — the bare v{opencode_version} tag is the canonical release.
  • Letter suffix is the build ordinal, starting at b for the second build. The letter a is never used — think of the suffix as counting rebuilds: b = 2nd, c = 3rd, d = 4th, …. For opencode version 1.14.20: first build v1.14.20, second v1.14.20b, third v1.14.20c, and so on.
  • A letter suffix is only used for container-level rebuilds — tooling changes, CVE fixes, doc-driven rebuilds, entrypoint bugfixes — that don't change the underlying opencode version.

CI produces four Docker Hub tags per release: vX.Y.Z[n], latest, vX.Y.Z[n]-omos, latest-omos.

When bumping the opencode version, also bump OPENCODE_VERSION in Dockerfile and update the comment in .env.example if it names a specific model/version for context.

Critical conventions

  • entrypoint.sh volume ownership loop — when adding a new named volume mount point, add it to the for dir in ... loop in entrypoint.sh so root-owned volumes get chowned on startup. The loop writes a .devbox-owner sentinel after a successful chown so subsequent starts skip the recursive walk. Users should not touch these files.
  • Two docs to keep in sync (automated)README.md is the source of truth. DOCKER_HUB.md is auto-generated by scripts/generate-dockerhub-md.py. When adding a new top-level section to README, either add it to SECTION_RULES in that script or the --check run will fail CI. .env.example must still be hand-updated to match Dockerfile/entrypoint behavior.
  • GitHub/Gitea-sourced binaries float by default — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to latest. Each build-time install step reads the /releases/latest Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same ARCH case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: OPENCODE_VERSION (drives the image tag), NODE_VERSION=22 (major pin), DEBIAN_VERSION=trixie-slim (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
  • Resolved versions are logged by the smoke testscripts/smoke-test.sh prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to latest.
  • Shell scripts use set -euo pipefail — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with || true.
  • MemPalace install path — installed via uv tool install into /opt/uv-tools/mempalace/. Both the mempalace CLI and the mempalace-mcp MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use pip install --break-system-packages — that was the previous approach and has been removed. Do not use ["python3", "-m", "mempalace.mcp_server"] in opencode.json — system Python can't import from the uv venv.
  • generate-config.py idempotency — the script MUST never overwrite an existing opencode.json. Users bind-mount their config directory or persist it across container recreations; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
  • Docker Hub description update — uses /v2/auth/token endpoint (not the deprecated /v2/users/login). Auth uses identifier/secret fields, returns access_token, sent as Bearer. Short description must be ≤100 bytes.

CI quirks

  • Both build jobs include an IPv4 preference step (gai.conf + driver-opts: network=host for buildx) to work around intermittent IPv6 failures on the Gitea runners.
  • update-description job runs only when both builds succeed (needs: [build-base, build-omos]).
  • Tags must be pushed to trigger the publish workflow. The validate workflow runs on push to main and PRs.
  • Smoke tests run on amd64 only (single-arch load into the local daemon). The multi-arch push happens after smoke passes.

Testing changes

The smoke test (scripts/smoke-test.sh) is the canonical check and runs automatically in CI. To run locally:

# Base image
docker compose build
bash scripts/smoke-test.sh opencode-devbox --variant base

# OMOS image
docker build --build-arg INSTALL_OMOS=true -t opencode-devbox:omos .
bash scripts/smoke-test.sh opencode-devbox:omos --variant omos

For manual/exploratory testing:

  1. docker compose run --rm devbox bash
  2. Check specific tools inside: nvim --version, bat --version, uv --version, mempalace --help, etc.
  3. For entrypoint changes: test with a non-1000 UID workspace to verify UID adjustment, volume ownership fixes, and the .devbox-owner sentinel behavior.
  4. For generate-config.py changes: run standalone with HOME=/tmp/fake OPENCODE_PROVIDER=anthropic python3 rootfs/usr/local/lib/opencode-devbox/generate-config.py.

Commit style

Imperative mood, first line summarizes the change. Multi-line body explains "why" when non-obvious. Examples from history:

  • Fix ownership of named volume mount points in entrypoint
  • Add uv package manager to base image for on-demand Python support
  • Upgrade base image from Debian bookworm to trixie (current stable)