docs: fix drift against main batch (SSH sidecar, proposed config, provenance, forked build)
Validate / docs-check (push) Successful in 7s
Validate / base-change-warning (push) Successful in 14s
Validate / validate-omos (push) Failing after 4m23s
Validate / validate-base (push) Failing after 5m8s

DRIFT-STALE fixes (stale/misleading text corrected):
- README.md:160 — "on native Linux it does nothing" → accurate always-render
  description (sidecar written on every OS; jump block still omitted on Linux)
- AGENTS.md:22 — setup-lan-access.sh "no-op on native Linux" → corrected to
  always-render + removed now-redundant inline ControlPath/UserKnownHostsFile
  sentence (that's what the sidecar does, not what the script detects)
- CHANGELOG.md:594 (v2.1.1 intro) — "no-op" → "(see v2.2.0)" historical note
- docs/plan-lan-access-and-pi-extensions.md:222 — annotated the completed
  checkbox with the v2.2.0 refinement

GAP additions:
- README.md:212 — custom config section now describes opencode.jsonc.proposed
  sidecar: written-on-diff, removed-on-match, never overwrites, one-line hint
- README.md build-args table — INSTALL_MEMPALACE_TOOLKIT row now mentions
  MEMPALACE_TOOLKIT_REPO alongside MEMPALACE_TOOLKIT_REF
- README.md (after build-args table) — two new sections:
    "Building a fork / relocated build" — MEMPALACE_TOOLKIT_REPO build-arg
    table + two-step docker build example + credentials note
    "Build provenance (labels + manifest)" — docker inspect + run examples,
    OCI label set, ground-truth manifest path

Version string drift: no hard-coded 1.17.7 found outside CHANGELOG (all
occurrences are historical release entries — correct by definition).
CI-internal items (resolve-versions, check-base-hash.sh): no existing doc
described these mechanics, so no update needed.
This commit is contained in:
pi
2026-06-19 20:14:12 +02:00
parent af11c32f4f
commit 1f0d06444b
4 changed files with 47 additions and 7 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
- `Dockerfile.variant``FROM`s the base and adds only opencode/omos installs gated by build args: `INSTALL_OPENCODE` (default true), `INSTALL_OMOS`, and `INSTALL_MEMPALACE`. All GitHub-sourced binaries are pinned with version ARGs. Two variants: `base` (`INSTALL_OPENCODE=true`) and `omos` (`+INSTALL_OMOS=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.
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), LAN-access setup (delegated to `setup-lan-access.sh`), a one-time npm-global prefix migration shim (legacy `~/.pi/npm-global``~/.config/opencode/npm-global`), skillset auto-deploy from mounted skillset repo, OMOS bundled-skills reconcile (symlinks the image's bundled skills into `~/.agents/skills/`), OMOS config 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` / `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/setup-lan-access.sh` — host-OS-agnostic LAN reachability helper. Always writes the writable `~/.ssh-local/config` sidecar on **every** host OS: a `Host *` block that redirects `ControlPath` into `~/.ssh-local/cm/` (first-value-wins over any read-only `~/.ssh`-bound per-host setting) plus `Include ~/.ssh/config`. On VM-backed hosts (macOS OrbStack / Docker Desktop, detected via `host.docker.internal` resolution) it additionally inserts the host-jump block; on native Linux that block is omitted (LAN is reachable directly) but the sidecar is still rendered. Previously the script exited early on native Linux, leaving `dssh`/`dscp` broken when `~/.ssh` was read-only there. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS` / `DEVBOX_LAN_AUTOJUMP_PRIVATE`. Ships the mechanism only (generic `host` jump alias); user targets stay host-side — named-peer `ProxyJump host` overrides go in a bind-mounted `~/.config/devbox-shell/ssh-lan.conf` (Included before `~/.ssh/config`), never baked into the image. **Scoping invariant:** every `Include` in the generated config MUST be preceded by a bare `Host *` reset — an `Include` is scoped to the enclosing `Host`/`Match` block, so without the reset the included config only applies when targeting `host`/`mac` and named peers fall back to SSH defaults. Non-fatal. Counted in the base hash, so editing it advances `base-latest`.
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint).
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
- `scripts/recreate-sanity-check.sh`**runtime** post-recreate verification (counterpart to the build-time `smoke-test.sh`). Run inside the container after `docker compose up -d --force-recreate` to confirm the new image is live (opencode version matches `Dockerfile.variant`'s `OPENCODE_VERSION`), persisted named volumes survived (mempalace palace, opencode.db, bash-history), omos runtime skill symlinks resolve, shell defaults re-seeded, and `/opt` toolkits intact. Not run by CI or the entrypoint — it needs the running container + volumes that smoke-test.sh (which uses `--entrypoint=""`) cannot see.
+1 -1
View File
@@ -591,7 +591,7 @@ Container-level rebuild on opencode `1.15.13` (unchanged) and pi `0.78.0` (uncha
The container can now reach LAN peers that the **host** can reach, regardless of host OS — addressing the macOS/Docker-Desktop limitation where a container in the Linux VM cannot see the host's directly-attached LAN.
- New `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh`, invoked (non-fatally) by `entrypoint-user.sh` on every start.
- **Detection:** on VM-backed hosts (macOS OrbStack / Docker Desktop, Windows Docker Desktop — detected via `host.docker.internal` resolution) it generates a writable `~/.ssh-local/config` that uses the host as an SSH **jump**. On native Linux Docker (LAN reachable directly) it is a **no-op**.
- **Detection:** on VM-backed hosts (macOS OrbStack / Docker Desktop, Windows Docker Desktop — detected via `host.docker.internal` resolution) it generates a writable `~/.ssh-local/config` that uses the host as an SSH **jump**. On native Linux Docker (LAN reachable directly) the jump block is omitted, but the sidecar is still written for the ControlPath redirect (see v2.2.0).
- **Mechanism, not policy:** ships a generic `host` (alias `mac`) jump entry + a generated jump key in the writable `~/.ssh-local/` sidecar (necessary because `~/.ssh` is bind-mounted read-only). Your own targets stay in your bind-mounted `~/.ssh/config` (add `ProxyJump host`), pulled in via `Include ~/.ssh/config`.
- New env knobs: `DEVBOX_LAN_ACCESS` (`auto`|`jump`|`off`, default `auto`), `HOST_SSH_USER`, `DEVBOX_HOST_ALIAS`. When `HOST_SSH_USER` is unset the entrypoint prints the public key to authorize on the host.
- New `dssh` / `dscp` aliases in `.bash_aliases` (wrap `ssh -F ~/.ssh-local/config`), guarded so they only appear when the jump config was generated.
+43 -4
View File
@@ -157,7 +157,7 @@ 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. 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.
On every start the entrypoint runs `setup-lan-access.sh`, which always writes a writable `~/.ssh-local/config` sidecar. The sidecar does two things regardless of host OS: redirect `ControlPath` into the writable `~/.ssh-local/cm/` (so ControlMaster sockets don't hit the read-only `~/.ssh` bind-mount) and `Include ~/.ssh/config`. On VM-backed hosts it additionally inserts the **SSH-jump-via-host block** so you can reach LAN peers; on native Linux that block is omitted (LAN is reachable directly) but the sidecar is still written — so `dssh`/`dscp` and ControlMaster work on native Linux with a read-only `~/.ssh` too. The jump keypair lives in `~/.ssh-local`, persisted by the `devbox-ssh-local` named volume — generated **once** and reused across container updates.
**To enable it on a VM-backed host (one-time setup per machine):**
@@ -209,7 +209,7 @@ Host my-remote
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. Changes to `opencode.jsonc` and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation. Auto-deployed skills are *not* stored here — skillset and OMOS skills are symlinked into `~/.agents/skills/` and rebuilt on every start (see [Custom skills](#custom-skills) and [docs/omos-skills.md](docs/omos-skills.md)).
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped — the live config is never overwritten. However, on each start `generate-config.py` checks whether the config it *would* generate for your current environment differs from the live one, and if so writes a **`opencode.jsonc.proposed`** sidecar in the same directory. This is a manual-merge reference only — opencode never loads `.proposed` files. It is removed automatically once your live config matches the current image defaults. A one-line hint is logged when one is present. Differences may reflect new image defaults (e.g. a newly-added MCP server) *or* your own past edits — the file header explains both.
**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount:
@@ -422,7 +422,7 @@ docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific versi
|---|---|---|
| `INSTALL_GO` | `false` | Go toolchain (resolves latest stable from go.dev when `GO_VERSION=latest`) |
| `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_REPO` at ref `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_OPENCODE` | `true` | Install opencode. Set `false` to build a base with no harness (still includes Bun if `INSTALL_OMOS=true`). |
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
@@ -603,7 +603,46 @@ Both wrappers are idempotent and dedup-aware — re-running them on unchanged in
For weekly automated runs, the toolkit ships ready-to-use scheduler templates (systemd user timer, launchd user agent, cron) in its [`contrib/`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/contrib) directory. The `*-devbox` variants are designed for this container: host-side schedulers that `docker exec` into the running opencode-devbox.
Disable the toolkit (keeps mempalace itself) with `--build-arg INSTALL_MEMPALACE_TOOLKIT=false`. Pin to a specific ref with `--build-arg MEMPALACE_TOOLKIT_REF=v0.3.0` once tagged releases exist.
Disable the toolkit (keeps mempalace itself) with `--build-arg INSTALL_MEMPALACE_TOOLKIT=false`. Pin to a specific ref with `--build-arg MEMPALACE_TOOLKIT_REF=v0.3.0` once tagged releases exist. Repoint the clone URL with `--build-arg MEMPALACE_TOOLKIT_REPO=<url>` for forked or air-gapped builds (see below).
### Building a fork / relocated build
The canonical build clones `mempalace-toolkit` from `gitea.jordbo.se`. That URL is an overridable build-arg (defaulting to the canonical origin), so a fork or a build on a host that can't reach that gitea can repoint it at a mirror, another host, or a local `file://` path **without editing the Dockerfiles**:
| Build-arg | Default | Dockerfile |
|---|---|---|
| `MEMPALACE_TOOLKIT_REPO` | `https://gitea.jordbo.se/joakimp/mempalace-toolkit.git` | base |
Each companion also has a matching `*_REF` arg (branch name or commit SHA). Example — build against a forked mempalace-toolkit:
```bash
# base first
docker build -f Dockerfile.base -t myorg/opencode-devbox:base-dev \
--build-arg MEMPALACE_TOOLKIT_REPO=https://github.com/myorg/mempalace-toolkit.git .
# then the variant FROM that base
docker build -f Dockerfile.variant -t myorg/opencode-devbox:dev \
--build-arg BASE_IMAGE=myorg/opencode-devbox:base-dev \
--build-arg OPENCODE_VERSION=1.17.8 .
```
Note: mempalace-toolkit clones anonymously (no token needed). Only the `resolve-versions` CI job calls the gitea API (which needs a token for public repos). A plain `docker build` like the above skips that job entirely, so no credentials are required.
Provenance build-args (all optional; populate the OCI labels and `/etc/opencode-devbox/build-manifest.json` — see below): `RELEASE_TAG`, `BUILD_DATE`, `SOURCE_REVISION`. CI sets these automatically; a manual build leaves them at harmless defaults.
### Build provenance (labels + manifest)
Every published image is self-describing. Inspect the OCI labels without pulling the filesystem:
```bash
docker inspect --format '{{json .Config.Labels}}' joakimp/opencode-devbox:latest | jq .
```
`org.opencontainers.image.{version,revision,created}` plus `se.jordbo.opencode-devbox.{opencode-version,install-omos,omos-version,mempalace-toolkit-ref}` record the intended opencode version, omos status, and companion refs. The on-disk `/etc/opencode-devbox/build-manifest.json` records **ground truth** — the live `opencode --version`, the installed `oh-my-opencode-slim` version (or `null` in the base variant), and the actual checked-out HEAD of `/opt/mempalace-toolkit` — so a tag is reconstructable after CI logs rotate:
```bash
docker run --rm --entrypoint= joakimp/opencode-devbox:latest cat /etc/opencode-devbox/build-manifest.json
```
### Storage
+2 -1
View File
@@ -219,7 +219,8 @@ Add (for `with-pi`/`omos-with-pi`/pi-devbox):
2. **Latest-but-pinned**: track latest (master HEAD), resolve to SHA in CI build-arg. ✓
3. **Refactor**: pi-devbox/Dockerfile -> `FROM` the with-pi variant; pi-install in ONE place. ✓
4. **LAN default** `DEVBOX_LAN_ACCESS=auto`: generate config + print authorize hint when
`HOST_SSH_USER` unset; silent no-op on native Linux. ✓
`HOST_SSH_USER` unset; silent no-op on native Linux. ✓ *(v2.2.0: sidecar now rendered on
all OSes; native Linux no longer skipped — jump block still omitted there)*
5. **No `DEVBOX_LAN_HOSTS`**: rely on user's bind-mounted `~/.ssh/config` (`ProxyJump host`). ✓
## Remaining verify-before-merge items