Compare commits
8 Commits
v1.15.13b
...
9c31c641d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c31c641d6 | |||
| d9dc85d825 | |||
| 0b78ab4a94 | |||
| 440218fc4c | |||
| a56a5846a5 | |||
| 053dac5308 | |||
| c71c03f0f1 | |||
| 1e98b53113 |
+11
-2
@@ -37,8 +37,11 @@ SSH_KEY_PATH=~/.ssh
|
||||
# directly-attached LAN peers by default. On native Linux Docker the LAN is
|
||||
# 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
|
||||
# as an SSH jump (use the `dssh` alias, or add `ProxyJump host` to targets
|
||||
# in your bind-mounted ~/.ssh/config).
|
||||
# as an SSH jump (use the `dssh` alias). Reach the host itself with
|
||||
# `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
|
||||
# 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.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) ─────────────────────────
|
||||
# If you have a skillset repo, the entrypoint auto-deploys skills and
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
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 (~3–5x 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 (~3–5x 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.
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ concurrency:
|
||||
env:
|
||||
BUILDKIT_PROGRESS: plain
|
||||
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 }}
|
||||
PROMOTE_LATEST: ${{ github.ref_type == 'tag' && 'true' || inputs.promote_latest }}
|
||||
|
||||
@@ -381,6 +386,7 @@ jobs:
|
||||
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
- env:
|
||||
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
|
||||
|
||||
smoke-omos-with-pi:
|
||||
@@ -430,6 +436,7 @@ jobs:
|
||||
- env:
|
||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_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
|
||||
|
||||
smoke-pi-only:
|
||||
@@ -477,6 +484,7 @@ jobs:
|
||||
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
- env:
|
||||
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
|
||||
|
||||
# ── Phase 4: multi-arch publish per variant ────────────────────────
|
||||
@@ -801,11 +809,14 @@ jobs:
|
||||
- name: Compute version-specific tags
|
||||
id: tags
|
||||
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 }}"
|
||||
{ echo "tags<<EOF"
|
||||
echo "${IMAGE}:${VERSION}-pi-only"
|
||||
echo "${PI_IMAGE}:base-pi-only-${VERSION}"
|
||||
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||
echo "${IMAGE}:latest-pi-only"
|
||||
echo "${PI_IMAGE}:base-pi-only"
|
||||
fi
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -20,6 +20,14 @@ name: Validate
|
||||
# release tags are the gate that fully validates base-image changes.
|
||||
# The base-change-warning job below surfaces a runtime warning when this
|
||||
# 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:
|
||||
push:
|
||||
|
||||
@@ -7,10 +7,10 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
||||
## 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.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-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).
|
||||
- `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).
|
||||
@@ -36,7 +36,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.**
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+118
-1
@@ -8,7 +8,124 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
|
||||
|
||||
## Unreleased
|
||||
|
||||
_(no changes since v1.15.13b)_
|
||||
### 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.
|
||||
|
||||
_(no other changes since v1.15.13d)_
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+5
-1
@@ -12,10 +12,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-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-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`.
|
||||
|
||||
> 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
|
||||
|
||||
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:
|
||||
|
||||
@@ -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` |
|
||||
| `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_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_GID` | Override container user GID | Auto-detect from `/workspace` |
|
||||
| `LANG` | System locale | `en_US.UTF-8` |
|
||||
@@ -154,26 +155,37 @@ 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.
|
||||
- **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`.
|
||||
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).
|
||||
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
|
||||
# in your host ~/.ssh/config (mounted read-only into the container)
|
||||
Host my-nas
|
||||
HostName 192.168.1.50
|
||||
User admin
|
||||
# ~/.config/devbox-shell/ssh-lan.conf — on the host, bind-mounted in
|
||||
# Only ProxyJump goes here; HostName/User/IdentityFile are inherited
|
||||
# (first-value-wins) from the matching block in your ~/.ssh/config.
|
||||
Host my-nas pve pbs
|
||||
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"]`).
|
||||
|
||||
### Custom opencode config
|
||||
|
||||
@@ -463,7 +475,7 @@ All six agents should respond if your provider authentication is working.
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -59,6 +59,11 @@ services:
|
||||
# allowing both native and containerized opencode on the same machine.
|
||||
- devbox-opencode-config:/home/developer/.config/opencode
|
||||
- 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
|
||||
# container manages its own skills directory independently — the
|
||||
@@ -95,6 +100,14 @@ services:
|
||||
# - ~/.bash_aliases:/home/developer/.bash_aliases: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)
|
||||
# Without this, 'uv python install' must be re-run after container removal.
|
||||
- devbox-uv:/home/developer/.local/share/uv
|
||||
@@ -126,6 +139,7 @@ services:
|
||||
volumes:
|
||||
devbox-opencode-config:
|
||||
devbox-pi-config:
|
||||
devbox-ssh-local:
|
||||
devbox-data:
|
||||
devbox-state:
|
||||
devbox-shell-history:
|
||||
|
||||
@@ -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
|
||||
> 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).
|
||||
>
|
||||
> **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)
|
||||
Add to the single `INSTALL_PI=true` block in `opencode-devbox/Dockerfile.variant`
|
||||
|
||||
@@ -88,6 +88,7 @@ for dir in \
|
||||
/home/"$USER_NAME"/.config/opencode \
|
||||
/home/"$USER_NAME"/.config/nvim \
|
||||
/home/"$USER_NAME"/.pi \
|
||||
/home/"$USER_NAME"/.ssh-local \
|
||||
/home/"$USER_NAME"/.agents/skills; do
|
||||
[ -d "$dir" ] || continue
|
||||
|
||||
|
||||
@@ -37,6 +37,30 @@
|
||||
# jump to authenticate. If unset we still generate the config but print
|
||||
# a hint with the public key to authorize on the host.
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# ── 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
|
||||
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
|
||||
KEY_JUST_GENERATED=1
|
||||
fi
|
||||
|
||||
# ── Render the writable config ────────────────────────────────────────
|
||||
@@ -84,9 +114,51 @@ if [ -n "${HOST_SSH_USER:-}" ]; then
|
||||
USER_LINE=" User ${HOST_SSH_USER}"
|
||||
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
|
||||
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
|
||||
|
||||
cat > "$CONFIG" <<EOF
|
||||
@@ -95,9 +167,15 @@ cat > "$CONFIG" <<EOF
|
||||
# (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.
|
||||
# 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 *
|
||||
UserKnownHostsFile ~/.ssh-local/known_hosts
|
||||
StrictHostKeyChecking accept-new
|
||||
ControlPath ~/.ssh-local/cm/%r@%h:%p
|
||||
|
||||
# The container host (OrbStack / Docker Desktop). 'host' and 'mac' are aliases.
|
||||
Host host mac
|
||||
@@ -109,25 +187,39 @@ ${USER_LINE}
|
||||
ControlPath ~/.ssh-local/cm/%r@%h:%p
|
||||
ControlPersist 4h
|
||||
ServerAliveInterval 30
|
||||
|
||||
# Your own target hosts: add 'ProxyJump host' to their entries in your
|
||||
# bind-mounted ~/.ssh/config, pulled in below.
|
||||
${INCLUDE_LINE}
|
||||
${LAN_CONF_BLOCK}
|
||||
${AUTOJUMP_BLOCK}
|
||||
${INCLUDE_BLOCK}
|
||||
EOF
|
||||
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
|
||||
cat <<EOF
|
||||
[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.
|
||||
To enable container -> host -> LAN-peer access:
|
||||
1. Set HOST_SSH_USER=<your host username> in the container env.
|
||||
2. Authorize this key on the host (append to ~/.ssh/authorized_keys):
|
||||
$(cat "${KEY}.pub" 2>/dev/null)
|
||||
2. Authorize this key on the host (run ON THE HOST, once):
|
||||
echo '${PUBKEY_TEXT}' >> ~/.ssh/authorized_keys
|
||||
3. Ensure the host's SSH server (Remote Login) is enabled.
|
||||
Then: dssh host (or add 'ProxyJump host' to targets in ~/.ssh/config)
|
||||
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
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -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-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-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`.
|
||||
|
||||
> 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
|
||||
|
||||
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:
|
||||
|
||||
+32
-3
@@ -30,6 +30,19 @@ fi
|
||||
FAILED=0
|
||||
pass() { echo " ✓ $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 a command inside the image and capture its output.
|
||||
@@ -206,6 +219,19 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
|
||||
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)" \
|
||||
'test -L $HOME/.pi/agent/keybindings.json && echo ok'
|
||||
exec_test "~/.pi/agent/extensions/*.ts ≥ 4 (pi-extensions)" \
|
||||
@@ -225,9 +251,9 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
|
||||
fi
|
||||
sleep 1
|
||||
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'
|
||||
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'
|
||||
|
||||
docker rm -f "$CID" >/dev/null 2>&1 || true
|
||||
@@ -361,9 +387,12 @@ echo " Uncompressed size: ${SIZE_MB} MB"
|
||||
# with-pi 2700→2900 and omos-with-pi 3700→3900: baking pi-fork +
|
||||
# pi-observational-memory node_modules into /opt (fork pulls its
|
||||
# @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
|
||||
# guardrail, not a performance limit.
|
||||
THRESHOLD=2500
|
||||
THRESHOLD=2600
|
||||
[ "$VARIANT" = "omos" ] && THRESHOLD=3300
|
||||
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2900
|
||||
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3900
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user