Compare commits

...

1 Commits

Author SHA1 Message Date
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
6 changed files with 194 additions and 17 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
# 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
View File
@@ -10,7 +10,7 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
- `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).
+46 -1
View File
@@ -8,7 +8,52 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
## Unreleased
_(no changes since v1.15.13c)_
_(no 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
+14 -7
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` |
| `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` |
@@ -161,19 +162,25 @@ On every start the entrypoint detects which case applies. On VM-backed hosts it
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`.
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`.)
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
@@ -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.
@@ -84,9 +108,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 +161,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,10 +181,9 @@ ${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
+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