Captures the three commits this release carries, the FIPS 0.4.1 upgrade state (2 nodes done, fleet not rolled), the peer-files diagnosis including what is explicitly NOT explained, the open decisions not taken, and the exact release + ISO ritual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
Handoff — 2026-07-20 — peer-files diagnosis, FIPS 0.4.1, mobile transport pill
Written for a fresh session that will cut the OTA release and build the ISO.
Everything below is already committed and pushed to gitea-ai/main. Last release
was v1.7.105-alpha (e2f83c01); the next one should be v1.7.106-alpha.
1. What this release carries (3 commits on top of v1.7.105-alpha)
| Commit | What | User-visible? |
|---|---|---|
9e3ac9ba |
Show the FIPS/Tor transport pill on mobile peer files | Yes |
3ab7fb52 |
Log the full anyhow error chain on RPC failures | No (diagnostics) |
5fd0d6c3 |
Generate fips.yaml from typed structs + enable mDNS LAN discovery |
Indirectly |
9e3ac9ba — mobile transport pill
PeerFiles.vue:15 wraps the peer title in hidden md:block (the global header
carries the name on mobile), and the transport pill was nested inside it — so it
vanished below 768px. Added a separate md:hidden pill next to the peer icon.
Frontend was rebuilt and the class verified present in the emitted bundle.
Caveats worth knowing (pre-existing, not introduced here):
- On this code path the backend only ever emits
fipsortor, so themeshandlanbranches intransportPill(PeerFiles.vue:609-627) are dead. - For received mesh messages,
mesh/mod.rs:1519-1533falls back to a hardcoded"tor"when the transport is unknown — that pill can genuinely lie. The peer-files pill does not.
3ab7fb52 — full error chain in logs
api/rpc/mod.rs:441 logged only the outermost anyhow context, so every
peer-files failure read exactly RPC error on content.browse-peer: Failed to connect to peer with the real cause discarded. Now {:#}. The client-facing
message still goes through sanitize_error_message(&e.to_string()) ({}), so
no internal detail leaks. This fix applies to every RPC method, not just
browse-peer.
5fd0d6c3 — typed FIPS config + mDNS
fips/config.rs built /etc/fips/fips.yaml by format!-ing a string literal.
Upstream's config structs are #[serde(deny_unknown_fields)], so a wrong key
does not degrade — the daemon refuses to start and the node leaves the mesh.
Now a typed serde struct tree, verified field-by-field against jmcorgan/fips
v0.4.1, with 4 tests: exact-output snapshot, determinism, mDNS key path, and
the pre-existing schema test. All pass.
Also enables node.discovery.lan.enabled (mDNS/DNS-SD, new upstream in v0.4.0)
so co-located nodes peer directly instead of depending on the public anchor.
⚠️ Expected one-time behaviour on first boot after this lands: the startup drift check at
server.rs:864compares the freshly rendered config against what's on disk. The render differs now, so it reinstalls the config and restarts the FIPS daemon once. This is the intended self-healing path and settles immediately. Do not mistake it for a regression.
Emitted unconditionally rather than version-gated: v0.3.0's DiscoveryConfig
has no lan field and no deny_unknown_fields, so v0.3.0 daemons ignore it
harmlessly (verified against the v0.3.0 source). It self-activates on upgrade.
2. FIPS 0.4.1 — validated, but the fleet is NOT rolled
Fleet was on FIPS 0.3.0 / 0.3.0-dev (2026-05-11). Upstream is v0.4.1 (2026-07-19). Verified before touching anything:
- Wire-compatible 0.3.0 → 0.4.0 → 0.4.1. Rolling upgrade, any order, no flag day.
- Config forward-compatible — every key we emit exists in 0.4.1.
- Asset names match what
fips/update.rsexpects (fips_<ver>_<arch>.deb+checksums-linux.txt), so the in-product updater should work.
Upgraded so far (2 of N)
| Node | Before | After | Result |
|---|---|---|---|
OptiPlex .198 / 100.114.134.21 |
0.3.0-dev-1 |
0.4.1 | ✅ anchor connected, is_parent: true, tree depth: 4 |
| thinkpad (this machine) | 0.3.0 |
0.4.1 | ✅ service active, but still islanded (see §4) |
The OptiPlex was still running the old string-rendered config and 0.4.1 accepted it — empirical confirmation of the compat analysis, not just desk work.
Upgrade recipe (nodes cannot reach GitHub — sideload)
# 1. On a host with GitHub access:
curl -sL -o fips_0.4.1_amd64.deb \
https://github.com/jmcorgan/fips/releases/download/v0.4.1/fips_0.4.1_amd64.deb
curl -sL -o checksums-linux.txt \
https://github.com/jmcorgan/fips/releases/download/v0.4.1/checksums-linux.txt
sha256sum fips_0.4.1_amd64.deb # must match checksums-linux.txt
# expected: 9befcc0990c7e08742b5a88f75d753a1088134b20525156688d559a317334ded
# 2. Sideload:
scp fips_0.4.1_amd64.deb archipelago@<node>:/tmp/
# 3. On the node — the same command update.rs uses:
sudo -n systemd-run --collect --wait --quiet --pipe -- \
env DEBIAN_FRONTEND=noninteractive dpkg --force-confold --force-downgrade -i \
/tmp/fips_0.4.1_amd64.deb
# 4. Restart the ACTIVE unit — it is archipelago-fips.service,
# NOT fips.service (which is inactive on these nodes):
sudo -n systemctl restart archipelago-fips.service
# 5. Verify:
fipsctl --version
sudo -n fipsctl show links # expect anchor 185.18.221.160:8443 connected
sudo -n fipsctl show tree # expect is_root: false, depth > 0
ISO implication (important)
image-recipe/build/auto-installer/Dockerfile.rootfs:23 builds FIPS from
unpinned upstream main (git clone --depth 1, no rev/tag/checksum, amd64
only). So a freshly built ISO will pick up whatever main is that day — probably
≥0.4.1, but it is not deterministic. Pinning is an open item in
docs/1.8.0-RELEASE-HARDENING-PLAN.md:319-322. Consider pinning to v0.4.1
before building the release ISO so the shipped version is knowable.
3. The original bug — peer cloud files not loading
Status: root-caused for the thinkpad; NOT fully explained. Being explicit because it would be easy to read this as closed.
What is established:
- FIPS was fully down on the thinkpad:
fipsctl show peers→[],show links→[],show tree→is_root: true, depth 0. An island. - Cause is network egress, not FIPS config: the thinkpad cannot reach the
public anchor
185.18.221.160(fips.v0l.io) at all — 100% packet loss on ICMP, 443/8443/8668 all time out.show transportsshowedpackets_sent: 760, packets_recv: 0on both UDP and TCP. - Local firewall is not the cause (nft/iptables policy
accept; only stock Tailscale anti-spoof DROPs). - The OptiPlex, on the same
/24, reaches the anchor fine → it's the thinkpad's WiFi segment (wlp3s0), which also blocks L2 to.198(ip neigh→FAILED). - With no FIPS tree, everything falls back to Tor. Every peer in
federation/nodes.jsonreadslast_transport: "tor", never"fips". - Tor itself is healthy: fetched the OptiPlex's
/contentover Tor 3×, HTTP 200 in 4.1–8.5s — well inside the 30s budget atcontent.rs:349.
What is not established: why three specific content.browse-peer calls
failed today (05:25, 16:37, 16:43 UTC). Tor tested healthy and was never
reproduced. Two hypotheses were tested and disproved: the Tor fallback logic
is correct (FIPS-unreachable returns None and falls through in Auto mode), and
the legs get independent timeouts (Tor gets a fresh 30s). Best remaining guess is
cold-circuit timeouts on first fetch after idle — a guess, not a finding.
3ab7fb52 means the next occurrence will log the actual cause.
Corrections to earlier claims in this session
- "Point FIPS at the Tailscale IP" was wrong. FIPS routes by npub; the
ip:portinfipsctl connectis only an underlay endpoint hint. - "The public anchor may be dead fleet-wide" was wrong. Its peer is healthy
(
delivery_ratio1.0 both directions, bloom filter syncing). Thebytes_recv: 0link counters are simply uninstrumented in 0.3.0.
4. Open items — decisions NOT taken
- Second FIPS anchor (user asked for this; not built). Needs a host running
FIPS that is reachable from the restricted WiFi. Candidate found: OVH
146.59.87.168— pings fine from the thinkpad and general egress works (github 200), while the upstream anchor fails even ICMP there. But it does not run FIPS yet, so this means installing FIPS on the box that hosts Gitea — a production change, deliberately not made unprompted. Code side is easy after:fips/anchors.rs:47-50is a single hardcoded anchor that should become a list (default_public_anchor()→default_public_anchors() -> Vec<SeedAnchor>). - Fleet rollout of FIPS 0.4.1 — only 2 nodes done.
.228(100.64.204.114) has been offline ~20h and could not be included. - Deploying the archipelago binary carrying
5fd0d6c3— no node has it yet, so mDNS is not actually live anywhere. That is what this OTA is for. - mDNS caveat: on the thinkpad's WiFi, multicast may also be blocked, so mDNS may not rescue that particular node even after the OTA. It will help co-located nodes on sane networks.
- Pin FIPS in the ISO build (see §2) — recommended before the release ISO.
5. Release ritual (from prior sessions — follow exactly)
Working tree at handoff had pre-existing unrelated dirt: core/Cargo.lock,
release-manifest.json, releases/manifest.json modified, and an untracked
neode-ui/vite.preview.config.mts. Stage explicitly by path — another
agent may share this tree; never git add -A.
V=1.7.106-alpha
# Frontend build — MUST verify dist actually changed (build can silently no-op)
cd neode-ui && npm run build # → web/dist/neode-ui/
grep -r "md:hidden" ../web/dist/neode-ui/assets/PeerFiles-*.js # sanity
# Backend
cd core && cargo build --release -p archipelago
# If you hit `rust-lld: undefined hidden symbol`, it's incremental-cache
# corruption — rebuild with CARGO_INCREMENTAL=0
# Tarball MUST be flat (files at root, no neode-ui/ wrapper) or every fleet UI 403s
tar -czf releases/v$V/archipelago-frontend-$V.tar.gz -C web/dist/neode-ui .
tar -tzf releases/v$V/archipelago-frontend-$V.tar.gz | head -3 # ./ then ./index.html
# Exclude the ~17MB companion APK from tarballs.
# Ship
scripts/create-release.sh $V
scripts/publish-release-assets.sh $V gitea-vps2
git push origin main && git push origin --tags # tag or the Releases page stays empty
git push gitea-ai main # main is protected; use the `ai` account
# Verify the live manifest
curl -fsS http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json
Notes: vps2 (146.59.87.168) is the primary OTA manifest host. Signing is
done at the user's TTY — do not attempt it unattended. Clean /tmp first
(past releases hit ENOSPC). Changelogs must be layman-readable, leading with
user benefit.
ISO
UNBUNDLED=1 bash image-recipe/build-debian-iso.sh
ISO builds are always unbundled — the default env silently builds the wrong
full-bundle variant. Only filebrowser + fmcd are baked in. Verify the output
filename contains unbundled and is ≈2.4G. The ISO's frontend source is
/opt/archipelago/web-ui — rsync dist there first and verify inside the ISO.
6. Node access quick reference
- thinkpad (
.116) is the local machine — do not SSH to it; readjournalctl -u archipelagoand/var/lib/archipelago/**directly. - OptiPlex
.198= Tailscalearchipelago-5/100.114.134.21, userarchipelago. Its LAN IP is unreachable from the thinkpad — use Tailscale. .228=archipelago-2/100.64.204.114— offline as of 2026-07-20, and it is in real use; don't touch uninvited.archipelago-1(100.82.34.38) is a Ryzen AI Max desktop, not the OptiPlex.- Nodes have no
sqlite3— usesudo -n python3to read the JSON stores. fipsctlneedssudo -n(socket isroot:fips0660).- Never run
archipelago --versionon fleet nodes (deployed binaries predate #74).