Compare commits

...

10 Commits

Author SHA1 Message Date
joakimp a208b073b0 Bump opencode to 1.14.40
Validate / docs-check (push) Successful in 1m5s
Validate / validate-base (push) Successful in 16m54s
Validate / validate-omos (push) Successful in 21m36s
Publish Docker Image / smoke-base (push) Successful in 11m17s
Publish Docker Image / smoke-omos (push) Successful in 14m52s
Publish Docker Image / build-base (push) Successful in 39m10s
Publish Docker Image / build-omos (push) Successful in 52m16s
Publish Docker Image / update-description (push) Successful in 14s
Rolls up upstream v1.14.34 through v1.14.40 (no v1.14.36 was published).
Highlights: .well-known/opencode remote config support; HTTP_PROXY honored
in desktop app; CORS/CSP fixes; warp-session-to-workspace feature; PTY
auth tickets; v2 session failure events; debug info CLI command. No
container-level changes.
2026-05-07 10:52:50 +02:00
joakimp a803fe4653 Fix smoke-test JSONC parsing to respect URLs
Validate / docs-check (push) Successful in 20s
Validate / validate-base (push) Successful in 13m50s
Validate / validate-omos (push) Successful in 14m37s
Publish Docker Image / smoke-base (push) Successful in 12m14s
Publish Docker Image / smoke-omos (push) Successful in 12m52s
Publish Docker Image / build-base (push) Successful in 43m6s
Publish Docker Image / build-omos (push) Successful in 45m45s
Publish Docker Image / update-description (push) Successful in 13s
The previous 'sed "s|//.*$||"' approach greedily stripped '//' from
URLs like https://mcp.context7.com/mcp, corrupting the JSON and causing
smoke-test failures with json.decoder.JSONDecodeError. Replaced the sed
step with a Python regex that respects string literals so URLs pass
through while only line comments are removed.
2026-05-03 10:34:16 +02:00
joakimp 79b697dea0 Bump opencode to 1.14.33
Validate / docs-check (push) Successful in 1m16s
Validate / validate-base (push) Has started running
Validate / validate-omos (push) Has started running
Publish Docker Image / smoke-base (push) Has been cancelled
Publish Docker Image / smoke-omos (push) Has been cancelled
Publish Docker Image / build-base (push) Has been cancelled
Publish Docker Image / build-omos (push) Has been cancelled
Publish Docker Image / update-description (push) Has been cancelled
2026-05-03 10:31:17 +02:00
Joakim Persson 3e3abc8672 Update docs for named volume config, skillset auto-deploy, opencode.jsonc
Validate / docs-check (push) Successful in 15s
Validate / validate-base (push) Failing after 10m35s
Validate / validate-omos (push) Failing after 13m15s
- README: rewrite config/skills sections for named volume and auto-deploy,
  add Context7 MCP docs, update all opencode.json→opencode.jsonc refs,
  add SKILLSET_CONTAINER_PATH to env var table
- CHANGELOG: add v1.14.32b entry documenting breaking changes and features
- AGENTS.md: update file roles, add skillset and config volume conventions
- DOCKER_HUB.md: regenerated (drop Context7 and Shell defaults sections
  to stay within 25KB Docker Hub limit)
- generate-dockerhub-md.py: add Context7 (drop) and Shell defaults (drop)
  to SECTION_RULES
2026-05-02 23:00:41 +00:00
Joakim Persson 59e58a9d00 Use named volume for opencode config instead of host bind mount
Validate / docs-check (push) Successful in 14s
Validate / validate-base (push) Has started running
Validate / validate-omos (push) Has been cancelled
Switching from a host bind mount (~/.config/opencode) to a named volume
(devbox-opencode-config) eliminates the symlink conflict between host
and container environments. Each manages its own skill/instruction
symlinks independently, allowing native opencode and containerized
opencode to coexist on the same machine.

Also removes the ~/.agents/skills bind mount recommendation — the
container manages its own skills directory via the entrypoint deploy,
and sharing it with the host causes relative-path conflicts.
2026-05-02 22:50:09 +00:00
Joakim Persson 26ce9aa490 Auto-deploy skillset on container start for portable skill resolution
Validate / docs-check (push) Successful in 18s
Validate / validate-base (push) Failing after 11m8s
Validate / validate-omos (push) Failing after 15m55s
Add entrypoint logic to detect and run the skillset deploy script on
container start. Detection order: SKILLSET_CONTAINER_PATH env var,
then ~/skillset dedicated mount, then /workspace/skillset fallback.

The deploy script (from the skillset repo) creates relative symlinks
that resolve inside the container regardless of the host path layout.

Also adds SKILLSET_PATH volume mount option to docker-compose files
and documents SKILLSET_CONTAINER_PATH in .env.example for hosts where
the skillset lives in a workspace subdirectory.
2026-05-02 22:21:57 +00:00
Joakim Persson 3d4e739529 Add Context7 remote MCP server to auto-generated config
Validate / docs-check (push) Successful in 18s
Validate / validate-base (push) Failing after 11m26s
Validate / validate-omos (push) Failing after 13m14s
Context7 provides up-to-date library documentation for LLMs via a
remote endpoint — no local binary needed. Always registered since it
has no PATH dependency.

Also switches generated config from .json to .jsonc so we can include
a comment about the optional API key for higher rate limits. The
existing-config check now detects both file extensions.
2026-05-02 21:24:04 +00:00
joakimp a6b0b59946 Bump opencode to 1.14.32
Validate / docs-check (push) Successful in 22s
Validate / validate-omos (push) Successful in 15m3s
Validate / validate-base (push) Successful in 16m13s
Publish Docker Image / smoke-base (push) Successful in 11m53s
Publish Docker Image / smoke-omos (push) Successful in 13m33s
Publish Docker Image / build-base (push) Successful in 42m37s
Publish Docker Image / build-omos (push) Successful in 47m20s
Publish Docker Image / update-description (push) Successful in 14s
2026-05-02 18:04:00 +02:00
Joakim Persson fc74a8f906 Collapse per-arch matrix back into single multi-arch push jobs
Validate / docs-check (push) Successful in 17s
Validate / validate-omos (push) Successful in 14m21s
Validate / validate-base (push) Successful in 14m50s
Publish Docker Image / smoke-base (push) Successful in 11m12s
Publish Docker Image / smoke-omos (push) Successful in 22m0s
Publish Docker Image / build-base (push) Successful in 42m25s
Publish Docker Image / build-omos (push) Failing after 1h16m24s
Publish Docker Image / update-description (push) Has been cancelled
v1.14.31c's matrix jobs failed on Upload digest with GHESNotSupportedError
— Gitea Actions doesn't support actions/upload-artifact@v4+.
Separately, build-omos arm64 hung silently for 12 min in Set-up job,
likely catthehacker pull contention between concurrent matrix children.

