Compare commits

..

13 Commits

Author SHA1 Message Date
pi ff6e17b732 v1.17.2: bump opencode 1.16.2->1.17.2, deprecate pi, pin+patch mempalace
Validate / base-change-warning (push) Successful in 7s
Validate / docs-check (push) Failing after 9s
Validate / validate-omos (push) Successful in 4m4s
Validate / validate-with-pi (push) Successful in 7m14s
Validate / validate-omos-with-pi (push) Successful in 5m46s
Publish Docker Image / base-decide (push) Successful in 9s
Publish Docker Image / resolve-versions (push) Successful in 4s
Validate / validate-pi-only (push) Successful in 6m27s
Validate / validate-base (push) Successful in 14m39s
Publish Docker Image / build-base (push) Successful in 31m9s
Publish Docker Image / smoke-base (push) Successful in 5m3s
Publish Docker Image / smoke-with-pi (push) Successful in 5m2s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 5m59s
Publish Docker Image / smoke-pi-only (push) Successful in 6m48s
Publish Docker Image / smoke-omos (push) Successful in 12m8s
Publish Docker Image / build-variant-base (push) Successful in 13m37s
Publish Docker Image / build-variant-with-pi (push) Successful in 17m8s
Publish Docker Image / build-variant-pi-only (push) Successful in 22m57s
Publish Docker Image / build-variant-omos (push) Successful in 19m4s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 28m5s
Publish Docker Image / promote-base-latest (push) Successful in 10s
Publish Docker Image / update-description (push) Successful in 12s
opencode-ai 1.16.2 -> 1.17.2 (OPENCODE_VERSION).

Deprecate all pi support ahead of v2.0.0 removal (pi now ships from the
standalone joakimp/pi-devbox image, v1.0.0+, which no longer FROMs
base-pi-only):
- build-time stderr deprecation warning when INSTALL_PI=true
- README / DOCKER_HUB.md / AGENTS.md mark the with-pi/omos-with-pi/pi-only
  variants + base-pi-only tag deprecated, point to pi-devbox
- docs/CLEANUP-v2.0.0.md committed as the removal plan
- CHANGELOG pre-announces the v2.0.0 NPM_CONFIG_PREFIX relocation

Harden mempalace install (mirrors pi-devbox):
- pin via MEMPALACE_VERSION ARG (default 3.4.0); unpinned install is what
  swept in the broken schema
- idempotent, self-deactivating patch stripping the top-level anyOf from
  mempalace_diary_write input_schema (Anthropic tools API rejects it).
  Upstream: MemPalace/mempalace#1728, PR #1735

Fold prior Unreleased smoke-test pi-extensions readiness fix into v1.17.2.
2026-06-10 19:31:49 +02:00
pi c6f9d1148b smoke: wait for pi-extensions deploy completion, not just keybindings
Validate / base-change-warning (push) Successful in 6s
Validate / docs-check (push) Successful in 11s
Validate / validate-base (push) Successful in 3m24s
Validate / validate-with-pi (push) Successful in 4m59s
Validate / validate-omos (push) Successful in 6m59s
Validate / validate-pi-only (push) Successful in 4m20s
Validate / validate-omos-with-pi (push) Successful in 14m33s
The entrypoint-deploy wait loop gated only on keybindings.json (written by
pi-toolkit, before pi-extensions), so the *.ts >= 4 assertion could sample
mid-deploy under parallel build load. v1.16.2 run 370: smoke-with-pi saw <4
while omos-with-pi/pi-only (same pi-extensions 357fcc6) saw 8, skipping
build-variant-with-pi. Now wait for the last-deployed artifact (mempalace.ts
bridge) AND a settled extension count (>=4), up to 45s. Test-only; no image
change, so no re-tag needed.
2026-06-08 22:49:09 +02:00
pi 56e6a782e3 Bump opencode 1.15.13 -> 1.16.2, pick up pi 0.79.0
Validate / base-change-warning (push) Successful in 10s
Validate / docs-check (push) Successful in 52s
Validate / validate-base (push) Successful in 3m7s
Validate / validate-omos (push) Successful in 6m40s
Validate / validate-omos-with-pi (push) Successful in 4m54s
Publish Docker Image / base-decide (push) Successful in 12s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / resolve-versions (push) Successful in 5s
Validate / validate-with-pi (push) Successful in 10m11s
Publish Docker Image / smoke-base (push) Successful in 3m5s
Publish Docker Image / smoke-omos (push) Successful in 4m24s
Validate / validate-pi-only (push) Successful in 6m6s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 5m0s
Publish Docker Image / smoke-pi-only (push) Successful in 3m39s
Publish Docker Image / build-variant-base (push) Successful in 15m24s
Publish Docker Image / build-variant-omos (push) Successful in 18m44s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 22m14s
Publish Docker Image / build-variant-pi-only (push) Successful in 21m13s
Publish Docker Image / smoke-with-pi (push) Successful in 4m0s
Publish Docker Image / build-variant-with-pi (push) Successful in 16m49s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Successful in 11s
Bump OPENCODE_VERSION in Dockerfile.variant to 1.16.2 (rolls up the
1.16.0/1.16.1/1.16.2 upstream releases of 2026-06-05). The pi-bearing
variants pick up pi 0.78.1 -> 0.79.0 via CI's resolve-versions job.

Preemptively raise smoke size thresholds +150 MB on opencode-bearing
variants (base/omos/with-pi/omos-with-pi) and +100 MB on pi-only ahead
of the combined minor opencode + pi bump. base (2506) and omos (3206)
were on ~94 MB headroom and minor bumps have tripped these before
(v1.15.0, v1.15.4); restores ~250 MB headroom to avoid a partial publish.

Promote CHANGELOG Unreleased -> v1.16.2.
2026-06-08 21:58:46 +02:00
pi 49d3e113ee docs: complete CHANGELOG/AGENTS + promote Unreleased -> v1.15.13e
Validate / docs-check (push) Successful in 7s
Validate / base-change-warning (push) Successful in 11s
Validate / validate-base (push) Successful in 3m33s
Validate / validate-with-pi (push) Successful in 4m35s
Validate / validate-omos (push) Successful in 6m59s
Validate / validate-pi-only (push) Successful in 3m30s
Validate / validate-omos-with-pi (push) Successful in 17m14s
Publish Docker Image / base-decide (push) Successful in 15s
Publish Docker Image / resolve-versions (push) Successful in 9s
Publish Docker Image / build-base (push) Successful in 31m45s
Publish Docker Image / smoke-base (push) Successful in 3m44s
Publish Docker Image / smoke-omos (push) Successful in 4m44s
Publish Docker Image / smoke-pi-only (push) Successful in 3m38s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 8m54s
Publish Docker Image / smoke-with-pi (push) Successful in 10m44s
Publish Docker Image / build-variant-base (push) Successful in 14m24s
Publish Docker Image / build-variant-omos (push) Successful in 19m43s
Publish Docker Image / build-variant-pi-only (push) Successful in 18m42s
Publish Docker Image / build-variant-with-pi (push) Successful in 17m45s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 32m54s
Publish Docker Image / promote-base-latest (push) Successful in 9s
Publish Docker Image / update-description (push) Successful in 12s
- CHANGELOG: add the missing entry for the ~/.config/devbox-shell compose-doc
  commit (440218f); promote Unreleased -> v1.15.13e (2026-06-04) with a release
  summary (letter-suffix rebuild on opencode 1.15.13, picks up pi 0.78.1 + LAN
  key persistence + devbox-ssh-local chown fix + validate.yml false-neg fix).
- AGENTS.md: document the STRICT_REGISTRATION smoke-gate knob under CI quirks
  (kept in lockstep with the validate.yml/docker-publish-split.yml change).

Docs only; no image/behavior change. Tagging v1.15.13e after this lands.
2026-06-04 22:41:30 +02:00
pi f1e879ca6c docs: per-host ControlPath under ~/.ssh breaks pi --ssh (read-only mount)
The bind-mounted ~/.ssh/config is read before the baked Host * default and
SSH uses the first ControlPath it sees. A per-host block pointing ControlPath
under ~/.ssh/ (CGNAT-multiplexing pattern) wins but fails in-container because
~/.ssh is read-only, silently breaking pi --ssh <host> (falls back to local
tools). Documented the host-side fix: drop the override or repoint at the
writable /tmp/sshcm/. README + CHANGELOG only, no image change.
2026-06-04 22:31:54 +02:00
pi 9c31c641d6 smoke: gate fork/recall registration checks behind STRICT_REGISTRATION (#12)
Validate / base-change-warning (push) Successful in 7s
Validate / docs-check (push) Successful in 8s
Validate / validate-omos (push) Successful in 4m31s
Validate / validate-with-pi (push) Successful in 4m29s
Validate / validate-pi-only (push) Successful in 3m38s
Validate / validate-base (push) Successful in 9m41s
Validate / validate-omos-with-pi (push) Successful in 5m14s
validate.yml builds variants FROM the published base-latest, which lags
the entrypoint in the current commit until a release tag rebuilds the
base. The fork/recall registration smoke checks depend on the base
entrypoint running 'pi install /opt/<pkg>', so a stale base-latest reded
push-to-main runs with a false negative even when the variant layer was
correct.

smoke-test.sh now gates the two registration assertions behind
STRICT_REGISTRATION (warn-only when unset). validate.yml leaves it unset;
docker-publish-split.yml, which builds the base fresh in the same run,
sets STRICT_REGISTRATION=1 on the pi-bearing smoke jobs. Build-time /opt
+ node_modules checks stay hard in both paths.
2026-06-04 21:59:39 +02:00
pi d9dc85d825 entrypoint: chown devbox-ssh-local volume so jump key generates
Validate / docs-check (push) Successful in 6s
Validate / base-change-warning (push) Successful in 13s
Validate / validate-omos (push) Successful in 4m28s
Validate / validate-base (push) Successful in 5m31s
Validate / validate-omos-with-pi (push) Successful in 5m17s
Validate / validate-with-pi (push) Successful in 10m30s
Validate / validate-pi-only (push) Successful in 5m43s
The named-volume persistence change for ~/.ssh-local did not update the
entrypoint's volume-ownership loop. Docker creates named volumes as
root:root, so setup-lan-access.sh (running as developer) silently failed
to mkdir/ssh-keygen, leaving no jump key and breaking LAN access on the
first --force-recreate. Add ~/.ssh-local to the chown list.
2026-06-04 14:59:46 +02:00
pi 0b78ab4a94 LAN jump key: persist via named volume + one-line authorize hint
Validate / docs-check (push) Successful in 7s
Validate / base-change-warning (push) Successful in 9s
Validate / validate-omos (push) Successful in 4m26s
Validate / validate-with-pi (push) Successful in 4m30s
Validate / validate-pi-only (push) Successful in 3m33s
Validate / validate-omos-with-pi (push) Successful in 8m44s
Validate / validate-base (push) Successful in 9m8s
Persist ~/.ssh-local (devbox-ssh-local named volume) so the generated
LAN-jump key survives 'docker compose up --force-recreate'. Authorize
it on the host once per machine instead of after every container update.

setup-lan-access.sh now prints a copy-paste
'echo <pubkey> >> ~/.ssh/authorized_keys' line whenever it generates a
new key (not only when HOST_SSH_USER is unset), and stays silent once
the key is persisted. README + CHANGELOG updated.
2026-06-04 14:33:58 +02:00
pi 440218fc4c compose: document optional ~/.config/devbox-shell mount (LAN ssh-lan.conf + bash_aliases bridge)
Validate / base-change-warning (push) Successful in 6s
Validate / docs-check (push) Successful in 14s
Validate / validate-base (push) Successful in 3m32s
Validate / validate-with-pi (push) Successful in 4m32s
Validate / validate-omos (push) Successful in 6m58s
Validate / validate-pi-only (push) Successful in 3m37s
Validate / validate-omos-with-pi (push) Successful in 17m51s
2026-06-04 13:34:10 +02:00
pi a56a5846a5 LAN-access: fix Include scope + read-only ControlPath, add ssh-lan.conf & RFC1918 autojump
Validate / docs-check (push) Successful in 6s
Validate / base-change-warning (push) Successful in 11s
Validate / validate-omos (push) Successful in 4m25s
Validate / validate-base (push) Successful in 5m21s
Validate / validate-omos-with-pi (push) Successful in 5m24s
Publish Docker Image / base-decide (push) Successful in 9s
Publish Docker Image / resolve-versions (push) Successful in 4s
Validate / validate-with-pi (push) Successful in 10m42s
Validate / validate-pi-only (push) Successful in 5m51s
Publish Docker Image / build-base (push) Successful in 30m30s
Publish Docker Image / smoke-base (push) Successful in 3m31s
Publish Docker Image / smoke-with-pi (push) Successful in 7m7s
Publish Docker Image / smoke-pi-only (push) Successful in 3m50s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 5m20s
Publish Docker Image / smoke-omos (push) Successful in 12m4s
Publish Docker Image / build-variant-base (push) Successful in 15m56s
Publish Docker Image / build-variant-pi-only (push) Successful in 16m6s
Publish Docker Image / build-variant-with-pi (push) Successful in 17m56s
Publish Docker Image / build-variant-omos (push) Successful in 22m32s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 33m41s
Publish Docker Image / update-description (push) Successful in 9s
Publish Docker Image / promote-base-latest (push) Successful in 13s
- Fix: Include ~/.ssh/config was scoped to the Host host/mac block, so
  dssh <peer> by name fell back to SSH defaults. Emit Host * scope reset
  before every Include.
- Fix: redirect ControlPath to writable ~/.ssh-local sidecar (Mac config's
  ~/.ssh/cm path is read-only in the container, broke multiplexed hosts).
- Add: Include host-owned ~/.config/devbox-shell/ssh-lan.conf for named-peer
  ProxyJump overrides (keeps image generic; peer names stay host-side).
- Add: opt-in DEVBOX_LAN_AUTOJUMP_PRIVATE=1 RFC1918 catch-all for roaming.
- Docs: README/.env.example/AGENTS/CHANGELOG + new ssh-lan.conf.example.
2026-06-04 00:52:42 +02:00
pi 053dac5308 docs: promote CHANGELOG Unreleased -> v1.15.13c
Validate / base-change-warning (push) Successful in 7s
Validate / docs-check (push) Successful in 14s
Validate / validate-omos (push) Successful in 4m23s
Validate / validate-with-pi (push) Failing after 4m38s
Publish Docker Image / base-decide (push) Successful in 11s
Publish Docker Image / resolve-versions (push) Successful in 5s
Publish Docker Image / build-base (push) Has been skipped
Validate / validate-base (push) Successful in 5m27s
Validate / validate-pi-only (push) Failing after 3m59s
Publish Docker Image / smoke-base (push) Successful in 3m33s
Publish Docker Image / smoke-omos (push) Successful in 7m4s
Publish Docker Image / smoke-with-pi (push) Successful in 4m35s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 5m31s
Validate / validate-omos-with-pi (push) Failing after 15m20s
Publish Docker Image / smoke-pi-only (push) Successful in 6m16s
Publish Docker Image / build-variant-base (push) Successful in 14m24s
Publish Docker Image / build-variant-omos (push) Successful in 19m13s
Publish Docker Image / build-variant-with-pi (push) Successful in 25m46s
Publish Docker Image / build-variant-pi-only (push) Successful in 15m39s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 27m57s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Successful in 7s
2026-06-03 21:46:37 +02:00
pi c71c03f0f1 fix: bump base smoke size threshold 2500->2600 MB
v1.15.13b's base crept to 2506 MB (LAN-access script + updated entrypoint
+ apt drift), tripping the zero-headroom 2500 ceiling. smoke-base failed,
which cascaded into skipping build-variant-base AND promote-base-latest,
so base-latest never advanced. All functional checks passed — this is a
guardrail bump, not a real regression. base-<hash> and the omos/with-pi/
omos-with-pi/pi-only variants did publish on the fresh base in run 354.
2026-06-03 21:46:15 +02:00
pi 1e98b53113 feat: publish pi-only build into the pi-devbox repo, not opencode-devbox (Option B)
The pi-only variant was published as opencode-devbox:latest-pi-only —
an 'opencode-devbox' tag containing no opencode, which confused users.

