release: v2.9.0 — agent-browser, manifest reader, opencode 1.18.13
Lint / docs-check (push) Successful in 5s
Lint / hadolint (push) Successful in 12s
Lint / actionlint (push) Has been cancelled

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.
This commit is contained in:
pi
2026-08-04 16:31:13 +02:00
parent 5fb07e0a39
commit 37960186c6
11 changed files with 366 additions and 18 deletions
+6 -2
View File
@@ -6,8 +6,12 @@
# Which provider to auto-configure (anthropic, openai, amazon-bedrock)
OPENCODE_PROVIDER=anthropic
# Model override (optional, defaults per provider)
# OPENCODE_MODEL=anthropic/claude-sonnet-5
# Model override (optional). Unset = the per-provider default baked into
# generate-config.py: anthropic/claude-opus-5, amazon-bedrock/
# global.anthropic.claude-opus-5, or openai/gpt-5.6. Set this to use any other
# model — the value is written verbatim as the `model` field, so it works for
# providers with no baked default too. Format: <provider>/<model>.
# OPENCODE_MODEL=anthropic/claude-opus-5
# ── API Keys (set the one matching your provider) ────────────────────
# ANTHROPIC_API_KEY=
+8 -2
View File
@@ -21,6 +21,7 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), LAN-access setup (delegated to `setup-lan-access.sh`), a one-time npm-global prefix migration shim (legacy `~/.pi/npm-global``~/.config/opencode/npm-global`), skillset auto-deploy from mounted skillset repo, OMOS bundled-skills reconcile (symlinks the image's bundled skills into `~/.agents/skills/`), image-baked fallback-skills reconcile (symlinks `/usr/local/share/opencode-devbox/skills/*` into `~/.agents/skills/` only-when-absent) + harness-instruction reconcile (symlinks `/usr/local/share/opencode-devbox/instructions/*.md` into `~/.config/opencode/instructions/`), OMOS config setup.
- `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh` — host-OS-agnostic LAN reachability helper. Always writes the writable `~/.ssh-local/config` sidecar on **every** host OS: a `Host *` block that redirects `ControlPath` into `~/.ssh-local/cm/` (first-value-wins over any read-only `~/.ssh`-bound per-host setting) plus `Include ~/.ssh/config`. On VM-backed hosts (macOS OrbStack / Docker Desktop, detected via `host.docker.internal` resolution) it additionally inserts the host-jump block; on native Linux that block is omitted (LAN is reachable directly) but the sidecar is still rendered. Previously the script exited early on native Linux, leaving `dssh`/`dscp` broken when `~/.ssh` was read-only there. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS` / `DEVBOX_LAN_AUTOJUMP_PRIVATE`. Ships the mechanism only (generic `host` jump alias); user targets stay host-side — named-peer `ProxyJump host` overrides go in a bind-mounted `~/.config/devbox-shell/ssh-lan.conf` (Included before `~/.ssh/config`), never baked into the image. **Scoping invariant:** every `Include` in the generated config MUST be preceded by a bare `Host *` reset — an `Include` is scoped to the enclosing `Host`/`Match` block, so without the reset the included config only applies when targeting `host`/`mac` and named peers fall back to SSH defaults. Non-fatal. Counted in the base hash, so editing it advances `base-latest`.
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint).
- `rootfs/usr/local/bin/opencode-devbox-version` — reader for the build manifest that `Dockerfile.variant` bakes at `/etc/opencode-devbox/build-manifest.json`. Three modes (`--json`, `--quiet`, default human) plus a live-vs-baked `opencode --version` drift check (a user `npm install -g opencode` lands on the persistent config volume and can shadow the baked `/usr` binary). Printed as the first line of `entrypoint-user.sh` so "which image am I in?" is answered at start. Added v2.9.0 — before that the manifest was baked but nothing read it. Lives under `rootfs/`, so editing it advances the base content hash.
- `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/recreate-sanity-check.sh`**runtime** post-recreate verification (counterpart to the build-time `smoke-test.sh`). Run inside the container after `docker compose up -d --force-recreate` to confirm the new image is live (opencode version matches `Dockerfile.variant`'s `OPENCODE_VERSION`), persisted named volumes survived (mempalace palace, opencode.db, bash-history), omos runtime skill symlinks resolve, shell defaults re-seeded, and `/opt` toolkits intact. Not run by CI or the entrypoint — it needs the running container + volumes that smoke-test.sh (which uses `--entrypoint=""`) cannot see.
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from a hand-maintained `HUB_TEMPLATE` constant. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
@@ -107,8 +108,13 @@ curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 |
- **Registry buildkit cache-export is currently disabled** — do NOT re-add `cache-from`/`cache-to` to the `build-base` step in `.gitea/workflows/docker-publish-split.yml` without first verifying that buildkit's `mode=max` cache-export to `registry-1.docker.io` no longer returns HTTP 400 from the Hub CDN edge. The regression surfaced ~2026-05-23 and broke five consecutive opencode-devbox publish attempts (runs #332/333/334/336 + a rerun); root-caused on 2026-05-28 by a manual host-side publish that reproduced the same 400 only on `--cache-to` while image push worked fine. Failure shape is stable (`Offset:0` in the `_state` token, HTML response body = CDN-tier rejection, not registry backend), repo-specific (we're the only repo writing `:base-buildcache` mode=max), and explains why pinning `setup-buildx-action@v4.0.0` didn't help (action pin doesn't change the bundled buildkit version on the catthehacker runner image). Trade-off: dockerfile.base changes pay a full ~3 min rebuild instead of pulling cached layers; unchanged bases short-circuit at the Hub-probe step in `base-decide` and never re-build anyway. Variants don't use registry cache so they're unaffected. Re-enable condition: upstream moby/buildkit fix lands AND a low-risk test run succeeds without 400s. See CHANGELOG v1.15.12 `Unreleased` block for the full diagnostic chain. Manual escape-hatch publish procedure: `docs/manual-host-publish.md`.
- **Push steps wrap `docker buildx build --push` in a 3-attempt retry loop** (15s, 30s backoff) for transient `registry-1.docker.io` blips — rate limits, brief 5xx, CDN flap. Implemented as inline `shell: bash` steps with `docker buildx build` raw rather than `docker/build-push-action@v7` so the loop is visible and tweakable. Affects the 1 base + 2 variant push steps in `.gitea/workflows/docker-publish-split.yml`; smoke-test builds (`load: true`, no push) are untouched. **This does NOT mask deterministic failures** — a true regression (like the cache-export 400 of 2026-05-23..28) fails all 3 attempts identically and the job still fails. Orthogonal to the cache-export disablement above: cache-export was about a deterministic protocol mismatch, retry is about absorbing genuine transients. Both are belt-and-braces with the `ci-release-watcher` skill's transient-rerun heuristic. If you change the matrix of push steps, keep the retry wrapper consistent across them — the pattern is duplicated rather than factored out because Gitea Actions doesn't support reusable composite shell steps cleanly.
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
- **Background subagents flag baked ON — experimental, watch for promotion** — `Dockerfile.base` sets `ENV OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`. opencode gates native background subagents behind this flag (`packages/opencode/src/tool/task.ts` fails 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 degraded without it. It's a base ENV (applies to both variants; harmless on plain opencode — only *enables* a capability) and stays runtime-overridable (`-e …=false`). It's counted in the base hash, so editing that line advances `base-latest`. **REMOVAL TRIGGER:** when opencode promotes background subagents out of `EXPERIMENTAL_` (flag renamed or made default), drop the ENV. No upstream roadmap date as of opencode 1.17.20 / omos 2.2.0 (2026-07). Documented in lockstep in README env table, `.env.example`, and asserted by `scripts/smoke-test.sh` (`bg-subagents env baked`).
- **MemPalace `diary_write` anyOf workaround — upstream watch target** — `Dockerfile.base` carries a perl RUN block that strips a root-level `anyOf` from `mempalace_diary_write`'s advertised `inputSchema`. Mempalace 3.3.x/3.4.0 advertise `anyOf: [{required:[entry]},{required:[content]}]`, which Anthropic's tools API (and Codex) reject at session start (`input_schema does not support oneOf, allOf, or anyOf at the top level`), making the whole MCP server fail to load. The workaround is idempotent and self-deactivating: when upstream ships the real fix the regex stops matching and the build prints `WARN: ... upstream may have changed shape` — **that WARN is the signal to delete the RUN block.** Upstream status (last checked **2026-06-14**): issue **#1728 is still OPEN**; PR **#1735 is CLOSED UNMERGED (2026-06-11) — do NOT watch it, it is dead**; PR **#1717 is the current live fix candidate**; mempalace PyPI latest is **3.4.0 (== our pin)**, so **no release contains the fix yet** and the workaround must stay. **Removal trigger:** a mempalace release **> 3.4.0** that actually strips the root-level `anyOf` lands on PyPI — then bump `MEMPALACE_VERSION` (in lockstep with pi-devbox) and drop the RUN block. NOTE: `MEMPALACE_VERSION` (the pip pin) and `MEMPALACE_TOOLKIT_REF` (the git ref for the `mempalace-toolkit` clone) are unrelated despite the shared prefix; do not conflate them.
- **Background subagents flag baked ON — experimental, watch for promotion** — `Dockerfile.base` sets `ENV OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`. opencode gates native background subagents behind this flag (`packages/opencode/src/tool/task.ts` fails 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 degraded without it. It's a base ENV (applies to both variants; harmless on plain opencode — only *enables* a capability) and stays runtime-overridable (`-e …=false`). It's counted in the base hash, so editing that line advances `base-latest`. **REMOVAL TRIGGER:** when opencode promotes background subagents out of `EXPERIMENTAL_` (flag renamed or made default), drop the ENV. No upstream roadmap date as of
opencode 1.18.13 / omos 2.2.9 (2026-08) — re-verified at all three of the
1.17.20, 1.18.12 and 1.18.13 tags that `packages/opencode/src/effect/runtime-flags.ts`
is unchanged and still gates the flag behind `OPENCODE_EXPERIMENTAL`. Documented in lockstep in README env table, `.env.example`, and asserted by `scripts/smoke-test.sh` (`bg-subagents env baked`).
- **agent-browser + Chromium is the base's size driver — thresholds are coupled** — `Dockerfile.base` installs the `agent-browser` CLI and a Playwright-managed Chromium (~625 MB after dropping the redundant `chromium_headless_shell-*` build). It is by far the largest single thing in the image and ships in **both** variants. The Chrome binary is reached via the stable symlink `/usr/local/bin/agent-chrome` (exposed as `AGENT_BROWSER_EXECUTABLE_PATH`) and the Dockerfile **`find`s** it rather than hardcoding a path, because Playwright's browser dir is per-version *and* per-arch (`chrome-linux` on arm64, `chrome-linux64` on amd64). If you add anything else large, or remove this layer in a fork, update the size thresholds in `scripts/smoke-test.sh` **in the same commit** — a threshold trip mid-release causes a partial publish and a letter-suffix recovery cycle. Do **not** copy pi-devbox's threshold number: it sums `docker history` while this repo uses `docker image inspect .Size`.
- **`yq` here means mikefarah's Go v4, not Debian's `yq`** — Debian/Ubuntu's `yq` apt package is the unrelated Python kislyuk/yq (a jq-syntax wrapper on a 3.x line). Since v2.9.0 the apt package is **removed** and the mikefarah binary is installed from GitHub, because the `cloud-init` repo's `provision.sh`/`deploy.sh` require v4 syntax — and because `THIRD_PARTY.md` had credited "yq (mikefarah)" while the image actually shipped the Python one. `scripts/smoke-test.sh` pins the contract with `yq --version | grep -qE 'mikefarah.*version v4'`, so both a regression to the apt package and a future yq v5 fail CI. Do not "simplify" this back into the apt list, and do not install both — with two `yq` binaries on PATH the meaning of `yq` silently depends on PATH order.
- **MemPalace pin — schema-regression watch target (workaround already removed)** — `MEMPALACE_VERSION` is deliberately pinned (currently **3.6.0**) rather than floated, because an unpinned `uv tool install mempalace` is what once silently swept in a broken `diary_write` schema. History: mempalace 3.3.x/3.4.0 advertised a root-level `anyOf` on `mempalace_diary_write`'s `inputSchema`, which Anthropic's tools API (and Codex) reject at session start (`input_schema does not support oneOf, allOf, or anyOf at the top level`), making the *whole* MCP server fail to load. `Dockerfile.base` used to carry a perl RUN block that stripped it. **That workaround is gone** — upstream fixed it in **3.5.0** (issue #1728 / PR #1717, merged 2026-06-14; `diary_write` now advertises `"required": ["agent_name"]` and enforces entry/content at dispatch), so the block was deleted when the pin moved to 3.5.0. **3.6.0** (2026-07-17) was reviewed for schema changes before bumping: it is purely additive/reliability (secure `serve` remote mode, optional Milvus, atomic KG `supersede()`, mining exclusions) and touches no MCP tool schema. **Ongoing rule:** before bumping this pin, diff the release notes for anything touching MCP tool schemas — that is the regression class this pin exists to catch — and bump **in lockstep with pi-devbox's `MEMPALACE_VERSION`**. NOTE: `MEMPALACE_VERSION` (the PyPI pin) and `MEMPALACE_TOOLKIT_REF` (the git ref for the `mempalace-toolkit` clone) are unrelated despite the shared prefix; do not conflate them.
- **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.jsonc` — system Python can't import from the uv venv.
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.jsonc` or legacy `opencode.json`. Config persists in the `devbox-opencode-config` named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
- **Skillset auto-deploy** — on every container start, `entrypoint-user.sh` looks for a skillset repo (detection order: `$SKILLSET_CONTAINER_PATH` → `$HOME/skillset` → `/workspace/skillset`) and runs `deploy-skills.sh --bootstrap --prune-stale`. This creates relative symlinks in `~/.agents/skills/` and `~/.config/opencode/instructions/`. Do NOT bind-mount `~/.agents/skills/` from the host — the container manages its own skills with relative symlinks that differ from the host's. The named volume `devbox-opencode-config` persists the deployed config across restarts.
+34
View File
@@ -6,6 +6,40 @@ Tags follow **independent semver** (since `v2.0.0`) — they version *this image
---
## v2.9.0 — 2026-08-04
Minor release. Headline: **real-browser verification lands in the base** (`agent-browser` + Playwright Chromium), the build manifest finally has a **reader** (`opencode-devbox-version`), and defaults move to **`claude-opus-5` / `gpt-5.6`** alongside opencode `1.17.20 → 1.18.13` and mempalace `3.5.0 → 3.6.0`. One behaviour change to read before upgrading: **`yq` is now mikefarah's Go v4, not Debian's Python `yq`** (see *Changed*). Several changes touch `Dockerfile.base`/`rootfs/`, so the **base image rebuilds** this release.
### Added
- **`agent-browser` + Playwright Chromium in the base — the agent can now drive a real browser.** Ported from pi-devbox. Lets an agent open pages, click/fill, `eval` JavaScript, snapshot the DOM and take screenshots, so front-end work can be **verified** (live DOM, layout, popup positioning, WebGL) instead of assumed. `AGENT_BROWSER_EXECUTABLE_PATH` is preset to `/usr/local/bin/agent-chrome`, a stable symlink the Dockerfile resolves with `find` rather than hardcoding — Playwright's browser directory 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. Playwright's redundant `chromium_headless_shell-*` build is deleted and the apt/npm caches cleaned, trimming the layer to **~625 MB** from ~960 MB. New floated build args `AGENT_BROWSER_VERSION` and `PLAYWRIGHT_VERSION`. This is now the single largest thing in the image and ships in **both** variants — a deliberate tradeoff, since verification is broadly useful.
- **`opencode-devbox-version` — the build manifest is no longer invisible.** The image has baked ground truth to `/etc/opencode-devbox/build-manifest.json` for several releases (release tag, build date, source commit, live `opencode --version`, installed omos version, `mempalace-toolkit` HEAD) but **nothing read it** and nothing printed it, so answering "which image am I running?" meant knowing the path by heart. The new command wraps it in three modes (default human summary, `--json` for scripting, `--quiet` for a one-line `tag (rev)` form) and `entrypoint-user.sh` prints it as its **first** output, before the setup noise. It also performs a **live-vs-baked drift check**: because `NPM_CONFIG_PREFIX` points at the persistent `devbox-opencode-config` volume, a user's `npm install -g opencode` can shadow the baked `/usr` binary — so the command reports the live version and flags a mismatch rather than trusting the manifest blindly.
- **`ENV COLORTERM=truecolor`.** Completes a true-colour story the image already half-shipped (it had `ncurses-term` + `kitty-terminfo` + the compiled `xterm-ghostty` alias + a system-wide Neovim `termguicolors` default, but never advertised 24-bit capability), so colour-aware tools like `bat` and `delta` stop falling back to 256 colours. Override with `COLORTERM=` (empty) from a terminal without true-colour support.
- **Smoke coverage for the new surfaces and for `release_tag`.** Adds assertions for `agent-browser --version`, that `agent-chrome` resolves to an *executable* (catches a Playwright layout change rather than merely a dangling symlink), `AGENT_BROWSER_EXECUTABLE_PATH`, `COLORTERM`, the manifest's `release_tag` field, and all three modes of `opencode-devbox-version`. The pre-existing *Build provenance* block (manifest present, component fields, and the `! grep -q '"unknown"'` unresolved-component guard) was already in place and is unchanged.
### Changed
- **CI: a push to `main` no longer builds an image — the three workflows now divide cleanly by cost.** `lint.yml` is the cheap-checks workflow and the **only** one a push to main triggers: actionlint + the Gitea shell guard, hadolint, and `docs-check` (the `DOCKER_HUB.md` sync check, **moved here from `validate.yml`** so it survives — anything that needs no image belongs in the workflow that actually runs on push). `validate.yml` keeps the amd64 build + smoke test but **lost its push trigger entirely**: `pull_request` and a new `workflow_dispatch` only. `docker-publish-split.yml` is unchanged, still tag-only. `validate-base`/`validate-omos` also keep a now-redundant `github.event_name != 'push'` clause as belt-and-braces, so re-adding a push trigger cannot silently re-enable builds. `lint.yml` was renamed `Lint workflows``Lint` since it now covers Dockerfiles and docs too (no references to the old name existed). This brings the repo in line with pi-devbox, where `lint.yml` is likewise the only push-triggered workflow. It is safe because the release path already fails closed — `docker-publish-split.yml` pushes variant tags only after `smoke-base`/`smoke-omos` pass and promotes `base-latest` last, so an aborted release leaves at worst an unreferenced `base-<hash>` blob on Hub, never a half-published version tag. The pre-tag safety net remains available three ways: open a PR, dispatch the Validate workflow, or dispatch `docker-publish-split.yml` against a throwaway tag with `promote_latest=false` (the only route that also exercises a **changed base**, which `validate.yml` structurally cannot — it builds variants on top of Hub's `base-latest`). Note the practical coverage lost is narrower than it looks: the build jobs were already skipped whenever a commit touched `Dockerfile.base`, `rootfs/`, or `entrypoint*.sh`, so they only ever ran for variant-only changes — most usefully a bare `OPENCODE_VERSION` bump, for which a dispatch before tagging is now the equivalent.
- **opencode bumped `1.17.20``1.18.13`** (`Dockerfile.variant` `OPENCODE_VERSION`; latest stable on npm, verified with `npm view opencode-ai version`). Verified as a safe minor-line jump by diffing upstream source at the tags rather than only reading release notes: `packages/core/src/config.ts`, `packages/core/src/config/provider.ts` and `packages/core/schema.json` are **byte-identical** to 1.17.20, so `generate-config.py` needs no change; no breaking Core changes (all "Desktop" notes are the Electron app, irrelevant here); nothing in the repo parses opencode CLI output beyond `--version`. Provider/MCP work in the range is net-positive (restored legacy MCP SDK client compatibility in 1.18.9, fixed MCP SSE reconnect loops in 1.18.11, better MCP OAuth in 1.18.8). The `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` removal-trigger was re-checked **at source level**`packages/opencode/src/effect/runtime-flags.ts` is unchanged across all three tags and still gates the flag behind `OPENCODE_EXPERIMENTAL` — so it has **not** fired and the ENV stays baked ON.
- **`1.18.12``1.18.13` (published mid-audit, 2026-08-04) re-verified separately.** 19 commits, and the 249-file compare payload is *under* GitHub's 300-file cap, so the change list is complete rather than truncated. Content is almost entirely the Electron desktop app and localisation — new `az`/`fi`/`hi`/`id`/`it`/`nl`/`pa`/`sv` locales, RTL support, `desktop-menu` native translations — plus a `fix(github): include pull request identity in context` touching only `packages/opencode/src/cli/cmd/github.handler.ts` (the `github` subcommand, unused here) and a revert of an unreleased "fix slow queries". Confirmed by **hashing the five contract-surface files at both tags**: `config.ts`, `config/provider.ts`, `schema.json`, `runtime-flags.ts` and `agent/subagent-permissions.ts` are all identical, so neither `generate-config.py` nor the subagent-permissions note below is affected.
- **One upstream behaviour change worth knowing:** opencode **1.18.2** stopped subagents launching *nested* subagents by default (`packages/opencode/src/agent/subagent-permissions.ts` now denies the `task` permission to a spawned subagent unless its own ruleset grants it). `oh-my-opencode-slim` already adapted in **2.2.3** ("remove redundant subagent depth limiting", released one day later) by dropping its own client-side depth limiting in favour of opencode's native mechanism. Since CI resolves omos to the current **2.2.9**, no action is required here — but this is the one change in the whole range with real behavioural teeth, so the omos variant's multi-agent flow is worth exercising once on this image.
- **`yq` is now mikefarah's Go `yq` v4, not Debian's Python `yq` — behaviour change.** The apt package on Debian/Ubuntu is the unrelated kislyuk/`yq`, a **jq-syntax wrapper** on a 3.x version line; it is a *different program* that happens to share the command name. It has been dropped from the apt list and the mikefarah binary is installed from GitHub instead (multi-arch, following the repo's `latest` convention, pin with `--build-arg YQ_VERSION=vX.Y.Z`). Two reasons: the `cloud-init` repo's `provision.sh`/`deploy.sh` require 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. Brings parity with pi-devbox (its v1.2.3). The smoke test now pins the contract with `yq --version | grep -qE 'mikefarah.*version v4'`, so both a regression to the apt package and a surprise future yq v5 fail CI loudly. **Action required only if** you have scripts in this image calling `yq` with jq-style syntax — they will need porting to v4 expressions.
- **Default models bumped to the Opus tier.** In `rootfs/usr/local/lib/opencode-devbox/generate-config.py`: `DEFAULT_MODELS["anthropic"]``anthropic/claude-opus-5` (was `claude-sonnet-5`; also feeds `FALLBACK_MODEL`), `DEFAULT_MODELS["amazon-bedrock"]``amazon-bedrock/global.anthropic.claude-opus-5`, and `DEFAULT_MODELS["openai"]``openai/gpt-5.6` (was `gpt-5.4`, which had gone stale — `gpt-5.6` shipped 2026-07-09, four days *before* the v2.8.0 cut). `claude-opus-5` was released 2026-07-24, i.e. after v2.8.0. Takes effect only for **new** containers with no `OPENCODE_MODEL` override and no existing config — `generate-config.py` still never overwrites an existing `opencode.jsonc`, which lives on a persistent volume. `.env.example` updated to match.
- **mempalace pinned version bumped `3.5.0``3.6.0`** (`Dockerfile.base`), in lockstep with pi-devbox as the pin's comment requires. 3.6.0 (2026-07-17) is additive/reliability — 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 (that being the exact regression class this pin exists to catch): there are **none**, and nothing touches `diary_write`. 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).
- **Smoke-test size thresholds lifted +650 MB** — base `2950 → 3600`, omos `3650 → 4300` — for the agent-browser/Chromium layer, sized to preserve roughly the same ~250 MB of headroom the previous values had so the guardrail keeps catching *runaway* growth rather than tripping on routine apt drift. A note was added warning **not** to copy pi-devbox's threshold number across: it sums `docker history` while this repo uses `docker image inspect .Size`.
- **Refreshed the bg-subagents removal-trigger "last-checked" markers** (`Dockerfile.base`, `AGENTS.md`) from `opencode 1.17.20 / omos 2.2.0` to `1.18.13 / 2.2.9`, now recording that the check was done against upstream source and not just changelog prose.
### Documentation
- **New README section "Choosing a provider and model"** — makes explicit that the baked defaults are *only* defaults and that nobody is locked to Anthropic or Bedrock: a per-provider default table, `OPENCODE_MODEL` override examples (including a non-Anthropic provider), and the three facts that actually trip people up — defaults seed only a *new* config, an existing `opencode.jsonc` on the persistent volume is never rewritten (so changing `OPENCODE_MODEL` later has no effect until you edit or delete it), and switching model needs no rebuild.
- **New README section "Browser automation (agent-browser)"** with usage examples, the symlink/versioned-path rationale, and an explicit size note for fork maintainers who'd rather drop the layer.
- **README: `opencode-devbox-version` documented** under *Build provenance* with sample output and the drift-check explanation; `COLORTERM` covered under *Terminal compatibility*; new `COLORTERM` and `AGENT_BROWSER_EXECUTABLE_PATH` rows in the env table; the `OPENCODE_MODEL` row now points at the new section.
- **README *Build Args* table drift fixed — four missing args added.** `AGENT_BROWSER_VERSION`, `PLAYWRIGHT_VERSION` and `YQ_VERSION` for the new tools, plus **`GITLEAKS_VERSION`**, which had been missing from the floated-args row despite existing as an ARG. Also added rows for the two *pinned* args that were absent entirely, `MEMPALACE_VERSION` and `DEBIAN_VERSION`, and refreshed the stale `--build-arg OPENCODE_VERSION=1.17.20` example. This is the third consecutive audit to find drift in this one table (v2.8.0 caught `MICRO`/`TEALDEER`/`TYPST`), which is why AGENTS.md carries a standing reminder about it.
- **AGENTS.md: the stale MemPalace `anyOf` convention rewritten.** It still described a perl RUN block that had already been **deleted** when the pin moved to 3.5.0, and asserted "mempalace PyPI latest is 3.4.0 (== our pin), no release contains the fix yet, the workaround must stay" — all three false. Replaced with an accurate account: the workaround is gone, upstream fixed it in 3.5.0, the pin is 3.6.0, and the *standing* rule is preserved (review release notes for MCP tool-schema changes before bumping, and bump in lockstep with pi-devbox).
- **AGENTS.md: two new conventions** — the agent-browser/Chromium size coupling (thresholds must move in the same commit; don't copy pi-devbox's number; why the Chrome path is `find`-ed) and the `yq` identity trap (don't revert it to the apt list, and never install both, because with two `yq` binaries on PATH the meaning of `yq` depends silently on PATH order). Also a *File roles* entry for `rootfs/usr/local/bin/opencode-devbox-version`.
- **THIRD_PARTY.md**: added `agent-browser`, Playwright, and Chromium. The existing "yq (mikefarah)" credit is now actually true.
- Folds in the three docs-only commits made after the v2.8.0 tag, which never got their own `Unreleased` block: a stale AGENTS.md push-step count (5 → 2 variant), a macOS NFD-filename gotcha noted for `dscp`/`scp` in the baked skill, and a re-sync of the vendored mempalace skill snapshot from skillset `63f3bf5`.
## v2.8.0 — 2026-07-13
Minor release. Headline: **default models move to `claude-sonnet-5`** and opencode bumps `1.17.15 → 1.17.20`. Rounds out with a large user-docs backfill — five v2.4.0v2.7.0 base features (typst PDF export, terminal terminfo, Neovim 24-bit colour, the first-shell host SSH reachability check, the baked global gitignore) that had shipped without README prose — plus the bg-subagents removal-trigger "last-checked" markers refreshed to current versions. The marker refresh edits `Dockerfile.base`, so `base-<hash>` advances and the **base image rebuilds** this release (not just the variants).
+86 -3
View File
@@ -50,7 +50,6 @@ RUN apt-get update && \
openssh-client \
gnupg \
jq \
yq \
ripgrep \
fd-find \
tree \
@@ -351,6 +350,29 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
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
@@ -367,7 +389,17 @@ ARG INSTALL_MEMPALACE=true
# `"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.)
ARG MEMPALACE_VERSION=3.5.0
#
# 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 \
@@ -439,6 +471,12 @@ 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
@@ -449,7 +487,9 @@ ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
# *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.17.20 / omos 2.2.0 (2026-07).
# 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) ──
@@ -458,6 +498,45 @@ RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesour
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" ;; \
@@ -551,6 +630,9 @@ RUN tic -x -o /usr/share/terminfo /usr/local/share/terminfo-src/ghostty.terminfo
# ── 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
@@ -561,6 +643,7 @@ 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
+1 -1
View File
@@ -39,7 +39,7 @@ ARG USER_NAME=developer
# 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.17.20
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 ; \
+76 -3
View File
@@ -59,6 +59,8 @@ docker compose run --rm devbox
- **Rust via rustup**`rustup-init` included; bootstrap Rust on demand with `rustup-init -y`
- **Optional runtimes** — Python (apt), Go via build args (Node.js always included — required for opencode v1.x)
- **Multi-agent orchestration** — optional [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) integration via build arg
- **Browser automation**`agent-browser` + a headless Chromium baked in, so the agent can drive a real browser to *verify* front-end work (live DOM, layout, WebGL) instead of guessing
- **YAML/JSON tooling**`jq` plus mikefarah **`yq` v4** (note: replaced Debian's Python `yq` in v2.9.0 — v4 syntax, not jq syntax)
- **AWS CLI v2** — built-in SSO/Bedrock authentication with headless device-code flow
- **Multi-arch** — amd64 and arm64
@@ -125,7 +127,7 @@ docker compose exec -u developer devbox aws --version
| Variable | Description | Default |
|---|---|---|
| `OPENCODE_PROVIDER` | LLM provider (`anthropic`, `openai`, `amazon-bedrock`) | `anthropic` |
| `OPENCODE_MODEL` | Model override | Provider default |
| `OPENCODE_MODEL` | Model override — any `<provider>/<model>` string, written verbatim to the config. See [Choosing a provider and model](#choosing-a-provider-and-model) | Provider default (see below) |
| `ANTHROPIC_API_KEY` | Anthropic API key | — |
| `OPENAI_API_KEY` | OpenAI API key | — |
| `AWS_REGION` | AWS region for Bedrock | `us-east-1` |
@@ -144,6 +146,8 @@ docker compose exec -u developer devbox aws --version
| `LANGUAGE` | Language priority list | `en_US:en` |
| `LC_ALL` | Override all locale settings | `en_US.UTF-8` |
| `EDITOR` | Default text editor | `nvim` |
| `COLORTERM` | Advertises 24-bit colour to colour-aware tools. Export empty (`COLORTERM=`) on a terminal without true-colour support | `truecolor` |
| `AGENT_BROWSER_EXECUTABLE_PATH` | Chromium binary used by `agent-browser` (a stable symlink into Playwright's versioned browser dir) | `/usr/local/bin/agent-chrome` |
| `ENABLE_OMOS` | Enable oh-my-opencode-slim multi-agent orchestration | `false` |
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
| `OMOS_SKILLS` | Symlink bundled OMOS skills from the image into `~/.agents/skills/` each start | `true` |
@@ -151,6 +155,35 @@ docker compose exec -u developer devbox aws --version
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Enable opencode's native background subagents. Baked on in the image because OMOS V2+ default orchestration depends on it. Set `false` to opt out. opencode marks this **experimental** — see [AGENTS.md](AGENTS.md) removal trigger | `true` |
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
### Choosing a provider and model
The image ships a sensible default model **per provider**, but nothing is hard-wired — you can change it without rebuilding.
| `OPENCODE_PROVIDER` | Default model baked in |
|---|---|
| `anthropic` (default) | `anthropic/claude-opus-5` |
| `amazon-bedrock` | `amazon-bedrock/global.anthropic.claude-opus-5` |
| `openai` | `openai/gpt-5.6` |
**These are only defaults.** They apply when `OPENCODE_MODEL` is unset, and only for the provider you selected. To use anything else, set `OPENCODE_MODEL` in your `.env`:
```bash
# A cheaper/faster Anthropic tier
OPENCODE_MODEL=anthropic/claude-sonnet-5
# A different provider entirely — no baked default needed, the value is
# written verbatim as the `model` field, so any provider opencode supports works
OPENCODE_PROVIDER=openai
OPENCODE_MODEL=openai/gpt-5.6-luna
```
A few things worth knowing:
- **You are not locked to Anthropic or Bedrock.** The defaults above lean Anthropic only because that's the most common setup here. Set `OPENCODE_PROVIDER` (plus `OPENCODE_MODEL` if the per-provider default isn't what you want) and the Anthropic defaults never come into play.
- **Defaults only seed a *new* config.** `generate-config.py` never overwrites an existing `~/.config/opencode/opencode.jsonc`, and that file lives on the persistent `devbox-opencode-config` volume — so if you hand-edit the model there, your edit survives restarts *and* image upgrades. Changing `OPENCODE_MODEL` afterwards will **not** rewrite it; edit the config directly, or delete it and let the entrypoint regenerate.
- **Switching model doesn't require a rebuild** — it's an env var, so `docker compose up -d --force-recreate` (with a fresh config, per the point above) is enough.
- Defaults are defined in one place: `DEFAULT_MODELS` in `rootfs/usr/local/lib/opencode-devbox/generate-config.py`.
### Reaching your LAN from the container
The devbox works the same way whether the host is **native Linux Docker** or a **VM-backed** runtime (macOS OrbStack / Docker Desktop, or Docker Desktop on Windows) — but their networking differs:
@@ -281,6 +314,22 @@ pandoc README.md -o readme.pdf --pdf-engine=typst
The bundled pandoc typst template defaults the font to `Libertinus Serif`, so a bare `--pdf-engine=typst` renders without needing `-V mainfont`. For higher-fidelity or complex layouts, install TeX Live on demand and use `--pdf-engine=xelatex` instead.
### Browser automation (agent-browser)
The base bakes the [`agent-browser`](https://www.npmjs.com/package/agent-browser) CLI plus a Playwright-managed headless Chromium, so an agent can drive a **real browser** — open pages, click/fill, `eval` JavaScript, snapshot the DOM, take screenshots — and thereby *verify* front-end work rather than assuming it renders correctly. Useful for checking live DOM state, layout, popup positioning, and WebGL.
`AGENT_BROWSER_EXECUTABLE_PATH` is preset to the baked browser, so it works with no setup:
```bash
agent-browser open https://example.com
agent-browser screenshot --path /workspace/shot.png
agent-browser skills get core --full # full command set, version-matched to the CLI
```
The browser is resolved through the stable symlink `/usr/local/bin/agent-chrome`, which points into Playwright's per-version, per-architecture browser directory — so image upgrades don't break the path. Playwright's redundant `chromium_headless_shell` build is removed at build time; `agent-browser` drives the full Chromium (headless included).
> **Size note:** Chromium is the single largest thing in the base (~625 MB). It ships in *both* variants because verification is broadly useful. If you maintain a fork and don't need it, drop the `agent-browser` layer from `Dockerfile.base` and lower the smoke-test size thresholds accordingly.
### Python development with uv
The image includes Python 3.13 (from Debian Trixie) and [uv](https://docs.astral.sh/uv/), a fast Python package manager that replaces pip, venv, and pyenv:
@@ -452,7 +501,7 @@ Enable optional language runtimes, pin a specific opencode version, or lock any
```bash
docker compose build --build-arg INSTALL_GO=true
docker compose build --build-arg OPENCODE_VERSION=1.17.20
docker compose build --build-arg OPENCODE_VERSION=1.18.13
docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific version
```
@@ -465,7 +514,9 @@ docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific versi
| `INSTALL_OPENCODE` | `true` | Install opencode. Set `false` to build a base with no harness (still includes Bun if `INSTALL_OMOS=true`). |
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
| `GOSU_VERSION`, `FZF_VERSION`, `GIT_LFS_VERSION`, `NVIM_VERSION`, `BAT_VERSION`, `EZA_VERSION`, `ZOXIDE_VERSION`, `UV_VERSION`, `GITEA_MCP_VERSION`, `GO_VERSION`, `OMOS_VERSION`, `MICRO_VERSION`, `TEALDEER_VERSION`, `TYPST_VERSION` | `latest` | All GitHub/Gitea/go.dev-hosted binaries resolve to the newest upstream release at build time. Override with a specific version to pin. Resolved versions are logged in CI output. |
| `MEMPALACE_VERSION` | *(pinned per release)* | MemPalace PyPI version. Deliberately pinned so every bump is a reviewable diff — a past unpinned install swept in an MCP schema regression. Bumped in lockstep with the sibling `pi-devbox` repo. Unrelated to `MEMPALACE_TOOLKIT_REF` despite the shared prefix. |
| `DEBIAN_VERSION` | `trixie-slim` | OS base image tag. Pinned to a codename; apt resolves updates within that release. |
| `GOSU_VERSION`, `FZF_VERSION`, `GIT_LFS_VERSION`, `GITLEAKS_VERSION`, `NVIM_VERSION`, `BAT_VERSION`, `EZA_VERSION`, `ZOXIDE_VERSION`, `UV_VERSION`, `GITEA_MCP_VERSION`, `GO_VERSION`, `OMOS_VERSION`, `MICRO_VERSION`, `TEALDEER_VERSION`, `TYPST_VERSION`, `YQ_VERSION`, `AGENT_BROWSER_VERSION`, `PLAYWRIGHT_VERSION` | `latest` | All GitHub/Gitea/go.dev/npm-hosted binaries resolve to the newest upstream release at build time. Override with a specific version to pin. Resolved versions are logged in CI output. |
> **Reproducibility note:** With `latest` defaults, two builds of the same `v{opencode}` tag may embed different tool versions if upstream releases have happened in between. This is intentional — it means every rebuild picks up upstream CVE fixes automatically. If you need a bit-for-bit reproducible build, pass explicit `*_VERSION` args. The CI smoke test logs the resolved versions for every release build.
@@ -704,6 +755,26 @@ docker inspect --format '{{json .Config.Labels}}' joakimp/opencode-devbox:latest
docker run --rm --entrypoint= joakimp/opencode-devbox:latest cat /etc/opencode-devbox/build-manifest.json
```
From **inside** a running container, `opencode-devbox-version` reads that manifest for you — and it is printed automatically as the first line of output when the container starts, so "which image am I in?" is answered before you ask:
```bash
opencode-devbox-version # human-readable summary
opencode-devbox-version --json # raw manifest, for scripting
opencode-devbox-version --quiet # one line: "v2.9.0 (a1b2c3d)"
```
```text
opencode-devbox v2.9.0
built: 2026-08-04T12:00:00Z (source a1b2c3d4e5f6)
opencode: 1.18.13
components:
opencode: 1.18.13
oh-my-opencode-slim: 2.2.9
mempalace-toolkit: 0123456789ab
```
It also performs a **drift check**: because `npm install -g` as the `developer` user lands on the persistent config volume, a locally-installed `opencode` can shadow the baked one. If the live version differs from the baked one, the command says so instead of silently reporting the manifest's value.
### Storage
Two separate named volumes keep different data classes apart:
@@ -811,6 +882,8 @@ rm ~/.bash_aliases
The base ships `ncurses-term` and `kitty-terminfo` on top of the default `ncurses-base`, plus a compiled `xterm-ghostty` alias, so modern terminal emulators resolve their `TERM` correctly over SSH instead of degrading to a dumb fallback. Covered out of the box: WezTerm, Alacritty, foot, st, kitty (`xterm-kitty`), Ghostty (`xterm-ghostty`), and iTerm2 / xterm (`xterm-256color`).
`COLORTERM=truecolor` is also baked in, so colour-aware tools (Neovim's auto-detect, `bat`, `delta`) render in 24-bit colour instead of falling back to 256 colours. Pairs with the system-wide Neovim `termguicolors` default. If you connect from a terminal that lacks true-colour support, export `COLORTERM=` (empty) to opt out.
## Global gitignore
The image bakes a `~/.gitignore_global` and wires it via `git config --global core.excludesFile`, so personal/tooling artifacts are ignored across every repo in the container without per-repo `.gitignore` entries. Seeded patterns include `*.bak`, `*.bak.*`, `*~`, `*.orig`, `*.swp`, `*.tmp`, and `**/.claude/settings.local.json` (Claude Code's per-machine settings, which can carry credentials). It is seeded only if absent — edit it freely, and your version survives recreate — and the `core.excludesFile` wiring is skipped if you already set one.
+3
View File
@@ -32,6 +32,9 @@ at `/usr/share/doc/<package>/copyright`.
| Typst | github.com/typst/typst | Apache-2.0 |
| Graphviz | graphviz.org | CPL-1.0 |
| ripgrep / fd / bat / eza / zoxide / tealdeer / yq (mikefarah) | respective repos | MIT / Apache-2.0 / Unlicense (varies) |
| agent-browser | npmjs.com/package/agent-browser | see package |
| Playwright | github.com/microsoft/playwright | Apache-2.0 |
| Chromium *(fetched by Playwright)* | chromium.org | BSD-3-Clause + others |
| bun *(`-omos` variant only)* | github.com/oven-sh/bun | MIT |
## Base OS
+8
View File
@@ -1,6 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# ── Startup banner: which opencode-devbox build is this? ───────────
# Printed FIRST, before the setup noise below, so it's the first thing visible
# when the container starts (CMD is `bash -l`, tty:true in compose, so this
# reaches the same stream as the interactive shell the user lands in). Reads the
# ground-truth manifest baked in Dockerfile.variant; a no-op with a short stderr
# notice on images built before it existed.
command -v opencode-devbox-version >/dev/null 2>&1 && opencode-devbox-version || true
# ── SSH ControlMaster socket dir ────────────────────────────────
# Companion to /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf in the
# base image — that file declares ControlPath=/tmp/sshcm/%r@%h:%p; this
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# opencode-devbox-version — show which opencode-devbox image build is running.
#
# WHY THIS EXISTS
# The image bakes ground-truth build info into
# /etc/opencode-devbox/build-manifest.json at `docker build` time (see
# Dockerfile.variant): the release tag, build date, source commit, the live
# `opencode --version` at build time, the installed oh-my-opencode-slim
# version (omos variant only), and the actual checked-out commit of the
# /opt/mempalace-toolkit clone. That answers "what image am I running?" —
# but only if you know to go look for the file. This wraps it into one
# command, prints it human-first at container start (see entrypoint-user.sh),
# and stays available on demand for the rest of the session.
#
# USAGE
# opencode-devbox-version human-readable summary (default)
# opencode-devbox-version --json raw manifest JSON (for scripting)
# opencode-devbox-version --quiet one-line "release_tag (source_revision)"
#
# EXIT STATUS
# 0 on success. 1 if the manifest is missing (e.g. an image built before
# this file existed, or a non-opencode-devbox base) — prints a short notice
# to stderr rather than failing silently.
set -euo pipefail
MANIFEST=/etc/opencode-devbox/build-manifest.json
MODE="human"
case "${1:-}" in
--json) MODE="json" ;;
--quiet|-q) MODE="quiet" ;;
--help|-h)
sed -n '2,22p' "$0" | sed 's/^# \?//'
exit 0
;;
esac
if [ ! -f "$MANIFEST" ]; then
echo "opencode-devbox-version: no build manifest at $MANIFEST" >&2
echo " (image predates the manifest, or this isn't an opencode-devbox image)" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "opencode-devbox-version: jq not found; dumping raw manifest instead" >&2
cat "$MANIFEST"
exit 0
fi
if [ "$MODE" = "json" ]; then
cat "$MANIFEST"
exit 0
fi
release_tag=$(jq -r '.release_tag' "$MANIFEST")
build_date=$(jq -r '.build_date' "$MANIFEST")
source_rev=$(jq -r '.source_revision' "$MANIFEST")
opencode_version_baked=$(jq -r '.opencode_version' "$MANIFEST")
if [ "$MODE" = "quiet" ]; then
printf '%s (%s)\n' "$release_tag" "${source_rev:0:7}"
exit 0
fi
# Live drift check: has `opencode` been upgraded since this container was built?
# The image is immutable, but `npm install -g` as the developer user lands on the
# persistent devbox-opencode-config volume (NPM_CONFIG_PREFIX is
# ~/.config/opencode/npm-global), which CAN shadow the baked /usr binary. So we
# report the live version and flag a mismatch rather than trusting the manifest
# blindly — same "ground truth over intent" spirit as how the manifest itself is
# generated in Dockerfile.variant.
opencode_version_live=""
if command -v opencode >/dev/null 2>&1; then
opencode_version_live=$(opencode --version 2>/dev/null | head -n1 | tr -d '\r\n')
fi
printf 'opencode-devbox %s\n' "$release_tag"
printf ' built: %s (source %s)\n' "$build_date" "${source_rev:0:12}"
if [ -n "$opencode_version_live" ] && [ "$opencode_version_live" != "$opencode_version_baked" ]; then
printf ' opencode: %s \033[33m(baked as %s — drift detected)\033[0m\n' \
"$opencode_version_live" "$opencode_version_baked"
else
printf ' opencode: %s\n' "${opencode_version_live:-$opencode_version_baked}"
fi
printf ' components:\n'
jq -r '.components | to_entries[] | select(.value != null) | " \(.key): \(.value[0:12])"' "$MANIFEST"
@@ -39,11 +39,20 @@ import shutil
import sys
from pathlib import Path
# Default model per provider. Update here when upstream changes.
# Default model per provider. Update here when upstream ships a newer model.
#
# THESE ARE ONLY DEFAULTS — they apply when OPENCODE_MODEL is unset, and only
# for the provider selected by OPENCODE_PROVIDER. Any of them is overridden by
# setting OPENCODE_MODEL=<provider>/<model> in .env (see .env.example), and the
# generated opencode.jsonc is never overwritten on later starts, so a hand-edit
# of the config also survives. Users who don't use Anthropic or Bedrock should
# set OPENCODE_PROVIDER (and OPENCODE_MODEL if the per-provider default below
# isn't what they want) rather than editing this file — see the README section
# "Choosing a provider and model".
DEFAULT_MODELS: dict[str, str] = {
"anthropic": "anthropic/claude-sonnet-5",
"openai": "openai/gpt-5.4",
"amazon-bedrock": "amazon-bedrock/global.anthropic.claude-sonnet-5",
"anthropic": "anthropic/claude-opus-5",
"openai": "openai/gpt-5.6",
"amazon-bedrock": "amazon-bedrock/global.anthropic.claude-opus-5",
}
# Fallback when OPENCODE_PROVIDER is set but not recognized.
+43 -3
View File
@@ -139,7 +139,13 @@ run "fzf" "fzf --version"
run "fd" "fd --version"
run "rg" "rg --version | head -1"
run "jq" "jq --version"
run "yq" "yq --version"
# yq MUST be mikefarah's Go yq v4, NOT Debian's `yq` apt package (the unrelated
# Python kislyuk/yq — a jq wrapper on a 3.x line with incompatible syntax). v2.9.0
# swapped the apt package for the mikefarah binary. Pinning the contract to major
# v4 makes BOTH a regression to the Python package AND a surprise future yq v5
# fail CI loudly, instead of silently breaking the cloud-init repo's
# provision.sh/deploy.sh which require v4 syntax.
run "yq is mikefarah v4" "yq --version | grep -qE 'mikefarah.*version v4' && yq --version"
run "git-crypt" "git-crypt --version | head -1"
run "gitleaks" "gitleaks version"
run "aws" "aws --version"
@@ -148,6 +154,17 @@ run "gosu" "gosu --version"
run "tmux" "tmux -V"
run "pandoc" "pandoc --version | head -1"
run "typst" "typst --version"
# agent-browser + its Chromium. The ENV must point at a resolvable executable:
# AGENT_BROWSER_EXECUTABLE_PATH -> /usr/local/bin/agent-chrome -> Playwright's
# per-version, per-ARCH chrome binary. Asserting the resolved target is
# executable catches a Playwright layout change (the reason the Dockerfile
# `find`s the binary instead of hardcoding the path) rather than just checking
# that a dangling symlink exists.
run "agent-browser" "agent-browser --version"
run "agent-chrome resolves to an executable" \
"test -x \"\$(readlink -f /usr/local/bin/agent-chrome)\" && readlink -f /usr/local/bin/agent-chrome"
run_expect "AGENT_BROWSER_EXECUTABLE_PATH baked" \
"printenv AGENT_BROWSER_EXECUTABLE_PATH" "/usr/local/bin/agent-chrome"
run "pandoc+typst PDF engine" "printf '# hi\n' | pandoc --pdf-engine=typst -o /tmp/_smoke.pdf - && test -s /tmp/_smoke.pdf; rm -f /tmp/_smoke.pdf"
run "graphviz (dot)" "dot -V"
run "tldr (tealdeer)" "tldr --version"
@@ -157,6 +174,9 @@ run "dot-watch" "test -x /usr/local/bin/dot-watch && bash -n /usr/local/
# and OMOS V2+ default orchestration depends on it. Baked ON as an ENV in
# Dockerfile.base — assert it's present in the image environment (both variants).
run_expect "bg-subagents env baked" "printenv OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS" "true"
# True-colour advertisement for colour-aware tools (bat, delta, Neovim's
# auto-detect). Pairs with the terminfo entries + sysinit.vim termguicolors.
run_expect "COLORTERM env baked" "printenv COLORTERM" "truecolor"
# SSH ControlMaster baked defaults: the config file must exist (image-level)
# and ssh -G must report ControlPath rooted at /tmp/sshcm/ for an arbitrary
@@ -242,6 +262,17 @@ run_expect "manifest records opencode_version" \
"cat /etc/opencode-devbox/build-manifest.json" '"opencode_version"'
run_expect "manifest records mempalace-toolkit component" \
"cat /etc/opencode-devbox/build-manifest.json" '"mempalace-toolkit"'
run_expect "manifest records release_tag" \
"cat /etc/opencode-devbox/build-manifest.json" '"release_tag"'
# The manifest is only useful if something can READ it. v2.9.0 added
# opencode-devbox-version as that reader (and entrypoint-user.sh prints it at
# container start), so assert the command itself works in all three modes —
# otherwise the manifest stays an invisible artifact, which is what it was for
# every release before this one.
run_expect "opencode-devbox-version --json emits the manifest" \
"opencode-devbox-version --json" '"release_tag"'
run "opencode-devbox-version --quiet" "opencode-devbox-version --quiet"
run "opencode-devbox-version (human)" "opencode-devbox-version | head -1"
# Every resolved component must be a real value, never the 'unknown'
# sentinel that rev()/version lookups emit on failure. (oh-my-opencode-slim
# is JSON null in the base variant — that is expected, not 'unknown'.)
@@ -403,8 +434,17 @@ echo " Uncompressed size: ${SIZE_MB} MB"
# headroom keeps the guardrail catching *runaway* growth (accidental texlive/
# chrome bake-in) rather than tripping on routine apt drift or a minor opencode
# bump. smoke still prints the actual landed size each run; tighten if low.
THRESHOLD=2950
[ "$VARIANT" = "omos" ] && THRESHOLD=3650
# v2.9.0: bumped +650 MB (2950->3600 base, 3650->4300 omos) for agent-browser +
# Playwright Chromium on the BASE layer (~625 MB after dropping the redundant
# chromium_headless_shell build). Sized to keep roughly the same ~250 MB of
# headroom the previous thresholds had, so the guardrail still catches *runaway*
# growth (an accidental texlive or a second browser) rather than tripping on
# routine apt drift. NOTE: do NOT copy pi-devbox's threshold number across — it
# sums `docker history` while this script uses `docker image inspect .Size`, so
# the two are not directly comparable. smoke prints the actual landed size every
# run; tighten these if they come in low.
THRESHOLD=3600
[ "$VARIANT" = "omos" ] && THRESHOLD=4300
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
else