From 37960186c614b9d0c7f13f634304d1942953d388 Mon Sep 17 00:00:00 2001 From: pi Date: Tue, 4 Aug 2026 16:31:13 +0200 Subject: [PATCH] =?UTF-8?q?release:=20v2.9.0=20=E2=80=94=20agent-browser,?= =?UTF-8?q?=20manifest=20reader,=20opencode=201.18.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .env.example | 8 +- AGENTS.md | 10 ++- CHANGELOG.md | 34 +++++++ Dockerfile.base | 89 ++++++++++++++++++- Dockerfile.variant | 2 +- README.md | 79 +++++++++++++++- THIRD_PARTY.md | 3 + entrypoint-user.sh | 8 ++ rootfs/usr/local/bin/opencode-devbox-version | 88 ++++++++++++++++++ .../lib/opencode-devbox/generate-config.py | 17 +++- scripts/smoke-test.sh | 46 +++++++++- 11 files changed, 366 insertions(+), 18 deletions(-) create mode 100755 rootfs/usr/local/bin/opencode-devbox-version diff --git a/.env.example b/.env.example index 3e0ee3b..e72c74e 100644 --- a/.env.example +++ b/.env.example @@ -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: /. +# OPENCODE_MODEL=anthropic/claude-opus-5 # ── API Keys (set the one matching your provider) ──────────────────── # ANTHROPIC_API_KEY= diff --git a/AGENTS.md b/AGENTS.md index 175a7a7..20eca17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 03897c1..7a4a0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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-` 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.0–v2.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-` advances and the **base image rebuilds** this release (not just the variants). diff --git a/Dockerfile.base b/Dockerfile.base index ba1d7fa..8b76ece 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -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 diff --git a/Dockerfile.variant b/Dockerfile.variant index 19b45c4..db32f50 100644 --- a/Dockerfile.variant +++ b/Dockerfile.variant @@ -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 ; \ diff --git a/README.md b/README.md index 9c2dd9b..aa6f1e1 100644 --- a/README.md +++ b/README.md @@ -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 `/` 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. diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md index 23a5b53..1cdfa61 100644 --- a/THIRD_PARTY.md +++ b/THIRD_PARTY.md @@ -32,6 +32,9 @@ at `/usr/share/doc//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 diff --git a/entrypoint-user.sh b/entrypoint-user.sh index 66ad4a1..bf99027 100644 --- a/entrypoint-user.sh +++ b/entrypoint-user.sh @@ -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 diff --git a/rootfs/usr/local/bin/opencode-devbox-version b/rootfs/usr/local/bin/opencode-devbox-version new file mode 100755 index 0000000..b79c163 --- /dev/null +++ b/rootfs/usr/local/bin/opencode-devbox-version @@ -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" diff --git a/rootfs/usr/local/lib/opencode-devbox/generate-config.py b/rootfs/usr/local/lib/opencode-devbox/generate-config.py index ecef2ac..f02ddc8 100755 --- a/rootfs/usr/local/lib/opencode-devbox/generate-config.py +++ b/rootfs/usr/local/lib/opencode-devbox/generate-config.py @@ -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=/ 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. diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 5574c48..4a7f9d3 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -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