fix(entrypoint,tests): register pi-fork — guard matched its own config block
The `pi install /opt/<pkg>` loop in entrypoint-user.sh guarded on a whole-file substring grep of ~/.pi/agent/settings.json. settings.example.json ships a top-level "pi-fork" CONFIG block (fork effort profiles, pi-toolkit adb6907, 2026-06-17), so `grep -q pi-fork settings.json` matched the config key itself and `pi install /opt/pi-fork` never ran — on fresh or preserved volumes. The `fork` tool has therefore been absent since v1.0.0. The non-destructive template merge runs earlier in the same startup than the install loop, so the mechanism that delivers new template keys to an old volume is what plants the string that defeats the guard. pi-observational- memory and pi-studio escaped only by luck: the template key is "observational-memory" (no pi- prefix) and there is no studio block. Guard now inspects the `packages` array via jq, with a grep fallback on the stored `.../opt/<name>"` path form, which a config key can never produce. Existing volumes self-heal on the next container start. Both test suites asserted the bug as green — smoke-test.sh:244 and recreate-sanity-check.sh:204 used the same whole-file grep, so "pi-fork registered (fork tool)" passed on every build and recreate while the tool was missing. Both now assert against packages[] with the entrypoint's predicate, labels say packages[], and the smoke readiness wait loop uses the array check plus `docker exec -u developer` + $HOME instead of a hard-coded /home/developer path. Evidence: zero `fork` tool calls across all 19 sessions on this volume; the v1.6.3 session that tuned pi-fork.deep to opus-5 was configuring a tool that never loaded.
This commit is contained in:
@@ -13,6 +13,45 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`pi-fork` was never registered — the `fork` tool has been missing since
|
||||||
|
v1.0.0.** `entrypoint-user.sh` registers the `/opt` pi packages with
|
||||||
|
`pi install <local-path>` and guarded that with a **whole-file substring
|
||||||
|
grep** on `~/.pi/agent/settings.json`. But `settings.example.json` carries a
|
||||||
|
top-level `"pi-fork"` **config block** (the fork effort profiles, added in
|
||||||
|
`pi-toolkit` `adb6907`, 2026-06-17), so `grep -q pi-fork settings.json`
|
||||||
|
matches on any settings file bootstrapped from — or template-merged with —
|
||||||
|
that template. The guard therefore concluded "already installed" and
|
||||||
|
`pi install /opt/pi-fork` never ran, on fresh *and* preserved volumes.
|
||||||
|
Compounding it, the non-destructive template merge runs **earlier in the same
|
||||||
|
startup** than the install loop, so the very mechanism that delivers new
|
||||||
|
template keys to an old volume is what plants the string that defeats the
|
||||||
|
guard. `pi-observational-memory` and `pi-studio` escaped only by luck: the
|
||||||
|
template key is `observational-memory` (no `pi-` prefix) and there is no
|
||||||
|
studio block.
|
||||||
|
|
||||||
|
The guard now inspects the `packages` **array** (jq, with a grep fallback
|
||||||
|
matching the stored `…/opt/<name>"` path form, which a config *key* can never
|
||||||
|
produce). Existing volumes self-heal on the next container start — the guard
|
||||||
|
returns false, `pi install /opt/pi-fork` runs, and `fork` registers on the
|
||||||
|
following pi start or `/reload`. No image rebuild is required to benefit if
|
||||||
|
you run `pi install /opt/pi-fork` by hand.
|
||||||
|
|
||||||
|
- **Both test suites asserted the bug as green.** `scripts/smoke-test.sh` and
|
||||||
|
`scripts/recreate-sanity-check.sh` checked registration with the *same*
|
||||||
|
whole-file grep, so "pi-fork registered (fork tool)" passed on every build
|
||||||
|
and every recreate while the tool was absent. Both now assert against
|
||||||
|
`packages[]` with the same predicate as the entrypoint guard, and the labels
|
||||||
|
say `packages[]` so the distinction is visible in CI output. The smoke-test
|
||||||
|
readiness wait loop was switched to the array check too (and to
|
||||||
|
`docker exec -u developer` + `$HOME` instead of a hard-coded
|
||||||
|
`/home/developer` path).
|
||||||
|
|
||||||
|
Detected by an agent session noticing `fork` was absent from its own tool
|
||||||
|
list on v1.6.3; zero `fork` calls exist across the 19 sessions on this
|
||||||
|
volume, confirming it never once loaded.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **Settings template now defaults to Claude Opus 5** (`pi-toolkit` @ `926f738`).
|
- **Settings template now defaults to Claude Opus 5** (`pi-toolkit` @ `926f738`).
|
||||||
|
|||||||
+29
-4
@@ -178,13 +178,38 @@ if command -v pi &>/dev/null; then
|
|||||||
# idempotent (no duplicate package entry on re-run), and stores a relative
|
# idempotent (no duplicate package entry on re-run), and stores a relative
|
||||||
# path that resolves into the image-layer /opt so it survives volume
|
# path that resolves into the image-layer /opt so it survives volume
|
||||||
# recreate. The tools/command register on the NEXT pi start (extensions
|
# recreate. The tools/command register on the NEXT pi start (extensions
|
||||||
# bind at startup). Guard on settings.json so we only install once per
|
# bind at startup) or on `/reload`. Guard on settings.json so we only
|
||||||
# volume. /opt/pi-studio is present only in the studio variant; the
|
# install once per volume. /opt/pi-studio is present only in the studio
|
||||||
# `[ -d ]` test makes this a no-op everywhere else.
|
# variant; the `[ -d ]` test makes this a no-op everywhere else.
|
||||||
|
#
|
||||||
|
# The guard MUST inspect the `packages` ARRAY, not merely grep the whole
|
||||||
|
# file for the package name. settings.example.json ships a top-level
|
||||||
|
# "pi-fork" CONFIG block (the fork effort profiles, pi-toolkit adb6907,
|
||||||
|
# 2026-06-17), so a whole-file substring grep matches on any settings.json
|
||||||
|
# that was bootstrapped from — or template-merged with — that template.
|
||||||
|
# Worse, the merge above runs FIRST, so it plants the matching string in the
|
||||||
|
# same startup that the loop then reads: `pi install /opt/pi-fork` was
|
||||||
|
# skipped forever and the `fork` tool never registered (v1.0.0 → v1.6.3).
|
||||||
|
# Its siblings escaped only by luck — the template key is
|
||||||
|
# "observational-memory" (no pi- prefix) and there is no studio block.
|
||||||
|
# jq reads the array; the grep fallback matches the stored relative-path
|
||||||
|
# form ("…/opt/<name>\""), which a config KEY can never produce.
|
||||||
|
_pi_pkg_registered() {
|
||||||
|
_pi_reg_settings="$HOME/.pi/agent/settings.json"
|
||||||
|
[ -f "$_pi_reg_settings" ] || return 1
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq -e --arg n "$1" \
|
||||||
|
'(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \
|
||||||
|
"$_pi_reg_settings" >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
grep -q "opt/$1\"" "$_pi_reg_settings"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
for _pkg in /opt/pi-fork /opt/pi-observational-memory /opt/pi-studio; do
|
for _pkg in /opt/pi-fork /opt/pi-observational-memory /opt/pi-studio; do
|
||||||
[ -d "$_pkg" ] || continue
|
[ -d "$_pkg" ] || continue
|
||||||
_name=$(basename "$_pkg")
|
_name=$(basename "$_pkg")
|
||||||
if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
if ! _pi_pkg_registered "$_name"; then
|
||||||
pi install "$_pkg" >/dev/null 2>&1 || \
|
pi install "$_pkg" >/dev/null 2>&1 || \
|
||||||
echo "WARN: pi install $_name failed (continuing)"
|
echo "WARN: pi install $_name failed (continuing)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
# nvim data, uv cache, ssh-local)
|
# nvim data, uv cache, ssh-local)
|
||||||
# - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink,
|
# - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink,
|
||||||
# ≥4 extensions, the mempalace.ts bridge, settings.json, and the pi-fork /
|
# ≥4 extensions, the mempalace.ts bridge, settings.json, and the pi-fork /
|
||||||
# pi-observational-memory / (studio variant) pi-studio package registrations
|
# pi-observational-memory / (studio variant) pi-studio package
|
||||||
|
# registrations in settings.json packages[]
|
||||||
# - Shell defaults re-seeded from /etc/skel-devbox
|
# - Shell defaults re-seeded from /etc/skel-devbox
|
||||||
# - /tmp/sshcm exists with mode 700 (ssh ControlMaster dir)
|
# - /tmp/sshcm exists with mode 700 (ssh ControlMaster dir)
|
||||||
# - /opt toolkits intact
|
# - /opt toolkits intact
|
||||||
@@ -199,21 +200,38 @@ if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.pi/agent/settings.json" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# pi package registrations (pi install <local-path> → recorded in settings.json)
|
# pi package registrations (pi install <local-path> → recorded in settings.json).
|
||||||
|
# Check the `packages` ARRAY, not the whole file: the settings template ships a
|
||||||
|
# top-level "pi-fork" CONFIG block (asserted just above), so `grep -q pi-fork
|
||||||
|
# settings.json` is a guaranteed false green — which is how an un-registered
|
||||||
|
# fork tool went unnoticed from v1.0.0 through v1.6.3. Same array check the
|
||||||
|
# fixed entrypoint-user.sh guard uses.
|
||||||
|
_pkg_registered() {
|
||||||
|
_s="$HOME/.pi/agent/settings.json"
|
||||||
|
[ -f "$_s" ] || return 1
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq -e --arg n "$1" \
|
||||||
|
'(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \
|
||||||
|
"$_s" >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
grep -q "opt/$1\"" "$_s"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
if [ -f "$HOME/.pi/agent/settings.json" ]; then
|
if [ -f "$HOME/.pi/agent/settings.json" ]; then
|
||||||
for pkg in pi-fork pi-observational-memory; do
|
for pkg in pi-fork pi-observational-memory; do
|
||||||
if grep -q "$pkg" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
if _pkg_registered "$pkg"; then
|
||||||
pass "$pkg registered in settings.json"
|
pass "$pkg registered in settings.json packages[]"
|
||||||
else
|
else
|
||||||
fail "$pkg not registered in settings.json"
|
fail "$pkg NOT in settings.json packages[] (tool will not load)"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$VARIANT" = "studio" ]; then
|
if [ "$VARIANT" = "studio" ]; then
|
||||||
if grep -q "pi-studio" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
if _pkg_registered pi-studio; then
|
||||||
pass "pi-studio registered in settings.json"
|
pass "pi-studio registered in settings.json packages[]"
|
||||||
else
|
else
|
||||||
fail "pi-studio not registered in settings.json (studio variant)"
|
fail "pi-studio NOT in settings.json packages[] (studio variant)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
+22
-9
@@ -18,7 +18,8 @@
|
|||||||
# - entrypoint deploys ≥4 extensions
|
# - entrypoint deploys ≥4 extensions
|
||||||
# - mempalace bridge symlink present
|
# - mempalace bridge symlink present
|
||||||
# - settings.json bootstrapped
|
# - settings.json bootstrapped
|
||||||
# - pi-fork + pi-observational-memory registered via `pi install`
|
# - pi-fork + pi-observational-memory registered in settings.json packages[]
|
||||||
|
# via `pi install`
|
||||||
# - pi-devbox-version command present + wraps the build manifest correctly
|
# - pi-devbox-version command present + wraps the build manifest correctly
|
||||||
# (human, --json, --quiet)
|
# (human, --json, --quiet)
|
||||||
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
|
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
|
||||||
@@ -234,28 +235,40 @@ exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills
|
|||||||
|
|
||||||
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
||||||
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
||||||
|
#
|
||||||
|
# Assert against the `packages` ARRAY, never a whole-file grep: the settings
|
||||||
|
# template ships a top-level "pi-fork" CONFIG block, so `grep -q pi-fork
|
||||||
|
# settings.json` passes even when `pi install /opt/pi-fork` never ran. That
|
||||||
|
# false green is exactly why the missing `fork` tool shipped unnoticed from
|
||||||
|
# v1.0.0 through v1.6.3.
|
||||||
|
pkg_registered_cmd() {
|
||||||
|
printf "jq -e --arg n %s '(.packages // []) | any((type == \"string\") and (. == \"npm:\" + \$n or endswith(\"/\" + \$n)))' \$HOME/.pi/agent/settings.json" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
for i in $(seq 1 15); do
|
for i in $(seq 1 15); do
|
||||||
if docker exec "$CID" grep -q pi-observational-memory \
|
if docker exec -u developer "$CID" sh -c "$(pkg_registered_cmd pi-observational-memory)" \
|
||||||
/home/developer/.pi/agent/settings.json 2>/dev/null; then
|
>/dev/null 2>&1; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
exec_test "pi-fork registered (fork tool)" 'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok'
|
exec_test "pi-fork registered in packages[] (fork tool)" \
|
||||||
exec_test "pi-observational-memory registered (recall tool)" 'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok'
|
"$(pkg_registered_cmd pi-fork)"
|
||||||
|
exec_test "pi-observational-memory registered in packages[] (recall tool)" \
|
||||||
|
"$(pkg_registered_cmd pi-observational-memory)"
|
||||||
|
|
||||||
# pi-studio registration (studio variant only) — registered by the same
|
# pi-studio registration (studio variant only) — registered by the same
|
||||||
# entrypoint-user.sh local-path install loop as fork/obsmem.
|
# entrypoint-user.sh local-path install loop as fork/obsmem.
|
||||||
if [ "${STUDIO_VARIANT:-0}" = "1" ]; then
|
if [ "${STUDIO_VARIANT:-0}" = "1" ]; then
|
||||||
for i in $(seq 1 15); do
|
for i in $(seq 1 15); do
|
||||||
if docker exec "$CID" grep -q pi-studio \
|
if docker exec -u developer "$CID" sh -c "$(pkg_registered_cmd pi-studio)" \
|
||||||
/home/developer/.pi/agent/settings.json 2>/dev/null; then
|
>/dev/null 2>&1; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
exec_test "pi-studio registered (/studio command + studio_* tools)" \
|
exec_test "pi-studio registered in packages[] (/studio command + studio_* tools)" \
|
||||||
'grep -q pi-studio $HOME/.pi/agent/settings.json && echo ok'
|
"$(pkg_registered_cmd pi-studio)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── /tmp/sshcm directory created by entrypoint ────────────────────────
|
# ── /tmp/sshcm directory created by entrypoint ────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user