From adcf56f8292d6cdd7821bc48d9764ed6feeafde6 Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Sun, 6 Sep 2026 20:40:02 +0200 Subject: [PATCH] release: audited bumps (pi 0.85.1, mempalace 3.9.0, atelier v0.10.1) + two guards Version audit for the next release. pi 0.84.4 -> 0.85.1, deliberately skipping 0.85.0 (it published internal experimental code and broke SDK imports, upstream #9132). mempalace 3.8.0 -> 3.9.0. pi-atelier v0.10.0 -> v0.10.1. PI_STUDIO_VERSION relabelled none -> v0.9.60-rc.0 so the floating main ref's RC status is visible at docker-inspect time instead of discovered later. PI_FORK_REF stays floating and adopts e69725c. The pi bump was verified by running it under a pty in five combinations rather than by reading the changelog, because this repo has already shipped a version pair no changelog flagged (atelier < 0.7.1 hangs pi >= 0.84). CPU delta 0.00-0.01s over 5s against a ~5s sustained-CPU hang signature, two-sided via the atelier sidebar painting identically to the 0.84.4 control. NODE_VERSION stays 22 on purpose: node 24 is technically safe (pi's five prebuilt addons are all NAPI, nothing declares a ceiling, agent-browser's engines.node >=24 is vestigial for the shipped aarch64 ELF), but this release already moves two minors and bakes an RC, and a node major would leave four suspects if the image misbehaves. Own release, smoke suite as the gate. Also corrects a stale claim at the mempalace ARG: synlig serves 3.8.0 server-side, not 3.7.1 (measured over ssh 2026-09-06). agent-browser volume shadowing: the image has shipped 0.35.2, but every session on mbp-m1-2020 ran 0.27.0 from a 2026-07-17 hand-install in ~/.pi/npm-global (a VOLUME, at PATH position 2 vs /usr/bin at 8). Third package hit by this hazard after pi and pi-atelier, so the guard is now generalised: entrypoint-user.sh retires the copy by moving it aside (reversible, only when the image ships its own), recreate-sanity-check.sh asserts resolution under /usr where the volume is real, smoke-test.sh carries the build-time half and says in the source why it is weak. The real damage was the stale BUNDLED SKILL (3 skillsets/17.6 KB vs 8/31.5 KB, ten subcommands undocumented to the agent) - a stale tool errors, a stale skill quietly teaches wrong commands. pi-fork capability floor (extensions: []): forks were measured across four dispatches ignoring their brief, answering in the user's voice, fabricating self-referential measurements, and once filing a diary entry as agent_name=pi. Cause is upstream by design - the child gets getHeader()+getBranch(), the whole active session branch, with the brief as the final user message. Not a model-capability problem: the same model as the fast profile obeyed the identical brief perfectly with a fresh session and no inherited context. extensions: [] runs children with --no-extensions, so the mempalace bridge is absent and palace writes are impossible by construction (verified by asking a child to enumerate its tools: read, bash, edit, write). Removes palace writes, not filesystem writes. --- CHANGELOG.md | 108 +++++++++++++++++++++++++++++++ Dockerfile.base | 27 ++++++-- Dockerfile.variant | 41 ++++++++++-- entrypoint-user.sh | 32 +++++++++ scripts/recreate-sanity-check.sh | 28 ++++++++ scripts/smoke-test.sh | 28 ++++++++ 6 files changed, 253 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38d03af..87e74b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,114 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). ## Unreleased +**Version audit + three pins moved, one deliberately not moved.** `pi` +0.84.4 -> 0.85.1, `mempalace` 3.8.0 -> 3.9.0, `pi-atelier` v0.10.0 -> v0.10.1, +and `PI_STUDIO_VERSION` relabelled `none` -> `v0.9.60-rc.0` to record what the +floating `main` ref actually resolves to. `PI_FORK_REF=master` stays floating +and therefore adopts e69725c. Each rationale is written at the ARG itself +rather than only here, because that is where the next person doing the audit +will be standing. + +0.85.0 is SKIPPED on purpose: it shipped internal experimental code and extra +subpaths that broke SDK imports (upstream #9132), and 0.85.1 exists to undo +exactly that. Neither release has a Breaking/Removed changelog heading, the +engine floor is unchanged (>=22.19.0 against the container's 22.23.2), and +runtime deps drop 20 -> 19. + +The pi bump was verified by RUNNING it, not by reading about it, because this +repo has already been burned by a version pair that no changelog flagged +(pi-atelier < 0.7.1 hangs pi >= 0.84 at startup with no error). 0.85.1 was +side-installed and driven under a pty in five combinations — each companion +extension plus atelier v0.10.0 AND v0.10.1 — with a CPU delta of 0.00-0.01s +over a 5s window where the known hang signature is ~5s of sustained CPU. The +check was two-sided: the atelier sidebar painted ACTIVITY+WORKSPACE markers +identically to the 0.84.4 control, so "alive" could be distinguished from +"silently absent". + +**NODE_VERSION stays 22 — audited, not overlooked.** node 24 is technically +safe: all five prebuilt native addons in pi use NAPI (ABI-stable, no +NODE_MODULE_VERSION lock, no binding.gyp), nothing in the image declares a node +CEILING, and the install is one token (`setup_${NODE_VERSION}.x`). agent-browser +0.36.0 declares `engines.node >=24`, but that field is vestigial for the +artifact actually shipped: the image's own 0.35.2 declares the same floor and +runs fine on 22.23.2 as a prebuilt aarch64 ELF. The reason to wait is +attribution, not compatibility — this release already moves pi a minor, +mempalace a minor and bakes a Studio RC, so adding a node major would leave four +suspects if the image misbehaves. Worth doing as its own release with the smoke +suite as the gate. (v22 is in maintenance until 2027-04-30; v24 is Active LTS +to 2026-10-20 and maintained to 2028-04-30, so there is real headroom.) + +mempalace's client bump carries a sequencing note that is now also CORRECT: the +comment at the ARG claimed synlig serves 3.7.1 server-side, which was stale. +Measured 2026-09-06 over ssh, synlig's uv tool entry last changed 2026-08-25 +and serves 3.8.0. Client 3.9.0 against server 3.8.0 is accepted skew until +synlig's compose stack is redeployed; 3.9.0's headline additions (release +awareness, `task create`/`task launch`) are SERVER-side and stay dark until +then — a client bump alone cannot light them up. + +**agent-browser was running 7 weeks stale, and the interesting part is why +nothing noticed.** The image has shipped 0.35.2 since the last base rebuild, +but every session on mbp-m1-2020 was executing 0.27.0 from a 2026-07-17 +hand-install: `npm i -g` writes into `~/.pi/npm-global`, which is the +devbox-pi-config VOLUME, and PATH puts that at position 2 against /usr/bin at +position 8. This is the third package hit by that exact hazard (pi itself and +pi-atelier already have guards), so the guard is now generalised instead of +re-invented a fourth time. + +The damage was not the binary. It was the BUNDLED SKILL, which is the part an +agent reads: 3 skillsets / 17.6 KB core in 0.27.0 versus 8 skillsets / 31.5 KB +core in 0.35.2, with ten subcommands present in the image and entirely +undocumented to the agent (a11y, browser, data, mcp, page, plugin, read, +selectors, to, webmcp). A stale tool announces itself with an error; a stale +skill just quietly teaches the wrong commands and everything looks fine. + +Three changes, at the three places this can be caught: +- `entrypoint-user.sh` retires a volume copy by MOVING it aside (reversible, + same instinct as the settings backups) and only when the image ships its own + copy, so a machine that deliberately hand-installs on an image without one + keeps it. The `bin/` shim is removed too — a dangling symlink would be a + worse failure than a stale version. +- `scripts/recreate-sanity-check.sh` asserts `agent-browser` resolves under + /usr. This is the check that matters, because it runs where the volume is + real. +- `scripts/smoke-test.sh` gets the build-time half, labelled WEAK in the source + for an honest reason: a `docker run` container has an empty config volume, so + it can never see the shadowing it is nominally testing for. + +**pi-fork gets a capability floor: `extensions: []`.** Forks were measured +twice (2026-09-01, 2026-09-06, four dispatches) ignoring their brief, answering +in the USER's voice, fabricating self-referential measurements, and once filing +a diary entry as `agent_name=pi` — which landed in `wing_pi`, where a +wing-scoped `diary_read` never sees it. + +The cause is upstream and by design, so there is nothing to wait for: the child +is handed `getHeader()+getBranch()`, i.e. the WHOLE active session branch, with +the brief appended as the final user message and the system prompt untouched +(pi-fork `src/index.ts`). In a long session the parent narrative simply +outweighs the task, and the child does the statistically obvious thing — it +continues the story it finds itself inside. Config offers no context knob +(extensions, environment, offline, costFooter, effort profiles only). + +Falsified the tempting explanation before acting on it: the failures are NOT a +too-small model. The same model as the `fast` profile (haiku, thinking off) +obeyed the identical brief perfectly when run as +`pi -p --mode json --session-id --no-extensions` — correct values, +exact format, no session recap, 3 seconds, $0.012. Model held constant, context +inheritance removed, failure gone. + +`extensions: []` is therefore a mechanical guarantee rather than an +instruction: the mempalace bridge is a pi EXTENSION, so a fork child now runs +with `--no-extensions` and cannot write to the shared palace under the parent's +identity. Verified by asking a child to enumerate its own tools: `read, bash, +edit, write` — no `mempalace_*`, no `recall`, no nested `fork`. Two honest +limits, stated so nobody over-trusts this: it removes PALACE writes, not +FILESYSTEM writes (`edit`/`write` remain), and it costs forks their palace +search and recall. Set the key to `null` to restore normal loading. + +Smoke asserts the floor is `[]` specifically, not merely falsy — `null` is the +unguarded state, so a "truthy or not" test would pass on exactly the +configuration being guarded against. + **`credential-incident-response` §5/§6 corrected — a stated mechanism was wrong, and this is the second time in three days this section named a wrong reason for a zero.** Docs only. diff --git a/Dockerfile.base b/Dockerfile.base index 12e194d..b6b2c1b 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -450,13 +450,26 @@ ARG INSTALL_MEMPALACE=true # the part that should stay manual. # # Deployment sequencing note for whoever ships this bump: synlig (the shared -# central palace host) currently serves mempalace 3.7.1 SERVER-SIDE via -# docker-compose.mempalace.yml, which reuses this same devbox image. Bumping -# this ARG changes only the CLIENT version baked into pi-devbox images: it -# introduces client/server skew until synlig's compose stack is separately -# rebuilt/redeployed with the new pin. Not something to code around here — -# just sequence the redeploy. -ARG MEMPALACE_VERSION=3.8.0 +# central palace host) serves mempalace 3.8.0 SERVER-SIDE via +# docker-compose.mempalace.yml, which reuses this same devbox image. (Measured +# 2026-09-06 over ssh: synlig's UV_TOOL_DIR mempalace entry last changed +# 2026-08-25 15:33 — this comment previously said 3.7.1, which was stale.) +# Bumping this ARG changes only the CLIENT version baked into pi-devbox +# images: it introduces client/server skew until synlig's compose stack is +# separately rebuilt/redeployed with the new pin. Not something to code around +# here — just sequence the redeploy. +# +# v1.8.13: 3.8.0 -> 3.9.0. Audited: no Breaking/Removed changelog headings. +# Adopted mainly for #2281 (`mempalace_mine` accepts a single conversation +# file again) — though note that does NOT unblock this image's own feeder, +# which was measured to mine DIRECTORIES, not files, so it was never hitting +# that bug. Four behaviour changes ride along and are skew-relevant while +# synlig stays on 3.8.0: hub-forward escaping, an HTTP lock split, similarity +# score semantics, and parsed-output compatibility. 3.9.0-only features +# (release awareness, `task create`/`task launch` MCP tools) are SERVER-side, +# so they stay dark until synlig is redeployed — a client bump alone cannot +# light them up. +ARG MEMPALACE_VERSION=3.9.0 ENV UV_TOOL_DIR=/opt/uv-tools ENV UV_TOOL_BIN_DIR=/usr/local/bin RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \ diff --git a/Dockerfile.variant b/Dockerfile.variant index 1f68259..cc57d09 100644 --- a/Dockerfile.variant +++ b/Dockerfile.variant @@ -95,7 +95,25 @@ ARG USER_NAME=developer # `.agents/skills//` directories were not discovered, and root Markdown # files such as README.md / AGENTS.md inside a skill dir were reported as # broken skills unless they declared valid skill frontmatter. -ARG PI_VERSION=0.84.4 +# +# v1.8.13: 0.84.4 -> 0.85.1. SKIP 0.85.0 deliberately — it accidentally +# published internal experimental code and extra subpaths, breaking SDK +# imports (upstream #9132); 0.85.1 exists specifically to undo that, with the +# supported SDK and stdio RPC API unchanged. Audited: no Breaking/Removed +# changelog headings in either release, engine floor unchanged (>=22.19.0, +# container runs 22.23.2), runtime deps 20 -> 19. User-visible changes are the +# streaming indicator moving into the editor border and faster fullscreen +# transcript search; no deprecation language anywhere. +# +# Verified EMPIRICALLY rather than from the changelog, because a pi bump has +# hung the TUI before (pi-atelier < 0.7.1 + pi >= 0.84): 0.85.1 was +# side-installed and driven under a pty against all four companion extensions, +# with atelier v0.10.0 AND v0.10.1 — five combinations, each rendering alive +# with a CPU delta of 0.00-0.01s over a 5s window, where the known hang +# signature is ~5s of sustained CPU. Two-sided check: the atelier sidebar +# painted ACTIVITY+WORKSPACE identically to the 0.84.4 control, so the test +# could distinguish "loaded" from "silently absent". +ARG PI_VERSION=0.85.1 ARG PI_TOOLKIT_REF=main ARG PI_EXTENSIONS_REF=main # Repo URLs default to the canonical gitea origin but are overridable so a @@ -147,9 +165,14 @@ ARG PI_OBSMEM_REF=master # the /opt checkout. Adding an install here would be a no-op that only costs # build time. ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git -ARG PI_ATELIER_REF=v0.10.0 +# v1.8.13: v0.10.0 -> v0.10.1. Refactor-only upstream (formatters, tests, +# panel identity); peerDependencies declare pi >=0.84.0, so it spans both the +# old and new pin. Included because it was already exercised: the pty matrix +# for PI_VERSION above ran atelier v0.10.1 against pi 0.85.1 and painted the +# sidebar identically to v0.10.0. +ARG PI_ATELIER_REF=v0.10.1 # Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label. -ARG PI_ATELIER_VERSION=v0.10.0 +ARG PI_ATELIER_VERSION=v0.10.1 RUN set -e && \ # git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name @@ -259,7 +282,17 @@ ARG PI_STUDIO_REF=main # PI_STUDIO_VERSION is the human-readable tag (e.g. v0.9.36) that PI_STUDIO_REF # was resolved from; recorded as a label below for at-a-glance identification. # Only meaningful for the studio variant (default `none` otherwise). -ARG PI_STUDIO_VERSION=none +# +# v1.8.13: PI_STUDIO_REF stays floating on `main`, which resolves to +# v0.9.60-rc.0 — a RELEASE CANDIDATE, not the latest stable (v0.9.59). +# Adopted deliberately (JOA, 2026-09-06); recording the label is what makes +# that choice visible via `docker inspect` instead of someone discovering an +# RC in production later. Of the 15 commits since 0.9.55, one adds an OPT-IN +# network binding for the Studio server: that is network-facing and deserves +# its own audit before anyone enables it. The container default is unchanged — +# pi-studio still hard-binds 127.0.0.1 (see the STUDIO_EXPOSE bridge below), +# so adopting the RC does not by itself expose Studio to the LAN. +ARG PI_STUDIO_VERSION=v0.9.60-rc.0 RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \ set -e; \ rm -rf /opt/pi-studio && mkdir -p /opt/pi-studio && \ diff --git a/entrypoint-user.sh b/entrypoint-user.sh index a55c84b..f7a38cf 100755 --- a/entrypoint-user.sh +++ b/entrypoint-user.sh @@ -471,6 +471,38 @@ if command -v pi &>/dev/null; then done fi +# ── agent-browser: retire a stale volume copy that shadows the image ─── +# Same hazard class as the pi-atelier retirement above, different delivery +# path — and this block exists because that guard did not generalise. +# ~/.pi/npm-global lives on the devbox-pi-config VOLUME, so anything ever +# installed there with `npm i -g` survives every image upgrade, and PATH puts +# it AHEAD of /usr/bin (position 2 vs 8). +# +# Measured on mbp-m1-2020, 2026-09-06: a 2026-07-17 hand-install pinned +# agent-browser 0.27.0 in the volume while the image shipped 0.35.2, so every +# session for ~7 weeks ran a stale CLI. The damaging part was not the binary +# but its BUNDLED SKILL, which is what the agent actually reads: 3 skillsets / +# 17.6 KB core in 0.27.0 vs 8 skillsets / 31.5 KB core in 0.35.2, with ten +# subcommands present in the image and undocumented to the agent (a11y, +# browser, data, mcp, page, plugin, read, selectors, to, webmcp). A stale tool +# announces itself; a stale skill quietly teaches the wrong commands. +# +# MOVE rather than delete (reversible, same instinct as the settings backups +# above), and only when the image ships its own copy — a machine that +# deliberately hand-installs agent-browser on an image WITHOUT one keeps it. +_ab_vol="$HOME/.pi/npm-global/lib/node_modules/agent-browser" +if [ -d "$_ab_vol" ] && [ -d /usr/lib/node_modules/agent-browser ]; then + _ab_park="$HOME/.pi/npm-global/.retired-agent-browser-$(date +%Y%m%d-%H%M%S)" + if mkdir -p "$_ab_park" 2>/dev/null && mv "$_ab_vol" "$_ab_park/" 2>/dev/null; then + # The bin shim is what PATH actually hits; leaving it behind would give a + # dangling symlink, which is a worse failure than a stale version. + rm -f "$HOME/.pi/npm-global/bin/agent-browser" 2>/dev/null || true + echo "agent-browser: retired stale volume copy -> ${_ab_park} (image copy now wins; delete the parked dir when satisfied)" + else + echo "WARN: agent-browser: stale volume copy at $_ab_vol shadows the image copy and could not be moved; retire it by hand" + fi +fi + # ── pi-studio: optional loopback bridge (opt-in) ────────────────────── # pi-studio binds its server to 127.0.0.1 inside the container, which a # published Docker port cannot reach. When STUDIO_EXPOSE is truthy (set in diff --git a/scripts/recreate-sanity-check.sh b/scripts/recreate-sanity-check.sh index 5076a0d..a4fcb73 100755 --- a/scripts/recreate-sanity-check.sh +++ b/scripts/recreate-sanity-check.sh @@ -374,6 +374,34 @@ if [ -f "$HOME/.pi/agent/settings.json" ]; then fi fi +# ── agent-browser must resolve to the image, not the config volume ──── +# The same volume-shadowing hazard already asserted for pi (above) and +# pi-atelier (just now), for the third package it has bitten. This check +# belongs HERE rather than only in smoke-test.sh: a build-time container has an +# empty ~/.pi/npm-global, so smoke-test can never see the stale copy that a +# real recreate inherits. Measured instance: 0.27.0 from 2026-07-17 shadowed +# the image's 0.35.2 for ~7 weeks on mbp-m1-2020, silently supplying an older +# BUNDLED SKILL (3 skillsets vs 8) — the agent read the stale instructions +# without any version mismatch ever being surfaced. +AB_PATH=$(command -v agent-browser 2>/dev/null || true) +if [ -z "$AB_PATH" ]; then + warn "agent-browser not on PATH (expected in v1.6.0+ images; skipping shadow check)" +else + AB_REAL=$(readlink -f "$AB_PATH" 2>/dev/null || echo "$AB_PATH") + AB_VER=$(agent-browser --version 2>/dev/null | head -n1) + case "$AB_REAL" in + /usr/*) + pass "agent-browser resolves to the image copy (${AB_VER:-version unknown})" + ;; + *) + fail "agent-browser resolves to $AB_REAL (${AB_VER:-version unknown}) — a ~/.pi/npm-global VOLUME copy is shadowing the image; the entrypoint retirement guard did not run or could not move it" + ;; + esac + if [ -d "$HOME/.pi/npm-global/lib/node_modules/agent-browser" ]; then + fail "stale agent-browser still present in the ~/.pi/npm-global volume (entrypoint guard did not retire it)" + fi +fi + # ── pi <-> pi-atelier compatibility floor ───────────────────────────── # atelier < 0.7.1 wraps pi's private TUI renderer in a way that recurses under # pi >= 0.84: pi hangs at startup burning CPU, with no error message. atelier's diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 4c3d57a..3d73807 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -718,6 +718,34 @@ exec_test "pi-atelier registered in packages[] (TUI sidebar)" \ exec_test "pi-atelier registered from /opt, not npm: (volume-shadowing guard)" \ 'jq -e "((.packages // []) | any((type == \"string\") and endswith(\"/pi-atelier\"))) and (((.packages // []) | any(. == \"npm:pi-atelier\")) | not)" $HOME/.pi/agent/settings.json' +# agent-browser: the third package hit by ~/.pi/npm-global volume shadowing +# (after pi itself and pi-atelier). This build-time check is deliberately WEAK +# and says so: a `docker run` container has an EMPTY config volume, so it can +# only prove the image ships a sane copy and nothing in the image itself +# shadows it. The check that actually bites lives in +# recreate-sanity-check.sh, which runs where the volume is real — that is +# where a 7-week-old 0.27.0 was caught shadowing 0.35.2 on 2026-09-06. +exec_test "agent-browser resolves under /usr (volume-shadowing guard, build-time half)" ' + p=$(command -v agent-browser) || { echo "agent-browser not on PATH" >&2; exit 1; } + r=$(readlink -f "$p") + echo "resolved=[$r] version=[$(agent-browser --version 2>/dev/null | head -n1)]" >&2 + case "$r" in /usr/*) ;; *) exit 1 ;; esac + test ! -d "$HOME/.pi/npm-global/lib/node_modules/agent-browser" || exit 1 + echo ok +' + +# pi-fork capability floor. `extensions: []` makes a fork child run with +# --no-extensions, which is the only MECHANICAL guarantee that a fork cannot +# file drawers or diary entries under the parent's identity — the mempalace +# bridge is an extension, so removing extensions removes the write path. +# Asserted because it is a security-shaped default that a settings merge or a +# hand-edit could silently drop, and its absence is invisible until a fork +# writes to the shared palace as you (measured twice: 2026-09-01, 2026-09-06). +# Deliberately compares to [] and not "is falsy": null means "load normal +# extensions", i.e. exactly the unguarded state this asserts against. +exec_test "pi-fork extensions floor is [] (forks cannot write to the palace)" \ + 'jq -e ".[\"pi-fork\"].extensions == []" $HOME/.pi/agent/settings.json' + # ── /tmp/sshcm directory created by entrypoint ──────────────────────── exec_test "/tmp/sshcm dir mode 700 (ssh ControlMaster)" \ 'test -d /tmp/sshcm && [ "$(stat -c %a /tmp/sshcm)" = "700" ] && echo ok'