Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13219d1d55 | |||
| 37960186c6 | |||
| 5fb07e0a39 | |||
| d2f2396c4a | |||
| bade89ff35 | |||
| 03b8fa431e |
+6
-2
@@ -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=
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
name: Lint workflows
|
||||
name: Lint
|
||||
|
||||
# Durable guard against CI-workflow bugs — most importantly the "bash-only
|
||||
# syntax under the default `sh`/dash shell" footgun. Ported from pi-devbox,
|
||||
# where this class broke resolve-versions (ed49b8d) and promote-base-latest
|
||||
# (b7197e8 → run 418). actionlint runs shellcheck against each `run:` step
|
||||
# using its *effective* shell, so `set -o pipefail` under dash is flagged as
|
||||
# SC3040 before any expensive build runs. This is cheap (~10s) and independent
|
||||
# of the build pipeline, so it fires on every push/PR — not just on release
|
||||
# tags, which is where docker-publish-split.yml is otherwise only triggered.
|
||||
# The repo's cheap-checks workflow: everything that can be verified WITHOUT
|
||||
# building an image. Runs on every push and PR, in ~30s, independent of the
|
||||
# build pipeline. Three jobs: workflow lint (actionlint + the Gitea shell
|
||||
# guard), Dockerfile lint (hadolint), and the DOCKER_HUB.md docs-sync check.
|
||||
#
|
||||
# Deliberately the ONLY workflow that runs on a push to main. validate.yml
|
||||
# (amd64 build + smoke) is PR/dispatch-only and docker-publish-split.yml is
|
||||
# tag-only, so pushing work-in-progress to main never starts an image build.
|
||||
#
|
||||
# Its original purpose, still the most important one: the "bash-only syntax
|
||||
# under the default `sh`/dash shell" footgun. Ported from pi-devbox, where this
|
||||
# class broke resolve-versions (ed49b8d) and promote-base-latest (b7197e8 → run
|
||||
# 418). actionlint runs shellcheck against each `run:` step using its
|
||||
# *effective* shell, so `set -o pipefail` under dash is flagged as SC3040
|
||||
# before any expensive build runs.
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
@@ -90,3 +97,20 @@ jobs:
|
||||
|
||||
- name: Run hadolint
|
||||
run: hadolint Dockerfile.base Dockerfile.variant
|
||||
|
||||
docs-check:
|
||||
# Fails if DOCKER_HUB.md is out of sync with what generate-dockerhub-md.py
|
||||
# would produce from HUB_TEMPLATE. Keeps the two docs from drifting.
|
||||
#
|
||||
# Lives here rather than in validate.yml because it needs no image: keeping
|
||||
# it in the cheap workflow means it still runs on every push to main now
|
||||
# that validate.yml is PR/dispatch-only. Reproduce locally with
|
||||
# `python3 scripts/generate-dockerhub-md.py --check`.
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check DOCKER_HUB.md is in sync with HUB_TEMPLATE
|
||||
run: python3 scripts/generate-dockerhub-md.py --check
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
name: Validate
|
||||
|
||||
# Lightweight validation on pushes to main. Builds single-arch (amd64),
|
||||
# runs the smoke test, and checks image size — without pushing anything
|
||||
# to Docker Hub. Tag pushes are handled by docker-publish-split.yml which
|
||||
# does the full multi-arch split-base build-and-push.
|
||||
# Image build + smoke test. This workflow has NO push trigger at all — it fires
|
||||
# only on pull_request and on explicit workflow_dispatch.
|
||||
#
|
||||
# Division of labour across this repo's three workflows:
|
||||
# lint.yml — cheap checks, every push/PR: workflow lint,
|
||||
# Dockerfile lint, DOCKER_HUB.md sync. No builds.
|
||||
# validate.yml (this file) — amd64 build + smoke test. PR / manual only.
|
||||
# docker-publish-split.yml — the release path, tag-only: multi-arch build,
|
||||
# smoke gates, then push + promote.
|
||||
#
|
||||
# Net effect: no image build can start from a push to main. A tag — or an
|
||||
# explicit dispatch — is required. That is safe because the release path fails
|
||||
# closed: variant tags are pushed only after smoke-base/smoke-omos pass, and
|
||||
# base-latest is promoted last, so an aborted release leaves at worst an
|
||||
# unreferenced base-<hash> blob on Hub, never a half-published version tag.
|
||||
#
|
||||
# To validate before tagging: open a PR, dispatch this workflow, or dispatch
|
||||
# docker-publish-split.yml against a throwaway tag with promote_latest=false
|
||||
# (the only route that also exercises a CHANGED BASE — see the trade-off below).
|
||||
#
|
||||
# Trade-off: variant builds here use the published `base-latest` image
|
||||
# from Docker Hub as their parent, NOT a locally-built base. This is
|
||||
@@ -22,18 +37,10 @@ name: Validate
|
||||
# blind-spot applies.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'CHANGELOG.md'
|
||||
- 'README.md'
|
||||
- 'DOCKER_HUB.md'
|
||||
- 'deploy/**'
|
||||
- '.gitleaks.toml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# Gitea Actions' default step shell is `sh` (dash); force bash workflow-wide so
|
||||
# no run: step silently falls through to dash. Enforced by lint.yml's
|
||||
@@ -44,20 +51,6 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
docs-check:
|
||||
# Fails if DOCKER_HUB.md is out of sync with what generate-dockerhub-md.py
|
||||
# would produce from README.md. Keeps the two docs from drifting.
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check DOCKER_HUB.md is in sync with README.md
|
||||
run: |
|
||||
python3 scripts/generate-dockerhub-md.py --check
|
||||
|
||||
base-change-warning:
|
||||
# Surfaces a warning when this commit changes base-image inputs
|
||||
# (Dockerfile.base, rootfs/, entrypoint*.sh) AND exports `base_changed` so
|
||||
@@ -97,12 +90,21 @@ jobs:
|
||||
printf '%s\n' "$changed"
|
||||
else
|
||||
echo "base_changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No base-image inputs changed in this commit — validate.yml fully exercises the published base-latest."
|
||||
echo "No base-image inputs changed in this commit — validate-base/validate-omos will build against the published base-latest."
|
||||
fi
|
||||
|
||||
validate-base:
|
||||
needs: [base-change-warning]
|
||||
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' }}
|
||||
# Two gates, both must hold for this job to run:
|
||||
# 1. base_changed != true — the documented blind spot: this workflow builds
|
||||
# variants on top of Hub's base-latest, so a commit changing base inputs
|
||||
# would either not exercise the change or hard-fail against a stale
|
||||
# base-latest (see base-change-warning).
|
||||
# 2. event_name != push — belt-and-braces. Redundant today (this workflow
|
||||
# has no push trigger), kept deliberately so that re-adding a push
|
||||
# trigger later cannot silently re-enable image builds on every push to
|
||||
# main. If you intend that, remove this clause explicitly.
|
||||
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' && github.event_name != 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
@@ -165,7 +167,8 @@ jobs:
|
||||
|
||||
validate-omos:
|
||||
needs: [base-change-warning]
|
||||
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' }}
|
||||
# Same two gates as validate-base — see the comment there.
|
||||
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' && github.event_name != 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
|
||||
@@ -21,15 +21,16 @@ 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).
|
||||
- `DOCKER_HUB.md` — **auto-generated** from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
|
||||
- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth.
|
||||
- `.gitea/README.md` — **read this first** if you're touching CI. Architectural overview of the build pipeline (production vs split-base), wall-clock estimates, NPM_CONFIG_PREFIX gotcha, runner expectations, migration plan.
|
||||
- `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
|
||||
- `.gitea/workflows/validate.yml` — amd64 build + smoke test for both variants. **No push trigger at all**: `pull_request` and `workflow_dispatch` only, so it never runs on a push to main. See the trigger-model convention below.
|
||||
- `.gitea/workflows/docker-publish-split.yml` — production CI pipeline on tag push (`v*`). Two-phase split-base: computes base hash, conditionally builds base, runs 2 parallel smoke tests, then 2 parallel multi-arch variant builds, promotes `base-latest` alias, updates Docker Hub description.
|
||||
- `.gitea/workflows/lint.yml` — cheap (~10s) workflow-lint on every push/PR/dispatch: a Gitea-accurate shell guard (`scripts/check-workflow-shell.sh`) plus pinned actionlint + shellcheck. The guard asserts every `run:` step resolves to `bash` under Gitea's `sh` default, closing the actionlint blind spot (actionlint models GitHub, whose default `run` shell is bash). Ported from pi-devbox.
|
||||
- `.gitea/workflows/lint.yml` — the repo's cheap-checks workflow (~30s) and the **only** workflow that runs on a push to main. Three jobs: (1) `actionlint` — a Gitea-accurate shell guard (`scripts/check-workflow-shell.sh`) plus pinned actionlint + shellcheck, asserting every `run:` step resolves to `bash` under Gitea's `sh` default and closing the actionlint blind spot (actionlint models GitHub, whose default `run` shell is bash); (2) `hadolint` — pinned 2.14.0 against both Dockerfiles, config in `.hadolint.yaml` so a local run reproduces CI; (3) `docs-check` — `DOCKER_HUB.md` vs `HUB_TEMPLATE` sync, moved here from `validate.yml` so it survives that workflow becoming PR/dispatch-only. Ported from pi-devbox.
|
||||
|
||||
## Versioning scheme
|
||||
|
||||
@@ -105,10 +106,15 @@ curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 |
|
||||
- **`OMOS_VERSION` MUST be passed by CI as a concrete version**, not left at the `latest` default. The npm install step in `Dockerfile.variant` (`oh-my-opencode-slim@${OMOS_VERSION}`) produces an identical layer-hash when the ARG value is byte-identical across builds; combined with the registry buildcache (`base-buildcache`) the layer gets reused even when `latest` would have resolved to a newer upstream. This is the same class of bug that bit pi-devbox v0.74.0 → v0.75.5 (silent same-bytes-across-releases regression discovered 2026-05-23, fixed in pi-devbox v0.75.5b). It is currently *masked* in opencode-devbox by `OPENCODE_VERSION` being a hard-coded ARG that bumps every release — that bump invalidates the parent-chain cache key for the downstream omos layer — but the masking would fail the moment a `vN.N.Nb` opencode-version-unchanged release ships that only bumps omos. Preventative fix: `.gitea/workflows/docker-publish-split.yml` has a `resolve-versions` job that runs `npm view oh-my-opencode-slim version`, exposing the concrete value as an output that the omos smoke + build jobs consume via build-args. Smoke tests assert via the `EXPECTED_OMOS_VERSION` env var — would catch the regression on the next release rather than several releases later. **If you change the variant build-args list, the resolve-versions job, or the smoke EXPECTED_*_VERSION wiring, audit all affected jobs in lockstep.**
|
||||
- **`resolve-versions` also pins `mempalace-toolkit` to a SHA** — `resolve-versions` resolves the `mempalace-toolkit` `main` HEAD to a commit SHA (`mempalace_toolkit_ref` output) via the gitea commits API (`/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main`; gitea allows **unauthenticated** public-repo listing, so no secret is required). Unlike every other dependency, `mempalace-toolkit` is cloned in **`Dockerfile.base`**, not the variant — so the resolve→build-arg→variant plumbing bypasses it. To make a moved toolkit actually land, the resolved SHA is **folded into the `base-decide` hash** (so `base_tag` changes → base rebuilds) AND passed to `build-base` as `--build-arg MEMPALACE_TOOLKIT_REF`. Consequently **`base-decide` now depends on `resolve-versions`** (they no longer run in parallel), and the base clone uses a SHA-capable `git fetch <ref> + checkout FETCH_HEAD` (a `git clone --branch <40-char-SHA>` would fail). Trade-off: `base_tag` now reflects a live gitea lookup — on an API blip it falls back to `main`, triggering one *extra* base rebuild, never a *missed* one. If you touch `resolve-versions`, `base-decide`'s hash inputs, or the `build-base` build-args, audit `.gitea/README.md` Step 1 in lockstep.
|
||||
- **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 + 5 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.
|
||||
- **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.
|
||||
@@ -122,7 +128,8 @@ curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 |
|
||||
|
||||
- Both build jobs include an IPv4 preference step (`gai.conf` + `driver-opts: network=host` for buildx) to work around intermittent IPv6 failures on the Gitea runners.
|
||||
- `update-description` job runs when the base variant published (`needs: [build-variant-base, build-variant-omos]`, gated with `always()` + an explicit `build-variant-base.result == 'success'` check so a partial-publish run still refreshes the Hub description).
|
||||
- Tags must be pushed to trigger the publish workflow. The validate workflow runs on push to main and PRs.
|
||||
- **No image build happens on a push — builds are tag-triggered or explicitly requested.** The three workflows divide cleanly: `lint.yml` is cheap checks on every push/PR (actionlint + shell guard, hadolint, `docs-check`) and is the **only** workflow a push to main triggers; `validate.yml` is the amd64 build + smoke test and has **no push trigger** (pull_request / workflow_dispatch only); `docker-publish-split.yml` is the release path (tag-only). So pushing work-in-progress to main is free. 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. `validate-base`/`validate-omos` additionally keep a now-redundant `github.event_name != 'push'` clause as belt-and-braces, so re-adding a push trigger can't silently re-enable builds. **To validate before tagging**, open a PR, `workflow_dispatch` the Validate workflow, or `workflow_dispatch` `docker-publish-split.yml` against a throwaway tag with `promote_latest=false` (the only option that also exercises a changed base, which `validate.yml` structurally cannot — it builds variants on top of Hub's `base-latest`). **Keep cheap checks in `lint.yml`, not `validate.yml`** — anything that needs no image belongs in the workflow that actually runs on push; that is why `docs-check` lives there. If you change this model, update this bullet, both workflow header comments, and the two *File roles* entries together.
|
||||
- Tags must be pushed to trigger the publish workflow. A push to main triggers `lint.yml` only.
|
||||
- Smoke tests run on amd64 only (single-arch load into the local daemon). The multi-arch push happens after smoke passes.
|
||||
- **Gitea Actions runner has ~40 GB disk, often 70%+ used at job start.** All `load: true` jobs (`validate-base`, `validate-omos`, `smoke-base`, `smoke-omos`) include a `Reclaim runner disk` step that strips catthehacker-resident toolchains and prunes stale docker state before `setup-buildx-action`. Build jobs use a lighter version (push-by-digest doesn't need `docker system prune`). Don't remove these steps without testing on a fresh runner.
|
||||
- **`docker/build-push-action@v7` with `platforms: linux/amd64,linux/arm64` handles multi-arch push natively in a single job** — produces a proper manifest list, no matrix or merge step needed. An earlier revision split into per-arch matrix jobs with digest artifacts, but that pattern requires `actions/{upload,download}-artifact@v4+` which Gitea Actions doesn't support (see below).
|
||||
@@ -147,6 +154,36 @@ the token matters mainly for private repos or rate-limit headroom; its
|
||||
lifecycle is host-managed, so there is nothing to revoke after use. Never
|
||||
echo the token value (including into logs).
|
||||
|
||||
**Gotcha — the jobs endpoint takes the internal `id`, NOT the `run_number` the
|
||||
UI shows as `#239`.** The two diverge widely (`id=534` was `run_number=238`),
|
||||
and `GET .../actions/runs/<run_number>/jobs` does **not** error — it silently
|
||||
returns the jobs of a *different* run. This cost a wrong conclusion once: while
|
||||
verifying that a push no longer triggers builds, querying `.../runs/238/jobs`
|
||||
returned `docs-check`/`validate-base`/`validate-omos`, making a `lint.yml` run
|
||||
look like it had built images. Always read `id` from the run listing and use
|
||||
that. Two reliable patterns:
|
||||
|
||||
```bash
|
||||
# Authoritative: which runs did MY commit trigger? Filter on head_sha — do not
|
||||
# trust ordering or run numbering.
|
||||
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
|
||||
"$GITEA_HOST/api/v1/repos/joakimp/opencode-devbox/actions/runs?limit=20" \
|
||||
| jq --arg sha "$(git rev-parse HEAD)" \
|
||||
'.workflow_runs[] | select(.head_sha==$sha) | {id, run_number, path, event, status, conclusion}'
|
||||
|
||||
# Then the per-job breakdown, keyed by the id from above (NOT run_number)
|
||||
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
|
||||
"$GITEA_HOST/api/v1/repos/joakimp/opencode-devbox/actions/runs/<id>/jobs" \
|
||||
| jq '.jobs[] | {name, status, conclusion}'
|
||||
```
|
||||
|
||||
Counting runs per commit is also the cheapest way to assert the trigger model
|
||||
from the previous section: commits before the split show **2** runs
|
||||
(`lint.yml` + `validate.yml`), commits after show **1** (`lint.yml`). Note
|
||||
`lint.yml` is cheap in CPU but not always in wall-clock — its `actionlint` job
|
||||
`apt-get install`s shellcheck inside the container and has taken 6–15 min on a
|
||||
busy runner, so a still-`in_progress` lint run is not evidence of a problem.
|
||||
|
||||
## Testing changes
|
||||
|
||||
The smoke test (`scripts/smoke-test.sh`) is the canonical check and runs automatically in CI. To run locally:
|
||||
|
||||
@@ -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.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-<hash>` advances and the **base image rebuilds** this release (not just the variants).
|
||||
|
||||
+86
-3
@@ -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
@@ -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 ; \
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+88
@@ -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.
|
||||
|
||||
@@ -44,4 +44,4 @@ and the entrypoint's never-overwrite-a-real-file guard never has to arbitrate.
|
||||
|
||||
cp <skillset>/skills/mempalace/SKILL.md skills/mempalace/SKILL.md
|
||||
|
||||
Snapshot provenance at last refresh: skillset `8e8db64`.
|
||||
Snapshot provenance at last refresh: skillset `63f3bf5`.
|
||||
|
||||
@@ -43,6 +43,30 @@ Run these immediately when a session begins, before responding to the user:
|
||||
|
||||
Do NOT announce this to the user. Just do it silently to orient yourself.
|
||||
|
||||
### Temporal grounding — compute time deltas, don't guess
|
||||
|
||||
Diary entries and drawers carry real timestamps (`timestamp`, `created_at`).
|
||||
Before describing *when* something happened — "yesterday", "earlier today",
|
||||
"last week", "a while back" — **establish the current date/time first and
|
||||
compute the delta against the actual timestamp.** Get "now" from the injected
|
||||
session date or by running `date` in a shell; never infer it.
|
||||
|
||||
**A container recreate or a fresh session is NOT a day boundary.** A devbox
|
||||
container (pi-devbox or opencode-devbox) is frequently restarted — often several
|
||||
times within the *same* day — and each restart begins a new session with a fresh
|
||||
wake-up. Do not reason "new session ⇒ last session was yesterday": two diary
|
||||
entries 90 minutes apart can straddle a container recreate. The only
|
||||
authoritative clock is the timestamp on the memory, not the session/container
|
||||
boundary.
|
||||
|
||||
**Practical rule:** prefer explicit, checkable phrasing — e.g. "earlier today,
|
||||
~8h ago (both 2026-06-25)" — over a vague relative term. If you catch yourself
|
||||
about to write "yesterday" / "last week", subtract `now − entry.timestamp` and
|
||||
state the computed result. (Remember timestamps may be UTC while the wall clock
|
||||
is local — reconcile the offset before computing the delta.) Note too that
|
||||
session feeders can lag up to a week (see *Multi-harness palace*), so a recent
|
||||
absence in `wing_conversations` is not proof nothing happened.
|
||||
|
||||
### Phase 2: Active Session (during work)
|
||||
|
||||
#### Search Before You Speak
|
||||
@@ -294,6 +318,7 @@ Entity-relationship triples with temporal validity. Query with `mempalace_kg_que
|
||||
## Anti-Patterns
|
||||
|
||||
- **Don't guess when you can search.** If a question touches past work, search first.
|
||||
- **Don't infer elapsed time from session or container boundaries.** A restart isn't a new day. Compare the actual timestamp (`timestamp` / `created_at`) against the current date/time before saying "yesterday", "last week", etc.
|
||||
- **Don't skip the diary.** A session without a diary entry is a session forgotten.
|
||||
- **Don't summarize drawer content.** File verbatim — the embedding model needs the original words.
|
||||
- **Don't mine .git directories or node_modules.** The CLI miner respects .gitignore by default.
|
||||
|
||||
@@ -96,6 +96,22 @@ are "command not found" there — you must spell out the underlying command.
|
||||
If a command "works in my terminal but not when the agent runs it," this alias
|
||||
gap is the first thing to suspect.
|
||||
|
||||
**`dscp`/`scp` with accented filenames on a macOS host.** macOS stores filenames
|
||||
in Unicode **NFD** (decomposed — e.g. `ä` is `a` + combining U+0308), while the
|
||||
string you type or paste is usually **NFC** (precomposed `ä`, U+00E4). The bytes
|
||||
differ, so a precomposed remote path *silently* fails to match on the host —
|
||||
`scp … "mac:'~/Desktop/Skärmavbild ….png'"` returns *No such file or directory*
|
||||
even though the file plainly exists. Sidestep the encoding entirely: let the
|
||||
**remote shell expand a wildcard**, or list the directory first and copy the
|
||||
exact name it prints.
|
||||
|
||||
```sh
|
||||
# glob dodges the NFC/NFD mismatch (the remote shell matches the real bytes):
|
||||
scp -F "$HOME/.ssh-local/config" "mac:~/Desktop/Sk*rmavbild*.png" ./
|
||||
# or read the exact filename first, then copy that:
|
||||
ssh -F "$HOME/.ssh-local/config" mac 'ls -1 ~/Desktop/*.png'
|
||||
```
|
||||
|
||||
## 3. Reaching the Docker host and its LAN over SSH
|
||||
|
||||
When the host is VM-backed (e.g. OrbStack / Docker Desktop on macOS) the
|
||||
|
||||
+43
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user