From 37fcbfcf043e1f8a2b66170f85f3ab0fe031b372 Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Tue, 22 Sep 2026 17:20:10 +0200 Subject: [PATCH] fix: three v1.9.4-acceptance findings (installer WARN, init sentinel, hash collation) Unreleased; no pin moves except pi-extensions master 25c1265 -> 143a214. 1. entrypoint-user.sh: first-run sentinel is now config.json, which is what `mempalace init` writes. The old test on palace/ (created by mining, not init) re-fired "Initializing MemPalace" on every boot of a container that never mined locally: v1.9.4 acceptance measured 1 line after one boot, 2 after a restart. Idempotent, so harmless; the comment lied. This file is a base-hash input, so the next tag rebuilds the base (326fb7c03949 predicted with the pinned sort below; f40c4b7b103d before). 2. Dockerfile.variant: `git config --system --add safe.directory` for the seven root-owned /opt clones (listed, not `*`). Since git 2.35.2 any git command in a repo owned by another user fails "dubious ownership"; as `developer` that made `git -C /opt/pi-atelier rev-parse` print nothing (one false FAIL in the v1.9.4 acceptance) and is the root cause of the per-boot "WARN: pi-extensions install.sh failed" (fixed at the source in pi-extensions 143a214; this is the belt to that suspender). Verified live in a v1.9.3 container: add -> rev-parse prints 25c1265, unset -> fatal again; /etc/gitconfig restored to its 5 lines afterwards. 3. docker-publish.yml: `find -print0 | LC_ALL=C sort -z` in base-decide. sort collates per locale; identical rootfs hashed to base-f40c4b7b103d under C/C.UTF-8 (== run 695) and base-d8df62216a81 under sv_SE/en_US.UTF-8. The runner exports LANG=C.UTF-8, so the hash was stable by accident. Pin is hash-neutral: pinned sort + HEAD inputs reproduces f40c4b7b103d exactly. Per-command prefix only; nobody's locale changes. CHANGELOG: new `## Unreleased` above v1.9.4 naming pi-extensions 143a214 (check 9 rc=0), the three fixes, and the synlig 3.10.0 hub upgrade that v1.9.4 listed as still open. One invented URL org (gwpl) caught before commit; upstream is elpapi42, as Dockerfile.variant:151 says. Gates: check-doc-drift rc=0, check-base-hash rc=0, lint-shell rc=0 (16 files), hadolint 2.15.1 rc=0, YAML parses (10 jobs), bash -n rc=0. --- .gitea/workflows/docker-publish.yml | 11 ++++- CHANGELOG.md | 77 +++++++++++++++++++++++++++++ Dockerfile.variant | 16 ++++++ entrypoint-user.sh | 8 ++- 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml index 9a96d6a..fa9b8ff 100644 --- a/.gitea/workflows/docker-publish.yml +++ b/.gitea/workflows/docker-publish.yml @@ -108,7 +108,14 @@ jobs: id: compute run: | # Hash inputs that determine the base image's contents. - # Order is fixed via `find -print0 | sort -z` for reproducibility. + # Order is fixed via `find -print0 | LC_ALL=C sort -z`. The LC_ALL=C is + # load-bearing: sort collates per locale, and a dictionary locale + # (sv_SE/en_US.UTF-8) orders rootfs differently from byte order, giving + # a different hash for identical content (measured 2026-09-22: + # base-f40c4b7b103d under C/C.UTF-8 vs base-d8df62216a81 under + # sv_SE.UTF-8). The runner ships LANG=C.UTF-8 today, so this pin + # changes nothing now; it stops the hash depending on that accident. + # Predicting base_tag locally MUST use the same prefix on this sort. # Junk filters: __pycache__/*.pyc and macOS metadata are gitignored # locally but still picked up by `find rootfs -type f` on a clean CI # checkout. Exclude them defensively. @@ -120,7 +127,7 @@ jobs: ! -name '*.pyc' \ ! -name '.DS_Store' \ ! -name '._*' \ - -print0 2>/dev/null | sort -z | xargs -0 cat 2>/dev/null + -print0 2>/dev/null | LC_ALL=C sort -z | xargs -0 cat 2>/dev/null cat entrypoint.sh entrypoint-user.sh # mempalace-toolkit is cloned in Dockerfile.base at a ref CI # resolves to a SHA; fold it in so base_tag changes when the diff --git a/CHANGELOG.md b/CHANGELOG.md index efdb3a3..75c753f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,83 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). --- +## Unreleased + +Three small fixes found by the v1.9.4 first-boot acceptance and the CI base +hash prediction, none release-worthy on its own. Nothing here changes a pin. +`entrypoint-user.sh` is in the base hash, so the next tag rebuilds the base +(~64 min) regardless of what else it carries. + +### Components that move with the next build + +| Component | Baked in v1.9.4 | Next build | Why | +|---|---|---|---| +| pi-extensions | `25c1265` | **`143a214`** | `install.sh`: skip hook activation on a clone this user cannot configure (below) | + +### Fixed + +- **Boot log: `WARN: pi-extensions install.sh failed (continuing)` on every start + of every device** — pi-extensions `143a214`. `/opt/pi-extensions` is root-owned + and `install.sh` runs as `developer`; git refuses the repo ("dubious + ownership"), `git config --local` fails silently, and the unguarded + `git config core.hooksPath hooks` exited 128 under `set -e`, aborting the + installer one line before `Done`. Damage was nil (all nine extension symlinks + were already created) but the WARN fired on v1.9.3 and v1.9.4 alike and would + have masked a real installer failure. `activate_hooks` now resolves the git dir + and requires a writable `.git/config`, else prints one note and returns 0 — + hooks are for clones you commit from. Measured with the function extracted and + run under `set -euo pipefail`: old function rc=128 on `/opt/pi-extensions` and + on a root-owned throwaway; new function rc=0 on those two, on a dir without + `.git`, and on a clone with `.git/config` mode 444; still activates on a + writable unset clone (config reads back `hooks`) and reports already-active on + a preset one; no root-owned config was touched. +- **`Initializing MemPalace for workspace` re-fired on every boot of a container + that had never mined locally** — `entrypoint-user.sh`. The first-run test + checked `$PALACE_DIR/palace`, but `mempalace init` writes `config.json` and + never creates `palace/` (mining does). v1.9.4 acceptance measured 1 + "Initializing" line after one boot and 2 after a restart. Idempotent, so + harmless, but the comment claimed it skipped. Sentinel is now `config.json` + (what init writes; also a 3.10.0 legacy-layout marker). Populated volumes skip + either way. +- **`developer` can now run git in the root-owned `/opt` clones** — + `Dockerfile.variant` adds `git config --system --add safe.directory` for + `/opt/{pi-toolkit,pi-extensions,pi-fork,pi-observational-memory,pi-atelier,mempalace-toolkit,pi-studio}` + (paths listed, not `*`, so the ownership check still protects `/workspace`; + the pi-studio entry is inert in the plain variant). Before: `git -C + /opt/pi-atelier rev-parse --short HEAD` as `developer` printed nothing, which + produced one false FAIL in the v1.9.4 acceptance and an earlier + "dubious ownership" wall on `/opt/mempalace-toolkit`. Mechanism verified live + in a v1.9.3 container (add → `25c1265`, unset → fatal again). + +### CI + +- **`base-decide` pins the collation of the hash input order: + `find -print0 | LC_ALL=C sort -z`** — `docker-publish.yml`. `sort` collates per + locale, and the order of `rootfs` files under a dictionary locale differs from + byte order, so identical content hashed differently: measured + `base-f40c4b7b103d` under `C` and `C.UTF-8` (== CI run 695) versus + `base-d8df62216a81` under `sv_SE.UTF-8` and `en_US.UTF-8`. The runner image + happens to export `LANG=C.UTF-8`, so the hash was stable by accident and the + comment "order is fixed via `sort -z`" was not true as written. The pin is + hash-neutral today (no spurious base rebuild from this commit; the workflow + file is not a hash input) and removes the dependency on the runner's locale. + Predicting `base_tag` locally must use the same per-command prefix — it is + scoped to that one `sort`, not to the shell, so it leaves `sv_SE.UTF-8` + everywhere else alone. + +### Still open + +- **pi 0.87.x + pi-observational-memory** — unchanged from v1.9.4: blocked on + upstream [PR #83](https://github.com/elpapi42/pi-observational-memory/pull/83) + (`shouldStopAfterTurn` → `finishTurn`); pi and pi-obsmem bump together. +- **synlig hub** — upgraded to mempalace 3.10.0 on 2026-09-22T14:42:53Z (uv + tool, hot backup first, 3 s downtime, 40996 embedding rows before == after). + Client-visible: `event_list` without a cursor now returns newest first; + search results carry `filed_at` / `authored_at_source` / `content_date`. + Recorded here because v1.9.4's "still open" listed it. + +--- + ## v1.9.4 — 2026-09-22 ### Dependency audit (2026-09-22) diff --git a/Dockerfile.variant b/Dockerfile.variant index fdc816c..c92af41 100644 --- a/Dockerfile.variant +++ b/Dockerfile.variant @@ -314,6 +314,22 @@ RUN set -e && \ echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)" && \ echo "pi-atelier at $(cd /opt/pi-atelier && git rev-parse --short HEAD) (${PI_ATELIER_VERSION})" +# ── git: let the unprivileged user read the root-owned /opt clones ────────── +# The clones above (and /opt/mempalace-toolkit from the base, /opt/pi-studio in +# the studio variant) are root-owned; the container runs as `developer`. Since +# git 2.35.2 (CVE-2022-24765) any git command in a repo owned by another user +# fails with "dubious ownership" — so `git -C /opt/pi-extensions rev-parse` +# returns nothing, `install.sh` used to abort on it, and acceptance checks that +# read a baked ref via git silently measured "" (v1.9.4 first-boot run: one +# false FAIL from exactly this). Listing the paths (not `*`) keeps the check +# meaningful for everything else, e.g. the virtiofs-mounted /workspace. +# Entries for paths absent in a variant (pi-studio) are inert. +RUN for d in pi-toolkit pi-extensions pi-fork pi-observational-memory pi-atelier \ + mempalace-toolkit pi-studio; do \ + git config --system --add safe.directory "/opt/${d}"; \ + done && \ + git config --system --get-all safe.directory + # ── Image-baked skill refresh: pi-extensions (Option 1 over Option 2) ── # rootfs ships a VENDORED snapshot of the pi-extensions skill at # /usr/local/share/pi-devbox/skills/pi-extensions/ (the "floor" — guarantees the diff --git a/entrypoint-user.sh b/entrypoint-user.sh index e84c2e5..5235049 100755 --- a/entrypoint-user.sh +++ b/entrypoint-user.sh @@ -108,7 +108,13 @@ if command -v mempalace &>/dev/null && [ -d /workspace ]; then # own resolution can no longer disagree about where the palace lives — a # disagreement that would make this branch fire on every start. PALACE_DIR="${MEMPALACE_CONFIG_DIR:-${HOME}/.mempalace}" - if [ ! -d "$PALACE_DIR/palace" ]; then + # Sentinel = config.json, because that is what `mempalace init` writes. + # It does NOT create palace/ — mining does — so the earlier test on palace/ + # re-fired on every start of a container that had never mined locally + # (v1.9.4 acceptance: 1 "Initializing" line after one boot, 2 after a + # restart). Harmless (init is idempotent) but the log lied. Populated + # volumes (config.json present) skip either way. + if [ ! -f "$PALACE_DIR/config.json" ]; then echo "Initializing MemPalace for workspace (non-interactive)..." #