Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13219d1d55 | |||
| 37960186c6 | |||
| 5fb07e0a39 | |||
| d2f2396c4a | |||
| bade89ff35 | |||
| 03b8fa431e | |||
| 046a5aeb3b | |||
| fac14e2c56 | |||
| cc890a90e7 | |||
| 1295c56930 | |||
| 0cbcea83dc | |||
| 4024c4d87b | |||
| 4499bcf6c3 | |||
| 281ccbaa70 | |||
| bf53a8eaa8 | |||
| a31ef52b00 | |||
| bab78044a1 | |||
| 61ec340e40 | |||
| ee7cfae1ff | |||
| fb6588ab1f | |||
| 703edbe4a1 |
+25
-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-4-6
|
||||
# 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=
|
||||
@@ -31,6 +35,19 @@ WORKSPACE_PATH=~/projects
|
||||
# Path to SSH keys on host
|
||||
SSH_KEY_PATH=~/.ssh
|
||||
|
||||
# ── MemPalace memory (local by default) ───────────────────────────
|
||||
# By default each container runs its OWN MemPalace (a local stdio server;
|
||||
# palace stored at ~/.mempalace). Uncomment the devbox-palace volume in
|
||||
# docker-compose.yml to persist it across container recreation.
|
||||
#
|
||||
# To instead share ONE MemPalace across several containers / harnesses
|
||||
# (pi + opencode + native), point every container at an external HTTP
|
||||
# endpoint. When MEMPALACE_REMOTE_URL is set, no local mempalace-mcp is
|
||||
# spawned and the devbox-palace volume is irrelevant.
|
||||
# Serve it with: mempalace-mcp --transport http --host 0.0.0.0 --port 8765
|
||||
# MEMPALACE_REMOTE_URL=http://mempalace.lan:8765/mcp
|
||||
# MEMPALACE_REMOTE_TOKEN= # optional — sent as: Authorization: Bearer <token>
|
||||
|
||||
# ── LAN access from the container (host-OS-agnostic) ─────────────────
|
||||
# On VM-backed hosts (macOS OrbStack / Docker Desktop, also Docker Desktop
|
||||
# on Windows) the container runs in a Linux VM and CANNOT reach the host's
|
||||
@@ -104,3 +121,9 @@ SSH_KEY_PATH=~/.ssh
|
||||
# # on image pull. Independent of ENABLE_OMOS.
|
||||
# # See docs/omos-skills.md
|
||||
# OMOS_RESET=false # Force regenerate oh-my-opencode-slim config on next start (does not affect skills)
|
||||
|
||||
# ── Background subagents (opencode experimental flag) ────────────────
|
||||
# Baked ON in the image (ENV in Dockerfile.base) because OMOS V2+ default
|
||||
# orchestration depends on opencode's native background subagents. Uncomment
|
||||
# to opt out (e.g. to force blocking/foreground orchestration).
|
||||
# OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false
|
||||
|
||||
@@ -27,6 +27,13 @@ GIT_USER_EMAIL=your.name@example.com
|
||||
# If you have per-user keys: SSH_KEY_PATH=~/<signum>/.ssh
|
||||
# SSH_KEY_PATH=~/.ssh
|
||||
|
||||
# ── MemPalace memory ─────────────────────────────────────────────────
|
||||
# Default: each container runs its own LOCAL palace. To share ONE external
|
||||
# MemPalace across containers/harnesses, set the URL (no local server is
|
||||
# spawned when set). MEMPALACE_REMOTE_TOKEN is an optional bearer token.
|
||||
# MEMPALACE_REMOTE_URL=http://mempalace.lan:8765/mcp
|
||||
# MEMPALACE_REMOTE_TOKEN=
|
||||
|
||||
# ── Locale (defaults to en_US.UTF-8) ────────────────────────────────
|
||||
# LANG=sv_SE.UTF-8
|
||||
# LANGUAGE=sv_SE:sv
|
||||
|
||||
@@ -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:
|
||||
@@ -66,3 +73,44 @@ jobs:
|
||||
# ("no project was found"). Globbing the workflow files is the
|
||||
# supported way to lint a non-GitHub layout.
|
||||
run: actionlint -color .gitea/workflows/*.yml
|
||||
|
||||
hadolint:
|
||||
# Lint the two Dockerfiles that ARE the project (the shell/actions linting
|
||||
# above never looked at them). Config — ignored rules + failure threshold
|
||||
# — lives in .hadolint.yaml, which hadolint reads automatically, so a local
|
||||
# `hadolint Dockerfile.base` reproduces CI exactly.
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install hadolint (pinned)
|
||||
env:
|
||||
HADOLINT_VERSION: 2.14.0
|
||||
run: |
|
||||
curl -fsSL \
|
||||
"https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-Linux-x86_64" \
|
||||
-o /usr/local/bin/hadolint
|
||||
chmod +x /usr/local/bin/hadolint
|
||||
hadolint --version
|
||||
|
||||
- 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,29 +51,21 @@ 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). validate.yml uses
|
||||
# Hub's base-latest as the parent for variant builds, so changes to
|
||||
# those files are NOT exercised here — only release tags rebuild the
|
||||
# base via docker-publish-split.yml.
|
||||
# (Dockerfile.base, rootfs/, entrypoint*.sh) AND exports `base_changed` so
|
||||
# validate-base/validate-omos can skip. validate.yml uses Hub's base-latest
|
||||
# as the parent for variant builds, so a changed base is NOT exercised here
|
||||
# — worse, if the same commit tightens smoke-test.sh in lockstep with the
|
||||
# base change (as the nano/micro commit did), those jobs HARD-FAIL against
|
||||
# the stale base-latest: a false red that self-heals only once the release
|
||||
# rebuilds base-latest. So we skip them here and defer full base validation
|
||||
# to the release path (docker-publish-split.yml on tag push).
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
outputs:
|
||||
base_changed: ${{ steps.detect.outputs.base_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -74,18 +73,38 @@ jobs:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect base-input changes
|
||||
id: detect
|
||||
run: |
|
||||
set -e
|
||||
if ! git diff --name-only HEAD~1 HEAD 2>/dev/null \
|
||||
| grep -qE '^(Dockerfile\.base|rootfs/|entrypoint.*\.sh)$'; then
|
||||
echo "No base-image inputs changed in this commit — validate.yml fully exercises the published base-latest."
|
||||
exit 0
|
||||
fi
|
||||
echo "::warning::This commit changes base-image inputs (Dockerfile.base, rootfs/, or entrypoint*.sh). validate.yml uses Hub's base-latest as the parent for variant builds, so the new base is NOT exercised by this workflow. Cut a release tag, or run a workflow_dispatch of docker-publish-split.yml against a test tag (e.g. v0.0.0-base-test, promote_latest=false) for end-to-end validation of the new base."
|
||||
# Base inputs baked into Dockerfile.base: Dockerfile.base itself, any
|
||||
# file under rootfs/ (COPYed into the base), and entrypoint*.sh. NB:
|
||||
# rootfs/ is a PREFIX match — the earlier '^(...|rootfs/|...)$' anchored
|
||||
# the whole group, so the rootfs/ branch only matched a file literally
|
||||
# named 'rootfs/' and never real paths like rootfs/usr/.../x.py.
|
||||
base_re='^(Dockerfile\.base$|rootfs/|entrypoint.*\.sh$)'
|
||||
changed="$(git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -E "$base_re" || true)"
|
||||
if [ -n "$changed" ]; then
|
||||
echo "base_changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::This commit changes base-image inputs (Dockerfile.base, rootfs/, or entrypoint*.sh). validate.yml uses Hub's base-latest as the parent for variant builds, so the new base is NOT exercised by this workflow — validate-base/validate-omos are SKIPPED to avoid a false failure against the stale base-latest. Cut a release tag, or run a workflow_dispatch of docker-publish-split.yml against a test tag (e.g. v0.0.0-base-test, promote_latest=false) for end-to-end validation of the new base."
|
||||
echo "Changed base-input files:"
|
||||
git diff --name-only HEAD~1 HEAD | grep -E '^(Dockerfile\.base|rootfs/|entrypoint.*\.sh)$'
|
||||
printf '%s\n' "$changed"
|
||||
else
|
||||
echo "base_changed=false" >> "$GITHUB_OUTPUT"
|
||||
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]
|
||||
# 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
|
||||
@@ -147,6 +166,9 @@ jobs:
|
||||
bash scripts/smoke-test.sh opencode-devbox:ci-base --variant base
|
||||
|
||||
validate-omos:
|
||||
needs: [base-change-warning]
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# hadolint configuration for opencode-devbox.
|
||||
#
|
||||
# Both Dockerfiles are linted in CI (.gitea/workflows/lint.yml → `hadolint`
|
||||
# job). hadolint reads this file automatically, so a local
|
||||
# `hadolint Dockerfile.base` reproduces CI exactly.
|
||||
#
|
||||
# The ignores below are DELIBERATE project choices — they mirror the
|
||||
# philosophy of the shellcheck excludes already applied to `run:` steps
|
||||
# (SHELLCHECK_OPTS in lint.yml). Anything NOT listed here still fails the
|
||||
# build at `warning` and above, so new Dockerfile smells are caught going
|
||||
# forward.
|
||||
ignored:
|
||||
- DL3008 # "pin apt versions" — intentionally unpinned: the base tracks
|
||||
# Debian stable and runs `apt-get upgrade`, so pinning point
|
||||
# versions would rot and fight security updates.
|
||||
- DL3016 # "pin npm versions" — opencode / oh-my-opencode-slim ARE pinned,
|
||||
# but via build-args (CI-resolved from npm), not the npm CLI.
|
||||
- DL4006 # "set -o pipefail before a pipe" — the piped RUNs are
|
||||
# download|extract steps with their own retries / `set -e`.
|
||||
# Switching the global SHELL to bash is a larger, base-affecting
|
||||
# change — tracked in IDEAS.md.
|
||||
- DL3003 # "use WORKDIR, not cd" — cosmetic in the few `cd` RUNs here.
|
||||
- SC2086 # "double-quote to prevent word-splitting" — the same code is
|
||||
# excluded for shell `run:` steps in lint.yml; splitting is
|
||||
# intentional in these contexts.
|
||||
|
||||
failure-threshold: warning
|
||||
@@ -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
|
||||
|
||||
@@ -51,8 +52,7 @@ repo, which decoupled from the pi tool version at its own `v1.0.0`.
|
||||
- **PATCH** — opencode/tool version bumps and small fixes that don't change the
|
||||
contract. When a release pairs a tool bump with a feature, the feature wins
|
||||
and it's a minor.
|
||||
- **Pre-flight check** — whenever an opencode bump is part of the release,
|
||||
verify it is real before claiming it in the CHANGELOG:
|
||||
- **Pre-flight check** — **before triggering any release CI build, check whether opencode is behind and offer to bump it.** Compare the pinned `OPENCODE_VERSION` in `Dockerfile.variant` against the latest npm release (`./check-versions.sh Dockerfile.variant` flags `→ X available` for opencode — note the base-only pins like `GOSU_VERSION` live in `Dockerfile.base`, so run it against that too for a full sweep). **If the latest stable `opencode-ai` is newer than the pin, ASK THE USER whether to bump before the build — never bump silently;** a version bump is a functional change they may want to hold or schedule. When a bump *is* part of the release, re-check the `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` removal-trigger against the new version's release notes (see the bg-subagents note under *Critical conventions*), then verify the pin is real before claiming it in the CHANGELOG:
|
||||
```bash
|
||||
npm view opencode-ai version # must equal the X.Y.Z you pin in Dockerfile.variant
|
||||
```
|
||||
@@ -101,14 +101,20 @@ curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 |
|
||||
Release-day checklist: README → (regenerate DOCKER_HUB.md only if HUB_TEMPLATE changed) → promote CHANGELOG Unreleased → grep AGENTS.md for stale counts → commit → tag → push tag.
|
||||
|
||||
**Between releases the same coupling applies.** Doc drift is not just a release-day concern — a workflow tweak, entrypoint change, or `generate-config.py` refactor can leave any of these four files lying. Before committing a non-release change, grep the docs for references to what you touched: `git diff --name-only HEAD | xargs -I{} grep -l 'thing-you-changed' README.md AGENTS.md DOCKER_HUB.md .gitea/README.md .env.example`. If a doc says "four variants" / "two phases" / "runs on amd64 only" and your change made that no longer true, fix it in the same commit.
|
||||
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, gitleaks, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64) — mind project-specific arch-name deviations (gitleaks uses `x64`, bat/eza/zoxide use `x86_64`/`aarch64`, gosu uses `amd64`/`arm64`). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
|
||||
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, gitleaks, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64) — mind project-specific arch-name deviations (gitleaks uses `x64`, bat/eza/zoxide use `x86_64`/`aarch64`, gosu uses `amd64`/`arm64`). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag — **and add its `*_VERSION` ARG to the README *Build Args* table** (the floated-`latest` row alongside `GOSU_VERSION, FZF_VERSION, …`). That table is a *separate* exhaustive list from the env-var table and has silently drifted before: `MICRO_VERSION`, `TEALDEER_VERSION`, and `TYPST_VERSION` were missing from it for three releases (v2.4.0–v2.7.0), caught only by the 2026-07-13 doc-drift audit.
|
||||
- **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`.
|
||||
- **`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`.
|
||||
- **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:
|
||||
|
||||
+231
@@ -6,6 +6,237 @@ 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).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Default models bumped to `claude-sonnet-5`.** In
|
||||
`rootfs/usr/local/lib/opencode-devbox/generate-config.py`,
|
||||
`DEFAULT_MODELS["anthropic"]` → `anthropic/claude-sonnet-5` (was
|
||||
`claude-sonnet-4-6`; also feeds `FALLBACK_MODEL`) and
|
||||
`DEFAULT_MODELS["amazon-bedrock"]` →
|
||||
`amazon-bedrock/global.anthropic.claude-sonnet-5` (was
|
||||
`...claude-sonnet-4-5-20250929-v1:0`). Also updates the commented
|
||||
`OPENCODE_MODEL` example in `.env.example`. Takes effect for new containers
|
||||
when no explicit `OPENCODE_MODEL` override is set. The `openai` default
|
||||
(`gpt-5.4`) is unchanged.
|
||||
- **opencode bumped `1.17.15` → `1.17.20`** (`Dockerfile.variant`
|
||||
`OPENCODE_VERSION`; latest stable on npm, verified via `npm view
|
||||
opencode-ai version`). 1.17.16–1.17.20 are TUI/cosmetic and internal fixes
|
||||
(command-palette flash, sub-agent task-row styling, an OpenAI Responses
|
||||
workaround removal) — no config-affecting or breaking changes. The
|
||||
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` removal-trigger was re-checked
|
||||
against these release notes and has **not** fired; the flag stays baked ON.
|
||||
- **Refreshed the bg-subagents removal-trigger "last-checked" markers** (`Dockerfile.base`, `AGENTS.md`) from `opencode 1.17.15 / omos 2.1.0` to `opencode 1.17.20 / omos 2.2.0`. Comment-only and the trigger has still **not** fired — but the `Dockerfile.base` edit changes the base-content hash, so `base-<hash>` advances and **the base image rebuilds** on the next release (not just the variant).
|
||||
|
||||
### Documentation
|
||||
|
||||
- **README backfill for v2.4.0–v2.7.0 features that shipped without user-facing docs:** PDF export via `pandoc --pdf-engine=typst` (new *Document conversion* section), terminal `TERM`/terminfo support (`ncurses-term` + `kitty-terminfo` + `xterm-ghostty`), Neovim 24-bit colour default (`termguicolors` via system `sysinit.vim`), the first-shell host SSH reachability check, and the baked global gitignore. Also adds the three floated build args missing from the Build Args table (`MICRO_VERSION`, `TEALDEER_VERSION`, `TYPST_VERSION`). Docs-only — no image change.
|
||||
- **README + AGENTS.md polish:** refreshed stale `OPENCODE_VERSION=` build-arg examples (`1.5.0`, `1.17.8` → the current `1.17.20`), linked the new `THIRD_PARTY.md` from the *License* section, and added an AGENTS.md reminder to add any new floated `*_VERSION` ARG to the README *Build Args* table (the coupling gap that hid `MICRO`/`TEALDEER`/`TYPST` for three releases).
|
||||
|
||||
## v2.7.0 — 2026-07-13
|
||||
|
||||
Minor release. Headline: **further parity with the sibling `pi-devbox` image** —
|
||||
ports the base additions from pi-devbox v1.4.0 + v1.5.0 that opencode-devbox
|
||||
lacked. **PDF export now works out of the box** (`typst` as the pandoc PDF
|
||||
engine — pandoc shipped since v2.6.0 as a front-end only), modern terminal
|
||||
emulators resolve their `TERM` over SSH (ncurses-term + kitty-terminfo + a
|
||||
compiled `xterm-ghostty` alias), Neovim renders in readable 24-bit colour by
|
||||
default, and a first-shell **host SSH reachability check** warns when the Mac
|
||||
host isn't reachable. Also adds repository hygiene (LICENSE, THIRD_PARTY.md,
|
||||
hadolint CI, IDEAS backlog) and a `.claude` gitignore-seed pattern. All the
|
||||
tooling/rootfs changes are base-affecting, so `base-<hash>` advances and the
|
||||
base rebuilds.
|
||||
|
||||
### Added
|
||||
|
||||
- **`typst` — PDF engine for pandoc (Markdown→PDF).** `pandoc` shipped in the
|
||||
base since v2.6.0 but as a front-end only — with no PDF back-end,
|
||||
`pandoc -o out.pdf` failed with "xelatex not found". The base now installs
|
||||
`typst`, a single ~30 MB static Rust binary (no LaTeX), used via
|
||||
`pandoc --pdf-engine=typst`; a fuller TeX Live remains the higher-fidelity
|
||||
fallback (install on demand). Also patches pandoc's bundled typst template
|
||||
(`/usr/share/pandoc/data/templates/template.typst`), whose empty `font: ()`
|
||||
default otherwise made a naked `--pdf-engine=typst` fail with "font fallback
|
||||
list must not be empty" — defaulted to `Libertinus Serif` so PDF export works
|
||||
without `-V mainfont`. Adds `xz-utils` to the apt layer (typst ships a
|
||||
`.tar.xz`). Tracks `latest`; pin with `--build-arg TYPST_VERSION=vX.Y.Z`.
|
||||
Ported from pi-devbox v1.4.0 (+ its v1.5.0 font-default fix). Base-affecting.
|
||||
|
||||
- **Terminal support beyond the default: `ncurses-term` + `kitty-terminfo` + a
|
||||
compiled `xterm-ghostty` alias.** The base previously shipped only
|
||||
`ncurses-base`, so SSHing in from a modern emulator degraded to a dumb
|
||||
fallback. Now installs `ncurses-term` (terminfo for WezTerm, Alacritty, foot,
|
||||
st, the base `ghostty` entry, and many more) and `kitty-terminfo`
|
||||
(`xterm-kitty`), and compiles an `xterm-ghostty` alias with `tic -x`
|
||||
(`use=ghostty`) — Ghostty connects as `TERM=xterm-ghostty` and no distro
|
||||
packages that name. iTerm2's `xterm-256color` is already in ncurses-base. New
|
||||
`rootfs/usr/local/share/terminfo-src/ghostty.terminfo`; the build asserts the
|
||||
entry landed via `infocmp`. Ported from pi-devbox v1.5.0. Base-affecting.
|
||||
|
||||
- **Readable Neovim colours out of the box.** New system-wide Neovim config
|
||||
(`rootfs/etc/xdg/nvim/sysinit.vim`) enables `termguicolors`. Vanilla Neovim
|
||||
otherwise fell back to a muddy, low-contrast 256-colour palette over ssh.
|
||||
`sysinit.vim` is Neovim's system vimrc — it loads for every user before any
|
||||
personal `~/.config/nvim` and can still be overridden per-user
|
||||
(`:set notermguicolors`). Pairs with `kitty-terminfo` above. Ported from
|
||||
pi-devbox v1.5.0. Base-affecting.
|
||||
|
||||
- **Host SSH reachability check at shell startup.** `~/.bash_aliases` (baked
|
||||
into the base) now runs a one-time SSH probe on the first bash session of each
|
||||
container. If the Mac host is not reachable (Remote Login disabled or the
|
||||
`devbox_jump` key not yet authorized) it prints a clear warning with the exact
|
||||
two steps to fix it, including the container's public key inline. Subsequent
|
||||
shells skip the check (a `/tmp` flag cleared on recreate); silent when SSH
|
||||
works. Complements the existing `setup-lan-access.sh` key-generation message.
|
||||
Ported from pi-devbox v1.4.0. Base-affecting.
|
||||
|
||||
- **Seeded global gitignore now ignores `**/.claude/settings.local.json`.**
|
||||
Claude Code's per-machine local settings file holds machine-specific
|
||||
permissions and can carry credentials, so it should never be committed. The
|
||||
seed (`rootfs/home/developer/.gitignore_global`, baked to `/etc/skel-devbox/`)
|
||||
gains the pattern. Existing containers are unaffected (the seed is copied only
|
||||
when `~/.gitignore_global` is absent). Base-affecting (rootfs COPY).
|
||||
|
||||
- **Repository hygiene: `LICENSE`, `THIRD_PARTY.md`, `hadolint` CI, `IDEAS.md`.**
|
||||
The repo declared MIT only in prose; it now ships an actual `LICENSE` file
|
||||
(MIT, © Joakim Persson) plus `THIRD_PARTY.md` recording that the published
|
||||
images bundle third-party software under its own terms (opencode, OMOS,
|
||||
mempalace — MIT; gosu/uv/typst Apache-2.0; Debian packages under their own
|
||||
licenses). A new `hadolint` job (pinned v2.14.0) in the lint workflow lints
|
||||
`Dockerfile.base` and `Dockerfile.variant`; `.hadolint.yaml` grandfathers the
|
||||
deliberate choices (unpinned apt/npm, `cd`-in-`RUN`, `SC2086` — mirroring the
|
||||
existing shellcheck excludes) and fails on anything new at `warning`+.
|
||||
`IDEAS.md` parks vetted-but-unscheduled follow-ups. Repo/CI only — not baked
|
||||
into the image.
|
||||
|
||||
## v2.6.0 — 2026-07-08
|
||||
|
||||
Minor release. Headline: **CLI-toolset parity with the sibling `pi-devbox`
|
||||
image** (adds `yq`, `pandoc`, `graphviz`, `tealdeer`/`tldr`, and the `dot-watch`
|
||||
wrapper) plus **background subagents enabled by default**
|
||||
(`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`), and an opencode bump
|
||||
`1.17.13 → 1.17.15`. Touches `Dockerfile.base` (apt list, new binary, new ENV)
|
||||
so the base image rebuilds (`base-<hash>` advances); the opencode bump rebuilds
|
||||
the variant layers.
|
||||
|
||||
### Added
|
||||
|
||||
- **CLI tooling parity with pi-devbox.** Added `yq` (YAML query/transform,
|
||||
pairs with `jq`), `pandoc` (Markdown↔HTML/etc. document converter), and
|
||||
`graphviz` (`dot` diagram rendering) via apt; `tealdeer` (the `tldr` command —
|
||||
quick command examples) as a ~5 MB static musl binary via the floated
|
||||
`TEALDEER_VERSION` arg; and the `dot-watch` wrapper (`rootfs/usr/local/bin/dot-watch`,
|
||||
auto-renders a `.dot` file to PNG on save, graphviz-only). Deliberately did
|
||||
**not** port `socat`, `imagemagick`, or the pi-studio-specific `studio-expose`
|
||||
bridge. smoke-test asserts presence of all five.
|
||||
- **Background subagents enabled by default (`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`).**
|
||||
Baked as a base-level `ENV` in `Dockerfile.base`. opencode gates native
|
||||
background subagents behind this experimental flag; OMOS V2+ makes background
|
||||
orchestration its default workflow, so the omos variant was effectively
|
||||
degraded without it. Overridable at runtime (`-e OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false`).
|
||||
Documented in README env table and `.env.example`; smoke-test asserts the var
|
||||
is present in the image environment. Marked experimental upstream — carries a
|
||||
removal-trigger note in `Dockerfile.base` + AGENTS.md for when opencode
|
||||
promotes it out of experimental (no upstream roadmap date as of opencode
|
||||
1.17.15 / omos 2.1.0).
|
||||
|
||||
### Changed
|
||||
|
||||
- **opencode `1.17.13` → `1.17.15`.** v1.17.14 adds a code-mode MCP adapter for
|
||||
confined orchestration scripts, hides the `execute` tool unless code mode is
|
||||
enabled, and **fixes paginated MCP tool catalogs losing tool metadata +
|
||||
output-schema validation** (relevant — this image wires the mempalace,
|
||||
gitea-mcp, and context7 MCP servers). v1.17.15 is bugfixes + desktop/TUI UX
|
||||
(Z.ai context-overflow error classification, macOS Sequoia titlebar, model
|
||||
picker). No breaking changes. Release notes:
|
||||
<https://github.com/anomalyco/opencode/releases>.
|
||||
|
||||
---
|
||||
|
||||
## v2.5.0 — 2026-07-02
|
||||
|
||||
Minor release. Headline: **optional shared/external MemPalace** —
|
||||
`generate-config.py` can now register mempalace as a `remote` MCP endpoint
|
||||
(`MEMPALACE_REMOTE_URL`, optional `MEMPALACE_REMOTE_TOKEN` → Bearer) so one
|
||||
palace can serve pi + opencode + native, instead of a per-container local
|
||||
server; adds `docker-compose.mempalace.yml` for the shared server. Also fixes a
|
||||
`validate.yml` false-red on base-changing commits (skips variant validation and
|
||||
corrects the `rootfs/` base-input detector regex). opencode stays `1.17.13`; the
|
||||
base image rebuilds (`rootfs/…/generate-config.py` changed), so the new config
|
||||
generator lands in `base-<hash>`.
|
||||
|
||||
### Added
|
||||
|
||||
- **Optional shared/external MemPalace via `MEMPALACE_REMOTE_URL`.** MemPalace
|
||||
can now be pointed at one shared HTTP endpoint instead of each container
|
||||
running its own local server. Set `MEMPALACE_REMOTE_URL=http://<host>:8765/mcp`
|
||||
(optionally `MEMPALACE_REMOTE_TOKEN`, sent as `Authorization: Bearer`) in
|
||||
`.env`; `generate-config.py` then registers the `mempalace` MCP server as a
|
||||
`remote` endpoint instead of the local `mempalace-mcp` stdio command (no local
|
||||
server is spawned, and the `devbox-palace` volume becomes irrelevant). Leaving
|
||||
it unset keeps the default local-per-container palace. Mirrors the same env
|
||||
contract as pi-devbox's `mempalace.ts` bridge, so one palace can serve pi +
|
||||
opencode + native.
|
||||
- **`docker-compose.mempalace.yml`** — an optional shared MemPalace server
|
||||
(`mempalace-mcp --transport http`) that several containers/harnesses can point
|
||||
`MEMPALACE_REMOTE_URL` at. Binds to `127.0.0.1:8765` by default (the HTTP
|
||||
transport is unauthenticated — keep it on a trusted network or behind a proxy).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`validate.yml` no longer reports a false failure on base-changing commits.**
|
||||
`validate-base`/`validate-omos` build variants from the published
|
||||
`base-latest`; when a commit changed base inputs *and* tightened
|
||||
`smoke-test.sh` in lockstep (as v2.4.0's nano/micro did), those jobs
|
||||
hard-failed against the still-stale `base-latest` until the release rebuilt it.
|
||||
The `base-change-warning` job now exports a `base_changed` output and those two
|
||||
jobs **skip** when base inputs changed, deferring full base validation to the
|
||||
release path (`docker-publish-split.yml`). The scary red becomes a neutral skip
|
||||
plus the existing warning. Also fixes the base-input detector itself: its
|
||||
`^(…|rootfs/|…)$` regex anchored the whole alternation, so the `rootfs/` branch
|
||||
only matched a file literally named `rootfs/` and never real paths like
|
||||
`rootfs/usr/…/generate-config.py` — `rootfs/` is now a prefix match, so
|
||||
rootfs-only base changes are detected too.
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 — 2026-07-01
|
||||
|
||||
Minor release. Adds two **non-modal editors** (`nano` + `micro`) alongside
|
||||
|
||||
+171
-2
@@ -15,7 +15,7 @@
|
||||
# content-addressed over this file, so any byte change invalidates the
|
||||
# cache. Recommended cadence: once per release for security updates.
|
||||
#
|
||||
# BASE_REBUILD_DATE: 2026-05-14 (v1.14.50b — fresh apt + first promote-base-latest)
|
||||
# BASE_REBUILD_DATE: 2026-07-13 (v2.7.0 — typst PDF engine + terminal terminfo (ncurses-term/kitty/xterm-ghostty) + nvim true-colour; pi-devbox parity)
|
||||
#
|
||||
# See the project README's "Build pipeline" section for the rationale.
|
||||
|
||||
@@ -72,6 +72,11 @@ RUN apt-get update && \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
nano \
|
||||
pandoc \
|
||||
xz-utils \
|
||||
graphviz \
|
||||
kitty-terminfo \
|
||||
ncurses-term \
|
||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -295,6 +300,79 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
|
||||
rm -rf /tmp/uv-* && \
|
||||
uv --version
|
||||
|
||||
# tealdeer — Rust port of tldr (community-maintained command examples)
|
||||
# Provides the `tldr` command; ~5 MB static binary, ~135 MB smaller than
|
||||
# the Node tldr global. Same UX as the Node version.
|
||||
ARG TEALDEER_VERSION=latest
|
||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||
V="${TEALDEER_VERSION}" && \
|
||||
if [ "$V" = "latest" ]; then \
|
||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||
fi && \
|
||||
V="${V#v}" && \
|
||||
[ -n "$V" ] && \
|
||||
echo "Installing tealdeer ${V}" && \
|
||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/download/v${V}/tealdeer-linux-${ARCH}-musl" -o /usr/local/bin/tldr && \
|
||||
chmod +x /usr/local/bin/tldr && \
|
||||
tldr --version
|
||||
|
||||
# ── typst — lightweight PDF engine for pandoc (Markdown→PDF) ─────────
|
||||
# pandoc (apt-installed above) is only a front-end; rendering PDF needs a
|
||||
# back-end engine. Rather than a ~600 MB TeX Live install, we ship typst:
|
||||
# a single ~30 MB static Rust binary with no LaTeX dependency, used via
|
||||
# `pandoc --pdf-engine=typst`. A fuller TeX Live remains the higher-fidelity
|
||||
# fallback for anyone who needs LaTeX-exact output (install on demand).
|
||||
# Ported from pi-devbox (its v1.4.0 + v1.5.0 font-default fix).
|
||||
#
|
||||
# Follows the `latest` GitHub-release convention (like tealdeer/uv/bat).
|
||||
# typst ships a `.tar.xz` asset (hence xz-utils in the apt layer above)
|
||||
# that extracts to typst-<arch>-unknown-linux-musl/typst. Pin a specific
|
||||
# tag with --build-arg TYPST_VERSION=vX.Y.Z.
|
||||
#
|
||||
# We also patch pandoc's bundled typst template
|
||||
# (/usr/share/pandoc/data/templates/template.typst): its conf() defaults the
|
||||
# document font to an empty tuple (`font: ()`), so a naked
|
||||
# `pandoc --pdf-engine=typst` fails with "font fallback list must not be empty"
|
||||
# unless the caller passes `-V mainfont=...`. We default it to Libertinus Serif
|
||||
# (typst's own bundled default font) so PDF export works out-of-the-box.
|
||||
ARG TYPST_VERSION=latest
|
||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||
V="${TYPST_VERSION}" && \
|
||||
if [ "$V" = "latest" ]; then \
|
||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||
fi && \
|
||||
V="${V#v}" && [ -n "$V" ] && \
|
||||
echo "Installing typst ${V}" && \
|
||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/download/v${V}/typst-${ARCH}-unknown-linux-musl.tar.xz" | tar -xJ -C /tmp && \
|
||||
install /tmp/typst-${ARCH}-unknown-linux-musl/typst /usr/local/bin/typst && \
|
||||
rm -rf /tmp/typst-${ARCH}-unknown-linux-musl && \
|
||||
typst --version && \
|
||||
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
|
||||
@@ -311,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 \
|
||||
@@ -383,7 +471,26 @@ 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
|
||||
# "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 broken without this. Baked here as
|
||||
# a base ENV (applies to both variants; harmless for plain opencode — it only
|
||||
# *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.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) ──
|
||||
ARG NODE_VERSION=22
|
||||
@@ -391,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" ;; \
|
||||
@@ -464,8 +610,29 @@ COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
|
||||
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
||||
COPY rootfs/home/developer/.gitignore_global /etc/skel-devbox/.gitignore_global
|
||||
|
||||
# ── Editor defaults: system-wide Neovim true-colour ──────────────────
|
||||
# /etc/xdg/nvim/sysinit.vim is Neovim's system vimrc: it loads for every user
|
||||
# (before any personal ~/.config/nvim) and can still be overridden per-user.
|
||||
# Enables termguicolors so the default theme renders in 24-bit colour instead
|
||||
# of a muddy 256-colour fallback. Pairs with kitty-terminfo (installed above).
|
||||
COPY rootfs/etc/xdg/nvim/sysinit.vim /etc/xdg/nvim/sysinit.vim
|
||||
|
||||
# ── Terminal support: xterm-ghostty terminfo alias ──────────────────
|
||||
# ncurses-term (installed above) covers wezterm/alacritty/foot/st and the base
|
||||
# `ghostty` entry, but Ghostty connects with TERM=xterm-ghostty, for which no
|
||||
# distro packages an entry. Ship a thin alias (use=ghostty) and compile it into
|
||||
# the system terminfo db with `tic -x`, so it inherits the maintained ghostty
|
||||
# capability set. The `infocmp` check fails the build if the entry didn't land.
|
||||
COPY rootfs/usr/local/share/terminfo-src/ghostty.terminfo /usr/local/share/terminfo-src/ghostty.terminfo
|
||||
RUN tic -x -o /usr/share/terminfo /usr/local/share/terminfo-src/ghostty.terminfo && \
|
||||
infocmp -x xterm-ghostty >/dev/null
|
||||
|
||||
# ── 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
|
||||
@@ -475,6 +642,8 @@ COPY rootfs/usr/local/share/opencode-devbox/ /usr/local/share/opencode-devbox/
|
||||
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.13
|
||||
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 ; \
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Ideas & backlog
|
||||
|
||||
A living list of potential improvements for opencode-devbox that are **not yet
|
||||
scheduled**. This is intentionally lightweight — a place to park ideas so they
|
||||
aren't lost between sessions. When an item ships, describe it in
|
||||
[`CHANGELOG.md`](CHANGELOG.md) and remove it from here.
|
||||
|
||||
Rough effort tags: 🟢 small · 🟡 medium · 🔴 large. Status: `idea` (unvetted) ·
|
||||
`planned` (agreed, not started).
|
||||
|
||||
---
|
||||
|
||||
## Supply-chain hardening
|
||||
|
||||
- 🟡 `planned` — **Pin CI actions to commit SHAs.** The workflows use floating
|
||||
major tags (`actions/checkout@v4`, `docker/build-push-action@v7`,
|
||||
`docker/setup-buildx-action@v4`, `docker/login-action@v3`,
|
||||
`docker/setup-qemu-action@v3`). This is inconsistent with the project's own
|
||||
philosophy of SHA-pinning *content* refs to defeat floating refs. Pin each
|
||||
action to a SHA with a trailing `# vX.Y.Z` comment. Pairs naturally with the
|
||||
renovate item below to keep the pins fresh.
|
||||
|
||||
- 🟡 `planned` — **Vulnerability scanning in CI.** No CVE scan runs on the
|
||||
published images today. Add a `trivy image` (or grype) job to
|
||||
`docker-publish-split.yml` after `smoke`. Start non-blocking (report only),
|
||||
then tighten to fail on `HIGH`/`CRITICAL` with an available fix.
|
||||
|
||||
- 🟢🟡 `planned` — **Standardize build provenance → buildx SBOM + attestations.**
|
||||
The image already carries hand-rolled provenance (OCI labels +
|
||||
`build-manifest`). `docker/build-push-action` can emit a standard SBOM and
|
||||
SLSA provenance attestation nearly for free (`provenance: mode=max`,
|
||||
`sbom: true`). Makes provenance machine-consumable and pairs well with the
|
||||
trivy item (scan the SBOM).
|
||||
|
||||
## Dockerfile hardening
|
||||
|
||||
- 🟡 `idea` — **Address hadolint DL4006 properly.** Currently ignored in
|
||||
`.hadolint.yaml`. The clean fix is `SHELL ["/bin/bash", "-o", "pipefail",
|
||||
"-c"]` so piped `RUN`s fail on the first non-zero stage. This changes the
|
||||
default `RUN` shell from `sh` to `bash` for all subsequent layers, so it is
|
||||
base-affecting and needs a careful pass over existing `RUN`s before removing
|
||||
the ignore.
|
||||
|
||||
## Developer experience
|
||||
|
||||
- 🟢 `idea` — **`Makefile`/`justfile` for local iteration.** Reproducing a CI
|
||||
build locally means hand-assembling many `--build-arg`s. Thin targets
|
||||
(`make build-base`, `make build-variant`, `make smoke`, `make lint`) would
|
||||
make local testing painless and document the canonical invocations.
|
||||
|
||||
- 🟡 `idea` — **Dependency-update automation (renovate).** With CI actions
|
||||
SHA-pinned (above), a `renovate.json` keeps those pins — plus the pinned tool
|
||||
versions (`ACTIONLINT_VERSION`, `HADOLINT_VERSION`, gosu, etc.) — current via
|
||||
automated PRs. Requires a renovate runner against the Gitea instance.
|
||||
|
||||
## Housekeeping
|
||||
|
||||
- 🟢 `idea` — **Registry retention for `base-<hash>` tags.** The base-hash
|
||||
caching scheme accumulates `base-<hash>` tags over time. Confirm whether the
|
||||
registry prunes old ones, and add a retention/cleanup step if not.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Joakim Persson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -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,12 +146,44 @@ 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` |
|
||||
| `OMOS_RESET` | Force regenerate OMOS config on next start (does not affect skills) | `false` |
|
||||
| `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:
|
||||
@@ -263,11 +297,39 @@ volumes:
|
||||
- ~/.config/nvim:/home/developer/.config/nvim:ro
|
||||
```
|
||||
|
||||
24-bit colour is on by default — a system-wide `sysinit.vim` (in `/etc/xdg/nvim/`) enables `termguicolors` so strings and comments render in full colour over SSH instead of a muddy 256-colour palette. It loads before any personal config and stays overridable per-user (`:set notermguicolors`, or your own `~/.config/nvim/init`).
|
||||
|
||||
> **Not a vi person?** The image also ships two non-modal editors alongside nvim:
|
||||
> **nano** (classic, minimal) and **micro** (modern — desktop-style `Ctrl+S`/`Ctrl+Q`
|
||||
> keys, mouse, syntax highlighting). `EDITOR` stays `nvim`; opt in per-shell with
|
||||
> `export EDITOR=nano` (or `micro`), or for git with `git config --global core.editor micro`.
|
||||
|
||||
### Document conversion (Markdown → PDF)
|
||||
|
||||
The image ships [`pandoc`](https://pandoc.org/) with [`typst`](https://typst.app/) as the PDF engine, so Markdown → PDF works out of the box with no LaTeX install:
|
||||
|
||||
```bash
|
||||
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:
|
||||
@@ -439,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.5.0
|
||||
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
|
||||
```
|
||||
|
||||
@@ -452,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` | `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.
|
||||
|
||||
@@ -594,6 +658,28 @@ Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/openc
|
||||
|
||||
This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries.
|
||||
|
||||
### Sharing one palace across containers (external server)
|
||||
|
||||
By default each container runs its own local `mempalace-mcp`. To share **one**
|
||||
palace across several containers/harnesses (pi + opencode + native), run the
|
||||
bundled server and point clients at it:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.mempalace.yml up -d # serves mempalace-mcp over HTTP
|
||||
```
|
||||
|
||||
Then set in each client's `.env`:
|
||||
|
||||
```bash
|
||||
MEMPALACE_REMOTE_URL=http://<reachable-host>:8765/mcp
|
||||
# MEMPALACE_REMOTE_TOKEN=... # optional; sent as Authorization: Bearer
|
||||
```
|
||||
|
||||
`generate-config.py` then registers mempalace as a `remote` MCP endpoint instead
|
||||
of the local stdio command — no local `mempalace-mcp` is spawned and the
|
||||
`devbox-palace` volume is irrelevant. ⚠ The HTTP transport is **unauthenticated**;
|
||||
keep it on a trusted network or behind a reverse proxy that enforces the token.
|
||||
|
||||
### Basic usage
|
||||
|
||||
```bash
|
||||
@@ -648,7 +734,7 @@ docker build -f Dockerfile.base -t myorg/opencode-devbox:base-dev \
|
||||
# then the variant FROM that base
|
||||
docker build -f Dockerfile.variant -t myorg/opencode-devbox:dev \
|
||||
--build-arg BASE_IMAGE=myorg/opencode-devbox:base-dev \
|
||||
--build-arg OPENCODE_VERSION=1.17.8 .
|
||||
--build-arg OPENCODE_VERSION=1.17.20 .
|
||||
```
|
||||
|
||||
Note: mempalace-toolkit clones anonymously (no token needed). Only the `resolve-versions` CI job calls the gitea API (which needs a token for public repos). A plain `docker build` like the above skips that job entirely, so no credentials are required.
|
||||
@@ -669,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:
|
||||
@@ -738,6 +844,7 @@ Defaults you get out of the box:
|
||||
- **Aliases** — `ls`/`ll`/`la` use `eza`, `cat` uses `bat`, `gs`/`gd`/`gl` for git, safe `rm`/`mv`/`cp`.
|
||||
- **Integrations** — `zoxide` (`z <fragment>` to jump), `fzf` Ctrl-R / Ctrl-T key bindings.
|
||||
- **Prompt marker** — `[devbox]` prefix so it's always obvious you're inside the container.
|
||||
- **Host SSH reachability check** — on the first shell of a freshly (re)created container, a one-time probe checks whether the Docker host is reachable over SSH. If it isn't (host Remote Login off, or the container's `devbox_jump` key not yet authorized), it prints a clear two-step fix with the container's public key inline; it stays silent once SSH works and is skipped on later shells.
|
||||
|
||||
### Overriding the defaults
|
||||
|
||||
@@ -771,6 +878,16 @@ cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
|
||||
rm ~/.bash_aliases
|
||||
```
|
||||
|
||||
## Terminal compatibility
|
||||
|
||||
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.
|
||||
|
||||
## Secret Scanning
|
||||
|
||||
A [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook prevents accidentally committing API keys, passwords, or other secrets.
|
||||
@@ -813,7 +930,11 @@ Container (Debian trixie)
|
||||
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
|
||||
├── AWS CLI v2 (SSO + Bedrock auth)
|
||||
├── neovim 0.12, nano, micro, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
|
||||
├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, curl, tree
|
||||
├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, yq, curl, tree
|
||||
├── pandoc + typst (Markdown→PDF via --pdf-engine=typst; HTML/etc.)
|
||||
├── graphviz (dot diagram rendering)
|
||||
├── tldr (tealdeer — quick command examples)
|
||||
├── dot-watch (auto-render .dot to PNG on save)
|
||||
├── Node.js (for MCP servers)
|
||||
├── Bun (optional — included with oh-my-opencode-slim)
|
||||
├── entrypoint.sh (UID adjustment, git config, provider setup)
|
||||
@@ -842,4 +963,4 @@ Container (Debian trixie)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT — see [`LICENSE`](LICENSE). The MIT license covers this repository's own contents (Dockerfiles, entrypoint scripts, `rootfs/` seeds, CI workflows, docs). The **published container images** additionally *bundle* third-party software, each under its own terms — see [`THIRD_PARTY.md`](THIRD_PARTY.md) for the summary.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Third-party notices
|
||||
|
||||
opencode-devbox is distributed under the MIT License (see [`LICENSE`](LICENSE)),
|
||||
which covers **this repository's own contents** — the Dockerfiles, entrypoint
|
||||
scripts, `rootfs/` seeds, CI workflows, and docs.
|
||||
|
||||
The **published container images** (`joakimp/opencode-devbox:*`) additionally
|
||||
*bundle* third-party software, each of which remains under its own license. This
|
||||
file is a good-faith summary; the authoritative sources are the upstream
|
||||
projects and, for OS packages, the per-package copyright files inside the image
|
||||
at `/usr/share/doc/<package>/copyright`.
|
||||
|
||||
## opencode and agent components (installed in the variant layer)
|
||||
|
||||
| Component | Upstream | License |
|
||||
| --- | --- | --- |
|
||||
| opencode | github.com/sst/opencode | MIT |
|
||||
| oh-my-opencode-slim (OMOS) *(`-omos` variant only)* | npm | MIT |
|
||||
| mempalace / mempalace-toolkit | authored by the maintainer (Joakim Persson) | MIT |
|
||||
| gitea-mcp | gitea.com/gitea/gitea-mcp | MIT |
|
||||
|
||||
## Tooling baked into the base image
|
||||
|
||||
| Component | Upstream | License (best effort) |
|
||||
| --- | --- | --- |
|
||||
| gosu | github.com/tianon/gosu | Apache-2.0 |
|
||||
| Node.js | nodejs.org | MIT (bundles components under their own licenses) |
|
||||
| uv | github.com/astral-sh/uv | Apache-2.0 OR MIT |
|
||||
| Neovim | neovim.io | Apache-2.0 + Vim license |
|
||||
| micro | github.com/zyedidia/micro | MIT |
|
||||
| Pandoc | pandoc.org | GPL-2.0-or-later |
|
||||
| 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
|
||||
|
||||
The image is built `FROM` a Debian base and installs packages via `apt`. Debian
|
||||
and its packages are distributed under their respective licenses (GPL, LGPL,
|
||||
MIT, BSD, and others). See each package's copyright file in the image under
|
||||
`/usr/share/doc/<package>/copyright`.
|
||||
|
||||
---
|
||||
|
||||
*Licenses marked "best effort" are widely known but were not each verified at
|
||||
the exact bundled version; consult the upstream project for authoritative
|
||||
terms. Corrections welcome.*
|
||||
@@ -0,0 +1,84 @@
|
||||
# Shared MemPalace server (optional) — one palace for many clients.
|
||||
#
|
||||
# Runs `mempalace-mcp` over HTTP so several containers/harnesses (pi +
|
||||
# opencode + native) can share ONE palace instead of each keeping its own.
|
||||
# Point every client at it by setting, in that client's .env:
|
||||
#
|
||||
# MEMPALACE_REMOTE_URL=http://<reachable-host>:8765/mcp
|
||||
#
|
||||
# (see .env.example). When set, the client connects over HTTP and does NOT
|
||||
# spawn its own local mempalace-mcp.
|
||||
#
|
||||
# Start: docker compose -f docker-compose.mempalace.yml up -d
|
||||
# Stop: docker compose -f docker-compose.mempalace.yml down
|
||||
# Logs: docker compose -f docker-compose.mempalace.yml logs -f
|
||||
#
|
||||
# Why reuse the devbox image? mempalace-mcp is already installed in it, and
|
||||
# reusing it GUARANTEES the server's mempalace version matches the clients'
|
||||
# (both are pinned by the same image build). Override with a slimmer image via
|
||||
# MEMPALACE_SERVER_IMAGE if you prefer (it must provide `mempalace-mcp`).
|
||||
#
|
||||
# ⚠ SECURITY: mempalace-mcp's HTTP transport has NO authentication of its own.
|
||||
# Do NOT expose port 8765 to an untrusted network. The default below binds to
|
||||
# 127.0.0.1 (host loopback) only. To let sibling containers reach it, either
|
||||
# attach them to the shared `mempalace-net` network (container-to-container, no
|
||||
# host port needed — use http://mempalace-server:8765/mcp), or front it with a
|
||||
# reverse proxy that enforces MEMPALACE_REMOTE_TOKEN as `Authorization: Bearer`.
|
||||
|
||||
name: mempalace-server
|
||||
|
||||
services:
|
||||
mempalace:
|
||||
image: ${MEMPALACE_SERVER_IMAGE:-joakimp/opencode-devbox:latest}
|
||||
container_name: mempalace-server
|
||||
# Bypass the devbox entrypoint (dev-shell/LAN/config setup) and run the
|
||||
# HTTP MCP server directly. HOME + explicit --palace pin the data path so
|
||||
# it does not depend on the image's default user/HOME. Runs as root so it
|
||||
# can initialise the fresh named volume; the volume is dedicated to this
|
||||
# server (clients reach it over HTTP, never by mounting it).
|
||||
entrypoint: []
|
||||
user: "0:0"
|
||||
environment:
|
||||
- HOME=/data
|
||||
command:
|
||||
- mempalace-mcp
|
||||
- --transport
|
||||
- http
|
||||
- --host
|
||||
- "0.0.0.0"
|
||||
- --port
|
||||
- "8765"
|
||||
- --palace
|
||||
- /data/.mempalace
|
||||
restart: unless-stopped
|
||||
# Loopback-only by default (see SECURITY note). Use "8765:8765" to expose on
|
||||
# all host interfaces, or drop `ports:` entirely and rely on mempalace-net.
|
||||
ports:
|
||||
- "127.0.0.1:8765:8765"
|
||||
volumes:
|
||||
# The shared palace data — precious; back this up.
|
||||
- mempalace-shared:/data/.mempalace
|
||||
# Embedding-model cache (~79 MB, disposable) so search does not re-download.
|
||||
- mempalace-shared-chroma:/data/.cache/chroma
|
||||
networks:
|
||||
- mempalace-net
|
||||
healthcheck:
|
||||
# A tools/list round-trip proves the server is answering MCP (python3 is
|
||||
# always present — mempalace itself is a python tool in the image).
|
||||
test:
|
||||
- CMD
|
||||
- python3
|
||||
- -c
|
||||
- "import urllib.request,json; d=json.dumps({'jsonrpc':'2.0','id':1,'method':'tools/list','params':{}}).encode(); r=urllib.request.Request('http://127.0.0.1:8765/mcp',data=d,headers={'Content-Type':'application/json','Accept':'application/json'}); urllib.request.urlopen(r,timeout=5).read()"
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
volumes:
|
||||
mempalace-shared:
|
||||
mempalace-shared-chroma:
|
||||
|
||||
networks:
|
||||
mempalace-net:
|
||||
name: mempalace-net
|
||||
@@ -38,6 +38,10 @@ services:
|
||||
- TERM=xterm-256color
|
||||
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
|
||||
- GITEA_HOST=${GITEA_HOST:-}
|
||||
# MemPalace transport: empty = LOCAL per-container palace (default);
|
||||
# set MEMPALACE_REMOTE_URL to share one external palace. See .env.example.
|
||||
- MEMPALACE_REMOTE_URL=${MEMPALACE_REMOTE_URL:-}
|
||||
- MEMPALACE_REMOTE_TOKEN=${MEMPALACE_REMOTE_TOKEN:-}
|
||||
volumes:
|
||||
# Host workspace — user's project directory
|
||||
- ${WORKSPACE_PATH:-~/src}:/workspace
|
||||
@@ -73,7 +77,9 @@ services:
|
||||
# Persist uv data (Python installs)
|
||||
- devbox-uv:/home/developer/.local/share/uv
|
||||
|
||||
# Optional: persist MemPalace data (conversation memory, knowledge graph)
|
||||
# Optional: persist MemPalace data (conversation memory, knowledge graph).
|
||||
# LOCAL palace only (default). In EXTERNAL mode (MEMPALACE_REMOTE_URL set)
|
||||
# the shared server owns the data, so this volume is irrelevant.
|
||||
# - devbox-palace:/home/developer/.mempalace
|
||||
|
||||
# Optional: persist ChromaDB embedding model cache (~79 MB)
|
||||
|
||||
+10
-1
@@ -36,6 +36,12 @@ services:
|
||||
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-}
|
||||
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
|
||||
- GITEA_HOST=${GITEA_HOST:-}
|
||||
# MemPalace transport: leave empty for a LOCAL per-container palace
|
||||
# (default). Set MEMPALACE_REMOTE_URL to share ONE external MemPalace
|
||||
# across containers/harnesses; MEMPALACE_REMOTE_TOKEN is an optional
|
||||
# bearer token. See .env.example.
|
||||
- MEMPALACE_REMOTE_URL=${MEMPALACE_REMOTE_URL:-}
|
||||
- MEMPALACE_REMOTE_TOKEN=${MEMPALACE_REMOTE_TOKEN:-}
|
||||
volumes:
|
||||
# Host workspace — mount your project here
|
||||
- ${WORKSPACE_PATH:-.}:/workspace
|
||||
@@ -128,7 +134,10 @@ services:
|
||||
- devbox-nvim-data:/home/developer/.local/share/nvim
|
||||
|
||||
# Optional: persist MemPalace data (conversation memory, knowledge graph,
|
||||
# embeddings). Without this, palace data is lost on container recreation.
|
||||
# embeddings). Applies to the LOCAL palace only (the default, when
|
||||
# MEMPALACE_REMOTE_URL is unset). Without this, palace data is lost on
|
||||
# container recreation. In EXTERNAL mode the shared server owns the data,
|
||||
# so this volume is irrelevant.
|
||||
# - devbox-palace:/home/developer/.mempalace
|
||||
|
||||
# Optional: persist ChromaDB embedding model cache (~79 MB, downloaded on
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
" opencode-devbox — system-wide Neovim defaults.
|
||||
"
|
||||
" This is Neovim's *system vimrc*: it loads for every user before any personal
|
||||
" ~/.config/nvim, and personal configs can still override it.
|
||||
"
|
||||
" Enable 24-bit ("true") colour. Without it, Neovim's default theme is squeezed
|
||||
" into a 256-colour palette where strings/comments become a muddy, low-contrast
|
||||
" dark colour — a common complaint over ssh/kitty where COLORTERM often isn't
|
||||
" propagated into the container. Modern terminals (kitty, WezTerm, iTerm2,
|
||||
" Alacritty, ...) all support true colour; the bundled kitty-terminfo also lets
|
||||
" Neovim auto-detect it, but forcing it here guarantees readable colour
|
||||
" regardless of how the terminal type / COLORTERM reach the container.
|
||||
"
|
||||
" Opt out for a session: :set notermguicolors
|
||||
" Override permanently: set your own value in ~/.config/nvim/init.lua
|
||||
if has('termguicolors')
|
||||
set termguicolors
|
||||
endif
|
||||
@@ -54,6 +54,38 @@ alias gs='git status'
|
||||
alias gd='git diff'
|
||||
alias gl='git log --oneline --graph --decorate -20'
|
||||
|
||||
# ── Host SSH reachability check (once per container lifetime) ─────────────
|
||||
# Warns at first shell startup if the Mac host is not reachable via SSH.
|
||||
# Only runs inside a container, only if the jump key exists, and only once
|
||||
# per container lifetime (/tmp flag is cleared on recreate).
|
||||
_devbox_check_host_ssh() {
|
||||
[ -f "/.dockerenv" ] || return 0
|
||||
local ssh_cfg="$HOME/.ssh-local/config"
|
||||
[ -f "$ssh_cfg" ] || return 0
|
||||
local key_pub="$HOME/.ssh-local/devbox_jump_ed25519.pub"
|
||||
[ -f "$key_pub" ] || return 0
|
||||
local flag="/tmp/.devbox_host_ssh_ok"
|
||||
[ -f "$flag" ] && return 0
|
||||
if ssh -F "$ssh_cfg" \
|
||||
-o BatchMode=yes \
|
||||
-o ConnectTimeout=2 \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
mac true 2>/dev/null; then
|
||||
touch "$flag"
|
||||
return 0
|
||||
fi
|
||||
local pub_key
|
||||
pub_key=$(cat "$key_pub")
|
||||
printf '\n\033[1;33m⚠ devbox: Mac host not reachable via SSH\033[0m\n'
|
||||
printf ' Some tools use SSH to run commands on the Mac host.\n'
|
||||
printf ' Fix (run both on the Mac):\n\n'
|
||||
printf ' \033[1mStep 1\033[0m System Settings → General → Sharing → Remote Login → ON\n\n'
|
||||
printf ' \033[1mStep 2\033[0m echo '"'"'%s'"'"' >> ~/.ssh/authorized_keys\n' "$pub_key"
|
||||
printf '\n Then open a new shell in the container to verify.\n\n'
|
||||
}
|
||||
_devbox_check_host_ssh
|
||||
unset -f _devbox_check_host_ssh
|
||||
|
||||
# ── LAN access via the host (dssh) ───────────────────────────────────
|
||||
# When running on a VM-backed host (macOS OrbStack / Docker Desktop), the
|
||||
# entrypoint's setup-lan-access.sh generates ~/.ssh-local/config so the host
|
||||
|
||||
@@ -9,3 +9,7 @@
|
||||
*.orig
|
||||
*.swp
|
||||
*.tmp
|
||||
|
||||
# Claude Code per-machine local settings: holds machine-specific permissions
|
||||
# and can carry credentials — must never be committed.
|
||||
**/.claude/settings.local.json
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# dot-watch — auto-rerender a graphviz .dot file to PNG on every save.
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# pi-studio renders mermaid natively but has no graphviz/DOT renderer.
|
||||
# Its markdown preview DOES render local image links (.png/.jpg/.gif/.webp),
|
||||
# and the editor offers "refresh from disk". This helper closes the loop:
|
||||
# edit a .dot file -> dot-watch regenerates <name>.png -> hit refresh in
|
||||
# Studio to see the update. Uses mtime polling (no inotify dependency,
|
||||
# which isn't in the trixie-slim base).
|
||||
#
|
||||
# USAGE
|
||||
# dot-watch <file.dot> [layout] [dpi]
|
||||
# layout: dot|neato|fdp|circo|twopi (default: dot)
|
||||
# dpi: output resolution (default: 150)
|
||||
# env: DOT_WATCH_INTERVAL=<seconds> poll interval (default: 1)
|
||||
#
|
||||
# EXAMPLES
|
||||
# dot-watch /workspace/graph.dot
|
||||
# dot-watch graph.dot neato 200
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SRC="${1:?usage: dot-watch <file.dot> [layout] [dpi]}"
|
||||
LAYOUT="${2:-dot}"
|
||||
DPI="${3:-150}"
|
||||
|
||||
[[ -f "$SRC" ]] || { echo "error: no such file: $SRC" >&2; exit 1; }
|
||||
command -v "$LAYOUT" >/dev/null || { echo "error: layout engine '$LAYOUT' not found" >&2; exit 1; }
|
||||
|
||||
OUT="${SRC%.dot}.png"
|
||||
INTERVAL="${DOT_WATCH_INTERVAL:-1}" # seconds between polls
|
||||
ERRLOG="$(mktemp -t dot-watch.XXXXXX.err)"
|
||||
trap 'rm -f "$ERRLOG"' EXIT
|
||||
|
||||
render() {
|
||||
if "$LAYOUT" -Tpng -Gdpi="$DPI" "$SRC" -o "$OUT" 2> "$ERRLOG"; then
|
||||
printf '[%s] rendered -> %s\n' "$(date +%H:%M:%S)" "$OUT"
|
||||
else
|
||||
printf '[%s] DOT error:\n' "$(date +%H:%M:%S)"
|
||||
sed 's/^/ /' "$ERRLOG"
|
||||
fi
|
||||
}
|
||||
|
||||
# portable mtime (GNU stat, fallback to BSD stat)
|
||||
mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
|
||||
|
||||
echo "watching $SRC ($LAYOUT, ${DPI}dpi) -> $OUT [Ctrl-C to stop]"
|
||||
render
|
||||
last="$(mtime "$SRC")"
|
||||
while true; do
|
||||
sleep "$INTERVAL"
|
||||
[[ -f "$SRC" ]] || continue
|
||||
now="$(mtime "$SRC")"
|
||||
if [[ "$now" != "$last" ]]; then
|
||||
last="$now"
|
||||
render
|
||||
fi
|
||||
done
|
||||
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"
|
||||
@@ -16,9 +16,13 @@ Environment variables:
|
||||
OPENCODE_MODEL Optional. Overrides the provider default model.
|
||||
AWS_REGION Bedrock only. Default: us-east-1.
|
||||
AWS_PROFILE Bedrock only. Default: default.
|
||||
MEMPALACE_REMOTE_URL Optional. Point MemPalace at a shared HTTP endpoint
|
||||
instead of spawning a local stdio server.
|
||||
MEMPALACE_REMOTE_TOKEN Optional. Bearer token for MEMPALACE_REMOTE_URL.
|
||||
|
||||
MCP servers are auto-registered for tools detected on PATH:
|
||||
- mempalace (if installed) — enabled
|
||||
- mempalace — LOCAL stdio if `mempalace-mcp` is installed, or EXTERNAL
|
||||
(remote HTTP) when MEMPALACE_REMOTE_URL is set (shared palace)
|
||||
- gitea-mcp (if installed) — registered but disabled by default
|
||||
|
||||
Output path: $HOME/.config/opencode/opencode.jsonc
|
||||
@@ -35,13 +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-4-6",
|
||||
"openai": "openai/gpt-5.4",
|
||||
"amazon-bedrock": (
|
||||
"amazon-bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
),
|
||||
"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.
|
||||
@@ -80,13 +91,27 @@ def register_mcp_servers(config: dict) -> list[str]:
|
||||
"""
|
||||
servers: dict[str, dict] = {}
|
||||
|
||||
# MemPalace — local-first AI memory (if installed).
|
||||
# `mempalace-mcp` is the entry-point binary shipped by the mempalace
|
||||
# Python package. `uv tool install mempalace` places it on PATH as a
|
||||
# shim whose shebang points at the isolated venv's Python, so system
|
||||
# `python3 -m mempalace.mcp_server` (which would fail — system
|
||||
# python3 can't import from the uv venv) is unnecessary here.
|
||||
if shutil.which("mempalace-mcp"):
|
||||
# MemPalace — AI memory. Two modes, same env contract as the mempalace.ts
|
||||
# pi extension (mempalace-toolkit), so one shared MemPalace can serve pi +
|
||||
# opencode + native:
|
||||
# LOCAL (default): stdio subprocess. `mempalace-mcp` is the entry-point
|
||||
# binary shipped by the mempalace Python package. `uv tool install
|
||||
# mempalace` places it on PATH as a shim whose shebang points at the
|
||||
# isolated venv's Python, so system `python3 -m mempalace.mcp_server`
|
||||
# (which would fail — system python3 can't import from the uv venv) is
|
||||
# unnecessary here.
|
||||
# EXTERNAL: set MEMPALACE_REMOTE_URL to a shared MemPalace HTTP endpoint
|
||||
# (e.g. http://mempalace.lan:8765/mcp). MEMPALACE_REMOTE_TOKEN, if set,
|
||||
# becomes a Bearer auth header. No local mempalace-mcp is spawned, so
|
||||
# the binary need not even be installed in this mode.
|
||||
remote_url = os.environ.get("MEMPALACE_REMOTE_URL", "").strip()
|
||||
if remote_url:
|
||||
entry: dict = {"type": "remote", "url": remote_url}
|
||||
token = os.environ.get("MEMPALACE_REMOTE_TOKEN", "").strip()
|
||||
if token:
|
||||
entry["headers"] = {"Authorization": f"Bearer {token}"}
|
||||
servers["mempalace"] = entry
|
||||
elif shutil.which("mempalace-mcp"):
|
||||
servers["mempalace"] = {
|
||||
"type": "local",
|
||||
"command": ["mempalace-mcp"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# xterm-ghostty — alias of the maintained ncurses `ghostty` terminfo entry.
|
||||
#
|
||||
# Ghostty sets TERM=xterm-ghostty by default, but the ncurses terminfo
|
||||
# database (Debian: ncurses-term) ships the entry under the name `ghostty`
|
||||
# only — there is no `xterm-ghostty` alias, and no distro packages one. This
|
||||
# thin alias makes xterm-ghostty resolve to the same upstream-maintained
|
||||
# capability set, so SSH sessions from a Ghostty terminal work without
|
||||
# vendoring Ghostty's full (Zig-generated) terminfo here.
|
||||
#
|
||||
# `use=ghostty` is resolved by `tic` at compile time against the base
|
||||
# `ghostty` entry from ncurses-term (installed in Dockerfile.base before the
|
||||
# compile step). Compiled with `tic -x`.
|
||||
xterm-ghostty|Ghostty terminal emulator (xterm-ghostty alias),
|
||||
use=ghostty,
|
||||
+69
-2
@@ -8,6 +8,10 @@
|
||||
# - Entrypoint runs cleanly as non-root after UID adjustment
|
||||
# - Generated opencode.json has the expected shape
|
||||
# - MCP wrapper works (when mempalace is installed)
|
||||
# - typst PDF engine for pandoc — `pandoc --pdf-engine=typst`
|
||||
# - terminfo for modern emulators: xterm-kitty, xterm-ghostty, wezterm,
|
||||
# alacritty, foot (kitty-terminfo + ncurses-term + compiled ghostty alias)
|
||||
# - Neovim true-colour default (sysinit.vim termguicolors)
|
||||
#
|
||||
# Usage: ./scripts/smoke-test.sh <image> [--variant base|omos]
|
||||
#
|
||||
@@ -119,6 +123,10 @@ run "node" "node --version"
|
||||
run "npm" "npm --version"
|
||||
run "git" "git --version"
|
||||
run "nvim" "nvim --version | head -1"
|
||||
run "nvim true-colour default (sysinit.vim)" "nvim --headless -c 'lua os.exit(vim.o.termguicolors and 0 or 1)'"
|
||||
run "kitty-terminfo" "infocmp -x xterm-kitty >/dev/null 2>&1"
|
||||
run "terminfo: modern emulators (ncurses-term)" 'for t in wezterm alacritty foot ghostty st-256color; do infocmp -x "$t" >/dev/null 2>&1 || exit 1; done'
|
||||
run "terminfo: xterm-ghostty alias (tic)" "infocmp -x xterm-ghostty >/dev/null 2>&1"
|
||||
run "nano" "nano --version | head -1"
|
||||
run "micro" "micro --version"
|
||||
run "bat" "bat --version"
|
||||
@@ -131,12 +139,44 @@ run "fzf" "fzf --version"
|
||||
run "fd" "fd --version"
|
||||
run "rg" "rg --version | head -1"
|
||||
run "jq" "jq --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"
|
||||
run "gitea-mcp" "gitea-mcp --version"
|
||||
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"
|
||||
run "dot-watch" "test -x /usr/local/bin/dot-watch && bash -n /usr/local/bin/dot-watch && echo ok"
|
||||
|
||||
# Background subagents: opencode gates them behind this experimental env var,
|
||||
# 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
|
||||
@@ -222,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'.)
|
||||
@@ -376,8 +427,24 @@ echo " Uncompressed size: ${SIZE_MB} MB"
|
||||
# ~250 MB headroom avoids a partial-publish + letter-suffix recovery cycle.
|
||||
# CI's smoke size print + resolved-versions table records the actual landed
|
||||
# sizes; tighten later if they come in low.
|
||||
THRESHOLD=2750
|
||||
[ "$VARIANT" = "omos" ] && THRESHOLD=3450
|
||||
# v2.6.0: bumped +200 MB (2750->2950 base, 3450->3650 omos). The pi-devbox
|
||||
# CLI-parity additions (pandoc ~150 MB + graphviz ~40 MB + tealdeer ~5 MB, all
|
||||
# on the base layer) consumed almost all of the ~250 MB headroom v1.16.2 had
|
||||
# restored — v2.6.0 passed 2750/3450 but with a slim margin. Restoring ~250 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.
|
||||
# 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