fix(ssh): wire git core.sshCommand to the writable sidecar, and assert it
Lint / hadolint (push) Successful in 13s
Lint / skill-floor (push) Successful in 14s
Lint / doc-drift (push) Successful in 18s
Lint / actionlint (push) Successful in 29s

~/.ssh is commonly bind-mounted READ-ONLY from the host, so a per-host
`ControlPath ~/.ssh/cm/%r@%h:%p` — the standard CGNAT multiplexing recipe, and
correct on the host — resolves inside an unwritable dir in the container. Every
push dies `unix_listener: cannot bind to path ...: Read-only file system`,
behind git's misleading "make sure you have the correct access rights".

setup-lan-access.sh already writes the fix: ~/.ssh-local/config overrides
ControlPath into the writable ~/.ssh-local/cm BEFORE `Include ~/.ssh/config`,
so -F repairs the socket path and keeps every per-host User/Port/IdentityFile.
entrypoint-user.sh now points git at it, guarded on the sidecar existing —
setup-lan-access.sh writes none on native Linux Docker, where -F at a missing
file would break every git-over-ssh call instead of fixing one. An existing
core.sshCommand is left alone (first-wins, as for the three git settings above).

Why code and not another doc line: the remedy was already in the global
AGENTS.md, in pi-devbox-environment SKILL.md §3, in 24 MemPalace drawers from
three devices, and printed verbatim by recreate-sanity-check.sh — and an agent
that had run that script two hours earlier still hit the failure and reinvented
a /tmp/sshcm workaround. A fifth copy was not the missing piece.

Assertions, each where it can actually pass:
- smoke-test.sh: two STATIC greps (wiring line + its [ -r ] guard). `run` uses
  --entrypoint="", so asserting the runtime value there would repeat the v1.8.0
  mistake of an assertion that cannot pass, unvalidated until the next tag.
- smoke-test.sh runtime phase: a BICONDITIONAL — sidecar present => must route
  through it; absent => must be unset. The absent arm is the one CI exercises
  (native Linux runner), so "is set" would have failed CI for a correct image.
- recreate-sanity-check.sh: the runtime assertion, plus an explicit fail for the
  inverted state (set while the sidecar is missing). The permanent "default ssh
  precedence" warning keeps its severity but now states that it is structural and
  can never reach zero, and whether git is wired, unwired, or has no sidecar.

All five arms exercised against the real script before commit; that caught a
defect in the first draft, which reported "git IS wired ... unaffected" about a
state where the sidecar was gone and every git-over-ssh call failed.

Host ~/.ssh/config needs no change: the same line is right on the host and
unusable through a read-only mount, so the fix belongs in the container layer.
This commit is contained in:
2026-09-22 23:08:59 +02:00
parent 37fcbfcf04
commit 2278b22ba7
4 changed files with 186 additions and 4 deletions
+63 -3
View File
@@ -14,9 +14,10 @@ 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.
hash prediction, none release-worthy on its own, plus one boot-time wiring fix
that stops a recurring `git push` failure inside the container. 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
@@ -26,6 +27,37 @@ hash prediction, none release-worthy on its own. Nothing here changes a pin.
### Fixed
- **`git push` from inside the container dies on a read-only ControlPath, and no
amount of documentation was fixing it** — `entrypoint-user.sh` now sets
`core.sshCommand` to `ssh -F $HOME/.ssh-local/config` when that sidecar exists.
`~/.ssh` is commonly bind-mounted **read-only** from the host, so a per-host
`ControlPath ~/.ssh/cm/%r@%h:%p` — the standard CGNAT multiplexing recipe, and
correct *on the host* — resolves inside an unwritable directory here. Every
push then dies `unix_listener: cannot bind to path ~/.ssh/cm/...: Read-only
file system`, hidden behind git's misleading `Please make sure you have the
correct access rights`, which sends the reader hunting for a key problem that
does not exist. `setup-lan-access.sh` already wrote the writable sidecar, whose
leading `Host *` block overrides `ControlPath` into `~/.ssh-local/cm` and only
*then* `Include`s the user's own config, so `-F` repairs the socket path while
keeping every per-host `User`/`Port`/`IdentityFile`. Measured on tor-ms22
(2026-09-22): `ssh -G -F` yields `user gitea`, `port 22`, `egl_ed25519`
inherited from the host's own block, with a writable ControlPath; a master
socket is created and multiplexing is preserved, which is the whole point of
the host's setting.
The reason this is code and not a doc line: the remedy was already in the
global `AGENTS.md`, in `pi-devbox-environment` SKILL.md §3, in 24 MemPalace
drawers from three devices, **and printed verbatim by
`recreate-sanity-check.sh`** — and an agent that had run that script two hours
earlier still hit the failure and reinvented a `/tmp/sshcm` workaround. A fifth
copy of the text was not the missing piece; removing the need to know was.
The `[ -r ]` guard is load-bearing: `setup-lan-access.sh` writes no sidecar on
native Linux Docker, and `-F` at a missing file would break *every*
git-over-ssh call instead of fixing one. An existing `core.sshCommand` is left
alone, matching the first-wins convention of the three git settings above it.
Host configs need no change — the same line that is right on the host is simply
unusable through a read-only mount, so the fix belongs in the container layer.
- **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
@@ -75,6 +107,34 @@ hash prediction, none release-worthy on its own. Nothing here changes a pin.
scoped to that one `sort`, not to the shell, so it leaves `sv_SE.UTF-8`
everywhere else alone.
- **`smoke-test.sh` + `recreate-sanity-check.sh` assert the sidecar wiring, and
the permanent ControlPath warning now says whether it matters** — three
assertions, each placed where it can actually pass. `smoke-test.sh` gets two
**static** greps against the image's `entrypoint-user.sh` (the wiring line, and
the `[ -r ]` guard around it), because `run` executes
`docker run --entrypoint=""`, so the entrypoint never runs there and asserting
the runtime *value* would repeat the v1.8.0 mistake of an assertion that cannot
pass, unvalidated until the next tag. Its runtime-deployment phase gets a
**biconditional**: sidecar present ⇒ `core.sshCommand` must route through it;
sidecar absent ⇒ it must be unset. Both arms are real, and the absent arm is
the one CI exercises, since a Gitea runner is native Linux Docker where
`setup-lan-access.sh` writes no sidecar — so "is set" would have failed CI for a
correct image.
`recreate-sanity-check.sh` gets the runtime assertion proper, with an explicit
`fail` for the inverted state (`core.sshCommand` set while the sidecar is
missing — which breaks every git-over-ssh call). The existing
"default ssh precedence" warning is unchanged in severity but now states its own
scope: it is **structural and can never reach zero** while a bind-mounted
config pins `ControlPath` inside the read-only `~/.ssh`, and it reports whether
git is wired, not wired, or has no sidecar to point at. That matters because the
script's own comment already warned that "a check that fires benignly every time
is one you learn to ignore" — and on 2026-09-22 that is exactly what happened,
with the remedy sitting inside the dismissed line. All five arms were exercised
against the real script before commit; doing so caught a defect in the first
draft, which reported "git IS wired … unaffected" about a state where the
sidecar was gone and every git-over-ssh call failed.
### Still open
- **pi 0.87.x + pi-observational-memory** — unchanged from v1.9.4: blocked on