Rather than downgrade artifacts to @v3, collapse the matrix entirely.
docker/build-push-action@v7 with platforms: linux/amd64,linux/arm64
publishes a proper multi-arch manifest in one job, so the
artifact-passing and imagetools create merge dance only existed to
support a matrix split we no longer need.

The matrix was designed around load: true disk exhaustion (v1.14.30b),
but push-by-digest streams straight to the registry with fundamentally
different disk profile. Reclaim step gives enough headroom for the
combined amd64+arm64 push case.

Workflow: 7 jobs → 5. docker-publish.yml: 263 → ~110 lines of YAML.

Also:
- timeout-minutes: 90 on build jobs so hung builds fail explicitly
- BUILDKIT_PROGRESS=plain at workflow level for line-by-line arm64 logs
- AGENTS.md §CI quirks documents the Gitea-specific traps
  (upload-artifact@v3-only, dash-not-bash, build-push-action@v7
  multi-arch convention, reclaim requirement)
2026-05-01 12:28:34 +00:00
Joakim Persson 5a2d06340e Fix dash-incompatible slash substitution and bump omos size threshold
Validate / docs-check (push) Successful in 18s
Validate / validate-base (push) Successful in 15m44s
Validate / validate-omos (push) Successful in 15m21s
Publish Docker Image / smoke-base (push) Successful in 14m30s
Publish Docker Image / smoke-omos (push) Successful in 15m51s
Publish Docker Image / build-base (linux/amd64) (push) Failing after 10m58s
Publish Docker Image / build-omos (linux/amd64) (push) Failing after 15m9s
Publish Docker Image / build-omos (linux/arm64) (push) Failing after 11m57s
Publish Docker Image / build-base (linux/arm64) (push) Failing after 39m30s
Publish Docker Image / merge-base (push) Has been skipped
Publish Docker Image / merge-omos (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
v1.14.31b made it through smoke-base and validate-base (reclaim worked),
but two narrow bugs blocked the rest:

1. 'Derive platform slug' in the per-arch matrix jobs used bash
   ${PLATFORM_PAIR//\//-} which dash (/bin/sh in the runner) can't
   parse — 'Bad substitution'. Rewrote with 'tr / -'.

2. smoke-omos image size 3107 MB tripped the 3000 MB guardrail. All
   functional checks pass; the mempalace-toolkit bake-in from v1.14.30b
   added ~100 MB and the threshold was stale. Bumped to 3200 MB.

No image-level changes.
2026-05-01 10:43:04 +00:00
13 changed files with 351 additions and 267 deletions
+25
View File
@@ -31,6 +31,31 @@ WORKSPACE_PATH=~/projects
# Path to SSH keys on host # Path to SSH keys on host
SSH_KEY_PATH=~/.ssh SSH_KEY_PATH=~/.ssh
# ── Skillset (agent skills and instructions) ─────────────────────────
# If you have a skillset repo, the entrypoint auto-deploys skills and
# instructions on container start using relative symlinks (portable
# across host/container).
#
# Detection is automatic if the skillset lives directly at the workspace
# root (i.e. WORKSPACE_PATH/skillset → /workspace/skillset in container).
#
# If the skillset lives in a subdirectory of your workspace, set
# SKILLSET_CONTAINER_PATH to its location *inside the container*. This
# is determined by the workspace mount: whatever is at
# WORKSPACE_PATH/<subpath> on the host becomes /workspace/<subpath>
# in the container.
#
# Examples:
# Host skillset at ~/projects/skillset → already at /workspace/skillset (auto-detected, no config needed)
# Host skillset at ~/projects/tools/skillset → SKILLSET_CONTAINER_PATH=/workspace/tools/skillset
# Host skillset at ~/projects/local/skillset → SKILLSET_CONTAINER_PATH=/workspace/local/skillset
#
# Alternatively, mount the skillset repo at a dedicated path using the
# SKILLSET_PATH volume in docker-compose.yml (see comments there). In
# that case the entrypoint finds it at ~/skillset automatically.
#
# SKILLSET_CONTAINER_PATH=
# ── Locale (defaults to en_US.UTF-8) ───────────────────────────────── # ── Locale (defaults to en_US.UTF-8) ─────────────────────────────────
# LANG=sv_SE.UTF-8 # LANG=sv_SE.UTF-8
# LANGUAGE=sv_SE:sv # LANGUAGE=sv_SE:sv
+106 -157
View File
@@ -6,7 +6,7 @@ on:
- 'v*' - 'v*'
# Serialize concurrent runs of the same workflow on the same ref so the # Serialize concurrent runs of the same workflow on the same ref so the
# matrix build jobs can't race `docker system prune` in the smoke gates # build jobs can't race `docker system prune` in the smoke gates
# (pruning from one job can nuke another job's in-flight buildx cache). # (pruning from one job can nuke another job's in-flight buildx cache).
# cancel-in-progress: false — tag pushes are release events, we never # cancel-in-progress: false — tag pushes are release events, we never
# want to silently drop one. # want to silently drop one.
@@ -14,16 +14,43 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false cancel-in-progress: false
# Plain progress output from BuildKit — critical for diagnosing stalls
# inside arm64-under-QEMU builds where the default collapsed progress UI
# hides which step is stuck.
env:
BUILDKIT_PROGRESS: plain
# Runner disk pressure notes: # Runner disk pressure notes:
# Gitea Actions runners use `catthehacker/ubuntu:act-latest` on a shared host # Gitea Actions runners use `catthehacker/ubuntu:act-latest` on a shared host
# with limited overlay space (~40 GB, often 70%+ used at start). Building both # with limited overlay space (~40 GB, often 70%+ used at start). Two jobs
# architectures of both variants on a single runner exhausted disk around the # per variant:
# nodejs dpkg unpack / git-lfs layer export. To fix this: # * smoke gate (amd64 only, `load: true` into local dockerd for smoke
# * smoke test (amd64 only, load into daemon) runs on its own runner # testing) — peak disk = tarball + unpacked image + buildx cache. The
# * each push target (variant × arch) runs on its own runner, pushes by # `Reclaim runner disk` step below strips catthehacker-resident
# digest (no local image store), uploads digest as an artifact # toolchains and prunes stale docker state before buildx starts.
# * a merge job composes the multi-arch manifest with `imagetools create` # * build job (amd64 + arm64, `push-by-digest` streaming directly to
# Per-runner disk pressure is now one-quarter of the old single-job peak. # Docker Hub, no local unpack). Peak disk on push-by-digest is
# BuildKit's content store only — much smaller than `load: true`.
# `docker/build-push-action@v7` with comma-separated platforms
# publishes a proper multi-arch manifest in one step.
#
# Why not matrix + digest artifacts?
# An earlier revision split each arch into its own matrix job and used
# `actions/upload-artifact` to pass digests to a merge job. On Gitea
# Actions, `actions/{upload,download}-artifact@v4+` fails with
# `GHESNotSupportedError` — v4 relies on a GitHub-specific Artifact
# API that Gitea doesn't implement. Rather than downgrade to @v3 (the
# last Gitea-compatible release) we collapsed back to single-job
# multi-arch push. The matrix only helps when the build literally
# cannot fit on one runner, which push-by-digest + reclaim no longer
# hits for this image.
#
# Gitea Actions gotchas baked into this file:
# * `actions/{upload,download}-artifact` must stay at @v3 on Gitea.
# * Step scripts run under /bin/sh (dash) — no bash-isms like
# ${VAR//a/b}. Use `tr` or explicit `shell: bash`.
# * `docker/build-push-action@v7` with `platforms: a,b` works for
# multi-arch push natively; no matrix/merge dance needed.
jobs: jobs:
# ── Smoke test (amd64 only, gates the push jobs) ──────────────────── # ── Smoke test (amd64 only, gates the push jobs) ────────────────────
@@ -137,18 +164,13 @@ jobs:
- name: Smoke test (amd64) - name: Smoke test (amd64)
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
# ── Per-arch push (by digest, no local image) ────────────────────── # ── Multi-arch push (single job per variant, comma-separated platforms)
build-base: build-base:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: smoke-base needs: smoke-base
timeout-minutes: 90
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -156,15 +178,35 @@ jobs:
- name: Force IPv4 for Docker Hub - name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Derive platform slug # Lighter reclaim than the smoke-gate version: push-by-digest
id: platform # doesn't write to host dockerd, so `docker system prune` adds
# little. BuildKit cache from prior runs is the thing to clear.
- name: Reclaim runner disk
run: | run: |
PLATFORM_PAIR="${{ matrix.platform }}" set -x
echo "pair=${PLATFORM_PAIR//\//-}" >> $GITHUB_OUTPUT df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker builder prune -af || true
df -h / || true
- name: Set up QEMU - name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
@@ -177,39 +219,26 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }} username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest - name: Extract version from tag
id: build id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build and push (multi-arch)
uses: docker/build-push-action@v7 uses: docker/build-push-action@v7
with: with:
context: . context: .
platforms: ${{ matrix.platform }} platforms: linux/amd64,linux/arm64
outputs: type=image,name=${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox,push-by-digest=true,name-canonical=true,push=true push: true
tags: |
- name: Export digest ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}
run: | ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-base-${{ steps.platform.outputs.pair }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-omos: build-omos:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: smoke-omos needs: smoke-omos
timeout-minutes: 90
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -217,15 +246,32 @@ jobs:
- name: Force IPv4 for Docker Hub - name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Derive platform slug - name: Reclaim runner disk
id: platform
run: | run: |
PLATFORM_PAIR="${{ matrix.platform }}" set -x
echo "pair=${PLATFORM_PAIR//\//-}" >> $GITHUB_OUTPUT df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker builder prune -af || true
df -h / || true
- name: Set up QEMU - name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
@@ -238,122 +284,25 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }} username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest - name: Extract version from tag
id: build id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build and push (multi-arch)
uses: docker/build-push-action@v7 uses: docker/build-push-action@v7
with: with:
context: . context: .
platforms: ${{ matrix.platform }} platforms: linux/amd64,linux/arm64
push: true
build-args: | build-args: |
INSTALL_OMOS=true INSTALL_OMOS=true
outputs: type=image,name=${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox,push-by-digest=true,name-canonical=true,push=true tags: |
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-omos-${{ steps.platform.outputs.pair }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# ── Merge per-arch digests into multi-arch tags ─────────────────────
merge-base:
runs-on: ubuntu-latest
needs: build-base
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-base-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }} \
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest \
$(printf '${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect \
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}
merge-omos:
runs-on: ubuntu-latest
needs: build-omos
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-omos-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos \
-t ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos \
$(printf '${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect \
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos
update-description: update-description:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [merge-base, merge-omos] needs: [build-base, build-omos]
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
steps: steps:
+11 -4
View File
@@ -8,8 +8,8 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
- `Dockerfile` — single multi-stage build for both variants. OMOS variant is controlled by `INSTALL_OMOS=true` build arg; mempalace is controlled by `INSTALL_MEMPALACE` (default `true`). All GitHub-sourced binaries are pinned with version ARGs. - `Dockerfile` — single multi-stage build for both variants. OMOS variant is controlled by `INSTALL_OMOS=true` build arg; mempalace is controlled by `INSTALL_MEMPALACE` (default `true`). All GitHub-sourced binaries are pinned with version ARGs.
- `entrypoint.sh` — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via `.devbox-owner` sentinel when ownership is already correct). Then drops to developer via gosu. - `entrypoint.sh` — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via `.devbox-owner` sentinel when ownership is already correct). Then drops to developer via gosu.
- `entrypoint-user.sh` — runs as developer: git config, opencode.json generation (delegated to `generate-config.py`), OMOS setup. - `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), skillset auto-deploy from mounted skillset repo, OMOS setup.
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.json` from env vars. Never overwrites an existing config. Auto-registers MCP servers for detected tools (mempalace via the `mempalace-mcp` entry point, gitea-mcp). - `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).
- `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/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/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow). - `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
- `DOCKER_HUB.md`**auto-generated** from README. 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. - `DOCKER_HUB.md`**auto-generated** from README. 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.
@@ -37,8 +37,10 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, 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). 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, 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). 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.
- **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`. - **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`.
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`. - **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
- **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.json` — system Python can't import from the uv venv. - **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.json`. Users bind-mount their config directory or persist it across container recreations; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this. - **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.
- **Config persistence via named volume** — `devbox-opencode-config` is a Docker named volume mounted at `~/.config/opencode/`. It is NOT a host bind mount by default. This separation allows both native and containerized opencode to coexist on the same machine without symlink conflicts. Users who need to override can replace the named volume with a host bind mount in their compose file.
- **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes. - **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes.
## CI quirks ## CI quirks
@@ -47,6 +49,11 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
- `update-description` job runs only when both builds succeed (`needs: [build-base, build-omos]`). - `update-description` job runs only when both builds succeed (`needs: [build-base, build-omos]`).
- Tags must be pushed to trigger the publish workflow. The validate workflow runs on push to main and PRs. - Tags must be pushed to trigger the publish workflow. The validate workflow runs on push to main and PRs.
- Smoke tests run on amd64 only (single-arch load into the local daemon). The multi-arch push happens after smoke passes. - 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 four `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).
- **`actions/upload-artifact` and `actions/download-artifact` must stay at @v3 on Gitea.** v4+ uses a GitHub-Enterprise-specific Artifact API; runs fail with `GHESNotSupportedError`. If you need artifacts for a new reason (build logs, SBOMs, etc.), pin @v3 explicitly.
- **Step scripts run under `/bin/sh` (dash), not bash.** Avoid bash-isms like `${VAR//a/b}` parameter-pattern substitution; use POSIX alternatives (`tr`, `sed`) or declare `shell: bash` on the step.
- **`BUILDKIT_PROGRESS=plain`** is set at workflow level on `docker-publish.yml` so arm64-under-QEMU builds log each layer line-by-line. The default collapsed progress UI hides which step is stalled, which made diagnosing earlier hangs expensive.
## Testing changes ## Testing changes
+54
View File
@@ -6,6 +6,60 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
--- ---
## v1.14.40 — 2026-05-07
Bump opencode to 1.14.40.
Rolls up upstream releases v1.14.34 → v1.14.40 (no v1.14.36). Highlights:
- **v1.14.40:** support `.well-known/opencode` configs that point to a separate remote config file; assistant text preserved in signed reasoning blocks; CORS, network options, web terminal, and Cloudflare AI Gateway provider fixes; Mistral Medium 3.5 variants restored.
- **v1.14.39:** desktop app respects `HTTP_PROXY` and friends; storage reads return `null` instead of failing when keys are missing.
- **v1.14.38:** embedded UI requests work with arbitrary `connect-src` origins under the default CSP; desktop trusts system CA certificates for HTTPS.
- **v1.14.37:** cancelling a task now cancels child subtask sessions; v2 session rendering improvements (cleaner tool states, better compaction summaries); new "warp a session into another workspace or back to local project" feature; Windows titlebar stable across zoom changes.
- **v1.14.35:** preserve diff patch boundaries so session diffs render correctly when file contents themselves contain `diff --git` text.
- **v1.14.34:** PTY connection tickets for authenticated terminal websockets; v2 session failure events for clients to detect failed runs; improved shell command handling for Bash/PowerShell/cmd; new `debug info` command; `--username` option for basic-auth server connections.
No container-level changes in this release. Dockerfile bump only.
## v1.14.33 — 2026-05-03
**Bump opencode to 1.14.33. Named volume for opencode config, skillset auto-deploy, Context7 MCP.**
Rolls up the image-structure changes originally planned for v1.14.32b onto the current opencode release. v1.14.32 was built but never deployed (wrong deploy dir caught the tag mid-flight); skipped in favor of landing everything together on 1.14.33.
- **Breaking:** `~/.config/opencode/` now uses a named volume (`devbox-opencode-config`) instead of a host bind mount. The container's config, skills, and instructions are independent from the host. Users who relied on the bind mount should either re-add it explicitly in their compose file (overriding the volume) or migrate hand-edits into the container.
- **Breaking:** `~/.agents/skills/` is no longer bind-mounted from the host. The container manages its own skills directory — the entrypoint deploys skills from the skillset repo on each start.
- **Feature:** Skillset auto-deploy on container start. The entrypoint runs `deploy-skills.sh --bootstrap --prune-stale` from the first skillset repo found at: `$SKILLSET_CONTAINER_PATH``~/skillset``/workspace/skillset`. Creates relative symlinks that resolve inside the container regardless of host path layout. Idempotent.
- **Feature:** Context7 remote MCP server registered in auto-generated config. No local binary; provides up-to-date library documentation to LLMs. Config file is now `opencode.jsonc` (supports comments) with a note about the optional API key for higher rate limits. Existing-config check detects both `.json` and `.jsonc`.
- **Env:** New `SKILLSET_CONTAINER_PATH` env var for specifying skillset repo location inside the container when it's not at `/workspace/skillset`.
- **Docs:** README updated for named volume config, skillset auto-deploy, Context7 MCP server, `opencode.jsonc` references. AGENTS.md, DOCKER_HUB.md regenerated.
Upstream opencode 1.14.32 notes (shipped in this build since v1.14.32 was skipped): shell-mode input in the prompt is editable again (backspace, cursor keys); HTTP API workspace adapters no longer lose instance context, restoring workspace create/sync/routing; experimental workspace creation requests that omit `extra` are fixed; OpenAPI parameter schemas now match the public API so generated clients stop drifting; unsupported image formats fall back to text reads instead of being sent as image attachments; agents can use the global temp directory without extra permission prompts; Bedrock sessions that include reasoning content no longer break when switching models; session archive timestamps reject non-finite values to avoid invalid JSON. TUI: reduced startup theme flashing under the system theme, animated logo avoids subpixel rendering on terminals without truecolor support.
Upstream opencode 1.14.33 release notes: see https://github.com/sst/opencode/releases/tag/v1.14.33.
## v1.14.31d — 2026-05-01
**CI: collapse per-arch matrix back into single multi-arch push jobs.**
- **Fix:** `v1.14.31c`'s per-arch matrix build jobs failed on `Upload digest` with `GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES`. Gitea Actions only implements the v3-compatible artifact API; `@v4` uses a GitHub-Enterprise-specific backend. Separately, `build-omos linux/arm64` hung silently for 12 minutes in "Set-up job" and then failed with no log output — likely catthehacker image-pull contention between concurrent matrix children on the same runner host.
- Rather than downgrade to `actions/{upload,download}-artifact@v3`, collapsed the per-arch matrix entirely. `docker/build-push-action@v7` with `platforms: linux/amd64,linux/arm64` publishes a proper multi-arch manifest in a single job, so the whole artifact-passing and `imagetools create` merge dance existed only to support a matrix split we no longer need.
- The original matrix split was designed around `load: true` disk exhaustion (v1.14.30b). With `push-by-digest`/`push: true` streaming straight to the registry — no local unpack — the peak disk story is fundamentally different. Validated in v1.14.31b that the reclaim step gives sufficient headroom for a single-job amd64 build; oracle-reviewed call that this should extend to the combined amd64+arm64 push case.
- Workflow goes from 7 jobs to 5 (smoke-base, smoke-omos, build-base, build-omos, update-description). 263 → ~110 lines of YAML in `docker-publish.yml`.
- **Add:** `timeout-minutes: 90` on both build jobs so a hung arm64 build produces an explicit failure with logs rather than runner-default silent truncation.
- **Add:** `BUILDKIT_PROGRESS=plain` at workflow level so arm64-under-QEMU build output is line-by-line (the default collapsed progress UI was obscuring earlier stalls).
- **Add:** `AGENTS.md §CI quirks` documents the Gitea-specific traps encountered this week: `upload-artifact@v3`-only on Gitea, `/bin/sh` is dash, `build-push-action@v7` does multi-arch natively with comma-separated platforms, reclaim step is mandatory on `load: true` jobs.
- No image changes. Rebuild of v1.14.31 content only.
## v1.14.31c — 2026-05-01
**CI: fix bash-specific parameter expansion and bump omos size threshold.**
- **Fix:** `Derive platform slug` step in the per-arch matrix build jobs (`build-base`, `build-omos`) used `${PLATFORM_PAIR//\//-}` which is a bash parameter-expansion. The runner container executes step scripts via `/bin/sh` (dash), which errored with `Bad substitution`. Rewrote using `tr / -` which is POSIX and behaves identically. Both `build-base` and `build-omos` matrix jobs were blocked on this on `v1.14.31b`.
- **Fix:** smoke-test image-size threshold for the `omos` variant bumped from 3000 MB to 3200 MB. The mempalace-toolkit bake-in added ~100 MB to omos; measured 3107 MB on `v1.14.31b`. All functional smoke checks (opencode, node, mempalace CLIs, toolkit wrappers, oh-my-opencode-slim) pass — this is a guardrail recalibration, not a performance concession. The underlying image genuinely grew.
- The runner-disk reclaim step from v1.14.31b did its job: `smoke-base` and `validate-base` now pass cleanly. Only `smoke-omos` was blocked this iteration, and only on the threshold.
- No image changes beyond what shipped in v1.14.31. Rebuild of v1.14.31 content only.
## v1.14.31b — 2026-05-01 ## v1.14.31b — 2026-05-01
**CI: reclaim runner disk before `load: true` smoke builds.** **CI: reclaim runner disk before `load: true` smoke builds.**
+19 -61
View File
@@ -69,9 +69,6 @@ Bind-mounted directories must exist on the host before starting the container. D
```bash ```bash
# Required: workspace for your projects # Required: workspace for your projects
mkdir -p ~/projects mkdir -p ~/projects
# If mounting opencode config (recommended for persistent settings)
mkdir -p ~/.config/opencode
``` ```
### Connecting to the container ### Connecting to the container
@@ -145,28 +142,34 @@ docker compose exec -u developer devbox aws --version
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` | | `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` | | `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` | | `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
### Custom opencode config ### Custom opencode config
For full control over opencode settings (MCP servers, custom models, and on the OMOS variantoh-my-opencode-slim agents), mount the entire config directory from the host: Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount:
```yaml ```yaml
volumes: volumes:
- ~/.config/opencode:/home/developer/.config/opencode - ~/.config/opencode:/home/developer/.config/opencode
``` ```
This persists all configuration changes across container restarts, including `opencode.json`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json`. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped. > **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.jsonc` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.json` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
### Custom skills ### Custom skills
Mount agent skills from the host: Skills are deployed automatically from a skillset repo on container start. The entrypoint detects the skillset location in this order:
```yaml 1. `SKILLSET_CONTAINER_PATH` env var (explicit path to skillset repo inside container)
volumes: 2. `~/skillset` mount (if present)
- ~/.agents/skills:/home/developer/.agents/skills:ro 3. `/workspace/skillset` fallback (if your workspace contains a `skillset/` directory)
```
When a skillset repo is detected, its skills are symlinked into `~/.agents/skills/` automatically. No manual configuration needed.
> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references.
### Neovim configuration ### Neovim configuration
@@ -414,7 +417,7 @@ Without the volume, palace data lives in the container's writable layer and is l
### MCP integration with opencode ### MCP integration with opencode
Add mempalace as an MCP server in your `opencode.json` (inside `~/.config/opencode/`): Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/opencode/`):
```json ```json
{ {
@@ -498,7 +501,7 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
GITEA_ACCESS_TOKEN=your_token_here GITEA_ACCESS_TOKEN=your_token_here
``` ```
3. Enable the gitea MCP server in your `opencode.json`: 3. Enable the gitea MCP server in your `opencode.jsonc`:
```json ```json
{ {
"mcp": { "mcp": {
@@ -516,51 +519,6 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
The server is installed but disabled by default — it requires authentication to be useful. The server is installed but disabled by default — it requires authentication to be useful.
## Shell defaults
The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade.
Defaults you get out of the box:
- **Prefix history search** on Up/Down arrows (type `git `, press Up, walk back through prior `git ...` commands only). Ctrl-Up / Ctrl-Down still step through full history.
- **Persistent history** — `$HISTFILE` points at `~/.cache/bash/history`, backed by the `devbox-shell-history` named volume so history survives container recreation. Timestamps, 100 000 entries, dedup.
- **Case-insensitive tab completion**, coloured completion lists, `show-all-if-ambiguous`.
- **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.
### Overriding the defaults
**Option A — bind-mount host files.** Uncomment the bind-mount lines in `docker-compose.yml`:
```yaml
- ~/.bash_aliases:/home/developer/.bash_aliases:ro
- ~/.inputrc:/home/developer/.inputrc:ro
```
> **Single-file bind-mount caveat (all platforms):** Docker bind-mounts the file's **inode**, not its path. When editors like vim, nvim, VS Code, or `sed -i` save a file, they write to a temp file and `rename()` it over the original — creating a new inode. The container stays pinned to the old (now unlinked) inode and never sees the update. This is a kernel limitation ([Docker #15793](https://github.com/moby/moby/issues/15793)), not fixable by Docker. Append-only writes (`echo "alias foo=bar" >> file`) are safe because they modify the same inode. **Workaround:** mount the parent directory instead of the single file (e.g. `~/.config/devbox-shell:/home/developer/.config/devbox-shell:ro`) and source files from there.
**Option B — customize inside the container.** Just edit `~/.bash_aliases` or `~/.inputrc` as normal. Pair this with a bind-mount or named volume on the home dir if you want the edits to survive container recreation.
### Restoring or diffing defaults
The skel files remain available inside every container at `/etc/skel-devbox/`. Useful commands:
```bash
# See what the image currently ships
cat /etc/skel-devbox/.bash_aliases
# Diff your current config against the upstream defaults
diff ~/.bash_aliases /etc/skel-devbox/.bash_aliases
# Reset to the baked defaults
cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
# …or delete the file and recreate the container — the entrypoint
# copies from /etc/skel-devbox/ on next start if the target is absent
rm ~/.bash_aliases
```
## Architecture ## Architecture
``` ```
@@ -598,9 +556,9 @@ Container (Debian trixie)
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains | | `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache | | `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions | | `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
| `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes | opencode.json, skills, plus `oh-my-opencode-slim.json` on the OMOS variant | | `/home/developer/.config/opencode` | Named volume `devbox-opencode-config` | ✅ Yes | `opencode.jsonc`, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
**opencode config** (`opencode.json`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, mount the config directory from the host (see Custom opencode config above). **opencode config** (`opencode.jsonc`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, use the named volume (default) or bind-mount from host (see Custom opencode config above).
## Source ## Source
+1 -1
View File
@@ -5,7 +5,7 @@ ARG DEBIAN_VERSION=trixie-slim
FROM debian:${DEBIAN_VERSION} AS base FROM debian:${DEBIAN_VERSION} AS base
ARG TARGETARCH ARG TARGETARCH
ARG OPENCODE_VERSION=1.14.31 ARG OPENCODE_VERSION=1.14.40
LABEL maintainer="joakimp" LABEL maintainer="joakimp"
LABEL description="Portable opencode developer container" LABEL description="Portable opencode developer container"
+28 -20
View File
@@ -49,9 +49,6 @@ Bind-mounted directories must exist on the host before starting the container. D
```bash ```bash
# Required: workspace for your projects # Required: workspace for your projects
mkdir -p ~/projects mkdir -p ~/projects
# If mounting opencode config (recommended for persistent settings)
mkdir -p ~/.config/opencode
``` ```
### Connecting to the container ### Connecting to the container
@@ -125,28 +122,34 @@ docker compose exec -u developer devbox aws --version
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` | | `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` | | `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` | | `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
### Custom opencode config ### Custom opencode config
For full control over opencode settings (MCP servers, custom models, and on the OMOS variantoh-my-opencode-slim agents), mount the entire config directory from the host: Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount:
```yaml ```yaml
volumes: volumes:
- ~/.config/opencode:/home/developer/.config/opencode - ~/.config/opencode:/home/developer/.config/opencode
``` ```
This persists all configuration changes across container restarts, including `opencode.json`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json`. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped. > **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.jsonc` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.json` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
### Custom skills ### Custom skills
Mount agent skills from the host: Skills are deployed automatically from a skillset repo on container start. The entrypoint detects the skillset location in this order:
```yaml 1. `SKILLSET_CONTAINER_PATH` env var (explicit path to skillset repo inside container)
volumes: 2. `~/skillset` mount (if present)
- ~/.agents/skills:/home/developer/.agents/skills:ro 3. `/workspace/skillset` fallback (if your workspace contains a `skillset/` directory)
```
When a skillset repo is detected, its skills are symlinked into `~/.agents/skills/` automatically. No manual configuration needed.
> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references.
### Neovim configuration ### Neovim configuration
@@ -294,9 +297,6 @@ cd ~/<signum>/opencode-devbox
cp /path/to/opencode-devbox/docker-compose.shared.yml docker-compose.yml cp /path/to/opencode-devbox/docker-compose.shared.yml docker-compose.yml
cp /path/to/opencode-devbox/.env.shared.example .env cp /path/to/opencode-devbox/.env.shared.example .env
# Create per-user config directory
mkdir -p ~/<signum>/.config/opencode
# Edit .env — set SIGNUM only if you're in shared-account mode # Edit .env — set SIGNUM only if you're in shared-account mode
vim .env vim .env
@@ -308,7 +308,7 @@ docker compose exec -u developer devbox opencode
Each user's container, config, and named volumes are fully isolated: Each user's container, config, and named volumes are fully isolated:
- Container name: `devbox-<signum>` (or `devbox-$USER` in own-account mode) - Container name: `devbox-<signum>` (or `devbox-$USER` in own-account mode)
- Named volumes: prefixed with the project name (`devbox-<signum>_devbox-data`, etc.) — the Docker daemon is system-wide, so directory-name prefixing alone is NOT sufficient for isolation - Named volumes: prefixed with the project name (`devbox-<signum>_devbox-data`, etc.) — the Docker daemon is system-wide, so directory-name prefixing alone is NOT sufficient for isolation
- Opencode config: `~/<signum>/.config/opencode/` (per-user settings, OMOS config, etc.) - Opencode config: persisted via per-user named volume (`devbox-<signum>_devbox-opencode-config`)
See `docker-compose.shared.yml` and `.env.shared.example` for the full configuration. See `docker-compose.shared.yml` and `.env.shared.example` for the full configuration.
@@ -468,7 +468,7 @@ Without the volume, palace data lives in the container's writable layer and is l
### MCP integration with opencode ### MCP integration with opencode
Add mempalace as an MCP server in your `opencode.json` (inside `~/.config/opencode/`): Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/opencode/`):
```json ```json
{ {
@@ -552,7 +552,7 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
GITEA_ACCESS_TOKEN=your_token_here GITEA_ACCESS_TOKEN=your_token_here
``` ```
3. Enable the gitea MCP server in your `opencode.json`: 3. Enable the gitea MCP server in your `opencode.jsonc`:
```json ```json
{ {
"mcp": { "mcp": {
@@ -570,6 +570,14 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
The server is installed but disabled by default — it requires authentication to be useful. The server is installed but disabled by default — it requires authentication to be useful.
## Context7 MCP server
The image auto-registers a [Context7](https://context7.com) MCP server, which provides up-to-date library documentation and code examples to LLMs at query time. This is a remote MCP server at `mcp.context7.com/mcp` — no local binary is needed.
- Auto-registered in the generated `opencode.jsonc` (no manual setup required)
- Provides documentation for any programming library/framework on demand
- Requires internet access — useless in air-gapped/offline environments
## Shell defaults ## Shell defaults
The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade. The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade.
@@ -680,9 +688,9 @@ Container (Debian trixie)
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains | | `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache | | `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions | | `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
| `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes | opencode.json, skills, plus `oh-my-opencode-slim.json` on the OMOS variant | | `/home/developer/.config/opencode` | Named volume `devbox-opencode-config` | ✅ Yes | `opencode.jsonc`, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
**opencode config** (`opencode.json`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, mount the config directory from the host (see Custom opencode config above). **opencode config** (`opencode.jsonc`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, use the named volume (default) or bind-mount from host (see Custom opencode config above).
## License ## License
+13 -2
View File
@@ -45,8 +45,18 @@ services:
# SSH keys — user-specific if available, else shared # SSH keys — user-specific if available, else shared
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro - ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
# Opencode config — per-user (persists settings across restarts) # Optional: mount skillset repo for automatic skill/instruction deployment.
- ${HOME}/${SIGNUM}/.config/opencode:/home/developer/.config/opencode # The entrypoint runs deploy-skills.sh --bootstrap on start, creating
# relative symlinks that resolve inside the container regardless of
# where the repo lives on the host. Set SKILLSET_PATH in .env.
# - ${SKILLSET_PATH}:/home/developer/skillset
# Persist opencode config (opencode.jsonc, oh-my-opencode-slim.json,
# instructions, etc.) across container recreations. Auto-generated on
# first start from env vars by generate-config.py and the skillset
# deploy script. Using a named volume keeps the container's symlinks
# independent from the host.
- devbox-opencode-config:/home/developer/.config/opencode
# Persist opencode data (auth, memory, session history) # Persist opencode data (auth, memory, session history)
- devbox-data:/home/developer/.local/share/opencode - devbox-data:/home/developer/.local/share/opencode
@@ -73,6 +83,7 @@ services:
# - ${HOME}/${SIGNUM}/.aws:/home/developer/.aws # - ${HOME}/${SIGNUM}/.aws:/home/developer/.aws
volumes: volumes:
devbox-opencode-config:
devbox-data: devbox-data:
devbox-shell-history: devbox-shell-history:
devbox-zoxide: devbox-zoxide:
+19 -6
View File
@@ -42,13 +42,25 @@ services:
# SSH keys (read-only) — for git push/pull # SSH keys (read-only) — for git push/pull
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro - ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
# Optional: mount opencode config directory (persists config changes across restarts) # Optional: mount skillset repo for automatic skill/instruction deployment.
# Includes opencode.json, oh-my-opencode-slim.json, skills, etc. # The entrypoint runs deploy-skills.sh --bootstrap on start, creating
# When mounted, OPENCODE_PROVIDER auto-config is skipped if opencode.json exists. # relative symlinks that resolve inside the container regardless of
# - ~/.config/opencode:/home/developer/.config/opencode # where the repo lives on the host. Set SKILLSET_PATH in .env.
# - ${SKILLSET_PATH}:/home/developer/skillset
# Optional: mount opencode agent skills from host # Persist opencode config (opencode.jsonc, oh-my-opencode-slim.json,
# - ~/.agents/skills:/home/developer/.agents/skills:ro # instructions, etc.) across container recreations. Auto-generated on
# first start from env vars by generate-config.py and the skillset
# deploy script. Using a named volume (not a host bind mount) keeps
# the container's skill/instruction symlinks independent from the host,
# allowing both native and containerized opencode on the same machine.
- devbox-opencode-config:/home/developer/.config/opencode
# NOTE: Do NOT bind-mount ~/.agents/skills/ from the host. The
# container manages its own skills directory independently — the
# entrypoint deploys skills from the skillset repo on each start.
# Sharing it with the host causes symlink conflicts (relative paths
# differ between host and container filesystem namespaces).
# Optional: mount neovim config from host (plugins auto-install on first start) # Optional: mount neovim config from host (plugins auto-install on first start)
# - ~/.config/nvim:/home/developer/.config/nvim:ro # - ~/.config/nvim:/home/developer/.config/nvim:ro
@@ -108,6 +120,7 @@ services:
# - ~/.aws:/home/developer/.aws # - ~/.aws:/home/developer/.aws
volumes: volumes:
devbox-opencode-config:
devbox-data: devbox-data:
devbox-state: devbox-state:
devbox-shell-history: devbox-shell-history:
+22
View File
@@ -44,6 +44,28 @@ fi
# generated) and no-ops if OPENCODE_PROVIDER is unset. # generated) and no-ops if OPENCODE_PROVIDER is unset.
python3 /usr/local/lib/opencode-devbox/generate-config.py python3 /usr/local/lib/opencode-devbox/generate-config.py
# ── Skillset: deploy skills/instructions from mounted skillset repo ──
# When the skillset repo is mounted (at $HOME/skillset or /workspace/skillset),
# run the deploy script to create relative symlinks for skills and instructions.
# This ensures skills resolve correctly inside the container regardless of
# where the repo lives on the host. Idempotent — second run is a no-op.
#
# Detection order:
# 1. SKILLSET_CONTAINER_PATH env var (explicit, for non-standard layouts)
# 2. $HOME/skillset (dedicated volume mount via SKILLSET_PATH in compose)
# 3. /workspace/skillset (skillset is directly inside workspace root)
SKILLSET_DEPLOY=""
if [ -n "${SKILLSET_CONTAINER_PATH:-}" ] && [ -x "${SKILLSET_CONTAINER_PATH}/deploy-skills.sh" ]; then
SKILLSET_DEPLOY="${SKILLSET_CONTAINER_PATH}/deploy-skills.sh"
elif [ -x "$HOME/skillset/deploy-skills.sh" ]; then
SKILLSET_DEPLOY="$HOME/skillset/deploy-skills.sh"
elif [ -x /workspace/skillset/deploy-skills.sh ]; then
SKILLSET_DEPLOY="/workspace/skillset/deploy-skills.sh"
fi
if [ -n "$SKILLSET_DEPLOY" ]; then
"$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true
fi
CONFIG_DIR="$HOME/.config/opencode" CONFIG_DIR="$HOME/.config/opencode"
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json" OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
@@ -96,6 +96,14 @@ def register_mcp_servers(config: dict) -> list[str]:
"enabled": False, "enabled": False,
} }
# Context7 — up-to-date library documentation for LLMs (remote).
# Free tier works without an API key; set CONTEXT7_API_KEY for higher
# rate limits. No local binary needed — purely a remote MCP endpoint.
servers["context7"] = {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
}
if servers: if servers:
config["mcp"] = servers config["mcp"] = servers
@@ -110,14 +118,17 @@ def main() -> int:
home = Path(os.environ.get("HOME", "/home/developer")) home = Path(os.environ.get("HOME", "/home/developer"))
config_dir = home / ".config" / "opencode" config_dir = home / ".config" / "opencode"
config_file = config_dir / "opencode.json" config_file = config_dir / "opencode.jsonc"
config_file_legacy = config_dir / "opencode.json"
# CRITICAL: never overwrite an existing config. Users may have # CRITICAL: never overwrite an existing config. Users may have
# bind-mounted their host config directory, or their config may be # bind-mounted their host config directory, or their config may be
# persisted in a named volume from a previous run. # persisted in a named volume from a previous run.
if config_file.exists(): # Check both .json and .jsonc variants.
if config_file.exists() or config_file_legacy.exists():
existing = config_file if config_file.exists() else config_file_legacy
print( print(
f"Existing opencode.json found at {config_file}" f"Existing config found at {existing}"
"skipping generation.", "skipping generation.",
file=sys.stderr, file=sys.stderr,
) )
@@ -140,8 +151,23 @@ def main() -> int:
added = register_mcp_servers(config) added = register_mcp_servers(config)
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
# Write as JSONC so we can include helpful comments.
content = json.dumps(config, indent=2)
# Insert a comment about Context7 API key after the context7 url line.
context7_comment = (
' "url": "https://mcp.context7.com/mcp"\n'
" // For higher rate limits, sign up at https://context7.com/dashboard\n"
' // and add: "headers": { "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" }'
)
content = content.replace(
' "url": "https://mcp.context7.com/mcp"',
context7_comment,
)
with config_file.open("w") as f: with config_file.open("w") as f:
json.dump(config, f, indent=2) f.write(content)
f.write("\n") f.write("\n")
if added: if added:
+2 -1
View File
@@ -66,7 +66,8 @@ SECTION_RULES: dict[str, str] = {
"AWS Bedrock Authentication": "keep", "AWS Bedrock Authentication": "keep",
"MemPalace — persistent AI memory": "keep", "MemPalace — persistent AI memory": "keep",
"Gitea MCP server": "keep", "Gitea MCP server": "keep",
"Shell defaults": "keep", "Context7 MCP server": "drop",
"Shell defaults": "drop", # detail, full README covers it
"Secret Scanning": "drop", # dev-only — gitleaks is for committers "Secret Scanning": "drop", # dev-only — gitleaks is for committers
"Architecture": "keep", "Architecture": "keep",
"License": "replace", # point at source repo instead "License": "replace", # point at source repo instead
+21 -11
View File
@@ -160,11 +160,11 @@ else
fi fi
rm -f "$tmpout" rm -f "$tmpout"
# Config generation with anthropic provider writes valid JSON with the # Config generation with anthropic provider writes valid JSONC with the
# expected shape. The script's log message goes to stderr (line 1 of # expected shape. The script's log message goes to stderr (line 1 of
# generate-config.py uses file=sys.stderr) so capturing only stdout # generate-config.py uses file=sys.stderr) so capturing only stdout
# gives us clean JSON. # gives us clean JSONC. We strip // comments before validating JSON.
label="generate-config produces valid opencode.json" label="generate-config produces valid opencode.jsonc"
tmp=$(mktemp -d) tmp=$(mktemp -d)
if docker run --rm \ if docker run --rm \
-e OPENCODE_PROVIDER=anthropic \ -e OPENCODE_PROVIDER=anthropic \
@@ -173,24 +173,31 @@ if docker run --rm \
"$IMAGE" sh -c ' "$IMAGE" sh -c '
mkdir -p /tmp/home mkdir -p /tmp/home
python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null
cat /tmp/home/.config/opencode/opencode.json cat /tmp/home/.config/opencode/opencode.jsonc
' > "$tmp/out.json" 2>/dev/null; then ' > "$tmp/out.jsonc" 2>/dev/null; then
# Strip single-line // comments for JSON validation (respecting strings)
if python3 -c " if python3 -c "
import json, sys import re, json, sys
c = json.load(open('$tmp/out.json')) text = open('$tmp/out.jsonc').read()
# Match either a string literal or a // comment; keep strings, drop comments
pattern = r'\"(?:\\\\.|[^\"\\\\])*\"|//[^\n]*'
stripped = re.sub(pattern, lambda m: m.group(0) if m.group(0).startswith('\"') else '', text)
c = json.loads(stripped)
assert c['model'].startswith('anthropic/'), c assert c['model'].startswith('anthropic/'), c
assert c['autoupdate'] is False assert c['autoupdate'] is False
assert c['share'] == 'disabled' assert c['share'] == 'disabled'
assert 'context7' in c.get('mcp', {}), 'context7 MCP not registered'
" 2>&1; then " 2>&1; then
pass "$label" pass "$label"
else else
fail "$label: output doesn't match expected shape: $(cat "$tmp/out.json")" fail "$label: output doesn't match expected shape: $(cat "$tmp/out.jsonc")"
fi fi
else else
fail "$label: container failed: $(cat "$tmp/out.json")" fail "$label: container failed: $(cat "$tmp/out.jsonc")"
fi fi
# Config generation is idempotent — running twice must not overwrite. # Config generation is idempotent — running twice must not overwrite.
# Tests both legacy .json and new .jsonc detection.
label="generate-config never overwrites existing config" label="generate-config never overwrites existing config"
if docker run --rm \ if docker run --rm \
-e OPENCODE_PROVIDER=anthropic \ -e OPENCODE_PROVIDER=anthropic \
@@ -214,9 +221,12 @@ SIZE_BYTES=$(docker image inspect --format='{{.Size}}' "$IMAGE")
SIZE_MB=$((SIZE_BYTES / 1024 / 1024)) SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
echo " Uncompressed size: ${SIZE_MB} MB" echo " Uncompressed size: ${SIZE_MB} MB"
# Thresholds (uncompressed): base 2500 MB, omos 3000 MB. Adjust as image content evolves. # Thresholds (uncompressed): base 2500 MB, omos 3200 MB. Adjust as image content evolves.
# omos bumped 3000→3200 on v1.14.31c — mempalace-toolkit bake-in pushed the
# omos variant to ~3.1 GB. Functional smoke checks all pass; this is a
# guardrail, not a performance limit.
THRESHOLD=2500 THRESHOLD=2500
[ "$VARIANT" = "omos" ] && THRESHOLD=3000 [ "$VARIANT" = "omos" ] && THRESHOLD=3200
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT" fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
else else