smoke: fix the snapshot canary that blocked v1.8.7, and make it bidirectional
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 27s
Publish Docker Image / resolve-versions (push) Successful in 1m5s
Publish Docker Image / base-decide (push) Successful in 12s
Publish Docker Image / build-base (push) Successful in 41m8s
Publish Docker Image / smoke (push) Successful in 4m49s
Publish Docker Image / smoke-studio (push) Successful in 18m28s
Publish Docker Image / build-variant (push) Successful in 15m46s
Publish Docker Image / promote-base-latest (push) Successful in 11s
Publish Docker Image / update-description (push) Successful in 20s
Publish Docker Image / build-variant-studio (push) Successful in 16m52s
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 27s
Publish Docker Image / resolve-versions (push) Successful in 1m5s
Publish Docker Image / base-decide (push) Successful in 12s
Publish Docker Image / build-base (push) Successful in 41m8s
Publish Docker Image / smoke (push) Successful in 4m49s
Publish Docker Image / smoke-studio (push) Successful in 18m28s
Publish Docker Image / build-variant (push) Successful in 15m46s
Publish Docker Image / promote-base-latest (push) Successful in 11s
Publish Docker Image / update-description (push) Successful in 20s
Publish Docker Image / build-variant-studio (push) Successful in 16m52s
Run 589 built the base cleanly and then failed both smoke jobs 81-passed/1-failed on 'mempalace skill snapshot is current'. That canary greps a phrase from the vendored mempalace skill to detect a stale snapshot, and the phrase it pinned was 'Attribute what you file yourself' — the heading of the hand-stamping instruction that THIS release withdraws. So it fired correctly: the snapshot changed and the expectation did not. Every publish job was skipped, so nothing reached the registry and v1.8.7 was never consumed. Rather than bump the string: * the assertion is now BIDIRECTIONAL — the new phrase must be present AND the withdrawn one absent. A one-way canary only catches half the drift: it cannot notice a re-vendored stale snapshot that happens to contain the pinned phrase. Verified against v1.8.6's snapshot, which now correctly fails. * the comment records the structural limit rather than just the fix: a phrase canary can only ever detect 'older than what I remembered to pin', never 'older than skillset main'. Only a diff against the skillset repo can do that, which is now a Still-open item — it needs a CI clone credential for a private repo, i.e. a policy decision, not a code change. Changelog consolidated: the SSH sidecar multiplexing default moves from Unreleased into v1.8.7, since the retag will sit on a commit that contains it, and the v1.8.7 summary now records the failed first attempt rather than quietly presenting the second one as the whole story.
This commit is contained in:
+90
-61
@@ -11,67 +11,6 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **The SSH sidecar now defaults to connection multiplexing, without overriding
|
|
||||||
anyone's explicit choice.** `~/.ssh-local/config` already forced `ControlPath`
|
|
||||||
into the writable sidecar dir, but nothing supplied `ControlMaster` for targets
|
|
||||||
coming from the user's own bind-mounted `~/.ssh/config`. An entry that never
|
|
||||||
mentioned it therefore opened a **fresh TCP connection per `ssh` call** — and
|
|
||||||
an agent doing a dozen calls in a few minutes is exactly the traffic shape that
|
|
||||||
trips fail2ban or a CGNAT flow-table cap. Observed 2026-08-25 on this fleet:
|
|
||||||
~12 connections to one host in 15 minutes, after which port 22 stopped
|
|
||||||
answering while HTTPS to the same estate stayed healthy in 0.44 s (that
|
|
||||||
asymmetry is the tell for rate-limiting rather than an outage).
|
|
||||||
|
|
||||||
**The fix is where the block sits, not what it says.** `ssh_config` is
|
|
||||||
first-value-wins, so position encodes intent, and the two settings need
|
|
||||||
opposite treatment:
|
|
||||||
|
|
||||||
| Setting | Position | Meaning | Why |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `ControlPath` | **before** `Include ~/.ssh/config` | override | the user's value points at read-only `~/.ssh`; it cannot work here, so it must lose |
|
|
||||||
| `ControlMaster auto` + `ControlPersist 10m` | **after** the `Include` | default | an explicit per-host `ControlMaster no` must keep winning; we only supply an opinion where the user expressed none |
|
|
||||||
|
|
||||||
*Force what is broken, default what is merely absent.* The first draft of this
|
|
||||||
put both in the leading block, which would have silently overridden an explicit
|
|
||||||
`ControlMaster no` — the counterfactual is in the test below.
|
|
||||||
|
|
||||||
Verified with `ssh -G` (the resolved-config oracle) rather than by reading the
|
|
||||||
man page, against a fixture with one host set to `no`, one silent, one set to
|
|
||||||
`auto`: the explicit `no` resolves to `controlmaster false` **and** still gets
|
|
||||||
the writable `ControlPath`, the silent host resolves to `auto`, and the same
|
|
||||||
fixture under the rejected layout flips the `no` host to `auto` — so the test
|
|
||||||
discriminates the *position*, not merely the presence of the block. Then
|
|
||||||
end-to-end: the real script rendered in a sandbox `HOME`, block last, `bash -n`
|
|
||||||
clean, `shellcheck -S error` clean (the gate added in v1.8.7).
|
|
||||||
|
|
||||||
Measured effect on the author's own config (41 host aliases): **22 were silent
|
|
||||||
about `ControlMaster` and gain `auto` + 10 m persist; 0 are overridden**, since
|
|
||||||
the fleet contains no explicit `no`. Worth noting *how* the one deliberate
|
|
||||||
exception is written — `proxmox002-vpn` carries `# No ControlMaster — VPN means
|
|
||||||
direct route, no CGNAT flow cap`, i.e. the intent is expressed as **absence
|
|
||||||
plus a comment**, which `ssh` cannot distinguish from "no opinion". That host
|
|
||||||
does now get multiplexing; its comment says multiplexing is *unnecessary*
|
|
||||||
there, not harmful. Anything that must stay unmultiplexed needs a literal
|
|
||||||
`ControlMaster no`.
|
|
||||||
|
|
||||||
Why the ordering matters beyond this one config: `~/.ssh/config` is
|
|
||||||
**per-machine**, differs across the fleet, and future machines' versions do not
|
|
||||||
exist yet to be audited. A default-not-override design is correct without
|
|
||||||
needing to inspect any of them.
|
|
||||||
|
|
||||||
`ControlPersist` is deliberately short (10 m idle, and each new session resets
|
|
||||||
the idle timer — long enough to collapse an agent's burst, short enough that an
|
|
||||||
abandoned socket ages out). A per-host entry that sets its own value keeps it:
|
|
||||||
hosts already specifying `ControlPersist 4h` still resolve to 4 h. The known
|
|
||||||
cost of multiplexing is the **stale master** — socket present, daemon gone,
|
|
||||||
after a suspend or network change — which makes every later `ssh` to that host
|
|
||||||
hang; recovery is `ssh -F ~/.ssh-local/config -O exit <host>`, now documented
|
|
||||||
in the `pi-devbox-environment` skill along with `-O check`.
|
|
||||||
|
|
||||||
## v1.8.7 — 2026-08-25
|
## v1.8.7 — 2026-08-25
|
||||||
|
|
||||||
Patch release, and the fastest turnaround in the series (~9 h after v1.8.6) for
|
Patch release, and the fastest turnaround in the series (~9 h after v1.8.6) for
|
||||||
@@ -93,6 +32,16 @@ time as usual. **Base rebuild is forced twice over** — `Dockerfile.base` chang
|
|||||||
mempalace-toolkit SHA ("otherwise a toolkit-only fix never lands") — so expect
|
mempalace-toolkit SHA ("otherwise a toolkit-only fix never lands") — so expect
|
||||||
~67 min, and note that either cause alone would have sufficed.
|
~67 min, and note that either cause alone would have sufficed.
|
||||||
|
|
||||||
|
⚠️ **The first tag of this version did not publish.** Run 589 built the base
|
||||||
|
fine, then **both** smoke jobs failed 81-passed/1-failed on a single assertion —
|
||||||
|
`mempalace skill snapshot is current`, a canary pinning a phrase from the
|
||||||
|
vendored skill. The phrase it pinned was the heading of the very instruction this
|
||||||
|
release *withdraws*, so refreshing the snapshot without re-pinning the canary
|
||||||
|
made it fire correctly on a healthy image. Every publish job was skipped, so
|
||||||
|
nothing reached the registry and the version was never consumed; the tag was
|
||||||
|
moved to include the fix below. Fixing the canary is what this release is
|
||||||
|
*for*, in miniature: the gate was right and the expectation was stale.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Palace writes now carry the device that made them, and diary entries say so
|
- **Palace writes now carry the device that made them, and diary entries say so
|
||||||
@@ -191,8 +140,77 @@ mempalace-toolkit SHA ("otherwise a toolkit-only fix never lands") — so expect
|
|||||||
does not exist would repeat the shipped-false-claim mistake corrected below).
|
does not exist would repeat the shipped-false-claim mistake corrected below).
|
||||||
Expect ~67 min, as for v1.8.5/v1.8.6.
|
Expect ~67 min, as for v1.8.5/v1.8.6.
|
||||||
|
|
||||||
|
- **The SSH sidecar now defaults to connection multiplexing, without overriding
|
||||||
|
anyone's explicit choice.** `~/.ssh-local/config` already forced `ControlPath`
|
||||||
|
into the writable sidecar dir, but nothing supplied `ControlMaster` for targets
|
||||||
|
coming from the user's own bind-mounted `~/.ssh/config`. An entry that never
|
||||||
|
mentioned it therefore opened a **fresh TCP connection per `ssh` call** — and
|
||||||
|
an agent doing a dozen calls in a few minutes is exactly the traffic shape that
|
||||||
|
trips fail2ban or a CGNAT flow-table cap. Observed 2026-08-25 on this fleet:
|
||||||
|
~12 connections to one host in 15 minutes, after which port 22 stopped
|
||||||
|
answering while HTTPS to the same estate stayed healthy in 0.44 s (that
|
||||||
|
asymmetry is the tell for rate-limiting rather than an outage).
|
||||||
|
|
||||||
|
**The fix is where the block sits, not what it says.** `ssh_config` is
|
||||||
|
first-value-wins, so position encodes intent, and the two settings need
|
||||||
|
opposite treatment:
|
||||||
|
|
||||||
|
| Setting | Position | Meaning | Why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ControlPath` | **before** `Include ~/.ssh/config` | override | the user's value points at read-only `~/.ssh`; it cannot work here, so it must lose |
|
||||||
|
| `ControlMaster auto` + `ControlPersist 10m` | **after** the `Include` | default | an explicit per-host `ControlMaster no` must keep winning; we only supply an opinion where the user expressed none |
|
||||||
|
|
||||||
|
*Force what is broken, default what is merely absent.* The first draft of this
|
||||||
|
put both in the leading block, which would have silently overridden an explicit
|
||||||
|
`ControlMaster no` — the counterfactual is in the test below.
|
||||||
|
|
||||||
|
Verified with `ssh -G` (the resolved-config oracle) rather than by reading the
|
||||||
|
man page, against a fixture with one host set to `no`, one silent, one set to
|
||||||
|
`auto`: the explicit `no` resolves to `controlmaster false` **and** still gets
|
||||||
|
the writable `ControlPath`, the silent host resolves to `auto`, and the same
|
||||||
|
fixture under the rejected layout flips the `no` host to `auto` — so the test
|
||||||
|
discriminates the *position*, not merely the presence of the block. Then
|
||||||
|
end-to-end: the real script rendered in a sandbox `HOME`, block last, `bash -n`
|
||||||
|
clean, `shellcheck -S error` clean (the gate added in v1.8.7).
|
||||||
|
|
||||||
|
Measured effect on the author's own config (41 host aliases): **22 were silent
|
||||||
|
about `ControlMaster` and gain `auto` + 10 m persist; 0 are overridden**, since
|
||||||
|
the fleet contains no explicit `no`. Worth noting *how* the one deliberate
|
||||||
|
exception is written — `proxmox002-vpn` carries `# No ControlMaster — VPN means
|
||||||
|
direct route, no CGNAT flow cap`, i.e. the intent is expressed as **absence
|
||||||
|
plus a comment**, which `ssh` cannot distinguish from "no opinion". That host
|
||||||
|
does now get multiplexing; its comment says multiplexing is *unnecessary*
|
||||||
|
there, not harmful. Anything that must stay unmultiplexed needs a literal
|
||||||
|
`ControlMaster no`.
|
||||||
|
|
||||||
|
Why the ordering matters beyond this one config: `~/.ssh/config` is
|
||||||
|
**per-machine**, differs across the fleet, and future machines' versions do not
|
||||||
|
exist yet to be audited. A default-not-override design is correct without
|
||||||
|
needing to inspect any of them.
|
||||||
|
|
||||||
|
`ControlPersist` is deliberately short (10 m idle, and each new session resets
|
||||||
|
the idle timer — long enough to collapse an agent's burst, short enough that an
|
||||||
|
abandoned socket ages out). A per-host entry that sets its own value keeps it:
|
||||||
|
hosts already specifying `ControlPersist 4h` still resolve to 4 h. The known
|
||||||
|
cost of multiplexing is the **stale master** — socket present, daemon gone,
|
||||||
|
after a suspend or network change — which makes every later `ssh` to that host
|
||||||
|
hang; recovery is `ssh -F ~/.ssh-local/config -O exit <host>`, now documented
|
||||||
|
in the `pi-devbox-environment` skill along with `-O check`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **The vendored-snapshot canary was one-way, and pinned a phrase the same
|
||||||
|
release deleted.** `mempalace skill snapshot is current` grepped for
|
||||||
|
*"Attribute what you file yourself"* — the heading of the hand-stamping
|
||||||
|
instruction withdrawn above. It therefore did its job (snapshot changed,
|
||||||
|
expectation did not) and blocked an otherwise-green build. Two changes rather
|
||||||
|
than a string bump: the assertion is now **bidirectional** (the new phrase must
|
||||||
|
be present **and** the withdrawn one absent, so a re-vendored *stale* snapshot
|
||||||
|
fails as loudly as a forgotten bump — verified by running it against v1.8.6's
|
||||||
|
snapshot, which correctly fails), and the comment now states the structural
|
||||||
|
limit: a phrase canary can only detect *"older than what I remembered to pin"*,
|
||||||
|
never *"older than skillset main"*.
|
||||||
|
|
||||||
- **Four build-provenance smoke assertions verified the presence of a manifest
|
- **Four build-provenance smoke assertions verified the presence of a manifest
|
||||||
field name and never looked at its value.** The originals were literally:
|
field name and never looked at its value.** The originals were literally:
|
||||||
|
|
||||||
@@ -262,6 +280,17 @@ mempalace-toolkit SHA ("otherwise a toolkit-only fix never lands") — so expect
|
|||||||
|
|
||||||
### Still open
|
### Still open
|
||||||
|
|
||||||
|
- **Make the vendored-snapshot check automatic instead of a remembered string.**
|
||||||
|
Tonight's failure is the third iteration of the same maintenance burden (v1.8.4:
|
||||||
|
phrase present in both copies; v1.8.7: phrase deleted by the release that
|
||||||
|
refreshed the snapshot). A phrase canary structurally cannot answer *"is this
|
||||||
|
snapshot older than skillset main?"* — only a diff can. Proposed: a lint job
|
||||||
|
that clones the skillset repo and compares
|
||||||
|
`rootfs/usr/local/share/pi-devbox/skills/mempalace/SKILL.md` against it,
|
||||||
|
failing with the diff when they drift. Open question first: the skillset repo is
|
||||||
|
**private**, so this needs a CI clone credential, which is a policy decision
|
||||||
|
rather than a code change.
|
||||||
|
|
||||||
- **Provenance stops at Chroma's metadata.** The hourly reconciler on the palace
|
- **Provenance stops at Chroma's metadata.** The hourly reconciler on the palace
|
||||||
host stamps `device`/`agent_kind` in `chroma.sqlite3`, but knowledge-graph
|
host stamps `device`/`agent_kind` in `chroma.sqlite3`, but knowledge-graph
|
||||||
triples and coordination events live in *separate* SQLite files
|
triples and coordination events live in *separate* SQLite files
|
||||||
|
|||||||
+15
-1
@@ -527,7 +527,21 @@ exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills
|
|||||||
# multiple harnesses", a phrase present in BOTH the stale and the fresh copy.
|
# multiple harnesses", a phrase present in BOTH the stale and the fresh copy.
|
||||||
# A snapshot canary must pin the NEWEST section, so update this string whenever
|
# A snapshot canary must pin the NEWEST section, so update this string whenever
|
||||||
# the snapshot is refreshed — that is the point of it.
|
# the snapshot is refreshed — that is the point of it.
|
||||||
exec_test "mempalace skill snapshot is current" 'grep -q "Attribute what you file yourself" $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
#
|
||||||
|
# v1.8.7: this fired for real, and on the release that changed the snapshot. The
|
||||||
|
# pinned phrase was "Attribute what you file yourself", the heading of the
|
||||||
|
# instruction telling agents to hand-stamp added_by — which that same release
|
||||||
|
# WITHDREW (RFC 001 §7.3.2 ranks agent-side stamping worst-possible; the bridge
|
||||||
|
# now does it). So the canary correctly reported "snapshot changed, expectation
|
||||||
|
# did not", and blocked publication of an otherwise-green build (81 passed, 1
|
||||||
|
# failed, twice). Two lessons kept in the assertion itself:
|
||||||
|
# * it is now BIDIRECTIONAL — the new phrase must be present AND the withdrawn
|
||||||
|
# one absent, so a re-vendored stale snapshot fails just as loudly as a
|
||||||
|
# forgotten bump. A one-way canary only catches half the drift.
|
||||||
|
# * a phrase canary can only ever detect "older than what I remembered to pin",
|
||||||
|
# never "older than skillset main". The real fix is a CI job diffing this
|
||||||
|
# file against the skillset repo — see the Unreleased changelog note.
|
||||||
|
exec_test "mempalace skill snapshot is current" 'f=$HOME/.agents/skills/mempalace/SKILL.md; grep -q "Provenance is stamped for you" "$f" && ! grep -q "Attribute what you file yourself" "$f" && echo ok'
|
||||||
# Link TARGETS, not just link existence: with no skillset mounted (as here) the
|
# Link TARGETS, not just link existence: with no skillset mounted (as here) the
|
||||||
# baked tree must be what resolves, for all three vendored skills.
|
# baked tree must be what resolves, for all three vendored skills.
|
||||||
exec_test "vendored skills resolve to the baked tree (no skillset mounted)" \
|
exec_test "vendored skills resolve to the baked tree (no skillset mounted)" \
|
||||||
|
|||||||
Reference in New Issue
Block a user