Skip to main content

max / makenotwork

sando: correct the self-update source doc, and warn off is-active polling deploy/README.md still described the pre-0.2.9 design: it said the bare repo advanced because a server release had already fetched the sha, which is the coupling 0.2.9 removed. sandod now fetches the deploy branch itself at the top of the /self-update handler. The shell script's header was updated in that pass and the README was not, so the two disagreed. Also record the polling trap that cost a misread during the 0.2.9 deploy: sando-update@<sha> reports activating for the whole build, so a naive until-not-is-active loop reads a running build as a finished one. And read the trigger sha off the remote rather than the working copy, since it is the remote that sandod fetches. Drop an unused mut in the page_smoke gate.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-21 23:18 UTC
Signed with PGP, not checked
Commit: 6e9eb7f5160caec705b0bf996c4bd04da5f72fc6
Parent: a52ec57
4 files changed, +88 insertions, -76 deletions
M sando/Cargo.lock +68 -68
@@ -1227,7 +1227,7 @@
1227 1227
1228 1228 [[package]]
1229 1229 name = "sando-daemon"
1230 - version = "0.2.9"
1230 + version = "0.2.10"
1231 1231 dependencies = [
1232 1232 "anyhow",
1233 1233 "async-trait",
@@ -2266,17 +2266,81 @@
2266 2266 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
2267 2267
2268 2268 [[patch.unused]]
2269 - name = "docengine"
2270 - version = "0.7.0"
2269 + name = "quasi-axum"
2270 + version = "0.46.0"
2271 +
2272 + [[patch.unused]]
2273 + name = "quasi-basics"
2274 + version = "0.46.0"
2275 +
2276 + [[patch.unused]]
2277 + name = "quasi-http"
2278 + version = "0.46.0"
2279 +
2280 + [[patch.unused]]
2281 + name = "quasi-immediate"
2282 + version = "0.46.0"
2283 +
2284 + [[patch.unused]]
2285 + name = "quasi-notifs"
2286 + version = "0.46.0"
2287 +
2288 + [[patch.unused]]
2289 + name = "quasi-router"
2290 + version = "0.46.0"
2291 +
2292 + [[patch.unused]]
2293 + name = "quasi-store"
2294 + version = "0.1.0"
2295 +
2296 + [[patch.unused]]
2297 + name = "quasi-tauri"
2298 + version = "0.46.0"
2299 +
2300 + [[patch.unused]]
2301 + name = "quasi-webview"
2302 + version = "0.46.0"
2303 +
2304 + [[patch.unused]]
2305 + name = "quasi-type"
2306 + version = "0.1.0"
2271 2307
2272 2308 [[patch.unused]]
2273 2309 name = "synckit-client"
2274 - version = "0.8.0"
2310 + version = "0.8.1"
2275 2311
2276 2312 [[patch.unused]]
2277 2313 name = "synckit-config"
2278 2314 version = "0.2.0"
2279 2315
2316 + [[patch.unused]]
2317 + name = "makeover-build"
2318 + version = "0.48.0"
2319 +
2320 + [[patch.unused]]
2321 + name = "makeover-immediate"
2322 + version = "0.31.0"
2323 +
2324 + [[patch.unused]]
2325 + name = "makeover-layout"
2326 + version = "0.32.0"
2327 +
2328 + [[patch.unused]]
2329 + name = "makeover-touch"
2330 + version = "0.22.1"
2331 +
2332 + [[patch.unused]]
2333 + name = "makeover-tui"
2334 + version = "0.31.0"
2335 +
2336 + [[patch.unused]]
2337 + name = "makeover-webview"
2338 + version = "0.54.0"
2339 +
2340 + [[patch.unused]]
2341 + name = "docengine"
2342 + version = "0.7.0"
2343 +
2280 2344 [[patch.unused]]
2281 2345 name = "kberg"
2282 2346 version = "0.1.0"
@@ -2288,67 +2352,3 @@
2288 2352 [[patch.unused]]
2289 2353 name = "tagtree"
2290 2354 version = "0.4.0"
2291 -
2292 - [[patch.unused]]
2293 - name = "quasi-type"
2294 - version = "0.1.0"
2295 -
2296 - [[patch.unused]]
2297 - name = "quasi-axum"
2298 - version = "0.44.0"
2299 -
2300 - [[patch.unused]]
2301 - name = "quasi-basics"
2302 - version = "0.44.0"
2303 -
2304 - [[patch.unused]]
2305 - name = "quasi-http"
2306 - version = "0.44.0"
2307 -
2308 - [[patch.unused]]
2309 - name = "quasi-immediate"
2310 - version = "0.44.0"
2311 -
2312 - [[patch.unused]]
2313 - name = "quasi-notifs"
2314 - version = "0.44.0"
2315 -
2316 - [[patch.unused]]
2317 - name = "quasi-router"
2318 - version = "0.44.0"
2319 -
2320 - [[patch.unused]]
2321 - name = "quasi-store"
2322 - version = "0.1.0"
2323 -
2324 - [[patch.unused]]
2325 - name = "quasi-tauri"
2326 - version = "0.44.0"
2327 -
2328 - [[patch.unused]]
2329 - name = "quasi-webview"
2330 - version = "0.44.0"
2331 -
2332 - [[patch.unused]]
2333 - name = "makeover-build"
2334 - version = "0.47.0"
2335 -
2336 - [[patch.unused]]
2337 - name = "makeover-immediate"
2338 - version = "0.30.0"
2339 -
2340 - [[patch.unused]]
2341 - name = "makeover-layout"
2342 - version = "0.31.0"
2343 -
2344 - [[patch.unused]]
2345 - name = "makeover-touch"
2346 - version = "0.22.0"
2347 -
2348 - [[patch.unused]]
2349 - name = "makeover-tui"
2350 - version = "0.30.0"
2351 -
2352 - [[patch.unused]]
2353 - name = "makeover-webview"
2354 - version = "0.52.0"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "sando-daemon"
3 - version = "0.2.9"
3 + version = "0.2.10"
4 4 edition = "2024"
5 5 license = "MIT"
6 6
@@ -36,11 +36,15 @@
36 36
37 37 Three hardening properties gate that install (postmortem 2026-07-09 recovery):
38 38
39 - - **Source = the local bare repo** `/srv/sando/mnw.git`, not a remote fetch. The
40 - self-update sha was already deployed as a server release, so `/rebuild` has
41 - force-updated the bare repo's `main` to include it — the sha and a trusted
42 - provenance ref are both present locally. A remote fetch gave the `sando` user
43 - no git creds and broke the moment git hosting was down.
39 + - **Build source = the local bare repo** `/srv/sando/mnw.git`, never a remote
40 + fetch by the build. Building from `git@ssh.makenot.work` gave the `sando` user
41 + no git creds and broke the moment git hosting was down (postmortem #7).
42 + `sandod` itself fetches the deploy branch into that bare repo at the top of the
43 + `/self-update` handler, so a sha pushed minutes ago is reachable. Until 0.2.9
44 + the repo advanced only when a *server* build fetched during `/rebuild`, which
45 + chained the controller's currency to the server's release cadence and made a
46 + controller fix unshippable while the server was red. `sandod` remains the only
47 + writer of that repo, so the provenance seal below is unchanged.
44 48 - **Clean build** (`cargo clean` first) so no stale incremental object survives
45 49 across shas. A reused pre-`node_health` `Gate` enum object once produced a
46 50 binary that crash-looped on the current config.
@@ -58,6 +62,11 @@
58 62 reports the running controller's package version (distinct from the tier
59 63 versions, which are the deployed product).
60 64
65 + Do **not** poll `systemctl is-active sando-update@<sha>` for that. The unit
66 + reports `activating` for the whole build, so `until ! systemctl is-active ...`
67 + exits immediately and reads a running build as a finished one. Poll
68 + `ActiveState` explicitly, or poll `/state` for the `sandod_version` you expect.
69 +
61 70 One-time install (as root), then every future controller deploy is one API call:
62 71
63 72 ```sh
@@ -74,7 +83,10 @@
74 83 Trigger + watch:
75 84
76 85 ```sh
77 - SHA=$(git -C ~/Code/MNW rev-parse HEAD)
86 + # The sha must be on the canonical remote's main — that is what sandod fetches
87 + # and what the updater's ancestry check seals against. Read it from the remote,
88 + # not from the working copy, which may be ahead of what was pushed.
89 + SHA=$(git -C ~/Code/MNW ls-remote mnw main | cut -f1)
78 90 curl -sS -X POST "$BASE/self-update" -H 'Content-Type: application/json' \
79 91 -H "Authorization: Bearer $SANDO_API_TOKEN" -d "{\"sha\":\"$SHA\"}"
80 92 journalctl -u "sando-update@$SHA" -f
@@ -2205,7 +2205,7 @@
2205 2205 };
2206 2206
2207 2207 let ceiling = std::time::Duration::from_secs(ctx.cfg.gate_timeout_secs);
2208 - let mut child = tokio::process::Command::new("sh")
2208 + let child = tokio::process::Command::new("sh")
2209 2209 .arg("-c")
2210 2210 .arg(cmd)
2211 2211 .env("BASE", base)