- build-variant-pi-only now pushes joakimp/pi-devbox:base-pi-only[-vX.Y.Z]
  instead of opencode-devbox:*-pi-only. New PI_IMAGE workflow env.
- Still built from the same Dockerfile.variant (single source of truth),
  still smoke-tested by smoke-pi-only / validate-pi-only before publish.
- De-advertised pi-only from README, DOCKER_HUB (HUB_TEMPLATE), AGENTS,
  .gitea/README. opencode-devbox now publishes 8 tags + base-latest.
- Documented in CHANGELOG (Unreleased) and the plan doc.

Note: old opencode-devbox:{latest,vX.Y.Z}-pi-only tags from v1.15.13b are
superseded and should be deleted from Docker Hub.
2026-06-03 17:04:21 +02:00
18 changed files with 895 additions and 54 deletions
+11 -2
View File
@@ -37,8 +37,11 @@ SSH_KEY_PATH=~/.ssh
# directly-attached LAN peers by default. On native Linux Docker the LAN is # directly-attached LAN peers by default. On native Linux Docker the LAN is
# reachable directly and nothing is needed. The entrypoint detects this and, # reachable directly and nothing is needed. The entrypoint detects this and,
# on VM-backed hosts, generates ~/.ssh-local/config so the host can be used # on VM-backed hosts, generates ~/.ssh-local/config so the host can be used
# as an SSH jump (use the `dssh` alias, or add `ProxyJump host` to targets # as an SSH jump (use the `dssh` alias). Reach the host itself with
# in your bind-mounted ~/.ssh/config). # `dssh host`. To reach named LAN peers, put `ProxyJump host` overrides in a
# host-owned ~/.config/devbox-shell/ssh-lan.conf (bind-mounted in) rather than
# editing your ~/.ssh/config — see ssh-lan.conf.example. Public-IP hosts (and
# anything reached via a public jump host) connect directly, no jump needed.
# #
# DEVBOX_LAN_ACCESS: auto (default) | jump | off # DEVBOX_LAN_ACCESS: auto (default) | jump | off
# auto = set up the jump only on VM-backed hosts; no-op on native Linux. # auto = set up the jump only on VM-backed hosts; no-op on native Linux.
@@ -54,6 +57,12 @@ SSH_KEY_PATH=~/.ssh
# #
# DEVBOX_HOST_ALIAS: host hostname to reach (default host.docker.internal). # DEVBOX_HOST_ALIAS: host hostname to reach (default host.docker.internal).
# DEVBOX_HOST_ALIAS=host.docker.internal # DEVBOX_HOST_ALIAS=host.docker.internal
#
# DEVBOX_LAN_AUTOJUMP_PRIVATE: 1 = ProxyJump ANY RFC1918 (private) IP through
# the host, so bare `dssh user@<ip>` works on whatever LAN the (roaming) host
# is currently joined to, without naming peers. Matches the typed address, not
# the resolved HostName, so named hosts with their own ProxyJump are unaffected.
# DEVBOX_LAN_AUTOJUMP_PRIVATE=0
# ── Skillset (agent skills and instructions) ───────────────────────── # ── Skillset (agent skills and instructions) ─────────────────────────
# If you have a skillset repo, the entrypoint auto-deploys skills and # If you have a skillset repo, the entrypoint auto-deploys skills and
+1 -1
View File
@@ -13,7 +13,7 @@ the build pipeline is shaped the way it is, you're in the right place.
## Why the split-base pipeline exists ## Why the split-base pipeline exists
opencode-devbox publishes **five image variants** (`base`, `omos`, `with-pi`, `omos-with-pi`, `pi-only`) × **two architectures** (amd64, arm64) = **ten image tags per release**. Today's runners are 2 self-hosted gitea Actions runners. arm64 builds are emulated under QEMU, which is the dominant cost (~35x slower than native). opencode-devbox builds **five image variants** (`base`, `omos`, `with-pi`, `omos-with-pi`, `pi-only`) × **two architectures** (amd64, arm64). Four opencode-bearing variants publish under this repo (**eight tags per release** + the floating `base-latest`); the `pi-only` build is pushed into the separate `joakimp/pi-devbox` repo as `base-pi-only` (so no opencode-less tag appears here). Today's runners are 2 self-hosted gitea Actions runners. arm64 builds are emulated under QEMU, which is the dominant cost (~35x slower than native).
The five variants share ~95% of their layers (Debian + apt + Node + AWS CLI + mempalace + dev tools + entrypoints). The original `Dockerfile` was a single multi-stage build with `INSTALL_*` build-args gating variant-specific RUNs. BuildKit's per-layer cache key is content-addressed, but as soon as a build-arg-gated `RUN` produces a different layer hash for variant A vs variant B, every subsequent layer also has a different parent → identical commands re-execute per variant. Result: minimal cross-variant cache reuse on a fresh build. The five variants share ~95% of their layers (Debian + apt + Node + AWS CLI + mempalace + dev tools + entrypoints). The original `Dockerfile` was a single multi-stage build with `INSTALL_*` build-args gating variant-specific RUNs. BuildKit's per-layer cache key is content-addressed, but as soon as a build-arg-gated `RUN` produces a different layer hash for variant A vs variant B, every subsequent layer also has a different parent → identical commands re-execute per variant. Result: minimal cross-variant cache reuse on a fresh build.
+13 -2
View File
@@ -37,6 +37,11 @@ concurrency:
env: env:
BUILDKIT_PROGRESS: plain BUILDKIT_PROGRESS: plain
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox
# The pi-only variant is built here (single source of truth for the pi stack)
# but published into the pi-devbox repo as an internal building-block tag,
# NOT under opencode-devbox — so opencode-devbox never shows a tag with no
# opencode in it. pi-devbox's own CI FROMs PI_IMAGE:base-pi-only.
PI_IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/pi-devbox
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }} RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }}
PROMOTE_LATEST: ${{ github.ref_type == 'tag' && 'true' || inputs.promote_latest }} PROMOTE_LATEST: ${{ github.ref_type == 'tag' && 'true' || inputs.promote_latest }}
@@ -381,6 +386,7 @@ jobs:
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }} PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
- env: - env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }} EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
STRICT_REGISTRATION: "1"
run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi
smoke-omos-with-pi: smoke-omos-with-pi:
@@ -430,6 +436,7 @@ jobs:
- env: - env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }} EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }} EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
STRICT_REGISTRATION: "1"
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
smoke-pi-only: smoke-pi-only:
@@ -477,6 +484,7 @@ jobs:
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }} PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
- env: - env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }} EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
STRICT_REGISTRATION: "1"
run: bash scripts/smoke-test.sh opencode-devbox:smoke-pi-only --variant pi-only run: bash scripts/smoke-test.sh opencode-devbox:smoke-pi-only --variant pi-only
# ── Phase 4: multi-arch publish per variant ──────────────────────── # ── Phase 4: multi-arch publish per variant ────────────────────────
@@ -801,11 +809,14 @@ jobs:
- name: Compute version-specific tags - name: Compute version-specific tags
id: tags id: tags
run: | run: |
# Option B: push the pi-only build into the pi-devbox repo as an
# internal building-block tag (base-pi-only[-<version>]), NOT under
# opencode-devbox. pi-devbox's CI FROMs ${PI_IMAGE}:base-pi-only.
VERSION="${{ env.RELEASE_TAG }}" VERSION="${{ env.RELEASE_TAG }}"
{ echo "tags<<EOF" { echo "tags<<EOF"
echo "${IMAGE}:${VERSION}-pi-only" echo "${PI_IMAGE}:base-pi-only-${VERSION}"
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
echo "${IMAGE}:latest-pi-only" echo "${PI_IMAGE}:base-pi-only"
fi fi
echo "EOF" echo "EOF"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
+8
View File
@@ -20,6 +20,14 @@ name: Validate
# release tags are the gate that fully validates base-image changes. # release tags are the gate that fully validates base-image changes.
# The base-change-warning job below surfaces a runtime warning when this # The base-change-warning job below surfaces a runtime warning when this
# blind-spot applies. # blind-spot applies.
#
# Because of this, the fork/recall *registration* smoke checks (which depend on
# the base entrypoint running `pi install /opt/<pkg>`) are warn-only here:
# smoke-test.sh leaves STRICT_REGISTRATION unset on this path, so a base-latest
# that lags the entrypoint in the current commit can't red the run with a false
# negative. The release smoke jobs build the base fresh and set
# STRICT_REGISTRATION=1 to enforce those checks. The build-time /opt +
# node_modules checks stay hard in both paths.
on: on:
push: push:
+13 -3
View File
@@ -4,13 +4,22 @@
Docker image packaging [opencode](https://opencode.ai) into a production-ready dev container. Image variants are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfiles, entrypoint scripts, docker-compose, documentation). Docker image packaging [opencode](https://opencode.ai) into a production-ready dev container. Image variants are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfiles, entrypoint scripts, docker-compose, documentation).
> **pi is deprecated here (since v1.17.2), removed in v2.0.0.** The
> `INSTALL_PI` build arg, the `with-pi` / `omos-with-pi` / `pi-only`
> variants, the `base-pi-only` published tag, and all `~/.pi`-related
> wiring are slated for removal. pi now ships from its own repo
> (`joakimp/pi-devbox`). Do not add new pi functionality here. Full
> removal plan + the `NPM_CONFIG_PREFIX` relocation: see
> `docs/CLEANUP-v2.0.0.md`. The pi-related descriptions below remain
> accurate only until the v2.0.0 removal lands.
## File roles ## File roles
- `Dockerfile.base` — variant-independent layers (apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints). Published as `joakimp/opencode-devbox:base-<sha12>`. Rebuilt only when its content hash changes. - `Dockerfile.base` — variant-independent layers (apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints). Published as `joakimp/opencode-devbox:base-<sha12>`. Rebuilt only when its content hash changes.
- `Dockerfile.variant``FROM`s the base and adds only opencode/omos/pi installs gated by build args: `INSTALL_OPENCODE` (default true), `INSTALL_OMOS`, `INSTALL_PI`, and `INSTALL_MEMPALACE`. All GitHub-sourced binaries are pinned with version ARGs. When `INSTALL_PI=true` it also clones `pi-fork` + `pi-observational-memory` (from `github.com/elpapi42`, refs `PI_FORK_REF`/`PI_OBSMEM_REF`) to `/opt` and runs `npm install` there at build time so the `fork`/`recall` extensions can load (a local-path `pi install` does not npm-install). The `pi-only` variant sets `INSTALL_OPENCODE=false`, `INSTALL_PI=true` — pi without opencode, the single source of truth for the separate `pi-devbox` image. - `Dockerfile.variant``FROM`s the base and adds only opencode/omos/pi installs gated by build args: `INSTALL_OPENCODE` (default true), `INSTALL_OMOS`, `INSTALL_PI`, and `INSTALL_MEMPALACE`. All GitHub-sourced binaries are pinned with version ARGs. When `INSTALL_PI=true` it also clones `pi-fork` + `pi-observational-memory` (from `github.com/elpapi42`, refs `PI_FORK_REF`/`PI_OBSMEM_REF`) to `/opt` and runs `npm install` there at build time so the `fork`/`recall` extensions can load (a local-path `pi install` does not npm-install). The `pi-only` variant sets `INSTALL_OPENCODE=false`, `INSTALL_PI=true` — pi without opencode, the single source of truth for the separate `pi-devbox` image. It is built and smoke-tested here, but **published into the `joakimp/pi-devbox` repo** as the internal building-block tag `base-pi-only[-vX.Y.Z]` (NOT under `opencode-devbox`), so an opencode-devbox tag never ships without opencode.
- `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.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.jsonc generation (delegated to `generate-config.py`), LAN-access setup (delegated to `setup-lan-access.sh`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, runtime `pi install /opt/{pi-fork,pi-observational-memory}` registration (idempotent), skillset auto-deploy from mounted skillset repo, OMOS setup. - `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), LAN-access setup (delegated to `setup-lan-access.sh`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, runtime `pi install /opt/{pi-fork,pi-observational-memory}` registration (idempotent), skillset auto-deploy from mounted skillset repo, OMOS setup.
- `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh` — host-OS-agnostic LAN reachability helper. Detects VM-backed hosts (macOS OrbStack / Docker Desktop, via `host.docker.internal` resolution) and generates a writable `~/.ssh-local/config` using the host as an SSH jump; no-op on native Linux. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS`. Ships the mechanism only (generic `host` jump alias); user targets stay in their bind-mounted `~/.ssh/config`. Non-fatal. Counted in the base hash, so editing it advances `base-latest`. - `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh` — host-OS-agnostic LAN reachability helper. Detects VM-backed hosts (macOS OrbStack / Docker Desktop, via `host.docker.internal` resolution) and generates a writable `~/.ssh-local/config` using the host as an SSH jump; no-op on native Linux. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS` / `DEVBOX_LAN_AUTOJUMP_PRIVATE`. Ships the mechanism only (generic `host` jump alias); user targets stay host-side — named-peer `ProxyJump host` overrides go in a bind-mounted `~/.config/devbox-shell/ssh-lan.conf` (Included before `~/.ssh/config`), never baked into the image. **Scoping invariant:** every `Include` in the generated config MUST be preceded by a bare `Host *` reset — an `Include` is scoped to the enclosing `Host`/`Match` block, so without the reset the included config only applies when targeting `host`/`mac` and named peers fall back to SSH defaults. The top `Host *` block also overrides `UserKnownHostsFile` and `ControlPath` into the writable `~/.ssh-local` sidecar (first-value-wins), because the bind-mounted `~/.ssh` is read-only — otherwise multiplexed hosts (`ControlPath ~/.ssh/cm/...`) fail to create their master socket. Non-fatal. Counted in the base hash, so editing it advances `base-latest`.
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint). - `rootfs/usr/local/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 a hand-maintained `HUB_TEMPLATE` constant. `--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 a hand-maintained `HUB_TEMPLATE` constant. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
@@ -36,7 +45,7 @@ Tags follow `v{opencode_version}[letter]` — e.g. `v1.14.20` for the first buil
Cautionary example: 2026-05-28 morning, `v1.15.12` was cut while opencode-ai was still at `1.15.11`. The commit message itself acknowledged "OPENCODE_VERSION stays at 1.15.11" but tagged `v1.15.12` anyway. Re-cut as `v1.15.11c` the same afternoon (see CHANGELOG). The `v1.15.12` git tag and Hub images stayed as historical artifacts; the slip cost a CI cycle and a CHANGELOG-rewrite. **Run the npm view check at the top of every release-day cut.** Cautionary example: 2026-05-28 morning, `v1.15.12` was cut while opencode-ai was still at `1.15.11`. The commit message itself acknowledged "OPENCODE_VERSION stays at 1.15.11" but tagged `v1.15.12` anyway. Re-cut as `v1.15.11c` the same afternoon (see CHANGELOG). The `v1.15.12` git tag and Hub images stayed as historical artifacts; the slip cost a CI cycle and a CHANGELOG-rewrite. **Run the npm view check at the top of every release-day cut.**
CI produces ten Docker Hub tags per release: `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`, `vX.Y.Z[n]-pi-only`, `latest-pi-only` — one tag pair (versioned + floating alias) per build variant (five variants). CI produces eight Docker Hub tags **under `opencode-devbox`** per release: `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` — one tag pair (versioned + floating alias) per opencode-bearing variant (four variants). A fifth build, `pi-only`, is built+smoked here but pushed into the **`joakimp/pi-devbox`** repo as `base-pi-only-vX.Y.Z` (+ `base-pi-only` on tag builds), where it becomes the base for that image.
When bumping the opencode version, bump `OPENCODE_VERSION` in `Dockerfile.variant` and update the comment in `.env.example` if it names a specific model/version for context. When bumping the opencode version, bump `OPENCODE_VERSION` in `Dockerfile.variant` and update the comment in `.env.example` if it names a specific model/version for context.
@@ -104,6 +113,7 @@ cd /tmp && npm pack @earendil-works/pi-coding-agent@0.75.5 && tar -xzf earendil-
- **`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. - **`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. - **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-split.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. - **`BUILDKIT_PROGRESS=plain`** is set at workflow level on `docker-publish-split.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.
- **`STRICT_REGISTRATION` gates the fork/recall *registration* smoke assertions.** `smoke-test.sh`'s two pi-extension registration checks (that `pi-fork`/`pi-observational-memory` registered in `~/.pi/agent/settings.json`) depend on the *base* entrypoint running `pi install /opt/<pkg>`. `validate.yml` builds variants from the **published** `base-latest`, which lags the in-repo entrypoint until a release rebuilds the base — so those checks would false-negative there. They are therefore warn-only unless `STRICT_REGISTRATION=1`: `validate.yml` leaves it unset (warn), and `docker-publish-split.yml` (which builds the base fresh in the same run) sets `STRICT_REGISTRATION: "1"` on the three pi-bearing smoke jobs to enforce them. Build-time `/opt` + `node_modules` checks stay hard in both paths. If you touch the registration checks or the base-freshness model, keep this flag wiring in lockstep across both workflows.
## Testing changes ## Testing changes
+275 -1
View File
@@ -8,7 +8,281 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
## Unreleased ## Unreleased
_(no changes since v1.15.13b)_ ## v1.17.2 — 2026-06-10
First container build on **opencode-ai `1.17.2`** (from `1.16.2`). This
release also **deprecates all pi support** ahead of its removal in v2.0.0,
and hardens the mempalace install.
### Bumped: opencode-ai 1.16.2 → 1.17.2
`OPENCODE_VERSION` ARG in `Dockerfile.variant`. Bare `v1.17.2` tag per the
`v{opencode_version}` scheme.
### Deprecated: pi support (removed in v2.0.0)
pi has been decoupled into its own self-contained image,
[`joakimp/pi-devbox`](https://gitea.jordbo.se/joakimp/pi-devbox) (v1.0.0+,
which no longer FROMs `base-pi-only`). The pi paths in opencode-devbox are
now dead weight and are **deprecated as of v1.17.2, scheduled for removal in
v2.0.0**:
- The `INSTALL_PI` build arg and all `PI_*` args.
- The `with-pi`, `omos-with-pi`, and `pi-only` build variants.
- The `base-pi-only[-vX.Y.Z]` tag published (to the `joakimp/pi-devbox`
repo) from this repo's CI.
- All `~/.pi`-related entrypoint wiring (pi-toolkit / pi-extensions deploy,
settings.json bootstrap, mempalace pi-bridge symlink, pi-fork / pi-obsmem
registration).
What this release does:
- Building with `INSTALL_PI=true` now prints a **build-time deprecation
warning** to stderr.
- README, DOCKER_HUB.md, and AGENTS.md mark the pi variants deprecated and
point to `joakimp/pi-devbox`.
- The full removal plan is documented in `docs/CLEANUP-v2.0.0.md`.
**Migration:** pull `joakimp/pi-devbox:latest` directly instead of any
`*-with-pi` / `pi-only` opencode-devbox tag. opencode-only users are
unaffected by this release.
#### ⚠ Heads-up for v2.0.0: global npm prefix relocation
Today `NPM_CONFIG_PREFIX` points at the pi-specific `~/.pi/npm-global`
(backed by the `devbox-pi-config` named volume), so `npm install -g` as the
developer user persists across recreates. **v2.0.0 will move the prefix to a
neutral opencode path** (e.g. `~/.config/opencode/npm-global`). Consequences
for existing opencode users at the v2.0.0 upgrade:
- Previously global-installed npm tools remain on disk in the old volume but
**drop off `PATH`** until migrated.
- The new prefix path is not currently a named volume, so persistence needs a
compose/volume update.
v2.0.0 will ship a **one-time migration shim** (copies old prefix contents to
the new path on first run) and an updated volume mapping. This notice is the
one-release-cycle advance warning.
### Hardened: mempalace install pinned + diary_write schema workaround
- `MEMPALACE_VERSION` is now an explicit ARG (default `3.4.0`) in
`Dockerfile.base`; the install uses `mempalace==${MEMPALACE_VERSION}`
instead of an unpinned `uv tool install mempalace`. An unpinned install is
what silently swept in the broken `diary_write` schema. Mirrors pi-devbox.
- Added an idempotent, self-deactivating post-install patch that strips the
**top-level `anyOf`** from `mempalace_diary_write`'s `input_schema`.
Mempalace 3.3.x/3.4.0 advertise `anyOf:[{required:[entry]},{required:[content]}]`
at the schema root, which the Anthropic tools API rejects outright
(`input_schema does not support oneOf, allOf, or anyOf at the top level`),
breaking pi/Claude tool registration at session start. The handler still
accepts `content` server-side. Upstream: MemPalace/mempalace#1728, PR #1735.
Remove once a fixed mempalace release is pinned.
### Fixed: smoke-test pi-extensions readiness race (test-only, no image change)
`scripts/smoke-test.sh`'s entrypoint-deploy wait loop gated only on
`keybindings.json` (written by pi-toolkit, which runs *before* pi-extensions),
so the `~/.pi/agent/extensions/*.ts ≥ 4` assertion could sample mid-deploy and
see fewer than 4 files under parallel build load. Observed on v1.16.2 run 370:
`smoke-with-pi` saw `<4` while `smoke-omos-with-pi` / `smoke-pi-only` (identical
pi-extensions `357fcc6`) both saw 8, skipping `build-variant-with-pi`. The wait
loop now blocks until the *last*-deployed artifact (the `mempalace.ts` bridge
symlink) exists **and** the extension count has settled ≥ 4 (up to 45s).
## v1.16.2 — 2026-06-08
First container build on the `opencode-ai@1.16.x` minor release (rolls up
`1.16.0``1.16.1``1.16.2`, all published 2026-06-05). Also picks up
**pi `0.78.1` → `0.79.0`** (resolved fresh by CI's `resolve-versions` job) in
the `with-pi`, `omos-with-pi`, and `pi-only` variants.
### Bumped: opencode-ai 1.15.13 → 1.16.2
`OPENCODE_VERSION` ARG in `Dockerfile.variant`. Highlights from the upstream
release (full notes: <https://github.com/anomalyco/opencode/releases>):
- **1.16.0** — ~38% faster startup (@StarpTech); managed workspace cloning that
keeps dirty/untracked files; move sessions between workspaces/directories;
proper OpenAI-via-Bedrock support; skill discovery + file-based agent loading;
`run --replay` for interactive session replay. Plus TUI/desktop polish and
numerous bugfixes (shell cancellation races, Windows path normalization, ACP
cancel/abort).
- **1.16.1** — internal/no user-visible notes (empty release body).
- **1.16.2** — reasoning summaries only run on supporting providers (avoids
GPT-5 request failures); edit operations refuse loose matches that could
overwrite the wrong code; Bedrock hang-before-first-token fix; diff-viewer
hunk navigation; subagents can be backgrounded; Snowflake Cortex provider.
### Picks up: pi 0.78.1 → 0.79.0
Resolved at build time for the pi-bearing variants. Headlines: project-trust
prompting for project-local settings/resources/instructions/packages (with
`--approve`/`--no-approve` and a `project_trust` extension event), cache-hit
rate in the interactive footer, richer SDK/RPC extension surfaces, plus a
stack of TUI and provider fixes. Full notes ship in the npm tarball's
`CHANGELOG.md`.
### Smoke size thresholds bumped +150 MB (preemptive)
Ahead of the combined minor opencode + pi bump, all opencode-bearing variant
thresholds in `scripts/smoke-test.sh` were raised: `base` 2600 → 2750,
`omos` 3300 → 3450, `with-pi` 2900 → 3050, `omos-with-pi` 3900 → 4050; and
`pi-only` 2750 → 2850 (+100, pi-only carries only the pi bump). Both `base`
(last 2506 MB) and `omos` (last 3206 MB) were on ~94 MB headroom, and a minor
opencode bump has tripped these ceilings before (v1.15.0 omos, v1.15.4
omos-with-pi), causing a partial publish + letter-suffix recovery cycle.
Restoring ~250 MB headroom avoids that. CI's smoke size print records actual
landed sizes — tighten later if they come in well under.
## v1.15.13e — 2026-06-04
Letter-suffix rebuild on opencode `1.15.13` (version unchanged). Picks up
**pi `0.78.1`** (resolved fresh by CI's `resolve-versions` job) plus the LAN-jump
key-persistence work, an entrypoint ownership fix for the new `devbox-ssh-local`
volume, a CI smoke false-negative fix, and documentation. Touches `entrypoint.sh`
and `setup-lan-access.sh` (both in the base hash), so `base-latest` /
`base-pi-only` advance and the fixes propagate to `pi-devbox`.
### Docs: per-host `ControlPath` overrides break `pi --ssh` (read-only `~/.ssh`)
Documented a gotcha in the README "Reaching your LAN" section: the bind-mounted
`~/.ssh/config` is read before the baked `Host *` default, and SSH uses the
first `ControlPath` it sees. A per-host block that sets `ControlPath` under
`~/.ssh/` (a common CGNAT-multiplexing pattern, e.g. `~/.ssh/cm/%r@%h:%p`) wins
but then fails inside the container because `~/.ssh` is mounted read-only — the
master socket can't bind. This silently breaks `pi --ssh <host>`: the SSH layer
fails and pi falls back to running its tools locally in the container. Fix is
host-side — drop the per-host `ControlPath` or repoint it at the writable
`/tmp/sshcm/%r@%h:%p` (works on both host and container, preserves multiplexing).
No image change; documentation only.
### Fixed: validate.yml false-negative on fork/recall registration checks
The push-to-main `validate.yml` builds variants FROM the published `base-latest`
image, which lags the entrypoint in the current commit until a release tag
rebuilds the base. The fork/recall *registration* smoke checks depend on the
base entrypoint running `pi install /opt/<pkg>`, so a stale `base-latest` reded
those runs with a false negative even when the variant layer was correct.
`smoke-test.sh` now gates the two registration assertions behind
`STRICT_REGISTRATION` (warn-only when unset). `validate.yml` leaves it unset;
the release pipeline (`docker-publish-split.yml`), which builds the base fresh
in the same run, sets `STRICT_REGISTRATION=1` on the pi-bearing smoke jobs to
enforce them. The build-time `/opt` + `node_modules` checks stay hard in both
paths.
### Added: persist the LAN-jump key + one-line authorize hint (authorize once per machine)
The jump keypair (`~/.ssh-local/devbox_jump_ed25519`) was stored on the
container's ephemeral overlay, so `docker compose up --force-recreate` (every
image update) regenerated it — forcing you to re-authorize the new key on the
host each time. The compose files now persist `~/.ssh-local` via a named volume
(`devbox-ssh-local`), matching the pattern already used for `.pi`, shell
history, etc. The key is generated **once** and reused across updates, so you
authorize it on the host **once per machine**.
`setup-lan-access.sh` now also prints a ready-to-paste authorize line whenever
it generates a **new** key (not just when `HOST_SSH_USER` is unset), e.g.
`echo 'ssh-ed25519 …' >> ~/.ssh/authorized_keys` — no helper file to locate, no
workspace path to guess. It stays silent once the key is persisted.
### Fixed: chown the `devbox-ssh-local` volume so the jump key can be generated
The previous change persisted `~/.ssh-local` via a named volume, but the
entrypoint's volume-ownership loop was never updated to include it. Docker
creates named volumes as `root:root`, so on a fresh volume `~/.ssh-local`
stayed root-owned while `setup-lan-access.sh` runs as `developer` — both its
`mkdir cm` and `ssh-keygen` failed silently (`|| true` / `|| exit 0`), leaving
**no jump key and no config**, breaking LAN access on the first recreate after
the persistence change. `entrypoint.sh` now chowns `~/.ssh-local` to the
developer user alongside the other named-volume mount points.
### Docs: document the optional `~/.config/devbox-shell` mount in the compose template
`docker-compose.yml` now carries a commented-out `~/.config/devbox-shell` bind
mount with an explanatory note. It's the recommended home for host-owned shell
config: the image's `~/.bash_aliases` sources `~/.config/devbox-shell/bash_aliases`
if present, and `setup-lan-access.sh` reads `~/.config/devbox-shell/ssh-lan.conf`
for named-peer `ProxyJump host` overrides. A directory mount is preferred over
the single-file `~/.bash_aliases` mount because it survives editors' atomic-save.
Template comment only; no behavior change.
## v1.15.13d — 2026-06-04
LAN-access fixes + ergonomics. Letter-suffix rebuild on opencode `1.15.13`
(version unchanged). Touches `setup-lan-access.sh`, which is in the base hash,
so `base-latest` / `base-pi-only` advance and the fix propagates to `pi-devbox`.
### Fixed: LAN-access `Include` was scoped to the `host`/`mac` block (named peers ignored)
The generated `~/.ssh-local/config` placed `Include ~/.ssh/config` *inside* the
`Host host mac` block. Because SSH scopes an `Include` to the enclosing
`Host`/`Match` block, the user's `~/.ssh/config` was only consulted when
targeting `host`/`mac` — so `dssh pve` / `dssh <peer>` by name silently fell
back to SSH defaults (wrong user, unresolved hostname) and never applied the
peer's settings or any `ProxyJump`. Fixed by emitting a bare `Host *` scope
reset before every `Include`.
### Fixed: read-only `~/.ssh/cm` ControlPath broke multiplexed hosts
The bind-mounted `~/.ssh/config` commonly sets `ControlPath ~/.ssh/cm/...`
(CGNAT flow-cap multiplexing), but `~/.ssh` is read-only in the container, so
every `ControlMaster`-enabled host (e.g. `pmx-jh`, `proxmox*`, `synlig`) failed
with `cannot bind to path … Read-only file system`. The generated config now
sets `ControlPath ~/.ssh-local/cm/%r@%h:%p` in the top `Host *` block
(first-value-wins) so master sockets land in the writable sidecar.
### Added: host-owned `ssh-lan.conf` for named-peer jump overrides
When the host bind-mounts `~/.config/devbox-shell/ssh-lan.conf`, the generated
config now Includes it *before* `~/.ssh/config`. Put `ProxyJump host` overrides
there (first-value-wins inherits HostName/User/IdentityFile from `~/.ssh/config`)
instead of editing the shared `~/.ssh/config` — which would break the host's own
direct access to those peers and is read-only from the container anyway. New
[`ssh-lan.conf.example`](ssh-lan.conf.example).
### Added: `DEVBOX_LAN_AUTOJUMP_PRIVATE=1` opt-in RFC1918 auto-jump
Emits a catch-all that ProxyJumps any private (RFC1918) IP through the host, so
bare `dssh user@<ip>` reaches whatever LAN the (roaming) host is currently on,
without naming peers. Matches the typed address (not the resolved HostName), so
named hosts carrying their own ProxyJump are unaffected; public IPs stay direct.
All three land in `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh`,
which is counted in the base hash → advances `base-latest` and propagates to
`pi-devbox` (built `FROM` the base).
## v1.15.13c — 2026-06-03
Follow-up to v1.15.13b: relocates the pi-only build out of the `opencode-devbox`
repo (Option B) and fixes the base size threshold that blocked `promote-base-latest`.
### Changed: `pi-only` build now publishes to the `joakimp/pi-devbox` repo (not `opencode-devbox`)
The `pi-only` variant (added in v1.15.13b) was published under `opencode-devbox`
as `latest-pi-only` / `vX.Y.Z-pi-only` — an "opencode-devbox" tag that contains
**no opencode**, which confused users browsing the tag list.
- The `build-variant-pi-only` CI job now pushes the artifact into the
**`joakimp/pi-devbox`** repo as `base-pi-only-vX.Y.Z` (+ floating `base-pi-only`
on tag builds) instead of `opencode-devbox:*-pi-only`. New `PI_IMAGE` workflow env.
- It is still built from the same `Dockerfile.variant` (single source of truth)
and still smoke-tested by `smoke-pi-only` / `validate-pi-only` before publish.
- `opencode-devbox` now publishes **eight** tags per release (four opencode-bearing
variants) plus `base-latest`; the pi-only pair lives in the pi-devbox repo.
- De-advertised the pi-only tag from the README, `DOCKER_HUB.md` (HUB_TEMPLATE),
and AGENTS docs.
- The old `opencode-devbox:latest-pi-only` / `vX.Y.Z-pi-only` tags from v1.15.13b
are superseded and should be deleted from Docker Hub.
### Fixed: base image size threshold (unblocks `promote-base-latest`)
- Bumped the `base` variant smoke size threshold 2500 → 2600 MB. In the v1.15.13b
run the base crept to 2506 MB (LAN-access script + updated entrypoint + apt
drift) and tripped the deliberately zero-headroom 2500 ceiling, which failed
`smoke-base` and cascaded into skipping `build-variant-base` **and**
`promote-base-latest` — so `base-latest` never advanced. (`base-<hash>` and the
omos/with-pi/omos-with-pi/pi-only variants did publish on the fresh base.)
## v1.15.13b — 2026-06-03 ## v1.15.13b — 2026-06-03
+8 -3
View File
@@ -10,12 +10,17 @@ Designed for teams who want a reproducible coding-agent setup that runs the same
|---|---| |---|---|
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools | | `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun | | `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/earendil-works/pi) as alternative/complementary harness (shares the mempalace install with opencode) | | `latest-with-pi` / `vX.Y.Z-with-pi` | **DEPRECATED (removed in v2.0.0)** Base + [pi](https://github.com/earendil-works/pi). Use [`joakimp/pi-devbox`](https://hub.docker.com/r/joakimp/pi-devbox) instead |
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together | | `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | **DEPRECATED (removed in v2.0.0)** OMOS + pi together |
| `latest-pi-only` / `vX.Y.Z-pi-only` | pi without opencode — the lean, pi-focused variant (basis of the separate `joakimp/pi-devbox` image) |
All variants support `linux/amd64` and `linux/arm64`. All variants support `linux/amd64` and `linux/arm64`.
> **Looking for pi?** The `*-with-pi` / `pi-only` builds and the `base-pi-only`
> tag are **deprecated since v1.17.2 and will be removed in v2.0.0**. pi now
> ships as its own self-contained image:
> [`joakimp/pi-devbox`](https://hub.docker.com/r/joakimp/pi-devbox). Pull that
> directly instead of any pi-bearing opencode-devbox tag.
## Quick Start ## Quick Start
For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files: For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files:
+37 -1
View File
@@ -259,14 +259,50 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
# Always installed in the base (variant-independent). Set # Always installed in the base (variant-independent). Set
# INSTALL_MEMPALACE=false at base-build time to shave ~300 MB. # INSTALL_MEMPALACE=false at base-build time to shave ~300 MB.
ARG INSTALL_MEMPALACE=true ARG INSTALL_MEMPALACE=true
# Pin mempalace explicitly (mirrors pi-devbox). An unpinned
# `uv tool install mempalace` is what silently swept in the broken
# diary_write top-level-anyOf schema (3.3.x/3.4.0) that breaks the
# Anthropic tools API; pinning makes every bump a deliberate, reviewable
# diff. Bump this in lockstep with pi-devbox's MEMPALACE_VERSION.
ARG MEMPALACE_VERSION=3.4.0
ENV UV_TOOL_DIR=/opt/uv-tools ENV UV_TOOL_DIR=/opt/uv-tools
ENV UV_TOOL_BIN_DIR=/usr/local/bin ENV UV_TOOL_BIN_DIR=/usr/local/bin
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \ RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
mkdir -p /opt/uv-tools && \ mkdir -p /opt/uv-tools && \
uv tool install --no-cache mempalace && \ uv tool install --no-cache "mempalace==${MEMPALACE_VERSION}" && \
/opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \ /opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
fi fi
# ── workaround: strip top-level anyOf from mempalace_diary_write schema ──
# Mempalace 3.3.x/3.4.0 advertise diary_write's input_schema with a
# top-level `anyOf: [{required:[entry]}, {required:[content]}]` to express
# "either entry or content must be supplied". Anthropic's tools API rejects
# top-level anyOf/oneOf/allOf, so pi/Claude fail at session start with
# `tools.<n>.custom.input_schema: input_schema does not support oneOf,
# allOf, or anyOf at the top level`.
#
# Patch the advertised schema to require ["agent_name", "entry"] and remove
# the anyOf block. The handler keeps accepting `content` server-side as a
# kwarg alias so existing callers still work.
#
# Idempotent and self-deactivating: once upstream releases the fix the
# regex no longer matches and this RUN is a silent no-op.
# Upstream tracking:
# https://github.com/MemPalace/mempalace/issues/1728
# https://github.com/MemPalace/mempalace/pull/1735
# TODO: remove this RUN once a mempalace release containing PR #1735 is on
# PyPI and installed by the line above.
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
MP_FILE="$(find /opt/uv-tools/mempalace -path '*/mempalace/mcp_server.py' | head -n1)" && \
if [ -z "$MP_FILE" ]; then echo "mempalace mcp_server.py not found" >&2; exit 1; fi && \
perl -0777 -i -pe 's/(?:[ \t]*\#[^\n]*\n)*[ \t]*"required":\s*\[\s*"agent_name"\s*\]\s*,\s*\n[ \t]*"anyOf":\s*\[\s*\n[ \t]*\{\s*"required":\s*\[\s*"entry"\s*\]\s*\}\s*,\s*\n[ \t]*\{\s*"required":\s*\[\s*"content"\s*\]\s*\}\s*,?\s*\n[ \t]*\]\s*,\s*\n/ "required": ["agent_name", "entry"],\n/s' "$MP_FILE" && \
if grep -q '"required": \["agent_name", "entry"\]' "$MP_FILE"; then \
echo "mempalace diary_write anyOf workaround: applied (or already clean)"; \
else \
echo "WARN: mempalace diary_write anyOf workaround did not match expected schema — upstream may have changed shape" >&2; \
fi ; \
fi
# ── mempalace-toolkit — bash wrappers for session/docs mining ──────── # ── mempalace-toolkit — bash wrappers for session/docs mining ────────
ARG INSTALL_MEMPALACE_TOOLKIT=true ARG INSTALL_MEMPALACE_TOOLKIT=true
ARG MEMPALACE_TOOLKIT_REF=main ARG MEMPALACE_TOOLKIT_REF=main
+20 -8
View File
@@ -10,14 +10,19 @@
# ───────────────── ──────────────── ──────────── ────────── # ───────────────── ──────────────── ──────────── ──────────
# base true false false # base true false false
# omos true true false # omos true true false
# with-pi true false true # with-pi *DEPR* true false true
# omos-with-pi true true true # omos-with-pi*DEPR* true true true
# pi-only false false true # pi-only *DEPR* false false true
# #
# The `pi-only` variant is the single source of truth for the pi-devbox # DEPRECATION (since v1.17.2): the three pi-bearing variants (with-pi,
# image (pi + companions, no opencode). It exists so pi-devbox can FROM it # omos-with-pi, pi-only) and the INSTALL_PI build path are DEPRECATED and
# without inheriting opencode, while the pi install logic stays defined # will be REMOVED in v2.0.0. pi now ships from its own self-contained image:
# here in one place. # joakimp/pi-devbox:latest (https://gitea.jordbo.se/joakimp/pi-devbox).
# See docs/CLEANUP-v2.0.0.md for the removal plan.
#
# Until v2.0.0 the `pi-only` variant remains the source of truth for the
# legacy pi build (pi + companions, no opencode); pi-devbox v1.0.0+ no
# longer FROMs it.
# #
# Pass `--build-arg BASE_IMAGE=<repo>:base-<hash>` to select the base. # Pass `--build-arg BASE_IMAGE=<repo>:base-<hash>` to select the base.
# The CI workflow computes the base hash from Dockerfile.base + rootfs/ # The CI workflow computes the base hash from Dockerfile.base + rootfs/
@@ -42,7 +47,7 @@ ARG USER_NAME=developer
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0.. # edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
# v0.75.5 cannot apply here. # v0.75.5 cannot apply here.
ARG INSTALL_OPENCODE=true ARG INSTALL_OPENCODE=true
ARG OPENCODE_VERSION=1.15.13 ARG OPENCODE_VERSION=1.17.2
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \ RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \ NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
opencode --version ; \ opencode --version ; \
@@ -81,6 +86,13 @@ ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
ARG PI_OBSMEM_REF=master ARG PI_OBSMEM_REF=master
RUN if [ "${INSTALL_PI}" = "true" ]; then \ RUN if [ "${INSTALL_PI}" = "true" ]; then \
set -e && \ set -e && \
printf '%s\n' \
"===========================================================" \
"DEPRECATION WARNING: INSTALL_PI is deprecated in opencode-devbox" \
"(since v1.17.2) and will be REMOVED in v2.0.0. Use the dedicated" \
"image joakimp/pi-devbox:latest instead." \
"See https://gitea.jordbo.se/joakimp/pi-devbox" \
"===========================================================" >&2 && \
git_clone_retry() { \ git_clone_retry() { \
url="$1"; ref="$2"; dest="$3"; \ url="$1"; ref="$2"; dest="$3"; \
for i in 1 2 3 4 5; do \ for i in 1 2 3 4 5; do \
+54 -12
View File
@@ -135,6 +135,7 @@ docker compose exec -u developer devbox aws --version
| `DEVBOX_LAN_ACCESS` | LAN-access mode: `auto` (jump only on VM-backed hosts), `jump` (always), `off` | `auto` | | `DEVBOX_LAN_ACCESS` | LAN-access mode: `auto` (jump only on VM-backed hosts), `jump` (always), `off` | `auto` |
| `HOST_SSH_USER` | Username to SSH into the host as (required for the LAN jump) | — | | `HOST_SSH_USER` | Username to SSH into the host as (required for the LAN jump) | — |
| `DEVBOX_HOST_ALIAS` | Hostname used to reach the container host | `host.docker.internal` | | `DEVBOX_HOST_ALIAS` | Hostname used to reach the container host | `host.docker.internal` |
| `DEVBOX_LAN_AUTOJUMP_PRIVATE` | `1` = ProxyJump *any* RFC1918 (private) IP through the host, so bare `dssh user@<ip>` works on whatever LAN the host is currently on | `0` |
| `USER_UID` | Override container user UID | Auto-detect from `/workspace` | | `USER_UID` | Override container user UID | Auto-detect from `/workspace` |
| `USER_GID` | Override container user GID | Auto-detect from `/workspace` | | `USER_GID` | Override container user GID | Auto-detect from `/workspace` |
| `LANG` | System locale | `en_US.UTF-8` | | `LANG` | System locale | `en_US.UTF-8` |
@@ -154,26 +155,53 @@ The devbox works the same way whether the host is **native Linux Docker** or a *
- **Native Linux Docker:** the host NATs container egress onto its LAN, so other devices on your LAN are reachable directly. Nothing to configure. - **Native Linux Docker:** the host NATs container egress onto its LAN, so other devices on your LAN are reachable directly. Nothing to configure.
- **VM-backed (macOS / Docker Desktop):** the container runs in a Linux VM behind the host's network stack. The host's *directly-attached* LAN peers are **not** bridged into the container by default — only the host itself and *routed* subnets are reachable. - **VM-backed (macOS / Docker Desktop):** the container runs in a Linux VM behind the host's network stack. The host's *directly-attached* LAN peers are **not** bridged into the container by default — only the host itself and *routed* subnets are reachable.
On every start the entrypoint detects which case applies. On VM-backed hosts it generates a writable `~/.ssh-local/config` that uses the **host as an SSH jump** to reach LAN peers; on native Linux it does nothing. On every start the entrypoint detects which case applies. On VM-backed hosts it generates a writable `~/.ssh-local/config` that uses the **host as an SSH jump** to reach LAN peers; on native Linux it does nothing. The jump keypair lives in `~/.ssh-local`, which is persisted by the `devbox-ssh-local` named volume — so it's generated **once** and reused across container updates.
**To enable it on a VM-backed host:** **To enable it on a VM-backed host (one-time setup per machine):**
1. Set `HOST_SSH_USER=<your host username>` in `.env`. 1. Set `HOST_SSH_USER=<your host username>` in `.env`.
2. Start the container once. The entrypoint prints a public key — append it to your host's `~/.ssh/authorized_keys`. 2. Start the container once. When it generates the jump key it prints a ready-to-paste line — run it **on the host** to authorize the key:
```bash
echo 'ssh-ed25519 AAAA…devbox-jump@…' >> ~/.ssh/authorized_keys
```
3. Ensure the host's SSH server is on (on macOS: System Settings → General → Sharing → Remote Login). 3. Ensure the host's SSH server is on (on macOS: System Settings → General → Sharing → Remote Login).
4. Reach the host with `dssh host`, and reach LAN peers by adding `ProxyJump host` to their entries in your bind-mounted `~/.ssh/config`: 4. Reach the host itself with `dssh host`. (`dssh`/`dscp` wrap `ssh -F ~/.ssh-local/config`.)
Because the key is persisted, you do this **once per machine** — not after every `docker compose up --force-recreate`. You'll only see the authorize line again if you reset the `devbox-ssh-local` volume.
That alone gets you `container → host`. To reach **named LAN peers** by name, give them a `ProxyJump host` override. Don't add it to the shared `~/.ssh/config` entries — the host itself reaches those peers *directly*, and a jump-through-`host` would break the host's own access (and that file is mounted read-only anyway). Instead, drop the overrides in a **host-owned** file that the container Includes ahead of your `~/.ssh/config`:
```sshconfig ```sshconfig
# in your host ~/.ssh/config (mounted read-only into the container) # ~/.config/devbox-shell/ssh-lan.conf — on the host, bind-mounted in
Host my-nas # Only ProxyJump goes here; HostName/User/IdentityFile are inherited
HostName 192.168.1.50 # (first-value-wins) from the matching block in your ~/.ssh/config.
User admin Host my-nas pve pbs
ProxyJump host ProxyJump host
``` ```
Then `dssh my-nas` routes container → host → LAN peer. (`dssh`/`dscp` wrap `ssh -F ~/.ssh-local/config`; the host config is pulled in via `Include`.) Now `dssh my-nas` routes container → host → LAN peer, pulling HostName/User/key from your existing `~/.ssh/config`. See [`ssh-lan.conf.example`](ssh-lan.conf.example).
> This ships the **mechanism** only — your specific target hosts live in your own `~/.ssh/config`, never baked into the image. Set `DEVBOX_LAN_ACCESS=off` to disable, or `=jump` to force it (e.g. native Linux with `extra_hosts: ["host.docker.internal:host-gateway"]`). **Roaming / unnamed peers.** Because the jump always targets `host` (= the host on whatever LAN it's currently joined to), you can reach the *current* LAN from anywhere. To make bare `dssh user@<private-ip>` jump automatically without naming peers, set `DEVBOX_LAN_AUTOJUMP_PRIVATE=1` — it ProxyJumps any RFC1918 address through the host. It matches the address you *type* (not the resolved HostName), so named hosts that already carry their own ProxyJump are unaffected.
**Public IPs go direct.** The container has normal internet egress, so a host with a public IP (or one reached via a *public* jump host) connects straight out — the local `host` jump is not involved. e.g. a `Host bastion` whose `HostName` is public, and everything that `ProxyJump bastion`, works from the container by name with no extra setup.
> This ships the **mechanism** only — your specific target hosts are facts about *your* network (and a laptop roams between several), so they live in your own host-side config, never baked into the image. Set `DEVBOX_LAN_ACCESS=off` to disable, or `=jump` to force it (e.g. native Linux with `extra_hosts: ["host.docker.internal:host-gateway"]`).
#### Gotcha: per-host `ControlPath` and `pi --ssh`
The base image bakes a `Host *` default (`/etc/ssh/ssh_config.d/00-devbox-controlmaster.conf`) that points `ControlPath` at the writable, per-container `/tmp/sshcm/` (created mode-700 on every start by `entrypoint-user.sh`). Multiplexing therefore works out of the box. **But your bind-mounted `~/.ssh/config` is read first, and SSH uses the first value it sees** — so any per-host block that sets its own `ControlPath` under `~/.ssh/` (a common CGNAT-multiplexing pattern, e.g. `ControlPath ~/.ssh/cm/%r@%h:%p`) **wins, and then fails inside the container** because `~/.ssh` is mounted **read-only** — the master socket can't bind (`cannot bind … Read-only file system`).
This bites `pi --ssh <host>` especially: the SSH layer fails to establish the master and pi silently falls back to running its `read`/`write`/`edit`/`bash` tools **locally in the container** instead of on the remote (watch for the missing `SSH ⚡` in the status bar — and `hostname` returning the container ID).
**Fix (host-side, one line):** in your host's `~/.ssh/config`, either drop the per-host `ControlPath` (to inherit the writable baked default) or point it at a path that's writable inside the container too:
```sshconfig
Host my-remote
# was: ControlPath ~/.ssh/cm/%r@%h:%p ← read-only in the container
ControlPath /tmp/sshcm/%r@%h:%p # writable on both host and container
```
`/tmp/sshcm/` is also writable on the host (macOS/Linux), so native (non-container) `ssh`/`pi --ssh` from the host keeps working and CGNAT multiplexing is preserved (`ControlMaster`/`ControlPersist` unchanged — only the socket *directory* moves). Note SSH does not create the `ControlPath` parent dir; the container makes `/tmp/sshcm` every start, but on the host run `mkdir -p /tmp/sshcm` once if it doesn't already exist.
### Custom opencode config ### Custom opencode config
@@ -392,7 +420,8 @@ 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` | **DEPRECATED (removed in v2.0.0)** — install pi alongside opencode. Use [`joakimp/pi-devbox`](https://gitea.jordbo.se/joakimp/pi-devbox) instead. |
| `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). **Note: the pi-only path is deprecated and removed in v2.0.0.** |
| `INSTALL_PI` | `false` | Install [pi](https://github.com/earendil-works/pi) 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. | | `INSTALL_PI` | `false` | Install [pi](https://github.com/earendil-works/pi) 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 `@earendil-works/pi-coding-agent`. Floats by default (image rebuild = pi update). | | `PI_VERSION` | `latest` | npm version of `@earendil-works/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. | | `PI_TOOLKIT_REF`, `PI_EXTENSIONS_REF` | `main` | Git refs for the toolkit/extensions clones. Pin to a tag/commit for reproducibility. |
@@ -459,11 +488,24 @@ All six agents should respond if your provider authentication is working.
## pi (alternative/complementary harness) ## pi (alternative/complementary harness)
> **⚠ DEPRECATED since v1.17.2 — removed in v2.0.0.** pi support in
> opencode-devbox (the `INSTALL_PI` build arg, the `*-with-pi` /
> `omos-with-pi` / `pi-only` variants, and the `base-pi-only` tag) is
> deprecated. pi now ships as its own self-contained image:
> **[`joakimp/pi-devbox`](https://gitea.jordbo.se/joakimp/pi-devbox)** — pull
> that directly. The section below documents the legacy path until removal.
>
> *Migration note for v2.0.0:* the global npm prefix
> (`NPM_CONFIG_PREFIX`) will move off the pi-specific `~/.pi/npm-global`
> path to a neutral opencode path. Globally `npm install -g`'d tools may
> need their volume/PATH refreshed; the v2.0.0 release notes will carry a
> one-time migration shim and details.
[pi](https://github.com/earendil-works/pi) 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. [pi](https://github.com/earendil-works/pi) 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.
### Setup ### Setup
Pre-built pi-enabled images are available on Docker Hub as `joakimp/opencode-devbox:latest-with-pi` (base + pi) and `joakimp/opencode-devbox:latest-omos-with-pi` (OMOS + pi). Pulling one of those tags is the fastest path. There is also a `latest-pi-only` variant (pi **without** opencode, `INSTALL_OPENCODE=false`) — it's the lean basis for the separate [`joakimp/pi-devbox`](https://gitea.jordbo.se/joakimp/pi-devbox) image. Alternatively, build from source: Pre-built pi-enabled images are available on Docker Hub as `joakimp/opencode-devbox:latest-with-pi` (base + pi) and `joakimp/opencode-devbox:latest-omos-with-pi` (OMOS + pi). Pulling one of those tags is the fastest path. If you want pi **without** opencode, use the separate, leaner [`joakimp/pi-devbox`](https://gitea.jordbo.se/joakimp/pi-devbox) image instead (it's built from the same `Dockerfile.variant` with `INSTALL_OPENCODE=false`, published in its own repo so an opencode-devbox tag never ships without opencode). Alternatively, build from source:
### Build ### Build
+14
View File
@@ -59,6 +59,11 @@ services:
# allowing both native and containerized opencode on the same machine. # allowing both native and containerized opencode on the same machine.
- devbox-opencode-config:/home/developer/.config/opencode - devbox-opencode-config:/home/developer/.config/opencode
- devbox-pi-config:/home/developer/.pi - devbox-pi-config:/home/developer/.pi
# Persist the generated LAN-jump keypair (~/.ssh-local) across recreates.
# setup-lan-access.sh generates this key once and reuses it; persisting
# it means you authorize it on the host ONCE rather than re-authorizing
# after every `docker compose up --force-recreate`.
- devbox-ssh-local:/home/developer/.ssh-local
# NOTE: Do NOT bind-mount ~/.agents/skills/ from the host. The # NOTE: Do NOT bind-mount ~/.agents/skills/ from the host. The
# container manages its own skills directory independently — the # container manages its own skills directory independently — the
@@ -95,6 +100,14 @@ services:
# - ~/.bash_aliases:/home/developer/.bash_aliases:ro # - ~/.bash_aliases:/home/developer/.bash_aliases:ro
# - ~/.inputrc:/home/developer/.inputrc:ro # - ~/.inputrc:/home/developer/.inputrc:ro
# Optional: host-owned shell config + LAN jump overrides (recommended
# over the single-file ~/.bash_aliases mount above — it's a directory,
# so it survives editors' atomic-save). The image's ~/.bash_aliases
# sources ~/.config/devbox-shell/bash_aliases if present, and
# setup-lan-access.sh reads ~/.config/devbox-shell/ssh-lan.conf for
# named-peer `ProxyJump host` overrides (see ssh-lan.conf.example).
# - ~/.config/devbox-shell:/home/developer/.config/devbox-shell:ro
# Optional: persist uv data (Python installs, tool installs) # Optional: persist uv data (Python installs, tool installs)
# Without this, 'uv python install' must be re-run after container removal. # Without this, 'uv python install' must be re-run after container removal.
- devbox-uv:/home/developer/.local/share/uv - devbox-uv:/home/developer/.local/share/uv
@@ -126,6 +139,7 @@ services:
volumes: volumes:
devbox-opencode-config: devbox-opencode-config:
devbox-pi-config: devbox-pi-config:
devbox-ssh-local:
devbox-data: devbox-data:
devbox-state: devbox-state:
devbox-shell-history: devbox-shell-history:
+221
View File
@@ -0,0 +1,221 @@
# PR-5: Retire pi from opencode-devbox
After pi-devbox has shipped v1.0.0 as a fully independent image (with
its own base + variant Dockerfiles, CI, smoke tests, and docs), the
pi-related paths in opencode-devbox become dead weight. This PR
removes them.
## Pre-conditions before merging
This PR should land **only after** all of the following are stable:
1. `pi-devbox v1.0.0` published, smoke tests passing, in active use
for at least one release cycle.
2. Anyone consuming `joakimp/pi-devbox:base-pi-only` directly (e.g.
forks pinned to it) has been notified and migrated.
3. The deprecation warning (PR-1 of this work — see below) has been
live for at least one release cycle so consumers have visible
notice.
## Files / sections to remove from opencode-devbox
### `Dockerfile.variant`
Remove these blocks entirely:
- The `INSTALL_PI` / `PI_VERSION` / `PI_TOOLKIT_REF` /
`PI_EXTENSIONS_REF` / `PI_FORK_REPO` / `PI_FORK_REF` /
`PI_OBSMEM_REPO` / `PI_OBSMEM_REF` build-args.
- The `RUN if [ "${INSTALL_PI}" = "true" ]; then ...` block (entire
block — git_clone_retry, git_fetch_ref, npm install
pi-coding-agent, the four /opt/pi-* clones, the npm installs in
/opt/pi-fork and /opt/pi-observational-memory, and the four
rev-parse echoes).
- All comments referencing pi-only as "the single source of truth for
the pi-devbox image" (the variant matrix table, the explanatory
paragraph, and the "rationale" comments at the top of the file
about pi-only existing for pi-devbox to FROM).
Update the variant matrix table at the top of `Dockerfile.variant`:
```
variant INSTALL_OPENCODE INSTALL_OMOS
───────────────── ──────────────── ────────────
base true false
omos true true
```
(only two variants now; pi-only and the with-pi/omos-with-pi axis are
gone).
### `entrypoint-user.sh`
Remove:
- The pi-toolkit and pi-extensions install hooks (the section that
runs `(cd /opt/pi-toolkit && ./install.sh --yes)` etc.).
- The `~/.pi/agent/settings.json` seeding from
`/opt/pi-toolkit/settings.example.json`.
- Any other pi-conditional blocks (search for `INSTALL_PI`, `pi-toolkit`,
`pi-extensions`, `~/.pi/`).
Verify that the AWS Bedrock auth bootstrap (the pi-toolkit AWS env
loader) is not relied on by opencode users. If it is, lift it out of
the pi-toolkit dependency (it's small and self-contained).
### `Dockerfile.base`
Remove:
- The `mkdir -p /home/${USER_NAME}/.pi/agent/extensions` line in the
standard-directories block. Replace with the equivalent opencode-
specific paths if any aren't already present (`~/.config/opencode`
is already there).
- `NPM_CONFIG_PREFIX=/home/${USER_NAME}/.pi/npm-global` — change to
`/home/${USER_NAME}/.config/opencode/npm-global` or a more neutral
path. Update the corresponding `PATH` env var.
Also update the long base-image header comment to remove the
"variants for pi-devbox" rationale.
### CI (`.gitea/workflows/docker-publish-split.yml` or equivalent)
Remove:
- The `pi-only` variant build job.
- The `with-pi` and `omos-with-pi` variant build jobs (they're
redundant with the standalone pi-devbox now).
- The `base-pi-only` tag publish step (which pushes to
`joakimp/pi-devbox:base-pi-only` from this repo).
- The `resolve-pi-version` job step (no longer needed).
- Smoke-test invocations with `--variant pi-only`, `--variant with-pi`,
`--variant omos-with-pi`.
Remaining variants in CI: `base`, `omos`. The "with-pi" axis is
fully retired.
### `scripts/smoke-test.sh`
Remove:
- The `--variant pi-only`, `--variant with-pi`, `--variant
omos-with-pi` branches.
- pi-related assertions: `pi --version`, the
`~/.pi/agent/extensions/*.ts ≥ 4` check, the mempalace.ts bridge
gate (mempalace itself stays, but its bridge into pi is no longer
this image's concern).
Remaining variant axis in smoke tests: `base`, `omos`.
### `README.md` (and `AGENTS.md`, `DOCKER_HUB.md`)
- Remove the pi-only variant from the "Image variants" table.
- Remove the with-pi / omos-with-pi variants if they were documented.
- Remove all sections about pi-toolkit, pi-extensions, pi-fork,
pi-observational-memory, ~/.pi paths, and pi-related env vars.
- Remove the "this image also produces base-pi-only for pi-devbox"
notes.
- Add a single-paragraph **"Looking for pi?"** section pointing to
`joakimp/pi-devbox`.
### `Dockerfile` references in `pi-devbox` repo (cleanup of cross-repo coupling)
This isn't a change to opencode-devbox, but it's part of the same
deprecation:
- Once pi-devbox v1.0.0 is the single source of truth, remove
pi-devbox/Dockerfile (the 5-line shim with the long
`joakimp/pi-devbox:base-pi-only` rationale comment). It's replaced
by `Dockerfile.base` + `Dockerfile.variant` produced by PR-1 of
this work.
## Two-step deprecation path (recommended)
Rather than a single big-bang removal, use a deprecation cycle:
### Step 1 — pre-PR (lands at the same time as pi-devbox v1.0.0)
Add a deprecation warning to opencode-devbox:
1. **Build-time message** — when `INSTALL_PI=true`,
`INSTALL_PI_DEPRECATED=warn` is the default; the variant build
prints to stderr:
```
===========================================================
DEPRECATION WARNING: INSTALL_PI is deprecated in opencode-devbox
and will be removed in v2.0.0. Use joakimp/pi-devbox:latest
instead. See https://gitea.jordbo.se/joakimp/pi-devbox
===========================================================
```
2. **CHANGELOG** entry on opencode-devbox: "INSTALL_PI build-arg path
deprecated; will be removed in v2.0.0."
3. **README and DOCKER_HUB** updates: mark `pi-only`, `with-pi`,
`omos-with-pi` variants as deprecated, point to pi-devbox.
4. The `base-pi-only` tag continues to be published but with a
notice in the description: "Internal artifact for pi-devbox.
Deprecated; pull joakimp/pi-devbox:latest directly."
### Step 2 — removal PR (this document)
Lands one release cycle (or one calendar month, whichever is later)
after step 1. Removes everything listed in the per-file sections
above. Tagged as opencode-devbox v2.0.0 (the major bump signals the
breaking change).
## Risk assessment
### What could go wrong
- **Someone is consuming `base-pi-only` directly** without going
through pi-devbox. The deprecation warning + one-cycle delay should
surface this.
- **Mempalace bridge in pi-extensions** — this stays in pi-devbox; no
impact on opencode-devbox.
- **Shared base assumptions** — opencode-devbox's
`~/.pi/npm-global` NPM_CONFIG_PREFIX was a pi-specific design. In
the cleanup we move it to a neutral path. Existing opencode-devbox
users get a one-time migration: their `npm install -g` packages
installed at the old path stop being on PATH. Document this in the
v2.0.0 changelog and add a one-liner that copies the old prefix
contents to the new one if the old one exists.
### What's safe
- The base apt set, the Go-binary installs, MemPalace, the SSH
ControlMaster setup, the entrypoint UID/GID dance — all of these
stay. They're not pi-specific.
- The `omos` variant — fully unaffected.
- Existing opencode-only users — no change to their workflow.
## Verification
After PR-5 lands, the following should be true:
- `grep -ri "INSTALL_PI\|pi-toolkit\|pi-extensions\|pi-fork\|pi-observational-memory\|base-pi-only" .` in opencode-devbox returns no matches.
- `docker history joakimp/opencode-devbox:latest` shows no pi-related layers.
- The opencode-devbox CI matrix builds only `base` and `omos` variants.
- pi-devbox CI is unaffected (it's a different repo).
- Both repos build cleanly in their own CI without referencing the other.
## Estimated effort
- Step 1 (deprecation warnings): ~2 hours.
- Step 2 (removal): ~4 hours including local testing of opencode-only
build paths.
- One release cycle of monitoring between them.
Total: ~1 working day of focused effort, spread over a calendar month.
## Order in the broader plan
1. PR-1 on pi-devbox — copy base + variant Dockerfiles, strip
opencode/omos paths, tag v1.0.0.
2. PR-2 on pi-devbox — add pandoc, graphviz, imagemagick, tldr, yq.
3. PR-3 on pi-devbox — add `:latest-studio` variant.
4. (Optional) PR-4 on pi-devbox — add `:latest-studio-tex` variant.
5. PR-pre on opencode-devbox — deprecation warnings (step 1 above).
6. **PR-5 on opencode-devbox — actual removal (this document, step 2).**
PRs 14 are independent and can land in any order on pi-devbox. PR-pre
should land alongside or shortly after pi-devbox v1.0.0 (PR-1) so
consumers know to migrate. PR-5 lands one release cycle after PR-pre.
+10
View File
@@ -127,6 +127,16 @@ image so pi-install logic (incl. the new fork/obsmem clones) lives in ONE place.
> (`INSTALL_OPENCODE=false`, `INSTALL_PI=true`) was added to opencode-devbox, and > (`INSTALL_OPENCODE=false`, `INSTALL_PI=true`) was added to opencode-devbox, and
> pi-devbox now `FROM`s `latest-pi-only`. Same single-source-of-truth win, but > pi-devbox now `FROM`s `latest-pi-only`. Same single-source-of-truth win, but
> pi-devbox stays lean (no opencode, ~145 MB lighter than with-pi). > pi-devbox stays lean (no opencode, ~145 MB lighter than with-pi).
>
> **Update 2 (2026-06-03, Option B):** publishing the pi-only variant as
> `opencode-devbox:latest-pi-only` meant an "opencode-devbox" Hub tag that
> contains no opencode — confusing. Final scheme: the pi-only build is still
> produced by opencode-devbox CI (single source of truth) but its
> `build-variant-pi-only` job pushes into the **`joakimp/pi-devbox`** repo as
> the internal building-block tag `base-pi-only` (+ `base-pi-only-vX.Y.Z`), and
> pi-devbox now `FROM`s `joakimp/pi-devbox:base-pi-only`. No opencode-less tag
> ever appears under opencode-devbox; pi-only is de-advertised from
> opencode-devbox's README/DOCKER_HUB. New `PI_IMAGE` workflow env.
### Build time — clone to /opt + npm install (mirror pi-toolkit/extensions pattern) ### Build time — clone to /opt + npm install (mirror pi-toolkit/extensions pattern)
Add to the single `INSTALL_PI=true` block in `opencode-devbox/Dockerfile.variant` Add to the single `INSTALL_PI=true` block in `opencode-devbox/Dockerfile.variant`
+1
View File
@@ -88,6 +88,7 @@ for dir in \
/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"/.pi \
/home/"$USER_NAME"/.ssh-local \
/home/"$USER_NAME"/.agents/skills; do /home/"$USER_NAME"/.agents/skills; do
[ -d "$dir" ] || continue [ -d "$dir" ] || continue
@@ -37,6 +37,30 @@
# jump to authenticate. If unset we still generate the config but print # jump to authenticate. If unset we still generate the config but print
# a hint with the public key to authorize on the host. # a hint with the public key to authorize on the host.
# DEVBOX_HOST_ALIAS — host hostname to reach (default host.docker.internal). # DEVBOX_HOST_ALIAS — host hostname to reach (default host.docker.internal).
# DEVBOX_LAN_AUTOJUMP_PRIVATE = 0 (default) | 1
# 1 → also emit a catch-all that ProxyJumps *any* RFC1918 (private) IP
# through the host. Lets bare `dssh user@<private-IP>` work on whatever
# LAN the (roaming) host is currently joined to, without naming peers.
# Matches by the address you TYPE, not the resolved HostName, so it never
# overrides named hosts that already carry their own ProxyJump.
#
# HOST-OWNED PEER POLICY (portable; keeps this image generic)
# Named LAN peers are facts about a *specific* host's network, not about the
# image — a roaming laptop sees different LANs. So we never bake peer names
# here. Instead, if the host bind-mounts ~/.config/devbox-shell/ssh-lan.conf
# (the same devbox-shell bridge dir used for shared aliases), we Include it
# *before* ~/.ssh/config. That file holds the host's own jump overrides, e.g.
# Host pve pve-2 pbs-vm
# ProxyJump host
# First-value-wins means ProxyJump is taken from there while HostName/User/
# IdentityFile are inherited from the matching block in ~/.ssh/config.
#
# SCOPING NOTE (important)
# `Include` is scoped to the enclosing Host/Match block. So every Include
# below is preceded by a bare `Host *` to reset the active context to
# match-all — otherwise the included config would only apply when targeting
# `host`/`mac` and named peers like `pve` would silently fall back to ssh
# defaults.
# #
# Idempotent: re-renders the config every run (cheap); never regenerates the # Idempotent: re-renders the config every run (cheap); never regenerates the
# key. Always non-fatal — never blocks container startup. # key. Always non-fatal — never blocks container startup.
@@ -73,9 +97,15 @@ mkdir -p "${SSH_LOCAL}/cm" 2>/dev/null || true
chmod 700 "${SSH_LOCAL}" "${SSH_LOCAL}/cm" 2>/dev/null || true chmod 700 "${SSH_LOCAL}" "${SSH_LOCAL}/cm" 2>/dev/null || true
# ── Jump key (generated once; preserved across restarts) ────────────── # ── Jump key (generated once; preserved across restarts) ──────────────
# Persisted via a named volume on ~/.ssh-local (see compose), so a fresh key
# is generated only on the very first start (or if the volume is wiped). When
# we DO generate one it must be (re-)authorized on the host, so we flag it and
# print a copy-paste authorize line below.
KEY_JUST_GENERATED=0
if [ ! -f "$KEY" ]; then if [ ! -f "$KEY" ]; then
ssh-keygen -t ed25519 -N '' -C "devbox-jump@${HOSTNAME:-container}" -f "$KEY" >/dev/null 2>&1 || exit 0 ssh-keygen -t ed25519 -N '' -C "devbox-jump@${HOSTNAME:-container}" -f "$KEY" >/dev/null 2>&1 || exit 0
chmod 600 "$KEY" 2>/dev/null || true chmod 600 "$KEY" 2>/dev/null || true
KEY_JUST_GENERATED=1
fi fi
# ── Render the writable config ──────────────────────────────────────── # ── Render the writable config ────────────────────────────────────────
@@ -84,9 +114,51 @@ if [ -n "${HOST_SSH_USER:-}" ]; then
USER_LINE=" User ${HOST_SSH_USER}" USER_LINE=" User ${HOST_SSH_USER}"
fi fi
INCLUDE_LINE="" # Optional host-owned named-peer jump overrides (portable: lives on the host,
# not in the image). Included BEFORE ~/.ssh/config so its ProxyJump wins.
SSH_LAN_CONF="${HOME}/.config/devbox-shell/ssh-lan.conf"
LAN_CONF_BLOCK=""
if [ -r "$SSH_LAN_CONF" ]; then
LAN_CONF_BLOCK=$(cat <<'EOF'
# Host-owned named-peer jump overrides (bind-mounted; edit on the host).
# Scope reset to match-all so the Include applies to every target host.
Host *
Include ~/.config/devbox-shell/ssh-lan.conf
EOF
)
fi
# Optional opt-in RFC1918 catch-all: ProxyJump every private IP through the
# host. Matches the typed address, never the resolved HostName, so named hosts
# with their own ProxyJump are unaffected. Network-agnostic → roaming-safe.
AUTOJUMP_BLOCK=""
if [ "${DEVBOX_LAN_AUTOJUMP_PRIVATE:-0}" = "1" ]; then
AUTOJUMP_BLOCK=$(cat <<'EOF'
# RFC1918 auto-jump (DEVBOX_LAN_AUTOJUMP_PRIVATE=1): reach any private IP on
# the host's CURRENT LAN via bare `dssh user@<ip>`. Public IPs are unmatched
# and go direct via the container's NAT egress. NOTE: also matches the
# container's own bridge subnet and any private IP the host can't actually
# reach — for non-LAN private hosts behind a different jump, use their named
# entry (which matches first by name and keeps its own ProxyJump).
Host 10.* 192.168.* 172.16.* 172.17.* 172.18.* 172.19.* 172.20.* 172.21.* 172.22.* 172.23.* 172.24.* 172.25.* 172.26.* 172.27.* 172.28.* 172.29.* 172.30.* 172.31.*
ProxyJump host
EOF
)
fi
INCLUDE_BLOCK=""
if [ -r "${HOME}/.ssh/config" ]; then if [ -r "${HOME}/.ssh/config" ]; then
INCLUDE_LINE="Include ~/.ssh/config" INCLUDE_BLOCK=$(cat <<'EOF'
# Your own target hosts. Scope reset to match-all so this Include applies to
# every target (an Include is otherwise scoped to the enclosing Host block).
# Add 'ProxyJump host' to LAN entries here (or in ssh-lan.conf above).
Host *
Include ~/.ssh/config
EOF
)
fi fi
cat > "$CONFIG" <<EOF cat > "$CONFIG" <<EOF
@@ -95,9 +167,15 @@ cat > "$CONFIG" <<EOF
# (or the dssh / dscp aliases). See the script header for the full rationale. # (or the dssh / dscp aliases). See the script header for the full rationale.
# ~/.ssh is typically mounted read-only, so keep our own known_hosts here. # ~/.ssh is typically mounted read-only, so keep our own known_hosts here.
# Also redirect ControlPath into the writable sidecar: the bind-mounted
# ~/.ssh/config commonly sets 'ControlPath ~/.ssh/cm/...' for CGNAT multiplexing,
# but ~/.ssh is read-only here so the master socket can't be created and those
# hosts fail to connect. First-value-wins: setting it here (before the Include)
# overrides the read-only path for every host. Harmless when ControlMaster is off.
Host * Host *
UserKnownHostsFile ~/.ssh-local/known_hosts UserKnownHostsFile ~/.ssh-local/known_hosts
StrictHostKeyChecking accept-new StrictHostKeyChecking accept-new
ControlPath ~/.ssh-local/cm/%r@%h:%p
# The container host (OrbStack / Docker Desktop). 'host' and 'mac' are aliases. # The container host (OrbStack / Docker Desktop). 'host' and 'mac' are aliases.
Host host mac Host host mac
@@ -109,25 +187,39 @@ ${USER_LINE}
ControlPath ~/.ssh-local/cm/%r@%h:%p ControlPath ~/.ssh-local/cm/%r@%h:%p
ControlPersist 4h ControlPersist 4h
ServerAliveInterval 30 ServerAliveInterval 30
${LAN_CONF_BLOCK}
# Your own target hosts: add 'ProxyJump host' to their entries in your ${AUTOJUMP_BLOCK}
# bind-mounted ~/.ssh/config, pulled in below. ${INCLUDE_BLOCK}
${INCLUDE_LINE}
EOF EOF
chmod 600 "$CONFIG" 2>/dev/null || true chmod 600 "$CONFIG" 2>/dev/null || true
# ── One-time hint when we can't authenticate yet ────────────────────── # ── Authorize hints ───────────────────────────────────────────────────
# Print the copy-paste authorize line whenever we either (a) can't yet
# authenticate (HOST_SSH_USER unset) or (b) just generated a NEW key that the
# host won't recognize. With ~/.ssh-local persisted via a named volume, case
# (b) fires only on first-ever start (or after the volume is reset) — so this
# is normally a one-time, one-line step per machine, with no file to locate.
PUBKEY_TEXT="$(cat "${KEY}.pub" 2>/dev/null)"
if [ -z "${HOST_SSH_USER:-}" ]; then if [ -z "${HOST_SSH_USER:-}" ]; then
cat <<EOF cat <<EOF
[devbox] LAN-access jump config generated at ~/.ssh-local/config, but [devbox] LAN-access jump config generated at ~/.ssh-local/config, but
HOST_SSH_USER is unset so it can't authenticate to the host yet. HOST_SSH_USER is unset so it can't authenticate to the host yet.
To enable container -> host -> LAN-peer access: To enable container -> host -> LAN-peer access:
1. Set HOST_SSH_USER=<your host username> in the container env. 1. Set HOST_SSH_USER=<your host username> in the container env.
2. Authorize this key on the host (append to ~/.ssh/authorized_keys): 2. Authorize this key on the host (run ON THE HOST, once):
$(cat "${KEY}.pub" 2>/dev/null) echo '${PUBKEY_TEXT}' >> ~/.ssh/authorized_keys
3. Ensure the host's SSH server (Remote Login) is enabled. 3. Ensure the host's SSH server (Remote Login) is enabled.
Then: dssh host (or add 'ProxyJump host' to targets in ~/.ssh/config) Then: dssh host (or add 'ProxyJump host' to targets in ~/.ssh/config)
EOF EOF
elif [ "$KEY_JUST_GENERATED" = "1" ]; then
cat <<EOF
[devbox] Generated a NEW LAN-jump key. Authorize it on the host (${HOST_SSH_USER}@host),
then 'dssh host' and your LAN peers will work. Run this ONCE, ON THE HOST:
echo '${PUBKEY_TEXT}' >> ~/.ssh/authorized_keys
(Ensure the host's SSH server / Remote Login is enabled.)
This key is persisted in the ~/.ssh-local volume, so you won't need to
repeat this on container updates — only if that volume is reset.
EOF
fi fi
exit 0 exit 0
+5 -1
View File
@@ -66,10 +66,14 @@ Designed for teams who want a reproducible coding-agent setup that runs the same
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun | | `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/earendil-works/pi) as alternative/complementary harness (shares the mempalace install with opencode) | | `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/earendil-works/pi) as alternative/complementary harness (shares the mempalace install with opencode) |
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together | | `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together |
| `latest-pi-only` / `vX.Y.Z-pi-only` | pi without opencode the lean, pi-focused variant (basis of the separate `joakimp/pi-devbox` image) |
All variants support `linux/amd64` and `linux/arm64`. All variants support `linux/amd64` and `linux/arm64`.
> A fifth, pi-without-opencode build is produced from the same `Dockerfile.variant`
> (`INSTALL_OPENCODE=false`) but is **not** published under this repo it ships as
> the separate [`joakimp/pi-devbox`](https://hub.docker.com/r/joakimp/pi-devbox)
> image so an "opencode-devbox" tag never lacks opencode.
## Quick Start ## Quick Start
For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files: For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files:
+58 -11
View File
@@ -30,6 +30,19 @@ fi
FAILED=0 FAILED=0
pass() { echo "$1"; } pass() { echo "$1"; }
fail() { echo "$1" >&2; FAILED=$((FAILED + 1)); } fail() { echo "$1" >&2; FAILED=$((FAILED + 1)); }
warn() { echo "$1" >&2; }
# Registration assertions (fork/recall installed by the BASE image's
# entrypoint-user.sh via `pi install /opt/<pkg>`) depend on the base, not the
# variant layer built here. validate.yml builds variants FROM the published
# base-latest, which can lag the entrypoint in the current commit (the base
# only rebuilds on a release tag), so a stale base-latest would red the
# push-to-main run with a false negative. These checks are therefore warn-only
# by default; the release pipeline (docker-publish-split.yml) builds the base
# fresh in the same run and sets STRICT_REGISTRATION=1 to enforce them hard.
# The build-time /opt + node_modules checks below stay hard in every path —
# those are produced by the variant layer and must always be correct.
STRICT_REGISTRATION="${STRICT_REGISTRATION:-0}"
run() { run() {
# Run a command inside the image and capture its output. # Run a command inside the image and capture its output.
@@ -187,10 +200,21 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
trap 'docker rm -f "$CID" >/dev/null 2>&1 || true' EXIT trap 'docker rm -f "$CID" >/dev/null 2>&1 || true' EXIT
# Wait for entrypoint-user.sh to finish deploying pi-toolkit + extensions. # Wait for entrypoint-user.sh to finish deploying pi-toolkit + extensions.
# Marker: keybindings.json symlink lands once pi-toolkit/install.sh has run. # The deploy order is: pi-toolkit (writes keybindings.json) -> pi-extensions
# Up to 30s — omos-with-pi has more setup work than base+pi. # (symlinks its *.ts) -> settings.json -> mempalace.ts bridge (LAST). Gating
for _ in $(seq 1 30); do # only on keybindings.json races: it lands when pi-toolkit finishes, before
if docker exec "$CID" test -L /home/developer/.pi/agent/keybindings.json 2>/dev/null; then # pi-extensions has symlinked its *.ts, so the "*.ts >= 4" check below could
# sample mid-deploy under parallel build load (observed v1.16.2 run 370:
# smoke-with-pi saw <4 .ts while omos-with-pi/pi-only saw 8). Wait for the
# LAST-deployed artifact (the mempalace.ts bridge symlink) AND a settled
# extension count so the deploy is fully complete before any assertion runs.
# Up to 45s — pi-bearing variants have more setup work under load.
for _ in $(seq 1 45); do
if docker exec "$CID" sh -c \
'test -L $HOME/.pi/agent/keybindings.json && \
test -L $HOME/.pi/agent/extensions/mempalace.ts && \
[ "$(ls -1 $HOME/.pi/agent/extensions/*.ts 2>/dev/null | wc -l)" -ge 4 ]' \
2>/dev/null; then
break break
fi fi
sleep 1 sleep 1
@@ -206,6 +230,19 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
fi fi
} }
# Like exec_test but warn-only unless STRICT_REGISTRATION=1 (see note at top).
exec_test_reg() {
local label="$1"; shift
local out
if out=$(docker exec -u developer "$CID" sh -c "$*" 2>&1); then
pass "$label ($(echo "$out" | head -1))"
elif [ "$STRICT_REGISTRATION" = "1" ]; then
fail "$label: $out"
else
warn "$label (warn-only — stale base-latest? set STRICT_REGISTRATION=1 to enforce): $out"
fi
}
exec_test "~/.pi/agent/keybindings.json (pi-toolkit)" \ exec_test "~/.pi/agent/keybindings.json (pi-toolkit)" \
'test -L $HOME/.pi/agent/keybindings.json && echo ok' 'test -L $HOME/.pi/agent/keybindings.json && echo ok'
exec_test "~/.pi/agent/extensions/*.ts ≥ 4 (pi-extensions)" \ exec_test "~/.pi/agent/extensions/*.ts ≥ 4 (pi-extensions)" \
@@ -225,9 +262,9 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
fi fi
sleep 1 sleep 1
done done
exec_test "pi-fork registered in settings.json (fork tool)" \ exec_test_reg "pi-fork registered in settings.json (fork tool)" \
'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok' 'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok'
exec_test "pi-observational-memory registered in settings.json (recall tool)" \ exec_test_reg "pi-observational-memory registered in settings.json (recall tool)" \
'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok' 'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok'
docker rm -f "$CID" >/dev/null 2>&1 || true docker rm -f "$CID" >/dev/null 2>&1 || true
@@ -361,15 +398,25 @@ echo " Uncompressed size: ${SIZE_MB} MB"
# with-pi 2700→2900 and omos-with-pi 3700→3900: baking pi-fork + # with-pi 2700→2900 and omos-with-pi 3700→3900: baking pi-fork +
# pi-observational-memory node_modules into /opt (fork pulls its # pi-observational-memory node_modules into /opt (fork pulls its
# @earendil-works peer deps, ~150 MB) adds to both pi-bearing variants. # @earendil-works peer deps, ~150 MB) adds to both pi-bearing variants.
# base 2500→2600 on v1.15.13c — base crept to 2506 MB (LAN-access script +
# updated entrypoint + routine apt-get upgrade drift), tripping the
# deliberately zero-headroom 2500 ceiling and skipping promote-base-latest.
# omos variant to ~3.1 GB. Functional smoke checks all pass; this is a # omos variant to ~3.1 GB. Functional smoke checks all pass; this is a
# guardrail, not a performance limit. # guardrail, not a performance limit.
THRESHOLD=2500 # v1.16.2: all thresholds bumped +150 MB preemptively ahead of the combined
[ "$VARIANT" = "omos" ] && THRESHOLD=3300 # opencode 1.15.13->1.16.2 (minor) + pi 0.78.1->0.79.0 (minor) bump. Both
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2900 # base (2506/2600) and omos (3206/3300) were sitting on ~94 MB headroom and
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3900 # a minor opencode bump has tripped them before (v1.15.0 omos, v1.15.4
# omos-with-pi). Restoring ~250 MB headroom avoids a partial-publish +
# letter-suffix recovery cycle. CI's smoke size print + resolved-versions
# table records the actual landed sizes; tighten later if they come in low.
THRESHOLD=2750
[ "$VARIANT" = "omos" ] && THRESHOLD=3450
[ "$VARIANT" = "with-pi" ] && THRESHOLD=3050
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=4050
# pi-only = with-pi minus opencode (its platform binary is ~145 MB), so it # pi-only = with-pi minus opencode (its platform binary is ~145 MB), so it
# lands a bit under base. Threshold 2750 leaves the same headroom pattern. # lands a bit under base. Threshold 2750 leaves the same headroom pattern.
[ "$VARIANT" = "pi-only" ] && THRESHOLD=2750 [ "$VARIANT" = "pi-only" ] && THRESHOLD=2850
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
+45
View File
@@ -0,0 +1,45 @@
# ssh-lan.conf.example — host-owned LAN-peer jump overrides for opencode-devbox
# ============================================================================
# WHAT THIS IS
# On a VM-backed host (macOS OrbStack / Docker Desktop) the container can't
# reach the host's LAN directly; it tunnels through the host via the `host`
# SSH jump that the entrypoint sets up (see the README "Reaching your LAN"
# section). To reach your LAN peers *by name*, they need `ProxyJump host`.
#
# WHY NOT JUST EDIT ~/.ssh/config?
# The host itself reaches those peers DIRECTLY — adding `ProxyJump host`
# there would break the host's own access (and ~/.ssh is mounted read-only
# into the container anyway). So container-only jump overrides live HERE.
#
# HOW IT'S WIRED
# If this file exists at ~/.config/devbox-shell/ssh-lan.conf on the host
# (the same bind-mounted devbox-shell bridge dir used for shared aliases),
# the generated ~/.ssh-local/config Includes it BEFORE your ~/.ssh/config.
# SSH's first-value-wins rule means ProxyJump is taken from here, while
# HostName / User / IdentityFile are inherited from the matching block in
# your ~/.ssh/config. So you only list the names + the jump — nothing else.
#
# SETUP
# 1. Copy to your host: cp ssh-lan.conf.example ~/.config/devbox-shell/ssh-lan.conf
# 2. Bind-mount ~/.config/devbox-shell into the container (most setups
# already do this for shared shell aliases).
# 3. List the host aliases (as named in your ~/.ssh/config) that should be
# reached through the host jump.
# 4. Restart the container, then: dssh <name>
#
# NOTE: these are facts about ONE host's LAN. A roaming laptop sees different
# networks — keep this per-host, never in the image. For ad-hoc private IPs on
# whatever LAN you're currently on, prefer DEVBOX_LAN_AUTOJUMP_PRIVATE=1
# instead of naming every peer.
# Example — names must match Host blocks already defined in your ~/.ssh/config:
Host pve pve-2 pbs-vm my-nas
ProxyJump host
# You can also give a peer its own settings here if it isn't in ~/.ssh/config
# at all (then specify everything, not just ProxyJump):
# Host lab-box
# HostName 192.168.1.77
# User admin
# IdentityFile ~/.ssh/id_ed25519
# ProxyJump host