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
+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.