fix: three v1.9.4-acceptance findings (installer WARN, init sentinel, hash collation)
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Successful in 9s
Lint / actionlint (push) Successful in 20s
Lint / doc-drift (push) Successful in 18s

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.
This commit is contained in:
2026-09-22 17:20:10 +02:00
parent 16fddebd43
commit 37fcbfcf04
4 changed files with 109 additions and 3 deletions
+9 -2
View File
@@ -108,7 +108,14 @@ jobs:
id: compute id: compute
run: | run: |
# Hash inputs that determine the base image's contents. # 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 # Junk filters: __pycache__/*.pyc and macOS metadata are gitignored
# locally but still picked up by `find rootfs -type f` on a clean CI # locally but still picked up by `find rootfs -type f` on a clean CI
# checkout. Exclude them defensively. # checkout. Exclude them defensively.
@@ -120,7 +127,7 @@ jobs:
! -name '*.pyc' \ ! -name '*.pyc' \
! -name '.DS_Store' \ ! -name '.DS_Store' \
! -name '._*' \ ! -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 cat entrypoint.sh entrypoint-user.sh
# mempalace-toolkit is cloned in Dockerfile.base at a ref CI # mempalace-toolkit is cloned in Dockerfile.base at a ref CI
# resolves to a SHA; fold it in so base_tag changes when the # resolves to a SHA; fold it in so base_tag changes when the
+77
View File
@@ -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 ## v1.9.4 — 2026-09-22
### Dependency audit (2026-09-22) ### Dependency audit (2026-09-22)
+16
View File
@@ -314,6 +314,22 @@ RUN set -e && \
echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)" && \ 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})" 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) ── # ── Image-baked skill refresh: pi-extensions (Option 1 over Option 2) ──
# rootfs ships a VENDORED snapshot of the pi-extensions skill at # rootfs ships a VENDORED snapshot of the pi-extensions skill at
# /usr/local/share/pi-devbox/skills/pi-extensions/ (the "floor" — guarantees the # /usr/local/share/pi-devbox/skills/pi-extensions/ (the "floor" — guarantees the
+7 -1
View File
@@ -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 # own resolution can no longer disagree about where the palace lives — a
# disagreement that would make this branch fire on every start. # disagreement that would make this branch fire on every start.
PALACE_DIR="${MEMPALACE_CONFIG_DIR:-${HOME}/.mempalace}" 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)..." echo "Initializing MemPalace for workspace (non-interactive)..."
# </dev/null: mempalace init has an interactive "Mine this directory # </dev/null: mempalace init has an interactive "Mine this directory
# now? [Y/n]" prompt that --yes does not auto-answer in all paths. # now? [Y/n]" prompt that --yes does not auto-answer in all paths.