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
+33
View File
@@ -250,6 +250,22 @@ run_expect "remote-palace-without-inbox skip is announced, not silent" \
"MemPalace catch-up skipped"
run "...and the skip notice names the variable that fixes it" \
"grep -A6 'MemPalace catch-up skipped' /usr/local/bin/entrypoint-user.sh | grep -q 'MEMPALACE_PI_SSH_TARGET'"
# git-over-ssh must be wired to the writable ssh sidecar, because ~/.ssh is
# commonly bind-mounted READ-ONLY and a per-host `ControlPath ~/.ssh/cm/...`
# inherited from it kills every push with `unix_listener: cannot bind ...:
# Read-only file system` behind git's misleading "correct access rights".
#
# STATIC assertion, deliberately. `run` executes `docker run --entrypoint=""`, so
# entrypoint-user.sh never runs here and `git config --global core.sshCommand` is
# necessarily unset — asserting the VALUE in this harness would repeat the v1.8.0
# mistake (an assertion that cannot pass, unvalidated until the next tag). What
# IS checkable at build time is that the wiring code shipped in the image. The
# runtime value is asserted below in the Runtime deployment section, where the
# real entrypoint chain has run.
run "entrypoint wires git core.sshCommand to the ssh sidecar" \
"grep -q 'core.sshCommand \"ssh -F' /usr/local/bin/entrypoint-user.sh"
run "...and guards it on the sidecar existing (native Linux has none)" \
"grep -B2 'core.sshCommand \"ssh -F' /usr/local/bin/entrypoint-user.sh | grep -q '\\[ -r \"\$HOME/.ssh-local/config\" \\]'"
# A remote mine that FAILS must not report success. MCP answers a hard tool
# failure with HTTP 200 and the tool's own JSON escaped inside
# result.content[].text, so the feeder's old `'\"error\"' in body` check could
@@ -659,6 +675,23 @@ exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/sett
exec_test "pi-devbox-environment skill linked" 'test -L $HOME/.agents/skills/pi-devbox-environment && test -f $HOME/.agents/skills/pi-devbox-environment/SKILL.md && echo ok'
exec_test "pi-extensions skill linked (fallback)" 'test -L $HOME/.agents/skills/pi-extensions && test -f $HOME/.agents/skills/pi-extensions/SKILL.md && echo ok'
exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills/mempalace && test -f $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
# git-over-ssh sidecar wiring, asserted as a BICONDITIONAL rather than "is set".
# setup-lan-access.sh writes ~/.ssh-local/config only on VM-backed hosts, and a
# CI runner is native Linux Docker — so "core.sshCommand is set" would fail here
# for a correct image, which is precisely the v1.8.0 trap (an assertion whose
# environment was never checked, unvalidated until the next tag). Both arms are
# real: sidecar present => must route through it; sidecar absent => must be UNSET,
# because -F pointing at a missing file breaks every git-over-ssh call and is
# worse than the problem being fixed. This arm is the one CI actually exercises,
# so CI validates the guard; the other is covered by the static greps above and
# by scripts/recreate-sanity-check.sh on a real device.
exec_test "git core.sshCommand matches sidecar presence" '
cmd=$(git config --global --get core.sshCommand 2>/dev/null || true)
if [ -r "$HOME/.ssh-local/config" ]; then
case "$cmd" in *"-F $HOME/.ssh-local/config"*) echo "wired: $cmd" ;; *) exit 1 ;; esac
else
[ -z "$cmd" ] || exit 1; echo "no sidecar, correctly unset"
fi'
# The vendored mempalace snapshot is refreshed MANUALLY per release (see
# rootfs/usr/local/share/pi-devbox/skills/VENDORED.md). Through v1.8.4 it also
# silently SHADOWED the live skillset copy, so staleness was invisible — and the