Compare commits

...

6 Commits

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

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

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

setup-lan-access.sh now prints a copy-paste
'echo <pubkey> >> ~/.ssh/authorized_keys' line whenever it generates a
new key (not only when HOST_SSH_USER is unset), and stays silent once
the key is persisted. README + CHANGELOG updated.
2026-06-04 14:33:58 +02:00
pi 440218fc4c compose: document optional ~/.config/devbox-shell mount (LAN ssh-lan.conf + bash_aliases bridge)
Validate / base-change-warning (push) Successful in 6s
Validate / docs-check (push) Successful in 14s
Validate / validate-base (push) Successful in 3m32s
Validate / validate-with-pi (push) Successful in 4m32s
Validate / validate-omos (push) Successful in 6m58s
Validate / validate-pi-only (push) Successful in 3m37s
Validate / validate-omos-with-pi (push) Successful in 17m51s
2026-06-04 13:34:10 +02:00
9 changed files with 176 additions and 9 deletions
@@ -386,6 +386,7 @@ jobs:
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
- env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
STRICT_REGISTRATION: "1"
run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi
smoke-omos-with-pi:
@@ -435,6 +436,7 @@ jobs:
- env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
STRICT_REGISTRATION: "1"
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
smoke-pi-only:
@@ -482,6 +484,7 @@ jobs:
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
- env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
STRICT_REGISTRATION: "1"
run: bash scripts/smoke-test.sh opencode-devbox:smoke-pi-only --variant pi-only
# ── Phase 4: multi-arch publish per variant ────────────────────────
+8
View File
@@ -20,6 +20,14 @@ name: Validate
# release tags are the gate that fully validates base-image changes.
# The base-change-warning job below surfaces a runtime warning when this
# blind-spot applies.
#
# Because of this, the fork/recall *registration* smoke checks (which depend on
# the base entrypoint running `pi install /opt/<pkg>`) are warn-only here:
# smoke-test.sh leaves STRICT_REGISTRATION unset on this path, so a base-latest
# that lags the entrypoint in the current commit can't red the run with a false
# negative. The release smoke jobs build the base fresh and set
# STRICT_REGISTRATION=1 to enforce those checks. The build-time /opt +
# node_modules checks stay hard in both paths.
on:
push:
+1
View File
@@ -104,6 +104,7 @@ cd /tmp && npm pack @earendil-works/pi-coding-agent@0.75.5 && tar -xzf earendil-
- **`actions/upload-artifact` and `actions/download-artifact` must stay at @v3 on Gitea.** v4+ uses a GitHub-Enterprise-specific Artifact API; runs fail with `GHESNotSupportedError`. If you need artifacts for a new reason (build logs, SBOMs, etc.), pin @v3 explicitly.
- **Step scripts run under `/bin/sh` (dash), not bash.** Avoid bash-isms like `${VAR//a/b}` parameter-pattern substitution; use POSIX alternatives (`tr`, `sed`) or declare `shell: bash` on the step.
- **`BUILDKIT_PROGRESS=plain`** is set at workflow level on `docker-publish-split.yml` so arm64-under-QEMU builds log each layer line-by-line. The default collapsed progress UI hides which step is stalled, which made diagnosing earlier hangs expensive.
- **`STRICT_REGISTRATION` gates the fork/recall *registration* smoke assertions.** `smoke-test.sh`'s two pi-extension registration checks (that `pi-fork`/`pi-observational-memory` registered in `~/.pi/agent/settings.json`) depend on the *base* entrypoint running `pi install /opt/<pkg>`. `validate.yml` builds variants from the **published** `base-latest`, which lags the in-repo entrypoint until a release rebuilds the base — so those checks would false-negative there. They are therefore warn-only unless `STRICT_REGISTRATION=1`: `validate.yml` leaves it unset (warn), and `docker-publish-split.yml` (which builds the base fresh in the same run) sets `STRICT_REGISTRATION: "1"` on the three pi-bearing smoke jobs to enforce them. Build-time `/opt` + `node_modules` checks stay hard in both paths. If you touch the registration checks or the base-freshness model, keep this flag wiring in lockstep across both workflows.
## Testing changes
+73 -1
View File
@@ -8,7 +8,79 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
## Unreleased
_(no changes since v1.15.13d)_
_(no changes since v1.15.13e)_
## v1.15.13e — 2026-06-04
Letter-suffix rebuild on opencode `1.15.13` (version unchanged). Picks up
**pi `0.78.1`** (resolved fresh by CI's `resolve-versions` job) plus the LAN-jump
key-persistence work, an entrypoint ownership fix for the new `devbox-ssh-local`
volume, a CI smoke false-negative fix, and documentation. Touches `entrypoint.sh`
and `setup-lan-access.sh` (both in the base hash), so `base-latest` /
`base-pi-only` advance and the fixes propagate to `pi-devbox`.
### Docs: per-host `ControlPath` overrides break `pi --ssh` (read-only `~/.ssh`)
Documented a gotcha in the README "Reaching your LAN" section: the bind-mounted
`~/.ssh/config` is read before the baked `Host *` default, and SSH uses the
first `ControlPath` it sees. A per-host block that sets `ControlPath` under
`~/.ssh/` (a common CGNAT-multiplexing pattern, e.g. `~/.ssh/cm/%r@%h:%p`) wins
but then fails inside the container because `~/.ssh` is mounted read-only — the
master socket can't bind. This silently breaks `pi --ssh <host>`: the SSH layer
fails and pi falls back to running its tools locally in the container. Fix is
host-side — drop the per-host `ControlPath` or repoint it at the writable
`/tmp/sshcm/%r@%h:%p` (works on both host and container, preserves multiplexing).
No image change; documentation only.
### Fixed: validate.yml false-negative on fork/recall registration checks
The push-to-main `validate.yml` builds variants FROM the published `base-latest`
image, which lags the entrypoint in the current commit until a release tag
rebuilds the base. The fork/recall *registration* smoke checks depend on the
base entrypoint running `pi install /opt/<pkg>`, so a stale `base-latest` reded
those runs with a false negative even when the variant layer was correct.
`smoke-test.sh` now gates the two registration assertions behind
`STRICT_REGISTRATION` (warn-only when unset). `validate.yml` leaves it unset;
the release pipeline (`docker-publish-split.yml`), which builds the base fresh
in the same run, sets `STRICT_REGISTRATION=1` on the pi-bearing smoke jobs to
enforce them. The build-time `/opt` + `node_modules` checks stay hard in both
paths.
### Added: persist the LAN-jump key + one-line authorize hint (authorize once per machine)
The jump keypair (`~/.ssh-local/devbox_jump_ed25519`) was stored on the
container's ephemeral overlay, so `docker compose up --force-recreate` (every
image update) regenerated it — forcing you to re-authorize the new key on the
host each time. The compose files now persist `~/.ssh-local` via a named volume
(`devbox-ssh-local`), matching the pattern already used for `.pi`, shell
history, etc. The key is generated **once** and reused across updates, so you
authorize it on the host **once per machine**.
`setup-lan-access.sh` now also prints a ready-to-paste authorize line whenever
it generates a **new** key (not just when `HOST_SSH_USER` is unset), e.g.
`echo 'ssh-ed25519 …' >> ~/.ssh/authorized_keys` — no helper file to locate, no
workspace path to guess. It stays silent once the key is persisted.
### Fixed: chown the `devbox-ssh-local` volume so the jump key can be generated
The previous change persisted `~/.ssh-local` via a named volume, but the
entrypoint's volume-ownership loop was never updated to include it. Docker
creates named volumes as `root:root`, so on a fresh volume `~/.ssh-local`
stayed root-owned while `setup-lan-access.sh` runs as `developer` — both its
`mkdir cm` and `ssh-keygen` failed silently (`|| true` / `|| exit 0`), leaving
**no jump key and no config**, breaking LAN access on the first recreate after
the persistence change. `entrypoint.sh` now chowns `~/.ssh-local` to the
developer user alongside the other named-volume mount points.
### Docs: document the optional `~/.config/devbox-shell` mount in the compose template
`docker-compose.yml` now carries a commented-out `~/.config/devbox-shell` bind
mount with an explanatory note. It's the recommended home for host-owned shell
config: the image's `~/.bash_aliases` sources `~/.config/devbox-shell/bash_aliases`
if present, and `setup-lan-access.sh` reads `~/.config/devbox-shell/ssh-lan.conf`
for named-peer `ProxyJump host` overrides. A directory mount is preferred over
the single-file `~/.bash_aliases` mount because it survives editors' atomic-save.
Template comment only; no behavior change.
## v1.15.13d — 2026-06-04
+24 -3
View File
@@ -155,15 +155,20 @@ The devbox works the same way whether the host is **native Linux Docker** or a *
- **Native Linux Docker:** the host NATs container egress onto its LAN, so other devices on your LAN are reachable directly. Nothing to configure.
- **VM-backed (macOS / Docker Desktop):** the container runs in a Linux VM behind the host's network stack. The host's *directly-attached* LAN peers are **not** bridged into the container by default — only the host itself and *routed* subnets are reachable.
On every start the entrypoint detects which case applies. On VM-backed hosts it generates a writable `~/.ssh-local/config` that uses the **host as an SSH jump** to reach LAN peers; on native Linux it does nothing.
On every start the entrypoint detects which case applies. On VM-backed hosts it generates a writable `~/.ssh-local/config` that uses the **host as an SSH jump** to reach LAN peers; on native Linux it does nothing. The jump keypair lives in `~/.ssh-local`, which is persisted by the `devbox-ssh-local` named volume — so it's generated **once** and reused across container updates.
**To enable it on a VM-backed host:**
**To enable it on a VM-backed host (one-time setup per machine):**
1. Set `HOST_SSH_USER=<your host username>` in `.env`.
2. Start the container once. The entrypoint prints a public key — append it to your host's `~/.ssh/authorized_keys`.
2. Start the container once. When it generates the jump key it prints a ready-to-paste line — run it **on the host** to authorize the key:
```bash
echo 'ssh-ed25519 AAAA…devbox-jump@…' >> ~/.ssh/authorized_keys
```
3. Ensure the host's SSH server is on (on macOS: System Settings → General → Sharing → Remote Login).
4. Reach the host itself with `dssh host`. (`dssh`/`dscp` wrap `ssh -F ~/.ssh-local/config`.)
Because the key is persisted, you do this **once per machine** — not after every `docker compose up --force-recreate`. You'll only see the authorize line again if you reset the `devbox-ssh-local` volume.
That alone gets you `container → host`. To reach **named LAN peers** by name, give them a `ProxyJump host` override. Don't add it to the shared `~/.ssh/config` entries — the host itself reaches those peers *directly*, and a jump-through-`host` would break the host's own access (and that file is mounted read-only anyway). Instead, drop the overrides in a **host-owned** file that the container Includes ahead of your `~/.ssh/config`:
```sshconfig
@@ -182,6 +187,22 @@ Now `dssh my-nas` routes container → host → LAN peer, pulling HostName/User/
> This ships the **mechanism** only — your specific target hosts are facts about *your* network (and a laptop roams between several), so they live in your own host-side config, never baked into the image. Set `DEVBOX_LAN_ACCESS=off` to disable, or `=jump` to force it (e.g. native Linux with `extra_hosts: ["host.docker.internal:host-gateway"]`).
#### Gotcha: per-host `ControlPath` and `pi --ssh`
The base image bakes a `Host *` default (`/etc/ssh/ssh_config.d/00-devbox-controlmaster.conf`) that points `ControlPath` at the writable, per-container `/tmp/sshcm/` (created mode-700 on every start by `entrypoint-user.sh`). Multiplexing therefore works out of the box. **But your bind-mounted `~/.ssh/config` is read first, and SSH uses the first value it sees** — so any per-host block that sets its own `ControlPath` under `~/.ssh/` (a common CGNAT-multiplexing pattern, e.g. `ControlPath ~/.ssh/cm/%r@%h:%p`) **wins, and then fails inside the container** because `~/.ssh` is mounted **read-only** — the master socket can't bind (`cannot bind … Read-only file system`).
This bites `pi --ssh <host>` especially: the SSH layer fails to establish the master and pi silently falls back to running its `read`/`write`/`edit`/`bash` tools **locally in the container** instead of on the remote (watch for the missing `SSH ⚡` in the status bar — and `hostname` returning the container ID).
**Fix (host-side, one line):** in your host's `~/.ssh/config`, either drop the per-host `ControlPath` (to inherit the writable baked default) or point it at a path that's writable inside the container too:
```sshconfig
Host my-remote
# was: ControlPath ~/.ssh/cm/%r@%h:%p ← read-only in the container
ControlPath /tmp/sshcm/%r@%h:%p # writable on both host and container
```
`/tmp/sshcm/` is also writable on the host (macOS/Linux), so native (non-container) `ssh`/`pi --ssh` from the host keeps working and CGNAT multiplexing is preserved (`ControlMaster`/`ControlPersist` unchanged — only the socket *directory* moves). Note SSH does not create the `ControlPath` parent dir; the container makes `/tmp/sshcm` every start, but on the host run `mkdir -p /tmp/sshcm` once if it doesn't already exist.
### Custom opencode config
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.
+14
View File
@@ -59,6 +59,11 @@ services:
# allowing both native and containerized opencode on the same machine.
- devbox-opencode-config:/home/developer/.config/opencode
- devbox-pi-config:/home/developer/.pi
# Persist the generated LAN-jump keypair (~/.ssh-local) across recreates.
# setup-lan-access.sh generates this key once and reuses it; persisting
# it means you authorize it on the host ONCE rather than re-authorizing
# after every `docker compose up --force-recreate`.
- devbox-ssh-local:/home/developer/.ssh-local
# NOTE: Do NOT bind-mount ~/.agents/skills/ from the host. The
# container manages its own skills directory independently — the
@@ -95,6 +100,14 @@ services:
# - ~/.bash_aliases:/home/developer/.bash_aliases:ro
# - ~/.inputrc:/home/developer/.inputrc:ro
# Optional: host-owned shell config + LAN jump overrides (recommended
# over the single-file ~/.bash_aliases mount above — it's a directory,
# so it survives editors' atomic-save). The image's ~/.bash_aliases
# sources ~/.config/devbox-shell/bash_aliases if present, and
# setup-lan-access.sh reads ~/.config/devbox-shell/ssh-lan.conf for
# named-peer `ProxyJump host` overrides (see ssh-lan.conf.example).
# - ~/.config/devbox-shell:/home/developer/.config/devbox-shell:ro
# Optional: persist uv data (Python installs, tool installs)
# Without this, 'uv python install' must be re-run after container removal.
- devbox-uv:/home/developer/.local/share/uv
@@ -126,6 +139,7 @@ services:
volumes:
devbox-opencode-config:
devbox-pi-config:
devbox-ssh-local:
devbox-data:
devbox-state:
devbox-shell-history:
+1
View File
@@ -88,6 +88,7 @@ for dir in \
/home/"$USER_NAME"/.config/opencode \
/home/"$USER_NAME"/.config/nvim \
/home/"$USER_NAME"/.pi \
/home/"$USER_NAME"/.ssh-local \
/home/"$USER_NAME"/.agents/skills; do
[ -d "$dir" ] || continue
@@ -97,9 +97,15 @@ mkdir -p "${SSH_LOCAL}/cm" 2>/dev/null || true
chmod 700 "${SSH_LOCAL}" "${SSH_LOCAL}/cm" 2>/dev/null || true
# ── Jump key (generated once; preserved across restarts) ──────────────
# Persisted via a named volume on ~/.ssh-local (see compose), so a fresh key
# is generated only on the very first start (or if the volume is wiped). When
# we DO generate one it must be (re-)authorized on the host, so we flag it and
# print a copy-paste authorize line below.
KEY_JUST_GENERATED=0
if [ ! -f "$KEY" ]; then
ssh-keygen -t ed25519 -N '' -C "devbox-jump@${HOSTNAME:-container}" -f "$KEY" >/dev/null 2>&1 || exit 0
chmod 600 "$KEY" 2>/dev/null || true
KEY_JUST_GENERATED=1
fi
# ── Render the writable config ────────────────────────────────────────
@@ -187,18 +193,33 @@ ${INCLUDE_BLOCK}
EOF
chmod 600 "$CONFIG" 2>/dev/null || true
# ── One-time hint when we can't authenticate yet ──────────────────────
# ── Authorize hints ───────────────────────────────────────────────────
# Print the copy-paste authorize line whenever we either (a) can't yet
# authenticate (HOST_SSH_USER unset) or (b) just generated a NEW key that the
# host won't recognize. With ~/.ssh-local persisted via a named volume, case
# (b) fires only on first-ever start (or after the volume is reset) — so this
# is normally a one-time, one-line step per machine, with no file to locate.
PUBKEY_TEXT="$(cat "${KEY}.pub" 2>/dev/null)"
if [ -z "${HOST_SSH_USER:-}" ]; then
cat <<EOF
[devbox] LAN-access jump config generated at ~/.ssh-local/config, but
HOST_SSH_USER is unset so it can't authenticate to the host yet.
To enable container -> host -> LAN-peer access:
1. Set HOST_SSH_USER=<your host username> in the container env.
2. Authorize this key on the host (append to ~/.ssh/authorized_keys):
$(cat "${KEY}.pub" 2>/dev/null)
2. Authorize this key on the host (run ON THE HOST, once):
echo '${PUBKEY_TEXT}' >> ~/.ssh/authorized_keys
3. Ensure the host's SSH server (Remote Login) is enabled.
Then: dssh host (or add 'ProxyJump host' to targets in ~/.ssh/config)
EOF
elif [ "$KEY_JUST_GENERATED" = "1" ]; then
cat <<EOF
[devbox] Generated a NEW LAN-jump key. Authorize it on the host (${HOST_SSH_USER}@host),
then 'dssh host' and your LAN peers will work. Run this ONCE, ON THE HOST:
echo '${PUBKEY_TEXT}' >> ~/.ssh/authorized_keys
(Ensure the host's SSH server / Remote Login is enabled.)
This key is persisted in the ~/.ssh-local volume, so you won't need to
repeat this on container updates — only if that volume is reset.
EOF
fi
exit 0
+28 -2
View File
@@ -30,6 +30,19 @@ fi
FAILED=0
pass() { echo "$1"; }
fail() { echo "$1" >&2; FAILED=$((FAILED + 1)); }
warn() { echo "$1" >&2; }
# Registration assertions (fork/recall installed by the BASE image's
# entrypoint-user.sh via `pi install /opt/<pkg>`) depend on the base, not the
# variant layer built here. validate.yml builds variants FROM the published
# base-latest, which can lag the entrypoint in the current commit (the base
# only rebuilds on a release tag), so a stale base-latest would red the
# push-to-main run with a false negative. These checks are therefore warn-only
# by default; the release pipeline (docker-publish-split.yml) builds the base
# fresh in the same run and sets STRICT_REGISTRATION=1 to enforce them hard.
# The build-time /opt + node_modules checks below stay hard in every path —
# those are produced by the variant layer and must always be correct.
STRICT_REGISTRATION="${STRICT_REGISTRATION:-0}"
run() {
# Run a command inside the image and capture its output.
@@ -206,6 +219,19 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
fi
}
# Like exec_test but warn-only unless STRICT_REGISTRATION=1 (see note at top).
exec_test_reg() {
local label="$1"; shift
local out
if out=$(docker exec -u developer "$CID" sh -c "$*" 2>&1); then
pass "$label ($(echo "$out" | head -1))"
elif [ "$STRICT_REGISTRATION" = "1" ]; then
fail "$label: $out"
else
warn "$label (warn-only — stale base-latest? set STRICT_REGISTRATION=1 to enforce): $out"
fi
}
exec_test "~/.pi/agent/keybindings.json (pi-toolkit)" \
'test -L $HOME/.pi/agent/keybindings.json && echo ok'
exec_test "~/.pi/agent/extensions/*.ts ≥ 4 (pi-extensions)" \
@@ -225,9 +251,9 @@ if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&
fi
sleep 1
done
exec_test "pi-fork registered in settings.json (fork tool)" \
exec_test_reg "pi-fork registered in settings.json (fork tool)" \
'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok'
exec_test "pi-observational-memory registered in settings.json (recall tool)" \
exec_test_reg "pi-observational-memory registered in settings.json (recall tool)" \
'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok'
docker rm -f "$CID" >/dev/null 2>&1 || true