Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f46c4ed017 | |||
| bf811f2170 | |||
| c76b1e8aa3 | |||
| 23bf383a37 | |||
| 5006b01170 | |||
| f51e9f52a1 | |||
| a208b073b0 | |||
| a803fe4653 | |||
| 79b697dea0 | |||
| 3e3abc8672 | |||
| 59e58a9d00 | |||
| 26ce9aa490 | |||
| 3d4e739529 | |||
| a6b0b59946 | |||
| fc74a8f906 | |||
| 5a2d06340e |
@@ -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
|
||||||
@@ -42,3 +67,28 @@ SSH_KEY_PATH=~/.ssh
|
|||||||
# OMOS_TMUX=false # Enable tmux multiplexer integration
|
# OMOS_TMUX=false # Enable tmux multiplexer integration
|
||||||
# OMOS_SKILLS=true # Install recommended skills (simplify, agent-browser, cartography)
|
# OMOS_SKILLS=true # Install recommended skills (simplify, agent-browser, cartography)
|
||||||
# OMOS_RESET=false # Force regenerate oh-my-opencode-slim config on next start
|
# OMOS_RESET=false # Force regenerate oh-my-opencode-slim config on next start
|
||||||
|
|
||||||
|
# ── pi coding-agent (alternative/complementary harness) ─────────────────
|
||||||
|
# Requires image built with INSTALL_PI=true.
|
||||||
|
# When the image is built with both INSTALL_OPENCODE=true (default) and
|
||||||
|
# INSTALL_PI=true, both harnesses share the same mempalace install and
|
||||||
|
# palace path — wing data is mutually visible to either harness.
|
||||||
|
#
|
||||||
|
# Pi version is baked at build time via PI_VERSION (default: latest at
|
||||||
|
# build). `pi update` inside the container would write to the npm global
|
||||||
|
# prefix, which is not on a named volume — updates do not persist across
|
||||||
|
# `--rm` containers. Rebuild the image to upgrade pi.
|
||||||
|
#
|
||||||
|
# Pi config (settings.json, extensions toggle state) persists in the
|
||||||
|
# devbox-pi-config named volume mounted at ~/.pi/.
|
||||||
|
#
|
||||||
|
# To launch pi from a `compose run` invocation:
|
||||||
|
# docker compose run --rm devbox pi
|
||||||
|
# To attach to a running container:
|
||||||
|
# docker compose exec -u developer devbox pi
|
||||||
|
# Default `compose run` (no args) drops to bash; pick the harness yourself.
|
||||||
|
#
|
||||||
|
# Build args (set in docker-compose.yml or via --build-arg on docker build):
|
||||||
|
# INSTALL_PI=true # default false; opt-in
|
||||||
|
# PI_VERSION=latest # pin a specific version, e.g. 0.73.0
|
||||||
|
# INSTALL_OPENCODE=false # build a pi-only image (still has Bun in -omos)
|
||||||
|
|||||||
+352
-159
@@ -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,10 @@ 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) ───────────────────────
|
smoke-with-pi:
|
||||||
build-base:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: smoke-base
|
|
||||||
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 +175,147 @@ 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 system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Build and load amd64 image for smoke test
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
build-args: |
|
||||||
|
INSTALL_PI=true
|
||||||
|
tags: opencode-devbox:smoke-with-pi
|
||||||
|
|
||||||
|
- name: Smoke test (amd64)
|
||||||
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi
|
||||||
|
|
||||||
|
smoke-omos-with-pi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
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 system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Build and load amd64 image for smoke test
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
build-args: |
|
||||||
|
INSTALL_OMOS=true
|
||||||
|
INSTALL_PI=true
|
||||||
|
tags: opencode-devbox:smoke-omos-with-pi
|
||||||
|
|
||||||
|
- name: Smoke test (amd64)
|
||||||
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
|
||||||
|
|
||||||
|
# ── Multi-arch push (single job per variant, comma-separated platforms) ─
|
||||||
|
build-base:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: smoke-base
|
||||||
|
timeout-minutes: 90
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
# Lighter reclaim than the smoke-gate version: push-by-digest
|
||||||
|
# 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: |
|
||||||
|
set -x
|
||||||
|
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 +328,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 +355,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 +393,160 @@ 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
|
||||||
|
|
||||||
|
build-with-pi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: smoke-with-pi
|
||||||
|
timeout-minutes: 90
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
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
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
with:
|
||||||
|
platforms: arm64
|
||||||
|
|
||||||
|
- 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: Build and push (multi-arch)
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
INSTALL_PI=true
|
||||||
|
tags: |
|
||||||
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-with-pi
|
||||||
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-with-pi
|
||||||
|
|
||||||
|
build-omos-with-pi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: smoke-omos-with-pi
|
||||||
|
timeout-minutes: 90
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
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
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
with:
|
||||||
|
platforms: arm64
|
||||||
|
|
||||||
|
- 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: Build and push (multi-arch)
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
INSTALL_OMOS=true
|
||||||
|
INSTALL_PI=true
|
||||||
|
tags: |
|
||||||
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos-with-pi
|
||||||
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos-with-pi
|
||||||
|
|
||||||
update-description:
|
update-description:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [merge-base, merge-omos]
|
needs: [build-base, build-omos, build-with-pi, build-omos-with-pi]
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -147,3 +147,116 @@ jobs:
|
|||||||
- name: Smoke test
|
- name: Smoke test
|
||||||
run: |
|
run: |
|
||||||
bash scripts/smoke-test.sh opencode-devbox:ci-omos --variant omos
|
bash scripts/smoke-test.sh opencode-devbox:ci-omos --variant omos
|
||||||
|
|
||||||
|
validate-with-pi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: |
|
||||||
|
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
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 system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Build with-pi image (amd64, load to local daemon)
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
build-args: |
|
||||||
|
INSTALL_PI=true
|
||||||
|
tags: opencode-devbox:ci-with-pi
|
||||||
|
|
||||||
|
- name: Smoke test
|
||||||
|
run: |
|
||||||
|
bash scripts/smoke-test.sh opencode-devbox:ci-with-pi --variant with-pi
|
||||||
|
|
||||||
|
validate-omos-with-pi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: |
|
||||||
|
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
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 system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Build omos+with-pi image (amd64, load to local daemon)
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
build-args: |
|
||||||
|
INSTALL_OMOS=true
|
||||||
|
INSTALL_PI=true
|
||||||
|
tags: opencode-devbox:ci-omos-with-pi
|
||||||
|
|
||||||
|
- name: Smoke test
|
||||||
|
run: |
|
||||||
|
bash scripts/smoke-test.sh opencode-devbox:ci-omos-with-pi --variant omos-with-pi
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
|||||||
|
|
||||||
## File roles
|
## File roles
|
||||||
|
|
||||||
- `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. Variants are gated by build args: `INSTALL_OMOS` (Bun + multi-agent layer), `INSTALL_OPENCODE` (default true), `INSTALL_PI` (default false), `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. Volume ownership loop covers `~/.pi/` when `INSTALL_PI=true`.
|
||||||
- `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`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, 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,13 @@ 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. **Same pattern for pi:** `devbox-pi-config` is mounted at `~/.pi/` and persists user toggles (`/ext`-disabled extensions) and `~/.pi/agent/settings.json` edits across container recreate.
|
||||||
|
- **pi install contract** — `INSTALL_PI=true` (default false) opt-in build arg. `pi` is npm-installed globally at build time; the npm prefix is NOT on a named volume, so `pi update` inside the container does not persist across `--rm` containers. Image rebuild is the upgrade path — same contract as `OPENCODE_VERSION`. The pi-toolkit and pi-extensions repos are git-cloned into `/opt/` at build time, then their `install.sh` runs from `entrypoint-user.sh` on each container start to symlink into `~/.pi/agent/` (which lives on the named volume). The mempalace pi-bridge is symlinked manually from `/opt/mempalace-toolkit/extensions/pi/mempalace.ts` — we do NOT call mempalace-toolkit's full `install.sh` because its `install_skill` step would race with skillset auto-deploy `--prune-stale`.
|
||||||
|
- **Pi deploy ordering matters in entrypoint-user.sh** — `pi-toolkit` runs first (creates `keybindings.json` symlink and writes pi-env.zsh), then `pi-extensions`, then `settings.json` template bootstrap, then mempalace bridge symlink. mempalace-toolkit's `check_pi_toolkit` probe (when called from the host install path) expects keybindings to already be present — not currently called from container, but ordering matches host convention.
|
||||||
|
- **Default CMD is `bash -l`** — not a harness. `docker compose run --rm devbox` drops the user into a login shell to choose: `aws sso login`, then `opencode` or `pi` (or any tool). Pass the harness explicitly to launch directly: `docker compose run --rm devbox opencode` / `docker compose run --rm devbox pi`. `docker compose exec` bypasses entrypoint+CMD entirely (existing user workflow unchanged).
|
||||||
- **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 +52,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
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,83 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.14.41 — 2026-05-08
|
||||||
|
|
||||||
|
Bump opencode to 1.14.41.
|
||||||
|
|
||||||
|
- **v1.14.41 (upstream):** restored formatter output handling for stdout/stderr writes; warping a session to another workspace can now carry over uncommitted file changes; restored custom provider setup in `/connect`; macOS Settings menu entry added; desktop local server split into a separate utility process; ACP clients restore last model/mode/effort when loading sessions and can close sessions cleanly.
|
||||||
|
|
||||||
|
No container-level changes in this release. Dockerfile bump only.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
**Optional pi as second harness.** Will become `v1.14.41b` on release.
|
||||||
|
|
||||||
|
- **Feature:** New `INSTALL_PI=true` build arg installs [pi](https://github.com/mariozechner/pi-coding-agent) as an alternative or complementary harness alongside opencode. Both harnesses share the same mempalace install and palace path — wing/diary entries are mutually visible. Adds ~150 MB to the image. Pi version pinned by `PI_VERSION` (default: latest at build time); `pi update` inside the container does not persist across `--rm` containers — image rebuild is the upgrade path, same contract as `OPENCODE_VERSION`.
|
||||||
|
- **Feature:** New `INSTALL_OPENCODE=false` build arg builds an image without opencode (e.g. for pi-only use). Default remains `true`. Existing builds and tags are unaffected.
|
||||||
|
- **Feature:** New `devbox-pi-config` named volume mounted at `~/.pi/` persists pi user state (settings.json, `/ext`-disabled extensions) across container recreate. Mirrors the `devbox-opencode-config` pattern from v1.14.33.
|
||||||
|
- **Feature:** Container clones [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) (keybindings, env loader, settings template) and [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) (6 extensions including ext-toggle, todo, ssh-controlmaster, notify, git-checkpoint, confirm-destructive) into `/opt/` at build time. New `PI_TOOLKIT_REF` and `PI_EXTENSIONS_REF` build args (default `main`) pin git refs. The mempalace pi-bridge `mempalace.ts` is symlinked from the existing `/opt/mempalace-toolkit/` clone.
|
||||||
|
- **Behavior change:** Default container CMD changed from `["opencode"]` to `["bash", "-l"]`. `docker compose run --rm devbox` (no command) now drops to a login shell so users can pick `opencode` or `pi` (or run `aws sso login` first). To preserve the old behavior, pass the harness explicitly: `docker compose run --rm devbox opencode`. `docker compose exec` workflows are unaffected (they bypass the entrypoint and CMD).
|
||||||
|
- **Performance:** chromadb's all-MiniLM-L6-v2 ONNX embedding model (~80 MB) is now pre-warmed at image build time under `~/.cache/chroma/onnx_models/`. Without this, mempalace's `init` step in entrypoint-user.sh would download the model silently on first container start (suppressed via `>/dev/null 2>&1`), stalling startup by minutes on a fresh image. Pre-warming runs as `gosu developer` so the cache lands at the right path and is owned by the runtime user.
|
||||||
|
- **Bugfix:** entrypoint-user.sh now redirects stdin from `/dev/null` for the `mempalace init --yes` call. Without this, the interactive `Mine this directory now? [Y/n]` prompt at the end of init would silently block forever when the container was started with `docker run -it` (TTY keeps stdin open). EOF on stdin makes the prompt fall through to its default.
|
||||||
|
- **Smoke-test:** New `--variant with-pi` (threshold 2700 MB) and `--variant omos-with-pi` (3400 MB). Pi-specific assertions verify pi binary, pi-toolkit clone, pi-extensions clone, deployed keybindings symlink, extension count ≥ 4, mempalace bridge symlink, and settings.json bootstrap. Pi state assertions use `docker exec` from the host (not `run`-inside-container) since the container has no docker CLI.
|
||||||
|
- **CI:** `.gitea/workflows/{validate,docker-publish}.yml` extended with `with-pi` and `omos-with-pi` matrix entries. Each release now produces eight Docker Hub tags: `vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos`, `vX.Y.Z[n]-with-pi`, `latest-with-pi`, `vX.Y.Z[n]-omos-with-pi`, `latest-omos-with-pi`.
|
||||||
|
- **Docs:** README adds a "pi (alternative/complementary harness)" section. AGENTS.md codifies pi install contract, deploy ordering in entrypoint-user.sh, and rationale for not calling mempalace-toolkit's full `install.sh` from container.
|
||||||
|
|
||||||
|
## 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
@@ -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 variant — oh-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
|
||||||
|
|
||||||
|
|||||||
+80
-5
@@ -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.41
|
||||||
|
|
||||||
LABEL maintainer="joakimp"
|
LABEL maintainer="joakimp"
|
||||||
LABEL description="Portable opencode developer container"
|
LABEL description="Portable opencode developer container"
|
||||||
@@ -271,9 +271,50 @@ RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesour
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ── Install opencode via npm ─────────────────────────────────────────
|
# ── Install opencode via npm ─────────────────────────────────────────
|
||||||
# v1.x is distributed as an npm package with platform-specific binaries
|
# v1.x is distributed as an npm package with platform-specific binaries.
|
||||||
RUN npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
# Disable with --build-arg INSTALL_OPENCODE=false to build a slimmer
|
||||||
opencode --version
|
# image without opencode (e.g. when only pi is needed). For a fully
|
||||||
|
# pi-only stripped image (no Bun, no opencode), see the pi-devbox repo.
|
||||||
|
ARG INSTALL_OPENCODE=true
|
||||||
|
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
||||||
|
npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
||||||
|
opencode --version ; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Optional: pi coding-agent ────────────────────────────────────────
|
||||||
|
# Installs pi as an alternative/complementary harness. Coexists with
|
||||||
|
# opencode in the same image — both share the mempalace install and
|
||||||
|
# palace path, so wing data is mutually visible to either harness.
|
||||||
|
#
|
||||||
|
# pi-toolkit (keybindings.json + pi-env.zsh + settings.example.json)
|
||||||
|
# and pi-extensions (confirm-destructive, ext-toggle, git-checkpoint,
|
||||||
|
# notify, ssh-controlmaster, todo, …) are cloned into /opt/ at build
|
||||||
|
# time. entrypoint-user.sh runs each repo's install.sh on container
|
||||||
|
# start so symlinks land under ~/.pi/agent/ on the named volume.
|
||||||
|
#
|
||||||
|
# Pi version is pinned by PI_VERSION (default: latest at build time).
|
||||||
|
# `pi update` inside the container would write to the npm global
|
||||||
|
# prefix, which is not on a volume — so updates do NOT persist across
|
||||||
|
# `--rm` containers. Same contract as OPENCODE_VERSION: rebuild the
|
||||||
|
# image to upgrade pi.
|
||||||
|
ARG INSTALL_PI=false
|
||||||
|
ARG PI_VERSION=latest
|
||||||
|
ARG PI_TOOLKIT_REF=main
|
||||||
|
ARG PI_EXTENSIONS_REF=main
|
||||||
|
RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
||||||
|
if [ "${PI_VERSION}" = "latest" ]; then \
|
||||||
|
npm install -g @mariozechner/pi-coding-agent ; \
|
||||||
|
else \
|
||||||
|
npm install -g @mariozechner/pi-coding-agent@${PI_VERSION} ; \
|
||||||
|
fi && \
|
||||||
|
pi --version && \
|
||||||
|
git clone --depth 1 --branch "${PI_TOOLKIT_REF}" \
|
||||||
|
https://gitea.jordbo.se/joakimp/pi-toolkit.git /opt/pi-toolkit && \
|
||||||
|
git clone --depth 1 --branch "${PI_EXTENSIONS_REF}" \
|
||||||
|
https://gitea.jordbo.se/joakimp/pi-extensions.git /opt/pi-extensions && \
|
||||||
|
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
||||||
|
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" ; \
|
||||||
|
fi
|
||||||
|
|
||||||
# ── AWS CLI v2 (for SSO/Bedrock authentication) ─────────────────────
|
# ── AWS CLI v2 (for SSO/Bedrock authentication) ─────────────────────
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in \
|
RUN ARCH=$(case "${TARGETARCH}" in \
|
||||||
@@ -342,14 +383,41 @@ RUN groupadd --gid ${USER_GID} ${USER_NAME} && \
|
|||||||
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${USER_NAME}
|
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${USER_NAME}
|
||||||
|
|
||||||
# Create standard directories
|
# Create standard directories
|
||||||
|
#
|
||||||
|
# ~/.pi/agent/extensions/ is created proactively so the named volume
|
||||||
|
# mount has a real owner from the first start. The directory is also
|
||||||
|
# what mempalace-toolkit's install_pi_extension probes to decide
|
||||||
|
# whether to deploy the pi↔mempalace bridge — must exist before that
|
||||||
|
# step runs in entrypoint-user.sh.
|
||||||
RUN mkdir -p /workspace \
|
RUN mkdir -p /workspace \
|
||||||
/home/${USER_NAME}/.config/opencode/skills \
|
/home/${USER_NAME}/.config/opencode/skills \
|
||||||
|
/home/${USER_NAME}/.pi/agent/extensions \
|
||||||
/home/${USER_NAME}/.agents/skills \
|
/home/${USER_NAME}/.agents/skills \
|
||||||
/home/${USER_NAME}/.local/share/opencode \
|
/home/${USER_NAME}/.local/share/opencode \
|
||||||
/home/${USER_NAME}/.cache/bash \
|
/home/${USER_NAME}/.cache/bash \
|
||||||
/home/${USER_NAME}/.ssh && \
|
/home/${USER_NAME}/.ssh && \
|
||||||
chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}
|
chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}
|
||||||
|
|
||||||
|
# ── Pre-warm chromadb embedding model ──────────────────────────────
|
||||||
|
# Mempalace uses chromadb's ONNXMiniLM_L6_V2 embedding function, which
|
||||||
|
# downloads ~80 MB of all-MiniLM-L6-v2 ONNX weights from chromadb's CDN
|
||||||
|
# on first use. Without pre-warming this happens silently (output is
|
||||||
|
# suppressed by the entrypoint init step) and stalls first container
|
||||||
|
# start by minutes on a slow network. We bake the cache at build time
|
||||||
|
# under the developer user's home so the runtime first-start is fast.
|
||||||
|
#
|
||||||
|
# Cache path comes from chromadb's hardcoded `Path.home() / .cache /
|
||||||
|
# chroma / onnx_models / all-MiniLM-L6-v2`. Run as gosu developer so
|
||||||
|
# Path.home() resolves correctly and ownership is right from the start.
|
||||||
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
||||||
|
gosu ${USER_NAME} /opt/uv-tools/mempalace/bin/python -c "\
|
||||||
|
from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2; \
|
||||||
|
ef = ONNXMiniLM_L6_V2(); \
|
||||||
|
_ = ef(['warmup']); \
|
||||||
|
print('chromadb embedding model warmed: all-MiniLM-L6-v2')" && \
|
||||||
|
ls -lh /home/${USER_NAME}/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ ; \
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Shell defaults (bash history, aliases, readline) ─────────────────
|
# ── Shell defaults (bash history, aliases, readline) ─────────────────
|
||||||
# Shipped under /etc/skel-devbox/ rather than copied directly to the
|
# Shipped under /etc/skel-devbox/ rather than copied directly to the
|
||||||
# user's home. The entrypoint copies them to /home/developer/ only if
|
# user's home. The entrypoint copies them to /home/developer/ only if
|
||||||
@@ -374,4 +442,11 @@ RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
|||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
ENTRYPOINT ["entrypoint.sh"]
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
CMD ["opencode"]
|
# Default to a login shell. `docker compose run --rm devbox` drops
|
||||||
|
# the user into bash to choose: `aws sso login`, then `opencode`
|
||||||
|
# or `pi`. To launch a harness directly, pass it explicitly:
|
||||||
|
# docker compose run --rm devbox opencode
|
||||||
|
# docker compose run --rm devbox pi
|
||||||
|
# `docker compose exec` bypasses the entrypoint and CMD entirely, so
|
||||||
|
# this default has no effect on attach-style workflows.
|
||||||
|
CMD ["bash", "-l"]
|
||||||
|
|||||||
@@ -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 variant — oh-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.
|
||||||
|
|
||||||
@@ -341,6 +341,10 @@ docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific versi
|
|||||||
| `INSTALL_MEMPALACE` | `true` | [MemPalace](https://github.com/MemPalace/mempalace) local AI memory system (~300 MB — disable to shrink image if you don't need MCP memory) |
|
| `INSTALL_MEMPALACE` | `true` | [MemPalace](https://github.com/MemPalace/mempalace) local AI memory system (~300 MB — disable to shrink image if you don't need MCP memory) |
|
||||||
| `INSTALL_MEMPALACE_TOOLKIT` | `true` | [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit) bash wrappers (`mempalace-session`, `mempalace-docs`). Cloned at build time from `MEMPALACE_TOOLKIT_REF` (default `main`). Requires `INSTALL_MEMPALACE=true`. |
|
| `INSTALL_MEMPALACE_TOOLKIT` | `true` | [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit) bash wrappers (`mempalace-session`, `mempalace-docs`). Cloned at build time from `MEMPALACE_TOOLKIT_REF` (default `main`). Requires `INSTALL_MEMPALACE=true`. |
|
||||||
| `INSTALL_OMOS` | `false` | [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration (installs Bun and plugin) |
|
| `INSTALL_OMOS` | `false` | [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration (installs Bun and plugin) |
|
||||||
|
| `INSTALL_OPENCODE` | `true` | Install opencode. Set `false` to build a pi-only image (still includes Bun if `INSTALL_OMOS=true`; for a fully stripped pi-only image see the `pi-devbox` repo). |
|
||||||
|
| `INSTALL_PI` | `false` | Install [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness. Both clones [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) (~5 MB) and [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) (~1 MB) into `/opt/`; entrypoint deploys them on container start. ~150 MB total image growth. |
|
||||||
|
| `PI_VERSION` | `latest` | npm version of `@mariozechner/pi-coding-agent`. Floats by default (image rebuild = pi update). |
|
||||||
|
| `PI_TOOLKIT_REF`, `PI_EXTENSIONS_REF` | `main` | Git refs for the toolkit/extensions clones. Pin to a tag/commit for reproducibility. |
|
||||||
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
| `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. |
|
| `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. |
|
| `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. |
|
||||||
@@ -402,6 +406,59 @@ ping all agents
|
|||||||
|
|
||||||
All six agents should respond if your provider authentication is working.
|
All six agents should respond if your provider authentication is working.
|
||||||
|
|
||||||
|
## pi (alternative/complementary harness)
|
||||||
|
|
||||||
|
[pi](https://github.com/mariozechner/pi-coding-agent) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the mempalace install and palace data — wing/diary entries created by one are visible to the other.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build --build-arg INSTALL_PI=true
|
||||||
|
# Or: pin a pi version
|
||||||
|
docker compose build --build-arg INSTALL_PI=true --build-arg PI_VERSION=0.73.0
|
||||||
|
# Or: pi-only image (no opencode, smaller)
|
||||||
|
docker compose build --build-arg INSTALL_PI=true --build-arg INSTALL_OPENCODE=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
The default `compose run --rm devbox` invocation drops to a login bash so you can choose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm devbox # bash, then `pi` or `opencode` or `aws sso login`
|
||||||
|
docker compose run --rm devbox pi # launch pi directly
|
||||||
|
docker compose run --rm devbox opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
For an attached `compose up -d` container, both harnesses are reachable via `compose exec`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec -u developer devbox pi
|
||||||
|
docker compose exec -u developer devbox opencode
|
||||||
|
docker compose exec -u developer devbox bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### What gets installed
|
||||||
|
|
||||||
|
- **`pi` CLI** — npm-installed globally at build time. Version pinned by `PI_VERSION`.
|
||||||
|
- **pi-toolkit** — keybindings.json (mosh/tmux newline fixes), pi-env.zsh (AWS env loader), settings.json template. Cloned to `/opt/pi-toolkit`; deployed to `~/.pi/agent/` on first container start.
|
||||||
|
- **pi-extensions** — 6 extensions: `confirm-destructive`, `ext-toggle` (`/ext` slash command), `git-checkpoint`, `notify`, `ssh-controlmaster`, `todo`. Cloned to `/opt/pi-extensions`; symlinked into `~/.pi/agent/extensions/`.
|
||||||
|
- **mempalace bridge** — `mempalace.ts` extension symlinked from the cloned mempalace-toolkit. Provides pi's MCP tools for palace search/diary/kg.
|
||||||
|
|
||||||
|
### Persistence
|
||||||
|
|
||||||
|
`~/.pi/` is mounted on the `devbox-pi-config` named volume. User toggles via `/ext`, edits to `~/.pi/agent/settings.json`, and any pi state survive container recreate. `pi update` writes to the npm global prefix which is *not* on a volume — image rebuild is the upgrade path.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The entrypoint copies `pi-toolkit/settings.example.json` to `~/.pi/agent/settings.json` on first start. Edit it to set provider/model:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec -u developer devbox $EDITOR ~/.pi/agent/settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The AWS env loader (`pi-env.zsh`) reads `~/.config/pi/.env` if you bind-mount one; otherwise pi uses container env vars passed via `.env`.
|
||||||
|
|
||||||
## AWS Bedrock Authentication
|
## AWS Bedrock Authentication
|
||||||
|
|
||||||
When using AWS Bedrock as your LLM provider, you need:
|
When using AWS Bedrock as your LLM provider, you need:
|
||||||
@@ -468,7 +525,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 +609,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 +627,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 +745,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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+24
-6
@@ -25,6 +25,9 @@ services:
|
|||||||
# args:
|
# args:
|
||||||
# INSTALL_GO: "false"
|
# INSTALL_GO: "false"
|
||||||
# INSTALL_OMOS: "false"
|
# INSTALL_OMOS: "false"
|
||||||
|
# INSTALL_PI: "false"
|
||||||
|
# # PI_VERSION: "latest"
|
||||||
|
# # INSTALL_OPENCODE: "true"
|
||||||
container_name: opencode-devbox
|
container_name: opencode-devbox
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
@@ -42,13 +45,26 @@ 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
|
||||||
|
- devbox-pi-config:/home/developer/.pi
|
||||||
|
|
||||||
|
# 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 +124,8 @@ services:
|
|||||||
# - ~/.aws:/home/developer/.aws
|
# - ~/.aws:/home/developer/.aws
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
devbox-opencode-config:
|
||||||
|
devbox-pi-config:
|
||||||
devbox-data:
|
devbox-data:
|
||||||
devbox-state:
|
devbox-state:
|
||||||
devbox-shell-history:
|
devbox-shell-history:
|
||||||
|
|||||||
+66
-1
@@ -25,7 +25,12 @@ if command -v mempalace &>/dev/null && [ -d /workspace ]; then
|
|||||||
PALACE_DIR="${HOME}/.mempalace"
|
PALACE_DIR="${HOME}/.mempalace"
|
||||||
if [ ! -d "$PALACE_DIR/palace" ]; then
|
if [ ! -d "$PALACE_DIR/palace" ]; then
|
||||||
echo "Initializing MemPalace for workspace (non-interactive)..."
|
echo "Initializing MemPalace for workspace (non-interactive)..."
|
||||||
mempalace init --yes /workspace >/dev/null 2>&1 || true
|
# </dev/null: mempalace init has an interactive "Mine this directory
|
||||||
|
# now? [Y/n]" prompt that --yes does not auto-answer in all paths.
|
||||||
|
# Without redirected stdin, the process blocks here forever when run
|
||||||
|
# from `docker run -it` (the TTY keeps stdin open). EOF on stdin
|
||||||
|
# makes the prompt fall through to its default (skip).
|
||||||
|
mempalace init --yes /workspace </dev/null >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -44,6 +49,66 @@ 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
|
||||||
|
|
||||||
|
# ── pi: deploy toolkit + extensions + mempalace bridge ─────────────
|
||||||
|
# Runs only when pi was baked into the image (INSTALL_PI=true at build).
|
||||||
|
# Each install.sh is idempotent and backs up real files before linking,
|
||||||
|
# so re-running across container restarts is safe.
|
||||||
|
#
|
||||||
|
# Order: pi-toolkit first (creates ~/.pi/agent/keybindings.json symlink
|
||||||
|
# and writes the AWS env loader), then pi-extensions (symlinks our 6
|
||||||
|
# extensions), then settings.json bootstrap from the toolkit template,
|
||||||
|
# then the mempalace bridge symlink (one-liner; mempalace-toolkit's
|
||||||
|
# install_skill is intentionally skipped to avoid racing with skillset
|
||||||
|
# auto-deploy below).
|
||||||
|
if command -v pi &>/dev/null; then
|
||||||
|
if [ -d /opt/pi-toolkit ]; then
|
||||||
|
(cd /opt/pi-toolkit && ./install.sh --yes) || \
|
||||||
|
echo "WARN: pi-toolkit install.sh failed (continuing)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /opt/pi-extensions ]; then
|
||||||
|
(cd /opt/pi-extensions && ./install.sh --yes) || \
|
||||||
|
echo "WARN: pi-extensions install.sh failed (continuing)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bootstrap settings.json from template if absent (pi rewrites this
|
||||||
|
# file at runtime — lastChangelogVersion, etc — so we can't symlink it).
|
||||||
|
if [ ! -f "$HOME/.pi/agent/settings.json" ] && \
|
||||||
|
[ -f /opt/pi-toolkit/settings.example.json ]; then
|
||||||
|
cp /opt/pi-toolkit/settings.example.json "$HOME/.pi/agent/settings.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pi↔mempalace MCP bridge — single extension symlink.
|
||||||
|
if [ -f /opt/mempalace-toolkit/extensions/pi/mempalace.ts ] && \
|
||||||
|
command -v mempalace &>/dev/null && \
|
||||||
|
[ ! -L "$HOME/.pi/agent/extensions/mempalace.ts" ]; then
|
||||||
|
ln -sf /opt/mempalace-toolkit/extensions/pi/mempalace.ts \
|
||||||
|
"$HOME/.pi/agent/extensions/mempalace.ts"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 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"
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ for dir in \
|
|||||||
/home/"$USER_NAME"/.vscode-server \
|
/home/"$USER_NAME"/.vscode-server \
|
||||||
/home/"$USER_NAME"/.config/opencode \
|
/home/"$USER_NAME"/.config/opencode \
|
||||||
/home/"$USER_NAME"/.config/nvim \
|
/home/"$USER_NAME"/.config/nvim \
|
||||||
|
/home/"$USER_NAME"/.pi \
|
||||||
/home/"$USER_NAME"/.agents/skills; do
|
/home/"$USER_NAME"/.agents/skills; do
|
||||||
[ -d "$dir" ] || continue
|
[ -d "$dir" ] || continue
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ SECTION_RULES: dict[str, str] = {
|
|||||||
"Usage": "keep",
|
"Usage": "keep",
|
||||||
"Configuration": "trim", # drop dev-build sub-sections
|
"Configuration": "trim", # drop dev-build sub-sections
|
||||||
"oh-my-opencode-slim (Multi-Agent Orchestration)": "keep",
|
"oh-my-opencode-slim (Multi-Agent Orchestration)": "keep",
|
||||||
|
"pi (alternative/complementary harness)": "drop", # full README only, would push past 25 kB
|
||||||
"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
|
||||||
|
|||||||
+92
-17
@@ -8,7 +8,7 @@
|
|||||||
# - Generated opencode.json has the expected shape
|
# - Generated opencode.json has the expected shape
|
||||||
# - MCP wrapper works (when mempalace is installed)
|
# - MCP wrapper works (when mempalace is installed)
|
||||||
#
|
#
|
||||||
# Usage: ./scripts/smoke-test.sh <image> [--variant base|omos]
|
# Usage: ./scripts/smoke-test.sh <image> [--variant base|omos|with-pi|omos-with-pi]
|
||||||
#
|
#
|
||||||
# Exit codes:
|
# Exit codes:
|
||||||
# 0 all checks passed
|
# 0 all checks passed
|
||||||
@@ -23,7 +23,7 @@ if [ "${2:-}" = "--variant" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$IMAGE" ]; then
|
if [ -z "$IMAGE" ]; then
|
||||||
echo "usage: $0 <image> [--variant base|omos]" >&2
|
echo "usage: $0 <image> [--variant base|omos|with-pi|omos-with-pi]" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -50,7 +50,12 @@ echo "-- Resolved component versions --"
|
|||||||
# always record what got baked into this image, even when Dockerfile
|
# always record what got baked into this image, even when Dockerfile
|
||||||
# ARGs default to "latest".
|
# ARGs default to "latest".
|
||||||
docker run --rm --entrypoint="" "$IMAGE" sh -c '
|
docker run --rm --entrypoint="" "$IMAGE" sh -c '
|
||||||
printf " %-15s %s\n" "opencode" "$(opencode --version 2>&1 | head -1)"
|
if command -v opencode >/dev/null 2>&1; then
|
||||||
|
printf " %-15s %s\n" "opencode" "$(opencode --version 2>&1 | head -1)"
|
||||||
|
fi
|
||||||
|
if command -v pi >/dev/null 2>&1; then
|
||||||
|
printf " %-15s %s\n" "pi" "$(pi --version 2>&1 | head -1)"
|
||||||
|
fi
|
||||||
printf " %-15s %s\n" "node" "$(node --version)"
|
printf " %-15s %s\n" "node" "$(node --version)"
|
||||||
printf " %-15s %s\n" "npm" "$(npm --version)"
|
printf " %-15s %s\n" "npm" "$(npm --version)"
|
||||||
printf " %-15s %s\n" "nvim" "$(nvim --version | head -1)"
|
printf " %-15s %s\n" "nvim" "$(nvim --version | head -1)"
|
||||||
@@ -77,7 +82,13 @@ docker run --rm --entrypoint="" "$IMAGE" sh -c '
|
|||||||
'
|
'
|
||||||
echo
|
echo
|
||||||
echo "-- Core binaries --"
|
echo "-- Core binaries --"
|
||||||
run "opencode" "opencode --version"
|
# opencode is gated on INSTALL_OPENCODE=true (default). When absent, the
|
||||||
|
# image is a pi-only build (or a pure base — no harness at all).
|
||||||
|
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v opencode" >/dev/null 2>&1; then
|
||||||
|
run "opencode" "opencode --version"
|
||||||
|
else
|
||||||
|
echo " - opencode not installed (INSTALL_OPENCODE=false)"
|
||||||
|
fi
|
||||||
run "node" "node --version"
|
run "node" "node --version"
|
||||||
run "npm" "npm --version"
|
run "npm" "npm --version"
|
||||||
run "git" "git --version"
|
run "git" "git --version"
|
||||||
@@ -117,8 +128,60 @@ elif docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v mempalace" >/dev
|
|||||||
echo " - mempalace-toolkit not installed (INSTALL_MEMPALACE_TOOLKIT=false)"
|
echo " - mempalace-toolkit not installed (INSTALL_MEMPALACE_TOOLKIT=false)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# bun: only in the omos variant
|
# pi: present when built with INSTALL_PI=true. Verifies pi itself plus
|
||||||
if [ "$VARIANT" = "omos" ]; then
|
# the runtime-deployed pi-toolkit + pi-extensions + mempalace bridge
|
||||||
|
# symlinks under ~/.pi/agent/. Note: extension symlinks are created by
|
||||||
|
# entrypoint-user.sh on first start, so we test by running the entry
|
||||||
|
# point chain (not just `docker run --entrypoint=""`).
|
||||||
|
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&1; then
|
||||||
|
run "pi" "pi --version"
|
||||||
|
run "pi-toolkit clone" "test -d /opt/pi-toolkit && git -C /opt/pi-toolkit rev-parse --short HEAD"
|
||||||
|
run "pi-extensions clone" "test -d /opt/pi-extensions && git -C /opt/pi-extensions rev-parse --short HEAD"
|
||||||
|
|
||||||
|
# Run the full entrypoint as developer to verify install.sh deployment.
|
||||||
|
# Spin up a long-running container so we can `docker exec` into it from
|
||||||
|
# the host — the `run` helper above invokes commands INSIDE the image
|
||||||
|
# and has no docker CLI to nest with.
|
||||||
|
CID=$(docker run -d --rm "$IMAGE" tail -f /dev/null)
|
||||||
|
trap 'docker rm -f "$CID" >/dev/null 2>&1 || true' EXIT
|
||||||
|
|
||||||
|
# Wait for entrypoint-user.sh to finish deploying pi-toolkit + extensions.
|
||||||
|
# Marker: keybindings.json symlink lands once pi-toolkit/install.sh has run.
|
||||||
|
# Up to 30s — omos-with-pi has more setup work than base+pi.
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
if docker exec "$CID" test -L /home/developer/.pi/agent/keybindings.json 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
exec_test() {
|
||||||
|
local label="$1"; shift
|
||||||
|
local out
|
||||||
|
if out=$(docker exec -u developer "$CID" sh -c "$*" 2>&1); then
|
||||||
|
pass "$label ($(echo "$out" | head -1))"
|
||||||
|
else
|
||||||
|
fail "$label: $out"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
exec_test "~/.pi/agent/keybindings.json (pi-toolkit)" \
|
||||||
|
'test -L $HOME/.pi/agent/keybindings.json && echo ok'
|
||||||
|
exec_test "~/.pi/agent/extensions/*.ts ≥ 4 (pi-extensions)" \
|
||||||
|
'count=$(ls -1 $HOME/.pi/agent/extensions/*.ts 2>/dev/null | wc -l); [ $count -ge 4 ] && echo "$count extensions"'
|
||||||
|
exec_test "~/.pi/agent/extensions/mempalace.ts (bridge)" \
|
||||||
|
'test -L $HOME/.pi/agent/extensions/mempalace.ts && echo ok'
|
||||||
|
exec_test "~/.pi/agent/settings.json (template bootstrap)" \
|
||||||
|
'test -f $HOME/.pi/agent/settings.json && echo ok'
|
||||||
|
|
||||||
|
docker rm -f "$CID" >/dev/null 2>&1 || true
|
||||||
|
trap - EXIT
|
||||||
|
else
|
||||||
|
echo " - pi not installed (INSTALL_PI=false)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# bun: only in the omos and omos-with-pi variants
|
||||||
|
if [ "$VARIANT" = "omos" ] || [ "$VARIANT" = "omos-with-pi" ]; then
|
||||||
run "bun (omos)" "bun --version"
|
run "bun (omos)" "bun --version"
|
||||||
run "bunx symlink (omos)" "test -L /usr/local/bin/bunx && readlink /usr/local/bin/bunx"
|
run "bunx symlink (omos)" "test -L /usr/local/bin/bunx && readlink /usr/local/bin/bunx"
|
||||||
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
||||||
@@ -160,11 +223,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 +236,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 +284,14 @@ 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, with-pi adds ~150 MB.
|
||||||
|
# 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
|
||||||
|
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2700
|
||||||
|
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3400
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user