feat: host-agnostic LAN access (base) + fork/recall in pi variants
Validate / base-change-warning (push) Successful in 22s
Validate / docs-check (push) Successful in 44s
Validate / validate-base (push) Successful in 3m27s
Validate / validate-omos (push) Successful in 7m3s
Validate / validate-with-pi (push) Failing after 4m33s
Validate / validate-omos-with-pi (push) Failing after 8m29s
Validate / base-change-warning (push) Successful in 22s
Validate / docs-check (push) Successful in 44s
Validate / validate-base (push) Successful in 3m27s
Validate / validate-omos (push) Successful in 7m3s
Validate / validate-with-pi (push) Failing after 4m33s
Validate / validate-omos-with-pi (push) Failing after 8m29s
Item A — LAN access (base image): - New rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh, invoked non-fatally from entrypoint-user.sh. On VM-backed hosts (macOS OrbStack / Docker Desktop, detected via host.docker.internal) it generates a writable ~/.ssh-local/config that uses the host as an SSH jump to reach LAN peers; no-op on native Linux. Ships the mechanism (generic 'host' jump alias), not policy (targets stay in the user's bind-mounted ~/.ssh/config). - New env knobs: DEVBOX_LAN_ACCESS (auto|jump|off), HOST_SSH_USER, DEVBOX_HOST_ALIAS. dssh/dscp aliases in .bash_aliases (guarded). Item B — pi-fork (fork) + pi-observational-memory (recall) in pi variants: - Dockerfile.variant clones both elpapi42 repos to /opt and runs npm install there at build time (local-path 'pi install' does not npm-install, so deps must be present to load). New args PI_FORK_REPO/REF, PI_OBSMEM_REPO/REF. - entrypoint-user.sh registers them at runtime via 'pi install /opt/<pkg>' (instant, in-place, idempotent; tools bind on next pi start). - CI resolve-versions resolves each repo's master HEAD to a commit SHA and passes PI_FORK_REF/PI_OBSMEM_REF — same cache-hit guard as PI_VERSION. - smoke-test asserts /opt clones + node_modules + settings.json registration; size thresholds bumped (with-pi 2700->2900, omos-with-pi 3700->3900). Versions unchanged (opencode 1.15.13, pi 0.78.0 — both still latest). Docs: README LAN section + env table, .env.example, AGENTS.md, CHANGELOG. Plan recorded in docs/plan-lan-access-and-pi-extensions.md.
This commit is contained in:
@@ -31,6 +31,30 @@ WORKSPACE_PATH=~/projects
|
|||||||
# Path to SSH keys on host
|
# Path to SSH keys on host
|
||||||
SSH_KEY_PATH=~/.ssh
|
SSH_KEY_PATH=~/.ssh
|
||||||
|
|
||||||
|
# ── LAN access from the container (host-OS-agnostic) ─────────────────
|
||||||
|
# On VM-backed hosts (macOS OrbStack / Docker Desktop, also Docker Desktop
|
||||||
|
# on Windows) the container runs in a Linux VM and CANNOT reach the host's
|
||||||
|
# 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).
|
||||||
|
#
|
||||||
|
# DEVBOX_LAN_ACCESS: auto (default) | jump | off
|
||||||
|
# auto = set up the jump only on VM-backed hosts; no-op on native Linux.
|
||||||
|
# jump = always set up (e.g. native Linux with extra_hosts host-gateway).
|
||||||
|
# off = disable entirely.
|
||||||
|
# DEVBOX_LAN_ACCESS=auto
|
||||||
|
#
|
||||||
|
# HOST_SSH_USER: your username on the host. REQUIRED for the jump to
|
||||||
|
# authenticate. On first start the entrypoint prints the public key to
|
||||||
|
# authorize on the host (append to the host's ~/.ssh/authorized_keys) and
|
||||||
|
# reminds you to enable the host's SSH server (e.g. macOS Remote Login).
|
||||||
|
# HOST_SSH_USER=
|
||||||
|
#
|
||||||
|
# DEVBOX_HOST_ALIAS: host hostname to reach (default host.docker.internal).
|
||||||
|
# DEVBOX_HOST_ALIAS=host.docker.internal
|
||||||
|
|
||||||
# ── 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
|
||||||
# instructions on container start using relative symlinks (portable
|
# instructions on container start using relative symlinks (portable
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
pi_version: ${{ steps.resolve.outputs.pi_version }}
|
pi_version: ${{ steps.resolve.outputs.pi_version }}
|
||||||
omos_version: ${{ steps.resolve.outputs.omos_version }}
|
omos_version: ${{ steps.resolve.outputs.omos_version }}
|
||||||
|
fork_ref: ${{ steps.resolve.outputs.fork_ref }}
|
||||||
|
obsmem_ref: ${{ steps.resolve.outputs.obsmem_ref }}
|
||||||
steps:
|
steps:
|
||||||
- name: Resolve pi + omos versions from npm registry
|
- name: Resolve pi + omos versions from npm registry
|
||||||
id: resolve
|
id: resolve
|
||||||
@@ -136,7 +138,23 @@ jobs:
|
|||||||
OMOS_VERSION=$(curl -sf "https://registry.npmjs.org/oh-my-opencode-slim/latest" | jq -r '.version')
|
OMOS_VERSION=$(curl -sf "https://registry.npmjs.org/oh-my-opencode-slim/latest" | jq -r '.version')
|
||||||
echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT"
|
echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "omos_version=${OMOS_VERSION}" >> "$GITHUB_OUTPUT"
|
echo "omos_version=${OMOS_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
# Resolve the pi-fork / pi-observational-memory git refs (default
|
||||||
|
# branch master) to concrete commit SHAs so the build-arg string
|
||||||
|
# changes whenever upstream moves — defeating the same registry-
|
||||||
|
# buildcache cache-hit footgun that PI_VERSION/OMOS_VERSION guard
|
||||||
|
# against. The Accept: application/vnd.github.sha media type returns
|
||||||
|
# the bare SHA. Falls back to the branch name if the API is
|
||||||
|
# unreachable/rate-limited (still functional, just cache-stale-prone).
|
||||||
|
FORK_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
|
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || echo "master")
|
||||||
|
OBSMEM_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
|
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || echo "master")
|
||||||
|
[ -n "$FORK_REF" ] || FORK_REF=master
|
||||||
|
[ -n "$OBSMEM_REF" ] || OBSMEM_REF=master
|
||||||
|
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "obsmem_ref=${OBSMEM_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Resolved PI_VERSION=${PI_VERSION}, OMOS_VERSION=${OMOS_VERSION}"
|
echo "Resolved PI_VERSION=${PI_VERSION}, OMOS_VERSION=${OMOS_VERSION}"
|
||||||
|
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
||||||
|
|
||||||
# ── Phase 2: build & push base (multi-arch), only when needed ──────
|
# ── Phase 2: build & push base (multi-arch), only when needed ──────
|
||||||
build-base:
|
build-base:
|
||||||
@@ -359,6 +377,8 @@ jobs:
|
|||||||
INSTALL_OMOS=false
|
INSTALL_OMOS=false
|
||||||
INSTALL_PI=true
|
INSTALL_PI=true
|
||||||
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
|
PI_FORK_REF=${{ needs.resolve-versions.outputs.fork_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 }}
|
||||||
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
|
||||||
@@ -405,6 +425,8 @@ jobs:
|
|||||||
INSTALL_PI=true
|
INSTALL_PI=true
|
||||||
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
|
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
|
||||||
|
PI_FORK_REF=${{ needs.resolve-versions.outputs.fork_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 }}
|
||||||
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
||||||
@@ -594,6 +616,8 @@ jobs:
|
|||||||
TAGS: ${{ steps.tags.outputs.tags }}
|
TAGS: ${{ steps.tags.outputs.tags }}
|
||||||
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
|
FORK_REF: ${{ needs.resolve-versions.outputs.fork_ref }}
|
||||||
|
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG_FLAGS=()
|
TAG_FLAGS=()
|
||||||
@@ -610,6 +634,8 @@ jobs:
|
|||||||
--build-arg "INSTALL_OMOS=false" \
|
--build-arg "INSTALL_OMOS=false" \
|
||||||
--build-arg "INSTALL_PI=true" \
|
--build-arg "INSTALL_PI=true" \
|
||||||
--build-arg "PI_VERSION=${PI_VERSION}" \
|
--build-arg "PI_VERSION=${PI_VERSION}" \
|
||||||
|
--build-arg "PI_FORK_REF=${FORK_REF}" \
|
||||||
|
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||||
"${TAG_FLAGS[@]}" \
|
"${TAG_FLAGS[@]}" \
|
||||||
.; then
|
.; then
|
||||||
echo "==> Attempt ${attempt} succeeded"
|
echo "==> Attempt ${attempt} succeeded"
|
||||||
@@ -666,6 +692,8 @@ jobs:
|
|||||||
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
||||||
|
FORK_REF: ${{ needs.resolve-versions.outputs.fork_ref }}
|
||||||
|
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG_FLAGS=()
|
TAG_FLAGS=()
|
||||||
@@ -683,6 +711,8 @@ jobs:
|
|||||||
--build-arg "INSTALL_PI=true" \
|
--build-arg "INSTALL_PI=true" \
|
||||||
--build-arg "PI_VERSION=${PI_VERSION}" \
|
--build-arg "PI_VERSION=${PI_VERSION}" \
|
||||||
--build-arg "OMOS_VERSION=${OMOS_VERSION}" \
|
--build-arg "OMOS_VERSION=${OMOS_VERSION}" \
|
||||||
|
--build-arg "PI_FORK_REF=${FORK_REF}" \
|
||||||
|
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||||
"${TAG_FLAGS[@]}" \
|
"${TAG_FLAGS[@]}" \
|
||||||
.; then
|
.; then
|
||||||
echo "==> Attempt ${attempt} succeeded"
|
echo "==> Attempt ${attempt} succeeded"
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
|||||||
## 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.
|
- `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).
|
||||||
- `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`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, skillset auto-deploy from mounted skillset repo, OMOS setup.
|
- `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/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).
|
||||||
|
|||||||
+22
-1
@@ -8,7 +8,28 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
_(no changes since v1.15.13)_
|
### Added: host-OS-agnostic LAN access (base image)
|
||||||
|
|
||||||
|
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**.
|
||||||
|
- **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.
|
||||||
|
- Because this touches `Dockerfile.base` inputs (`rootfs/`, `entrypoint-user.sh`), the base image rebuilds and `base-latest` advances.
|
||||||
|
|
||||||
|
### Added: pi-fork (`fork`) + pi-observational-memory (`recall`) in pi variants
|
||||||
|
|
||||||
|
The `with-pi` and `omos-with-pi` variants now bake in two pi extensions from `github.com/elpapi42`:
|
||||||
|
|
||||||
|
- `Dockerfile.variant` clones both repos to `/opt/pi-fork` and `/opt/pi-observational-memory` and runs `npm install` there at **build** time (a local-path `pi install` does not npm-install, so deps must be present for the extension to load).
|
||||||
|
- `entrypoint-user.sh` registers them at runtime via `pi install /opt/<pkg>` (instant, in-place, idempotent; `fork`/`recall` tools bind on the next pi start).
|
||||||
|
- CI (`resolve-versions`) resolves the `master` HEAD of each repo to a concrete commit SHA and passes it as `PI_FORK_REF` / `PI_OBSMEM_REF` build-args — same registry-buildcache cache-hit guard used for `PI_VERSION` / `OMOS_VERSION`.
|
||||||
|
- New build-args: `PI_FORK_REPO`, `PI_FORK_REF`, `PI_OBSMEM_REPO`, `PI_OBSMEM_REF`.
|
||||||
|
- Smoke test asserts the `/opt` clones + baked `node_modules` exist and that both packages register in `settings.json`. Size thresholds bumped: `with-pi` 2700→2900 MB, `omos-with-pi` 3700→3900 MB (fork's `@earendil-works` peer deps add ~150 MB).
|
||||||
|
|
||||||
|
_Versions unchanged: opencode-ai `1.15.13`, pi `0.78.0` (both still latest at time of writing)._
|
||||||
|
|
||||||
## v1.15.13 — 2026-05-29
|
## v1.15.13 — 2026-05-29
|
||||||
|
|
||||||
|
|||||||
+29
-1
@@ -62,6 +62,17 @@ ARG INSTALL_PI=false
|
|||||||
ARG PI_VERSION=latest
|
ARG PI_VERSION=latest
|
||||||
ARG PI_TOOLKIT_REF=main
|
ARG PI_TOOLKIT_REF=main
|
||||||
ARG PI_EXTENSIONS_REF=main
|
ARG PI_EXTENSIONS_REF=main
|
||||||
|
# pi-fork (fork tool) + pi-observational-memory (recall tool) live on GitHub
|
||||||
|
# under elpapi42. Refs default to the tracked branch for local dev; CI resolves
|
||||||
|
# them to concrete commit SHAs (see resolve-versions in docker-publish-split.yml)
|
||||||
|
# so the build-arg string changes when upstream moves — same registry-buildcache
|
||||||
|
# cache-hit footgun the PI_VERSION/OMOS_VERSION pins guard against. The clone
|
||||||
|
# helper for these uses `git fetch <ref>` (not `--branch`) so it accepts both
|
||||||
|
# branch names and raw commit SHAs.
|
||||||
|
ARG PI_FORK_REPO=https://github.com/elpapi42/pi-fork.git
|
||||||
|
ARG PI_FORK_REF=master
|
||||||
|
ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
|
||||||
|
ARG PI_OBSMEM_REF=master
|
||||||
RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
||||||
set -e && \
|
set -e && \
|
||||||
git_clone_retry() { \
|
git_clone_retry() { \
|
||||||
@@ -74,6 +85,17 @@ RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
|||||||
done; \
|
done; \
|
||||||
return 1; \
|
return 1; \
|
||||||
} && \
|
} && \
|
||||||
|
git_fetch_ref() { \
|
||||||
|
url="$1"; ref="$2"; dest="$3"; \
|
||||||
|
rm -rf "$dest"; mkdir -p "$dest"; \
|
||||||
|
git -C "$dest" init -q && git -C "$dest" remote add origin "$url" && \
|
||||||
|
for i in 1 2 3 4 5; do \
|
||||||
|
if git -C "$dest" fetch --depth 1 origin "$ref" && git -C "$dest" checkout -q FETCH_HEAD; then return 0; fi; \
|
||||||
|
echo "git fetch $url@$ref failed (attempt $i/5), retrying in $((i*5))s..."; \
|
||||||
|
sleep $((i*5)); \
|
||||||
|
done; \
|
||||||
|
return 1; \
|
||||||
|
} && \
|
||||||
if [ "${PI_VERSION}" = "latest" ]; then \
|
if [ "${PI_VERSION}" = "latest" ]; then \
|
||||||
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \
|
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \
|
||||||
else \
|
else \
|
||||||
@@ -82,8 +104,14 @@ RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
|||||||
pi --version && \
|
pi --version && \
|
||||||
git_clone_retry https://gitea.jordbo.se/joakimp/pi-toolkit.git "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
|
git_clone_retry https://gitea.jordbo.se/joakimp/pi-toolkit.git "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
|
||||||
git_clone_retry https://gitea.jordbo.se/joakimp/pi-extensions.git "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
|
git_clone_retry https://gitea.jordbo.se/joakimp/pi-extensions.git "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
|
||||||
|
git_fetch_ref "${PI_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \
|
||||||
|
git_fetch_ref "${PI_OBSMEM_REPO}" "${PI_OBSMEM_REF}" /opt/pi-observational-memory && \
|
||||||
|
(cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \
|
||||||
|
(cd /opt/pi-observational-memory && npm install --omit=dev --no-audit --no-fund) && \
|
||||||
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
||||||
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" ; \
|
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" && \
|
||||||
|
echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \
|
||||||
|
echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)" ; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Optional: Go ─────────────────────────────────────────────────────
|
# ── Optional: Go ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ docker compose exec -u developer devbox aws --version
|
|||||||
| `GIT_USER_EMAIL` | Git commit author email | — |
|
| `GIT_USER_EMAIL` | Git commit author email | — |
|
||||||
| `WORKSPACE_PATH` | Host path to mount | `.` |
|
| `WORKSPACE_PATH` | Host path to mount | `.` |
|
||||||
| `SSH_KEY_PATH` | Host SSH key directory | `~/.ssh` |
|
| `SSH_KEY_PATH` | Host SSH key directory | `~/.ssh` |
|
||||||
|
| `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` |
|
||||||
| `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` |
|
||||||
@@ -144,6 +147,34 @@ docker compose exec -u developer devbox aws --version
|
|||||||
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
|
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
|
||||||
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
|
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
|
||||||
|
|
||||||
|
### Reaching your LAN from the container
|
||||||
|
|
||||||
|
The devbox works the same way whether the host is **native Linux Docker** or a **VM-backed** runtime (macOS OrbStack / Docker Desktop, or Docker Desktop on Windows) — but their networking differs:
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
**To enable it on a VM-backed host:**
|
||||||
|
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```sshconfig
|
||||||
|
# in your host ~/.ssh/config (mounted read-only into the container)
|
||||||
|
Host my-nas
|
||||||
|
HostName 192.168.1.50
|
||||||
|
User admin
|
||||||
|
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`.)
|
||||||
|
|
||||||
|
> 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"]`).
|
||||||
|
|
||||||
### Custom opencode config
|
### Custom opencode config
|
||||||
|
|
||||||
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
|
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# Plan: LAN-access mechanism + pi-fork/pi-observational-memory in the builds
|
||||||
|
|
||||||
|
Status: PROPOSED (2026-06-03, decisions folded in). Author: pi (devbox session).
|
||||||
|
Scope: opencode-devbox base + variant, pi-devbox. Two independent work items.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layering decision
|
||||||
|
|
||||||
|
| Capability | Lives in | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| **LAN-access (smart-detect host-jump)** | opencode-devbox **base** | Both opencode-devbox and pi-devbox inherit it; not pi-specific. |
|
||||||
|
| **pi-fork + pi-observational-memory** | **pi layer** (variant `with-pi`/`omos-with-pi` + pi-devbox/Dockerfile) | Only meaningful when `pi` is present. Runtime deploy via the shared base `entrypoint-user.sh`, guarded by `command -v pi`. |
|
||||||
|
|
||||||
|
Guiding principle for LAN access: **ship the mechanism, not the policy.**
|
||||||
|
The image provides a generic `host` jump alias + writable SSH config + detection.
|
||||||
|
A user's *specific* targets (e.g. pve/pve-2) come from their bind-mounted
|
||||||
|
`~/.ssh/config` (`ProxyJump host`) or an env list — never hardcoded in the image.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ITEM A — LAN access (opencode-devbox base)
|
||||||
|
|
||||||
|
### Why it can't "just work" unattended
|
||||||
|
- macOS (OrbStack / Docker Desktop): container is in a Linux VM behind the host's
|
||||||
|
stack. Directly-attached LAN peers are not bridged by default; only the host +
|
||||||
|
routed subnets are reachable.
|
||||||
|
- Linux Docker: default bridge already NATs container egress onto the host's LAN,
|
||||||
|
so LAN peers are usually directly reachable. The jump is unnecessary.
|
||||||
|
- The jump path needs the host running sshd + the container's pubkey authorized.
|
||||||
|
The average DockerHub t"kick the tires" user has neither → setup must be
|
||||||
|
**opt-in / non-fatal**, never block startup.
|
||||||
|
|
||||||
|
### New file: `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh`
|
||||||
|
COPY'd automatically (base already does `COPY rootfs/usr/local/lib/opencode-devbox/`).
|
||||||
|
|
||||||
|
Behavior, driven by `DEVBOX_LAN_ACCESS=auto|jump|off` (default `auto`):
|
||||||
|
|
||||||
|
1. `off` → return immediately.
|
||||||
|
2. Detect environment:
|
||||||
|
- VM-backed Docker (OrbStack / Docker Desktop) iff `getent hosts host.docker.internal`
|
||||||
|
resolves (OrbStack also exposes `host.orb.internal`). Native Linux → no resolution
|
||||||
|
(unless the user added `extra_hosts: host.docker.internal:host-gateway`).
|
||||||
|
3. `auto` + native Linux → do nothing (direct LAN works); print one info line.
|
||||||
|
4. `auto` + VM-backed, or `jump` forced →
|
||||||
|
- Create writable `~/.ssh-local/{,cm/}`, `chmod 700`.
|
||||||
|
- Generate `~/.ssh-local/devbox_jump_ed25519` if absent (preserve across restarts).
|
||||||
|
- Render `~/.ssh-local/config`:
|
||||||
|
```
|
||||||
|
Host *
|
||||||
|
UserKnownHostsFile ~/.ssh-local/known_hosts
|
||||||
|
StrictHostKeyChecking accept-new
|
||||||
|
Host host mac # 'mac' kept as friendly alias
|
||||||
|
HostName host.docker.internal
|
||||||
|
User ${HOST_SSH_USER} # REQUIRED for auth; see below
|
||||||
|
IdentityFile ~/.ssh-local/devbox_jump_ed25519
|
||||||
|
IdentitiesOnly yes
|
||||||
|
ControlMaster auto
|
||||||
|
ControlPath ~/.ssh-local/cm/%r@%h:%p
|
||||||
|
ControlPersist 4h
|
||||||
|
# Optional per-target blocks generated from DEVBOX_LAN_HOSTS (see below)
|
||||||
|
Include ~/.ssh/config # user's bind-mounted targets still resolve
|
||||||
|
```
|
||||||
|
- If `HOST_SSH_USER` unset → still render config but print a clear hint block:
|
||||||
|
the generated **public key** + the one-liner to authorize it on the host
|
||||||
|
(`echo '<pubkey>' >> ~/.ssh/authorized_keys`) + "enable Remote Login".
|
||||||
|
- Idempotent: re-render config each start (cheap); never regenerate the key.
|
||||||
|
- DECISION #5: NO `DEVBOX_LAN_HOSTS` env. Keep the image policy-free. Users add
|
||||||
|
`ProxyJump host` to their own target entries in the bind-mounted `~/.ssh/config`
|
||||||
|
(pulled in by the `Include ~/.ssh/config` line).
|
||||||
|
|
||||||
|
### `entrypoint-user.sh`
|
||||||
|
Call `setup-lan-access.sh` right after the existing `/tmp/sshcm` block
|
||||||
|
(non-fatal: `… || true`). It's environment-gated so it self-skips on Linux.
|
||||||
|
|
||||||
|
### `rootfs/home/developer/.bash_aliases` (per your note — alias goes HERE)
|
||||||
|
Append, guarded:
|
||||||
|
```bash
|
||||||
|
# dssh — ssh using the container's writable LAN-access config (host-jump).
|
||||||
|
# Only useful when setup-lan-access.sh generated ~/.ssh-local/config.
|
||||||
|
if [ -r "$HOME/.ssh-local/config" ]; then
|
||||||
|
alias dssh='ssh -F "$HOME/.ssh-local/config"'
|
||||||
|
alias dscp='scp -F "$HOME/.ssh-local/config"'
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
Migration caveat: skel `.bash_aliases` is only copied when absent, so existing
|
||||||
|
volumes/containers won't get `dssh` until they `rm ~/.bash_aliases` and recreate,
|
||||||
|
OR drop the alias into the host-shared `~/.config/devbox-shell/bash_aliases`
|
||||||
|
(already sourced at the top of the skel file).
|
||||||
|
|
||||||
|
### Dockerfile.base
|
||||||
|
No structural change required (script ships via existing rootfs COPY). Optionally
|
||||||
|
document `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_LAN_HOSTS` in `.env.example`
|
||||||
|
and README.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ITEM B — pi-fork + pi-observational-memory (pi layer)
|
||||||
|
|
||||||
|
Sources (pinned this week):
|
||||||
|
- `github.com/elpapi42/pi-fork` (registers `fork`; ~v0.1.0)
|
||||||
|
- `github.com/elpapi42/pi-observational-memory` (registers `recall`; default branch **master**, v3.0.2)
|
||||||
|
|
||||||
|
### B1 RESOLVED (verified live 2026-06-03 in this container)
|
||||||
|
- `pi install <local-path>` is INSTANT (~0.5s): NO copy, NO npm install. pi registers
|
||||||
|
the path and loads the extension IN PLACE from that dir.
|
||||||
|
- settings.json stores a RELATIVE path (e.g. `../../../opt/pi-fork` from ~/.pi/agent).
|
||||||
|
Points into the image-layer `/opt` → stable across volume recreate. Good.
|
||||||
|
- Idempotent: a second `pi install <same path>` does NOT duplicate the entry.
|
||||||
|
- CONSEQUENCE: because pi does NOT npm-install a local path, deps must already exist
|
||||||
|
at `/opt/<pkg>/node_modules`. pi-fork imports `@sinclair/typebox` + `@earendil-works/*`
|
||||||
|
peers; git-install produced a 148 MB node_modules. So we MUST `npm install` inside
|
||||||
|
each `/opt/<pkg>` AT BUILD TIME.
|
||||||
|
- BAKE RECIPE: clone to /opt -> `npm install` there (build) -> `pi install /opt/<pkg>`
|
||||||
|
at runtime (instant, idempotent).
|
||||||
|
- (Optional size win, verify-first: prune to external-only deps if pi provides the
|
||||||
|
`@earendil-works/*` peers from its own runtime resolution. ~148M is mostly those.)
|
||||||
|
|
||||||
|
### DECISION #3: refactor to remove duplication
|
||||||
|
`pi-devbox/Dockerfile` currently duplicates the pi-install + /opt-clone logic from
|
||||||
|
`Dockerfile.variant`. Refactor `pi-devbox/Dockerfile` to `FROM` the `with-pi` variant
|
||||||
|
image so pi-install logic (incl. the new fork/obsmem clones) lives in ONE place.
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
(after refactor, pi-devbox inherits it):
|
||||||
|
```dockerfile
|
||||||
|
ARG PI_FORK_REPO=https://github.com/elpapi42/pi-fork.git
|
||||||
|
ARG PI_FORK_REF=<pin: tag or commit SHA>
|
||||||
|
ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
|
||||||
|
ARG PI_OBSMEM_REF=master # pin to SHA in CI to dodge cache-hit footgun
|
||||||
|
# ... inside the INSTALL_PI / pi-install RUN, after the pi-toolkit/extensions clones:
|
||||||
|
git_clone_retry "$PI_FORK_REPO" "$PI_FORK_REF" /opt/pi-fork && \
|
||||||
|
git_clone_retry "$PI_OBSMEM_REPO" "$PI_OBSMEM_REF" /opt/pi-observational-memory && \
|
||||||
|
(cd /opt/pi-fork && npm install --no-audit --no-fund) && \
|
||||||
|
(cd /opt/pi-observational-memory && npm install --no-audit --no-fund) && \
|
||||||
|
echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \
|
||||||
|
echo "pi-obsmem at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)"
|
||||||
|
```
|
||||||
|
NOTE: `git_clone_retry` uses `--branch "$ref"`, which accepts tags & branches but
|
||||||
|
NOT arbitrary commit SHAs. For SHA pinning use `git clone <url> <dest> && git -C
|
||||||
|
<dest> checkout <sha>` for these two repos.
|
||||||
|
|
||||||
|
### Why not bake the install result
|
||||||
|
`~/.pi` is a named volume mounted at runtime — anything `pi install`'d into
|
||||||
|
`~/.pi/agent/...` at BUILD time is hidden by the volume. Same reason
|
||||||
|
pi-toolkit/extensions deploy at runtime via `entrypoint-user.sh`. So:
|
||||||
|
|
||||||
|
### Runtime deploy — `entrypoint-user.sh` (shared base, in the `command -v pi` block)
|
||||||
|
After the pi-extensions `install.sh` call, add an idempotent install of each /opt pkg:
|
||||||
|
```bash
|
||||||
|
for pkg in /opt/pi-fork /opt/pi-observational-memory; do
|
||||||
|
[ -d "$pkg" ] || continue
|
||||||
|
name=$(basename "$pkg")
|
||||||
|
# skip if already registered in settings.json packages
|
||||||
|
if ! grep -q "$name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
||||||
|
(cd "$HOME" && pi install "$pkg") || echo "WARN: pi install $name failed (continuing)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
`fork` + `recall` tools register on the NEXT pi start after deploy (exts bind at
|
||||||
|
startup). First deploy after a volume recreate pays an `npm install` cost
|
||||||
|
(pi-fork pulls ~133 deps) — acceptable, one-time per volume lifetime.
|
||||||
|
|
||||||
|
OPEN ITEM B1 (verify before finalizing): exact `pi install <local-path>` semantics
|
||||||
|
— does it copy/symlink, and does it npm-install at run each time? If it re-resolves
|
||||||
|
deps every start, pre-populate `/opt/<pkg>/node_modules` at build (`npm install
|
||||||
|
--omit=dev`) and confirm the runtime install reuses it. Quick test in this container:
|
||||||
|
`pi install /opt/pi-fork` twice, observe settings.json + timing + tool registration.
|
||||||
|
|
||||||
|
### CI — `.gitea/workflows/docker-publish-split.yml` (DECISION #2: latest-but-pinned)
|
||||||
|
- USE LATEST CONTENT, BUT RESOLVE TO A SHA IN CI (same pattern as PI_VERSION/OMOS).
|
||||||
|
The existing `resolve-versions` job curls npm `latest` for pi/omos to defeat the
|
||||||
|
build-arg cache-hit footgun. Add an analogous resolve for the two git repos:
|
||||||
|
query the GitHub API for the HEAD commit SHA of the tracked branch (master) and
|
||||||
|
pass it as `PI_FORK_REF` / `PI_OBSMEM_REF` build-args, so the layer hash changes
|
||||||
|
when upstream moves AND we still get newest-at-build-time.
|
||||||
|
- Passing a bare branch name would be byte-identical across builds -> stale cached
|
||||||
|
layer (the documented footgun). SHA resolution fixes both.
|
||||||
|
- Pass the new build-args in the `with-pi` and `omos-with-pi` build steps.
|
||||||
|
- The resolved SHAs print in build logs (and ideally as image labels) so a bad
|
||||||
|
upstream is diagnosable and we can pin back to a known-good SHA.
|
||||||
|
|
||||||
|
### Version coupling risk (carry-over from prior session)
|
||||||
|
pi-fork/obsmem extensions are coupled to the host pi version (AGENTS.md warns).
|
||||||
|
pi-fork had a `fix/effort-string-enum-schema` branch from recent API churn. So:
|
||||||
|
- Pin against the SAME `PI_VERSION` the image ships.
|
||||||
|
- smoke-test must assert the tools actually register (below), not just that files exist.
|
||||||
|
|
||||||
|
### Smoke test — `scripts/smoke-test.sh`
|
||||||
|
Add (for `with-pi`/`omos-with-pi`/pi-devbox):
|
||||||
|
1. `/opt/pi-fork/package.json` and `/opt/pi-observational-memory/package.json` exist.
|
||||||
|
2. Run a container, then assert `~/.pi/agent/settings.json` "packages" includes both.
|
||||||
|
3. Best-effort: headless `pi` tool-list contains `fork` and `recall` (if pi exposes a
|
||||||
|
non-interactive list; otherwise step 2 is the gate).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions — RESOLVED 2026-06-03
|
||||||
|
1. **B1**: VERIFIED. Local-path install is instant/in-place; bake `npm install` into
|
||||||
|
`/opt/<pkg>` at build; runtime `pi install /opt/<pkg>` is instant + idempotent. ✓
|
||||||
|
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. ✓
|
||||||
|
5. **No `DEVBOX_LAN_HOSTS`**: rely on user's bind-mounted `~/.ssh/config` (`ProxyJump host`). ✓
|
||||||
|
|
||||||
|
## Remaining verify-before-merge items
|
||||||
|
- Confirm the fork/recall extensions LOAD at runtime from `/opt/<pkg>` WITH the baked
|
||||||
|
node_modules (smoke test asserts tool registration, not just files).
|
||||||
|
- Optional: confirm whether pi supplies `@earendil-works/*` peers at runtime so /opt
|
||||||
|
node_modules can be pruned to external-only deps (size optimization, ~148M -> small).
|
||||||
|
|
||||||
|
## Rollout order
|
||||||
|
1. Verify B1 in this live container (cheap, no build).
|
||||||
|
2. Land ITEM A in base (rootfs script + entrypoint call + alias) → rebuild base → smoke.
|
||||||
|
3. Land ITEM B in variant + pi-devbox + CI resolve + smoke assertions.
|
||||||
|
4. CHANGELOG + tag both repos; CI rebuild; verify fork+recall+dssh survive a volume recreate.
|
||||||
@@ -12,6 +12,16 @@ set -euo pipefail
|
|||||||
mkdir -p /tmp/sshcm
|
mkdir -p /tmp/sshcm
|
||||||
chmod 700 /tmp/sshcm
|
chmod 700 /tmp/sshcm
|
||||||
|
|
||||||
|
# ── LAN access: generic host-OS-agnostic reachability helper ────────
|
||||||
|
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
|
||||||
|
# reach the host's directly-attached LAN peers by default; this generates a
|
||||||
|
# writable ~/.ssh-local/config that uses the host as an SSH jump. On native
|
||||||
|
# Linux (LAN reachable directly) it is a no-op. Controlled by DEVBOX_LAN_ACCESS
|
||||||
|
# (auto|jump|off) + HOST_SSH_USER. Always non-fatal. See the script header.
|
||||||
|
if [ -r /usr/local/lib/opencode-devbox/setup-lan-access.sh ]; then
|
||||||
|
bash /usr/local/lib/opencode-devbox/setup-lan-access.sh || true
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Shell defaults: copy baked files from /etc/skel-devbox/ if absent
|
# ── Shell defaults: copy baked files from /etc/skel-devbox/ if absent
|
||||||
# Respects host bind-mounts and user customizations — existing files
|
# Respects host bind-mounts and user customizations — existing files
|
||||||
# are never overwritten. To restore defaults: rm ~/.bash_aliases (or
|
# are never overwritten. To restore defaults: rm ~/.bash_aliases (or
|
||||||
@@ -96,6 +106,24 @@ if command -v pi &>/dev/null; then
|
|||||||
ln -sf /opt/mempalace-toolkit/extensions/pi/mempalace.ts \
|
ln -sf /opt/mempalace-toolkit/extensions/pi/mempalace.ts \
|
||||||
"$HOME/.pi/agent/extensions/mempalace.ts"
|
"$HOME/.pi/agent/extensions/mempalace.ts"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# pi-fork (fork tool) + pi-observational-memory (recall tool).
|
||||||
|
# These are pi packages (not symlink-style extensions): they're cloned to
|
||||||
|
# /opt with node_modules baked at BUILD time, then registered here via
|
||||||
|
# `pi install <local-path>`. Verified 2026-06-03: a local-path install is
|
||||||
|
# instant + in-place (pi loads the extension directly from /opt) + idempotent
|
||||||
|
# (no duplicate package entry on re-run), and stores a relative path that
|
||||||
|
# resolves into the image-layer /opt so it survives volume recreate. The
|
||||||
|
# fork/recall tools register on the NEXT pi start (extensions bind at
|
||||||
|
# startup). Guard on settings.json so we only install once per volume.
|
||||||
|
for _pkg in /opt/pi-fork /opt/pi-observational-memory; do
|
||||||
|
[ -d "$_pkg" ] || continue
|
||||||
|
_name=$(basename "$_pkg")
|
||||||
|
if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
||||||
|
pi install "$_pkg" >/dev/null 2>&1 || \
|
||||||
|
echo "WARN: pi install $_name failed (continuing)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Skillset: deploy skills/instructions from mounted skillset repo ──
|
# ── Skillset: deploy skills/instructions from mounted skillset repo ──
|
||||||
|
|||||||
@@ -54,6 +54,17 @@ alias gs='git status'
|
|||||||
alias gd='git diff'
|
alias gd='git diff'
|
||||||
alias gl='git log --oneline --graph --decorate -20'
|
alias gl='git log --oneline --graph --decorate -20'
|
||||||
|
|
||||||
|
# ── LAN access via the host (dssh) ───────────────────────────────────
|
||||||
|
# When running on a VM-backed host (macOS OrbStack / Docker Desktop), the
|
||||||
|
# entrypoint's setup-lan-access.sh generates ~/.ssh-local/config so the host
|
||||||
|
# can be used as an SSH jump to reach LAN peers. These aliases wrap `ssh -F`
|
||||||
|
# / `scp -F` against that config. Guarded so they only appear when the config
|
||||||
|
# was actually generated (no-op / absent on native Linux hosts).
|
||||||
|
if [ -r "$HOME/.ssh-local/config" ]; then
|
||||||
|
alias dssh='ssh -F "$HOME/.ssh-local/config"'
|
||||||
|
alias dscp='scp -F "$HOME/.ssh-local/config"'
|
||||||
|
fi
|
||||||
|
|
||||||
# Safety: confirm before destructive ops
|
# Safety: confirm before destructive ops
|
||||||
alias rm='rm -i'
|
alias rm='rm -i'
|
||||||
alias mv='mv -i'
|
alias mv='mv -i'
|
||||||
|
|||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# setup-lan-access.sh — generic, host-OS-agnostic LAN reachability helper.
|
||||||
|
#
|
||||||
|
# THE PROBLEM
|
||||||
|
# On macOS (OrbStack / Docker Desktop) and Docker Desktop on Windows, the
|
||||||
|
# container runs inside a Linux VM behind the host's network stack. The
|
||||||
|
# host's *directly-attached* LAN peers (e.g. other boxes on 192.168.1.0/24)
|
||||||
|
# are NOT bridged into the container by default — only the host itself and
|
||||||
|
# *routed* subnets are reachable. On native Linux Docker the default bridge
|
||||||
|
# already NATs container egress onto the host's LAN, so LAN peers are usually
|
||||||
|
# reachable directly and no workaround is needed.
|
||||||
|
#
|
||||||
|
# THE APPROACH ("detect, and on a VM-backed host use the host as a jump")
|
||||||
|
# The one thing reachable from a container on every OS is the host itself
|
||||||
|
# (host.docker.internal). So on VM-backed hosts we generate a writable SSH
|
||||||
|
# config that reaches the host and lets the user ProxyJump onward to LAN
|
||||||
|
# peers the host can reach. On native Linux we do nothing.
|
||||||
|
#
|
||||||
|
# We ship the MECHANISM (a generic `host` jump alias + writable config),
|
||||||
|
# never the POLICY: the user's specific target hosts live in their own
|
||||||
|
# bind-mounted ~/.ssh/config (add `ProxyJump host` to those entries) — which
|
||||||
|
# is pulled in via the `Include ~/.ssh/config` line below.
|
||||||
|
#
|
||||||
|
# WHY A WRITABLE SIDECAR (~/.ssh-local)
|
||||||
|
# The devbox typically bind-mounts the host's ~/.ssh READ-ONLY (so agents
|
||||||
|
# can read keys for git but can't tamper with config/known_hosts/authorized_
|
||||||
|
# keys). That means we cannot edit ~/.ssh/config or write ~/.ssh/known_hosts.
|
||||||
|
# So everything generated here lives under the writable ~/.ssh-local, used
|
||||||
|
# via `ssh -F ~/.ssh-local/config` (the `dssh`/`dscp` aliases wrap that).
|
||||||
|
#
|
||||||
|
# CONTROLS (env)
|
||||||
|
# DEVBOX_LAN_ACCESS = auto (default) | jump | off
|
||||||
|
# auto → set up the jump config only on VM-backed hosts; no-op on Linux.
|
||||||
|
# jump → always set up (e.g. native Linux with extra_hosts host-gateway).
|
||||||
|
# off → do nothing.
|
||||||
|
# HOST_SSH_USER — the username to SSH into the host as. REQUIRED for the
|
||||||
|
# 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).
|
||||||
|
#
|
||||||
|
# Idempotent: re-renders the config every run (cheap); never regenerates the
|
||||||
|
# key. Always non-fatal — never blocks container startup.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
MODE="${DEVBOX_LAN_ACCESS:-auto}"
|
||||||
|
[ "$MODE" = "off" ] && exit 0
|
||||||
|
|
||||||
|
HOST_ALIAS_HOSTNAME="${DEVBOX_HOST_ALIAS:-host.docker.internal}"
|
||||||
|
SSH_LOCAL="${HOME}/.ssh-local"
|
||||||
|
CONFIG="${SSH_LOCAL}/config"
|
||||||
|
KEY="${SSH_LOCAL}/devbox_jump_ed25519"
|
||||||
|
|
||||||
|
# ── Detection: is this a VM-backed host (macOS / Docker Desktop)? ──────
|
||||||
|
# host.docker.internal resolves on OrbStack and Docker Desktop (mac/win) but
|
||||||
|
# NOT on native Linux Docker (unless the user added extra_hosts: host-gateway,
|
||||||
|
# in which case the jump is still harmless / usable, and they can force it
|
||||||
|
# with DEVBOX_LAN_ACCESS=jump).
|
||||||
|
is_vm_backed() {
|
||||||
|
getent hosts "$HOST_ALIAS_HOSTNAME" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$MODE" = "auto" ] && ! is_vm_backed; then
|
||||||
|
# Native Linux host: LAN peers are reachable directly. Nothing to do.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# From here: MODE=jump, or MODE=auto on a VM-backed host.
|
||||||
|
|
||||||
|
command -v ssh-keygen >/dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
|
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) ──────────────
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Render the writable config ────────────────────────────────────────
|
||||||
|
USER_LINE=""
|
||||||
|
if [ -n "${HOST_SSH_USER:-}" ]; then
|
||||||
|
USER_LINE=" User ${HOST_SSH_USER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
INCLUDE_LINE=""
|
||||||
|
if [ -r "${HOME}/.ssh/config" ]; then
|
||||||
|
INCLUDE_LINE="Include ~/.ssh/config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$CONFIG" <<EOF
|
||||||
|
# AUTO-GENERATED by setup-lan-access.sh on every container start. Do not edit
|
||||||
|
# by hand — edits are overwritten. Used via: ssh -F ~/.ssh-local/config <host>
|
||||||
|
# (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.
|
||||||
|
Host *
|
||||||
|
UserKnownHostsFile ~/.ssh-local/known_hosts
|
||||||
|
StrictHostKeyChecking accept-new
|
||||||
|
|
||||||
|
# The container host (OrbStack / Docker Desktop). 'host' and 'mac' are aliases.
|
||||||
|
Host host mac
|
||||||
|
HostName ${HOST_ALIAS_HOSTNAME}
|
||||||
|
${USER_LINE}
|
||||||
|
IdentityFile ~/.ssh-local/devbox_jump_ed25519
|
||||||
|
IdentitiesOnly yes
|
||||||
|
ControlMaster auto
|
||||||
|
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}
|
||||||
|
EOF
|
||||||
|
chmod 600 "$CONFIG" 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── One-time hint when we can't authenticate yet ──────────────────────
|
||||||
|
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)
|
||||||
|
3. Ensure the host's SSH server (Remote Login) is enabled.
|
||||||
|
Then: dssh host (or add 'ProxyJump host' to targets in ~/.ssh/config)
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
+27
-2
@@ -171,6 +171,13 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
|
|||||||
fi
|
fi
|
||||||
run "pi-toolkit clone" "test -d /opt/pi-toolkit && git -C /opt/pi-toolkit rev-parse --short HEAD"
|
run "pi-toolkit clone" "test -d /opt/pi-toolkit && git -C /opt/pi-toolkit rev-parse --short HEAD"
|
||||||
run "pi-extensions clone" "test -d /opt/pi-extensions && git -C /opt/pi-extensions rev-parse --short HEAD"
|
run "pi-extensions clone" "test -d /opt/pi-extensions && git -C /opt/pi-extensions rev-parse --short HEAD"
|
||||||
|
# pi-fork (fork tool) + pi-observational-memory (recall tool): cloned to
|
||||||
|
# /opt with node_modules baked at build time (a local-path `pi install` does
|
||||||
|
# NOT npm-install, so deps MUST already be present for the extension to load).
|
||||||
|
run "pi-fork clone + node_modules" \
|
||||||
|
"test -f /opt/pi-fork/package.json && test -d /opt/pi-fork/node_modules && echo ok"
|
||||||
|
run "pi-observational-memory clone + node_modules" \
|
||||||
|
"test -f /opt/pi-observational-memory/package.json && test -d /opt/pi-observational-memory/node_modules && echo ok"
|
||||||
|
|
||||||
# Run the full entrypoint as developer to verify install.sh deployment.
|
# Run the full entrypoint as developer to verify install.sh deployment.
|
||||||
# Spin up a long-running container so we can `docker exec` into it from
|
# Spin up a long-running container so we can `docker exec` into it from
|
||||||
@@ -208,6 +215,21 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
|
|||||||
exec_test "~/.pi/agent/settings.json (template bootstrap)" \
|
exec_test "~/.pi/agent/settings.json (template bootstrap)" \
|
||||||
'test -f $HOME/.pi/agent/settings.json && echo ok'
|
'test -f $HOME/.pi/agent/settings.json && echo ok'
|
||||||
|
|
||||||
|
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
||||||
|
# `pi install /opt/<pkg>` (records a relative path into settings.json
|
||||||
|
# packages). That runs slightly after the keybindings marker, so wait for it.
|
||||||
|
for _ in $(seq 1 15); do
|
||||||
|
if docker exec "$CID" grep -q pi-observational-memory \
|
||||||
|
/home/developer/.pi/agent/settings.json 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
exec_test "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)" \
|
||||||
|
'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
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
else
|
else
|
||||||
@@ -336,12 +358,15 @@ echo " Uncompressed size: ${SIZE_MB} MB"
|
|||||||
# omos-with-pi bumped 3500→3700 on v1.15.4b — omos+pi compounded as both
|
# omos-with-pi bumped 3500→3700 on v1.15.4b — omos+pi compounded as both
|
||||||
# upstream packages grew (opencode 1.15.0→1.15.4, pi 0.74.0→0.75.3) and
|
# upstream packages grew (opencode 1.15.0→1.15.4, pi 0.74.0→0.75.3) and
|
||||||
# the variant landed just over 3500 in v1.15.4's smoke.
|
# the variant landed just over 3500 in v1.15.4's smoke.
|
||||||
|
# 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.
|
||||||
# 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
|
THRESHOLD=2500
|
||||||
[ "$VARIANT" = "omos" ] && THRESHOLD=3300
|
[ "$VARIANT" = "omos" ] && THRESHOLD=3300
|
||||||
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2700
|
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2900
|
||||||
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3700
|
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3900
|
||||||
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
||||||
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user