1052 Commits

Author SHA1 Message Date
Dorian
78e7c59e78 release(v1.6.0-alpha): refresh with bulletproof FIPS + VPN label fix
Supersedes the earlier v1.6.0-alpha artifacts from today (which were
identical to v1.5.0-alpha and only existed for the update-flow smoke
test). This drop actually changes behaviour:

- archipelago-fips auto-activates on startup when fips_key exists; no
  Activate button needed.
- fips_key on-disk format migrated to bech32 nsec; legacy raw-byte
  files from v1.5.0-alpha self-heal when this version reads them.
- fips.yaml schema matches upstream jmcorgan/fips 0.3+.
- VPN status row shows "Not configured" instead of "Starting…" when
  wg0 isn't up — no VPN peer added yet is not a failure state.

New SHA256s + sizes in manifest.json. Fleet nodes .116/.228/.253 will
notice within 30 min (periodic update-check). Also lets .198 self-heal
its crashlooping archipelago-fips when it picks up the update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:13:58 -04:00
Dorian
b643b30bba fix(fips,iso): bulletproof FIPS from install — no Activate button needed
Problems addressed (all observed on .198):
  * fips_key was written as raw 32 bytes; upstream fips daemon reads it
    with read_to_string() and bailed with "stream did not contain valid
    UTF-8", crashlooping indefinitely.
  * Activate button racy: user had to hit it, and it would keep failing
    silently because the daemon couldn't parse its own config.
  * FIPS schema drift (already fixed in 7d8a5864) put the config write
    path behind the same broken "Activate" flow, so the fix alone
    didn't help existing nodes.
  * Journal was on tmpfs — every reboot wiped install/onboarding history,
    making post-hoc debugging impossible.

Changes:
  * identity.rs: write fips_key as bech32 nsec + newline. load_fips_keys
    now auto-migrates legacy 32-byte files to bech32 the first time it
    reads them, so OTA updates from v1.5.0-alpha self-heal without user
    action.
  * server.rs: post-onboarding auto-activate task runs on every
    archipelago startup. If fips_key exists it ensures /etc/fips/fips.yaml
    is schema-current and starts archipelago-fips.service. Pre-onboarding
    nodes stay quiet (guarded on fips_key_exists).
  * ISO build: un-mask archipelago-fips + archipelago-wg + wg-address —
    all use ConditionPathExists on their key files, so systemd silently
    skips them pre-onboarding (no MOTD [FAILED]). Only nostr-vpn stays
    masked (legacy service, superseded by upstream fips).
  * Journald made persistent via /var/log/journal + 500M cap, so
    install and first-boot logs survive reboots for diagnosis.

After this, a fresh install + onboarding should bring FIPS up automatically
with no user interaction. The UI "Activate" button can stay as an escape
hatch (the RPC is still there) but is no longer on the critical path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:33:21 -04:00
Dorian
5eb2d34dd7 release(v1.6.0-alpha): smoke-test release for system-update flow
No functional changes from v1.5.0-alpha — this release exists only to
validate the in-app update pipeline end-to-end (manifest check → staged
download → apply → restart → version bump in UI sidebar).

Dropping just the manifest + artifacts; no manual deploy to the fleet.
.116/.228/.253 should notice within 30 min (periodic update-check
interval) and surface the update in the dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:12:28 -04:00
Dorian
ceba09e811 fix(iso): verify_backend_version uses fixed-string substring match
Anchored regex was too strict — `strings` concatenates adjacent printable
bytes so the version never sits on its own line. The 1.5.0-alpha binary
DOES contain the version but as part of `1.5.0-alpharpcNot Found`. Fixed
by switching to `grep -qF $VERSION`: substring match is safe because the
version string is specific enough that accidental collisions are
vanishingly unlikely.

Caught mid-build today: check rejected the correct local binary, fell
through to container source-build — ISO still produced correctly but
wasted ~10 min on an unnecessary rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:41:48 -04:00
Dorian
7d8a586401 fix(fips,iso): match upstream fips schema + guard ISO against stale binary
1. FIPS daemon config schema drifted: upstream jmcorgan/fips now takes
   `node.identity.persistent: true` (keys read from config-dir/fips.key)
   and `transports.udp.bind_addr: "0.0.0.0:PORT"` instead of
   `identity.key_file/pub_file` + `transports.udp.enabled/port`. The
   `tor:` transport was dropped entirely; archipelago handles Tor
   fallback itself. fips.yaml generated by archipelago::fips::config
   now matches the upstream schema, and archipelago-fips.service stops
   crashlooping on Activate. Observed on .198: 52 restarts with
   "data did not match any variant of untagged enum TransportInstances
   at line 7 column 3".

2. ISO backend-binary capture didn't verify that the captured binary
   matched the checked-out Cargo.toml version. Today's 14:40 ISO
   shipped a stale 1.4.0 binary because `core/target/release/archipelago`
   pre-dated the 1.5.0-alpha bump — the build grabbed it via the
   first-priority "local release build" path without looking at it.
   All four capture sources now go through verify_backend_version()
   which greps the binary for the expected version string; mismatches
   are skipped so the build falls through to the source-build path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:19:56 -04:00
Dorian
27bf9c2e7c fix(iso): pass installer-env script as bind-mounted file, not inline bash -c
On this host (and potentially others with a particular podman/overlay
state), passing the multi-hundred-line stage-2 script via
`debian:trixie bash -c '...'` caused debootstrap to fail at
"Extracting apt... tar failed" on the very first package — no matter
what patch, storage cleanup, or env-reset we tried.

Running the exact same script body via a bind-mounted file
(`bash /installer-env.sh`) succeeds. So: write the body to a temp
file in WORK_DIR, bind-mount it read-only, and have the container
bash execute it from the file. Same behavior, different invocation,
works.

Was blocking every ISO rebuild since ~10:57 local. First successful
build since: 14:40, sha256 41fad2ff…, 2.3GB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:40:52 -04:00
Dorian
d22ea432dd Revert "fix(iso): patch debootstrap for Trixie apt 3.0.3 tar dup-entry bug"
This reverts commit 9d2af70767.
2026-04-19 13:49:48 -04:00
Dorian
9d2af70767 fix(iso): patch debootstrap for Trixie apt 3.0.3 tar dup-entry bug
Debian Trixie apt 3.0.3's data.tar has duplicate entries for the same
path (regular file + symlink at e.g. libapt-private.so.0.0), and tar
bails on the second entry with "Cannot create symlink: File exists",
failing debootstrap on the very first package. Patch debootstrap's
tar invocation to use --skip-old-files so the duplicate is ignored.

Was blocking every unbundled ISO rebuild since the Trixie apt bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:23:20 -04:00
Dorian
626568c10e fix(iso): escape $svc in mask-loop heredoc (was expanding to empty)
Previous build's STEP 47/50 log showed:
  RUN for svc in nostr-vpn archipelago-wg archipelago-wg-address; do
    rm -f /etc/systemd/system/.service
    ln -sf /dev/null /etc/systemd/system/.service
  done

The Dockerfile is generated via <<DOCKERFILE heredoc in the build
script, so unescaped $svc resolved in the outer bash BEFORE Docker
ever saw it, leaving nostr-vpn/wg masks as a hidden `.service` file
with no effect. nostr-vpn still tries to start on boot → [FAILED].

Fixed with \$svc so the literal lands in the Dockerfile for Docker's
shell to expand per iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:37:00 -04:00
Dorian
6b58659a52 Revert "fix(iso): enable upstream fips.service so fresh installs show "active""
This reverts commit 810c111ba7.
2026-04-19 10:00:25 -04:00
Dorian
810c111ba7 fix(iso): enable upstream fips.service so fresh installs show "active"
Fresh install of .198 reported "FIPS has an npub but says inactive".
The debian package writes /etc/fips/fips.pub during install (whence
the npub) but leaves the upstream fips.service disabled. Result:
FipsStatus.service_active = false, dashboard shows "inactive" until
the user hits Activate. Explicit `systemctl enable fips.service`
in the Dockerfile so first boot brings the daemon up immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:56:10 -04:00
Dorian
186f014747 fix(iso): 3 first-boot issues from .198 reinstall report
1. nostr-vpn still failing despite last mask attempt — confirmed in
   the 6th ISO's rootfs.tar: the .service file was present but
   not in multi-user.target.wants. Previous `systemctl mask` silently
   no-oped because the real file was already there. Fixed properly
   with explicit `rm -f` + `ln -sf /dev/null` for nostr-vpn,
   archipelago-wg, and archipelago-wg-address — same /dev/null
   symlink state that `mask` would produce on a clean install.

2. Kiosk didn't come up on first boot, only on reboot. Extended the
   ExecStartPre health-poll from 30s → 120s (unbundled ISO takes
   longer to settle on first boot: archipelago initializes state,
   pulls FileBrowser, frontend settles), raised TimeoutStartSec to
   180s, and added After=systemd-user-sessions.service +
   After=network-online.target so X / Chromium aren't racing.

3. /init: line 29: can't create /root/etc/network/interfaces error
   on installer boot — debootstrap --variant=minbase omits ifupdown
   so the target has no /etc/network/ directory, and live-boot's
   init tries to seed it. Non-fatal but noisy. Added ifupdown +
   isc-dhcp-client to the debootstrap --include list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:54:12 -04:00
Dorian
d2ba9d5523 fix(iso): add clang/libclang/nftables deps — rustables gateway feature uses bindgen
5th ISO attempt died in rustables's build.rs (which uses bindgen to
wrap libnftnl) with "couldn't find any valid shared libraries
matching: libclang". bindgen requires libclang.so at build time
to parse C headers. rustables also needs libnftnl-dev + libmnl-dev
for the actual wrappers.

Added to the fips-builder stage apt install line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:01:59 -04:00
Dorian
7127532c73 fix(iso): build fips with --features gateway so fips-gateway binary exists
Third time's the charm. The upstream fips Cargo.toml puts fips-gateway
behind features.gateway = ["dep:rustables"], so the previous two
attempts (--bins, --workspace --bins) never produced the binary —
only the default feature set was compiled. cargo deb --no-build then
panics looking for the missing binary.

Inspected /tmp/fips-investigate (fresh clone of upstream main on
2026-04-19) to confirm — the feature flag is the gate, not a
workspace layout issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:54:53 -04:00
Dorian
d69715c3c5 fix(iso): use --workspace --bins so cargo builds fips-gateway member crate
Plain `cargo build --release --bins` only built the root crate's
binary targets. fips-gateway is a workspace member, so we need
--workspace to pull every member's bins. Without it cargo deb
--no-build panics looking for target/release/fips-gateway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:44:39 -04:00
Dorian
bb4b7dd640 feat(web5): anchor connectivity badge on FipsNetworkCard
Consumes the new authenticated_peer_count + anchor_connected fields
from fips.status. Shows a cyan dot + "connected" when fips.v0l.io is
in the identity cache (DHT routing to unknown npubs will work), or
an orange "not reached" with a one-line explainer that federation
and messaging will fall back to Tor until the anchor reconnects.

Peer count appears on the same row so users see "3 peers" when the
fleet-pair script has been run, or "0 peers" on a fresh install
still waiting for the anchor handshake.

Block only renders when service_active — pre-onboarding the FIPS
package is masked so there's nothing meaningful to report.

Covers the UI half of task #20. Multi-anchor defaulting is still
open (need real anchor addresses beyond fips.v0l.io).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:42:50 -04:00
Dorian
122d00f81e feat(fips): surface anchor connectivity + peer count in FipsStatus
Two new fields on the /rpc fips.status payload:

- authenticated_peer_count: how many FIPS peers the daemon has an
  authenticated session to right now. 0 means isolated / not on
  the mesh; >0 means traffic to any known npub can DHT-route.
- anchor_connected: true when the public anchor (fips.v0l.io,
  npub1zv58cn7…) is present in the daemon's identity cache. The
  anchor bootstraps DHT routing for general-case deployments, so
  this is the best single-value indicator the UI can show for
  "will federation traffic over FIPS work between previously-
  unknown peers?"

Implementation: fips::service::peer_connectivity_summary shells
out to `sudo -n fipsctl show peers` + `... show identity-cache`
(archipelago user already has NOPASSWD:ALL per the ISO sudoers
and live fleet nodes, confirmed). Failure returns (0, false) so
the UI degrades to "unknown" state without crashing.

Only queried when service_active — pre-onboarding / daemon-down
nodes skip the fipsctl call entirely.

UI side (FipsNetworkCard) consumes the full status JSON, so the
two new fields are available via existing prop plumbing; visual
treatment can come later.

Also fixes ISO build (commit 3e04456c wasn't sufficient): the
Dockerfile needs `cargo build --release --bins` — upstream FIPS
added a `fips-gateway` binary target, and plain `cargo build
--release` only builds the default bin list, which caused
`cargo deb --no-build` to fail hunting for the missing binary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:40:31 -04:00
Dorian
ec5f14166a feat(federation): periodic sync every 30 minutes
Until now federation.sync-state only fired on (a) user clicking Sync
in the UI or (b) server-name push. That meant own_fips_npub,
last_transport, peer state updates — all the things v1.5 added for
auto-upgrade from Tor to FIPS — didn't propagate until the user
poked the button.

Fix: spawn a background task in server.rs that runs
federation::sync_with_peer for every Trusted peer every 30 minutes.
First run is 60s after boot (let onboarding settle) and peers are
staggered 5s apart to not hammer Tor's SOCKS proxy with concurrent
connects.

The sync path already prefers FIPS (via PeerRequest), so once peers
have learned each other's fips_npub (now automatic thanks to the
own_fips_npub broadcast in state snapshots), subsequent periodic
syncs route over FIPS — transport badge cycles from 'tor' to 'fips'
on its own without user action.

Covers task #30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:32:11 -04:00
Dorian
3e04456c52 fix(iso): rebuild-blocker — FIPS needs libdbus-1-dev + libssl-dev
rust:1-slim-bookworm doesn't include dbus/ssl dev headers, and
jmcorgan/fips upstream started linking against libdbus-sys + openssl
at some recent commit. Observed during the 2026-04-19 v1.5.0-alpha
rebuild: libdbus-sys's build.rs panics when pkg-config can't find
dbus-1.pc, which kills the whole cargo build → the whole ISO build
→ ships an ISO without FIPS installed.

Also mask nostr-vpn.service + archipelago-wg*.service in the rootfs
Dockerfile: these have WantedBy=multi-user.target so systemd pulls
them into the default boot target, but their EnvironmentFile + an
ExecStartPre guard cause them to [FAILED] in the boot MOTD on every
fresh install until onboarding writes their env files. Masking
keeps the startup clean; the onboarding / install RPC handlers
unmask + start them when prerequisites exist (same model as
archipelago-fips).

Bonus discovery from same diag: the default build was silently
reusing a stale rootfs cache from Apr 12 — before the FIPS
integration landed. So the v1.5.0-alpha ISO I shipped had no FIPS
package at all. Rebuild pass with --rebuild forces fresh rootfs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:27:22 -04:00
Dorian
f52ba92f68 release: refresh v1.5.0-alpha artifacts to match shipped binary
The original v1.5.0-alpha cut (commit 77206a89) captured the binary
at that exact point in main. Since then ~12 commits landed that are
now live on every fleet node — own_fips_npub in state sync,
bidirectional peer flow, lazy-bind peer listener, self-peer guard,
multi-relay nostr publish + UI, avatar/banner upload, bidirectional
network requests. Rather than bump to v1.5.1-alpha for an in-flight
alpha iteration, refresh the artifacts in place so the manifest +
binary + frontend all agree.

- archipelago: sha 77206a89→e3df7a68, 40,020,488→40,078,936 bytes
- archipelago-frontend-1.5.0-alpha.tar.gz: sha 83fbacf3→2bf973ee,
  53,359,792→53,358,751 bytes
- changelog extended from 7 to 15 lines to cover every landed item.

check-release-manifest.sh passes — safe to publish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:06:12 -04:00
Dorian
fe29e09b09 release: document v1.5.0-alpha unbundled ISO + sha256 + scp path
ISO itself stays out of git (2.4GB), but this pointer file records
the hash + retrieval path so anyone flashing to a fresh .253-style
install knows exactly what they're getting. Summary of code landed
in this ISO listed at the bottom so the changelog isn't scattered
across commit messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 05:19:32 -04:00
Dorian
6167913133 feat(web5): avatar + banner upload on the profile editor
Previously the profile editor only accepted external URLs for
picture/banner — typing in a URL works, but anyone without their
own image host couldn't use an avatar at all. Now there's an
"Upload" button next to each field that pushes the selected file
to /api/blob and pastes the returned capability-signed local URL
(`/blob/<cid>?cap=…&exp=…&peer=…`) straight into the form field.

- Two new refs: avatarUploading / bannerUploading so each button
  shows "Uploading…" independently.
- uploadAsset(ev, 'picture' | 'banner') wraps the POST, validates
  HTTP 200 + presence of self_test_url, surfaces failures in the
  existing profileError banner.
- File input is re-cleared on completion so the user can pick the
  same file again without refreshing.
- Live preview in the <img> at the top of the editor updates
  immediately because profileForm[field] is reactive.

Image persists through Save & Publish via the existing
identity.update-profile + identity.publish-profile (both now
multi-relay). The image URL is still local-only — external nostr
clients won't resolve it until we integrate a public image host
(noted in task #29).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 05:01:57 -04:00
Dorian
996f6aa837 feat(web5): surface multi-relay publish result on profile save
Pairs with the backend change that broadcasts kind:0 to every
enabled relay. Web5Identities.vue's publishProfile() now reads the
richer response ({accepted, rejected, relays_attempted, published})
and shows one of three states:

- all relays accepted → "Published to all N relays (event_id…)"
- partial → "Published to X/N relays" plus a warning with the first
  relay's rejection reason
- zero → "Published to 0/N relays — check Manage Relays" (error)

User can now tell at a glance whether their profile actually made
it to the wider nostr network or only the local relay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:47:44 -04:00
Dorian
f1c982bc95 fix(nostr): profile publish broadcasts to ALL enabled relays
Previously handle_identity_publish_profile defaulted to a single
hard-coded relay (ws://localhost:18081) so the user's kind:0 profile
event only ever landed on the local relay — hence "Manage Relays
shows N connected, but profile edits don't propagate" from testing.

Fix — two-layer change:

- identity_manager::publish_profile now takes `&[String]` relays
  instead of one URL. Adds each relay to the nostr-sdk client,
  gives 15s for handshakes, publishes, then surfaces per-relay
  accept/reject in a new ProfilePublishOutcome struct so the UI
  can show WHICH relays accepted vs. rejected and WHY.
- RPC handle_identity_publish_profile no longer defaults to the
  local relay: pulls the ENABLED list from nostr_relays::list_relays
  (the same table that powers Manage Relays) and publishes to every
  entry. Accepts an optional `relays: [...]` override for tests.
- At-least-one-accept guarantee: if every relay rejects, the call
  errors instead of silently reporting published=true. User gets a
  real error message listing the failures.
- Response shape: `{event_id, accepted: [urls], rejected: [[url,
  reason]], relays_attempted: N, published: bool}` so the UI can
  show a useful status block after clicking Publish.

relay_url_matches is tolerant of trailing-slash / case differences
since nostr-sdk canonicalises URLs internally.

Covers the publishing half of task #29; avatar/banner upload UI is
still open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
Dorian
2d78c2ef2b feat(peers): bidirectional /network peer requests
Before: Alice sent /network.send-request to Bob, Bob accepted via
/network.accept-request and gained Alice in his peers list, but Alice
was never notified — her pending row sat there and she had to
manually add Bob separately. User complaint: "it's strange you have
to do it both ways."

Fix — the accept now fires a best-effort connection_accepted message
back to the requester:

- handle_network_accept_request: after writing the local peer record,
  assembles a `{type: "connection_accepted", request_id, from_did,
  from_onion, from_pubkey}` JSON, signs + encrypts + POSTs it to the
  requester via node_message::send_to_peer. Uses PeerRequest internally
  so it prefers FIPS and falls back to Tor.
- handle_node_message: parses incoming plaintext as JSON; on a match
  for type=connection_accepted, auto-adds the sender to peers.json
  (the existing self-pubkey guard in add_peer still applies) and
  short-circuits the normal store_received path so the acceptance
  doesn't also land as a chat message in Alice's inbox.

Offline handling: if Alice is offline when Bob accepts, the notify
warns and the local accept still succeeds. Alice will receive any
subsequent message from Bob normally; future iteration could
retry on reconnect.

Federation-invite flow (federation.accept-invite → notify_join) was
already bidirectional; this closes the gap for the peer flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:34:37 -04:00
Dorian
84943aaa04 feat(server): lazy-bind FIPS peer listener so fips.install doesn't
need an archipelago restart

Previously the server checked `fips0` once at startup; if the
interface wasn't up (pre-onboarding, or post-onboarding before the
user clicked Activate FIPS), the peer listener never bound and stayed
unreachable until the next archipelago restart.

Replaced with a `peer_late_bind_loop` background task: polls every
30s for an fd00::/8 address on `fips0` and binds the listener the
moment one appears. First tick fires immediately so the hot path —
fips0 already up at startup — is still zero-cost. Cancellation
cascades through the same `tokio::sync::watch` channel the main
listener uses.

Side effects:
- main.rs no longer computes peer_addr eagerly; dropped the unused
  param from serve_with_shutdown.
- FipsTransport::is_available already caches the service probe so
  the 30s poll doesn't thrash systemctl.

Covers task #21. Unblocks the first-boot + onboarding flow for
fresh ISO installs on .253.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:21:20 -04:00
Dorian
bfe2603f69 feat(federation): advertise own_fips_npub in state snapshots
Pre-v1.4 federation pairs (who exchanged invites before fips_npub
was part of the invite code) had no path to learn each other's FIPS
npub — they'd stay Tor-only forever even after upgrading. Fix:
every state snapshot now carries the sender's own_fips_npub, and
update_node_state refreshes the stored fips_npub on the receiver
side whenever it differs.

- NodeStateSnapshot.own_fips_npub (serde default for back-compat).
- build_local_state takes own_fips_npub alongside the other
  single-value fields.
- handle_federation_get_state populates own_fips_npub from
  identity::fips_npub, with a fallback to the upstream daemon's
  /etc/fips/fips.pub for legacy nodes that never materialised a
  seed-derived key.
- storage::update_node_state now writes fips_npub into the
  FederatedNode when a new value arrives and trims whitespace
  before comparing, so key rotations also flow through.
- Test fixtures (storage + transport/delta + sync) updated for the
  new field; existing tests pass.

Net effect: on the next sync, .116 and .228 learn each other's
fips_npub (currently null from the old invite) and subsequent
federation calls route FIPS-first automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:16:05 -04:00
Dorian
3c83440a60 fix(peers): reject self-add in add_peer()
Observed on .228: /var/lib/archipelago/peers.json contained an entry
matching the node's own node_key.pub pubkey. It had been added
2026-03-02 and stuck around forever since add_peer() only dedupes by
pubkey — nothing stops a pubkey that happens to be ours.

How it probably got there: somewhere in the auto-add paths
(node-message receive, mesh federation bridge, invite back-and-forth)
a message we'd sent was fed back and the receiver-side add used the
echoed from_pubkey without realising it was us. Doesn't matter which
path — the guard belongs in storage.

add_peer now short-circuits when the candidate pubkey matches
data_dir/identity/node_key.pub. Helper is_own_pubkey best-effort:
unreadable identity → returns false so normal peers aren't blocked.

Also manually purged the one stray entry on .228 (1 removed, 2 real
peers remain). Future deploys include this guard so the phantom can't
come back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:02:15 -04:00
Dorian
3393837ed2 fix(deploy): force nginx sites-enabled symlink so config updates actually apply
Real 413 root cause on .116 and .228 turned out not to be the body-size
limit — their /etc/nginx/sites-enabled/archipelago was a stale regular
FILE, not a symlink to sites-available, so every nginx update since
someone froze the active config had been invisible to running nginx.
The /api/blob location, added at some point after that freeze, didn't
exist in sites-enabled, so every attachment upload hit nginx's default
1m client_max_body_size and returned 413 regardless of attachment
size.

Deploy now re-creates the symlink on every run: if sites-enabled is a
regular file or missing, we replace it with a symlink to
sites-available. Idempotent if it's already correct.

Also applied the fix live on all 4 fleet nodes — /api/blob now
responds 401 (session-auth required, as designed) instead of 413 on
2MB+ test uploads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 03:45:16 -04:00
Dorian
b3a96dcf26 fix(nginx): raise body-size limit 10m → 256m for mesh/content/dwn peer paths
Was seeing "upload failed: 413" on mesh attachment sends between
federated nodes — a ~7MB image becomes ~10MB base64 in the
typed_envelope wire and hit the 10m client_max_body_size on
/archipelago/, /content/, and /dwn/. Bumped those six locations
(two per server block, regular + HTTPS) to 256m so modern
attachments/blobs don't trip the proxy. /rpc/ stays at 1m —
internal JSON-RPC calls are small and don't need the headroom.

Applied to all 4 fleet nodes live; ISO source config updated so
fresh installs get the same limits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 03:36:12 -04:00
Dorian
77206a8928 release: v1.5.0-alpha + version hygiene fixes
Versioning was drifting on three axes — fixed all of them:

1. Cargo.toml → 1.5.0-alpha (was 1.5.0). User wants `-alpha` suffix
   on every pre-stable release; this is the current state of main.
2. neode-ui/package.json was still 1.3.5 — brought in line.
3. /opt/archipelago/build-info.txt was stale on .198 (1.3.4) and
   .253 (1.3.5), absent on .116/.228. That file OVERRIDES the
   binary's CARGO_PKG_VERSION for the UI sidebar, which is why
   .198/.253 kept showing old versions even with fresh binaries.
   scripts/deploy-to-target.sh now writes build-info.txt on every
   deploy, reading the version straight from Cargo.toml — so the
   sidebar can never drift from the binary again.

Release artifacts + manifest:
- releases/v1.5.0-alpha/archipelago (40M, sha in manifest)
- releases/v1.5.0-alpha/archipelago-frontend-1.5.0-alpha.tar.gz (51M)
- releases/manifest.json bumped with full 7-line changelog covering
  FIPS-first routing, Settings toggle, transitive federation, cancel
  button, transport badges, peer listener, and the build-info fix.
- scripts/check-release-manifest.sh — new pre-publish guard. Refuses
  to pass if: Cargo.toml ≠ manifest version, changelog is empty
  (release notes are mandatory), or any component's sha256/size
  doesn't match the file on disk. Run locally or from CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 03:23:18 -04:00
Dorian
4c8c4ebc47 feat(federation): v1.5.0 bump + transport badge on each node card
Every federated node card now shows a colored badge indicating how
archipelago actually reached the peer on the most recent successful
call — FIPS / TOR / LAN / MESH — not a prediction based on available
addresses. The badge is hidden when we've never reached the peer.

Backend:
- Cargo.toml: 1.4.0 → 1.5.0 (visible in the sidebar health endpoint).
- FederatedNode gains last_transport + last_transport_at (serde
  default for back-compat with v1.4 nodes.json files).
- federation::storage::record_peer_transport(did, onion, transport)
  — writes both fields plus last_seen after each successful peer
  call. Matches by DID first, falls back to onion.
- federation::sync::sync_with_peer now calls record_peer_transport
  immediately after a successful PeerRequest return, so the badge
  on the sync'ing peer's card reflects the transport the call
  actually rode (fips vs tor).

Frontend:
- types.ts FederatedNode gains last_transport / last_transport_at
  (union-typed to the four known kinds).
- NodeList.vue: new transportBadge(node) returns {label, cls, title}
  tuned per transport. Hidden when last_transport is absent so we
  never lie. Tooltip shows "Last reached via <x> · <time ago>" so
  stale data is self-evident. Removed the predictive icon from the
  transport store — badge is now 100% ground-truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 02:51:26 -04:00
Dorian
95f52572fc feat(federation): cancel button for outbound pending peer requests
Previously the Pending Peer Requests panel only had Approve/Reject for
inbound rows; outbound rows in the 'sent' state had no action and
would sit there until the target explicitly approved or rejected. Now
you can Cancel an outbound request — the local row is dropped and a
PeerCancel nostr DM is sent so the target's inbound row also
disappears.

Backend:
- HandshakeMessage::PeerCancel {reason: Option<String>} variant.
- nostr_handshake::send_peer_cancel() mirrors send_peer_reject.
- handshake.poll handler dispatches inbound PeerCancel: finds the
  matching inbound pending row (same from_nostr_pubkey, state=Pending)
  and deletes it. Reply shape gains `cancelled_inbound: [id]`.
- federation::pending::delete() — hard-remove (set_state only
  transitions; we don't want 'Cancelled' ghosts in the audit trail).
- federation.cancel-request RPC: outbound+Sent only, default
  notify=true (cancelling silently is a footgun), best-effort DM
  (relay failure doesn't block local deletion). Wired in dispatcher.

Frontend:
- PendingRequestsPanel.vue: Cancel button appears only on
  outbound+sent rows. Emits 'cancel' event with request id.
- Federation.vue: cancelPending(id) handler calls
  rpcClient.federationCancelRequest and reloads the list.
- rpcClient.federationCancelRequest(id, reason?, notify=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 02:28:16 -04:00
Dorian
a658e924e1 fix(ui,ops): TransportPrefsCard import path + fleet unpair script
- TransportPrefsCard.vue: import from '@/api/rpc-client' (not
  '@/api/rpc') so vue-tsc resolves the module during build.
- scripts/fleet-fips-unpair.sh: companion to the fleet-pair script —
  rewrites each node's fips.yaml to anchor-only (fips.v0l.io) so we
  can prove the general-case deployment works without the LAN
  fast-path. Prints per-node peer counts + DHT AAAA resolution for
  every cross-node pair after the change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 02:08:32 -04:00
Dorian
4d8c8c89a2 feat(federation): transitive peer learning via state-sync
When Alice syncs state with a Trusted peer Bob, she now learns about
Bob's other Trusted peers and auto-adds them as Observers on her side
— so Carol's fips_npub is known locally and subsequent federation
traffic to Carol can route directly over FIPS without a separate
invite round-trip.

- NodeStateSnapshot gains a `federated_peers: Vec<FederationPeerHint>`
  field (serde default for backward compat with v1.4 snapshots).
- FederationPeerHint is a minimal projection: did, pubkey, onion,
  name, fips_npub — excludes per-receiver fields (trust_level,
  added_at, last_seen, last_state).
- build_local_state takes the local federation list and includes only
  Trusted peers. Observer/Untrusted peers are NOT re-exported — a
  node shouldn't launder other people's federation through its own
  authority.
- sync_with_peer merges the received hints via merge_transitive_peers
  when the source is Trusted: existing entries get fips_npub
  refreshed if missing; unknown DIDs are added at Observer trust
  (never auto-promoted to Trusted).
- Bounded to 1 hop: merged Observer entries do NOT get re-exported in
  the local node's own snapshots. So Bob → Alice learns Carol, but
  Alice's snapshots to Dave do not include Carol.
- Tests: round-trip + filter-non-trusted-from-snapshot coverage.
- Storage + delta test fixtures updated for the new field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:58:21 -04:00
Dorian
bcd9b9aa56 ops(fips): fleet LAN fast-path pairing script (dev nodes only)
scripts/fleet-fips-pair.sh writes a deterministic /etc/fips/fips.yaml
on each of our 4 dev fleet nodes (.116/.198/.228/.253), listing the
other three as static FIPS peers over their LAN IPs (UDP 2121 / TCP
8443). Also flips `node.identity.persistent: true` so the npub stays
stable across restarts — without this the daemon rolls a new keypair
on every restart and federation invites that carried the previous
npub go stale.

The script is NOT the general deployment mechanism:
- Every archipelago install already ships fips.v0l.io as an anchor
  peer, so any node can DHT-route to any npub that has ever announced
  on the public mesh.
- Federation invites (v1.4+) carry the peer's fips_npub, so accepting
  an invite is enough for crate::fips::dial::peer_base_url(npub) to
  reach the peer through the anchor network.
- This script is a LAN fast-path optimization so intra-fleet traffic
  stays on the wire instead of bouncing through fips.v0l.io.

Usage:
  scripts/fleet-fips-pair.sh           # apply to all nodes
  scripts/fleet-fips-pair.sh --verify  # print current peer state

Verified: all 4 fleet nodes now report 3 authenticated peers each
(their 3 fleet siblings), plus fips.v0l.io in the identity cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:50:20 -04:00
Dorian
683553dfde feat(settings): per-service FIPS/Tor transport preference
Adds a user-configurable toggle for how each peer-to-peer service
reaches federated peers. Three options per service:

- Auto (default) — FIPS preferred, Tor fallback (current behavior).
- FIPS only — fail rather than fall through to Tor.
- Tor only — explicit opt-in to onion anonymity for that service.

Services covered (matching the UI rows):
- Federation — state sync, invites, peer notifications
- Peers — address/DID rotation broadcasts
- Peer Files — content catalog download/browse/preview
- Messaging — archipelago channel + mesh bridge
- Mesh File Sharing — content_ref blob fetches

Implementation:
- settings::transport — persisted struct + process-wide OnceLock handle
  (so deep call sites don't need data_dir threaded through signatures).
  On-disk file: <data_dir>/settings/transport_preferences.json; missing
  or corrupt → defaults (Auto everywhere).
- settings::transport::init() called from main.rs after config load.
- fips::dial::PeerRequest gains a .service(kind) builder; send_* checks
  the preference before choosing a transport. FIPS-only fails loudly
  when FIPS is unavailable (so users who pick it know when something
  falls back).
- Every FIPS-first migration site tags its PeerRequest with the
  matching PeerService so the toggle actually applies.
- transport.preferences + transport.set-preference RPCs added; wired
  into the dispatcher.
- neode-ui/src/views/settings/TransportPrefsCard.vue — standalone card
  with a 5-row Auto/FIPS/Tor tri-state. Not wired into Settings.vue —
  the user places components themselves (see feedback_ui_entry_points).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:44:41 -04:00
Dorian
be37825613 feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first
Migrates the remaining Tor-direct peer call sites to PeerRequest so
FIPS is the default when the peer is federated and running the daemon:

- node_message::send_to_peer / check_peer_reachable: gain a
  fips_npub parameter. Error messages updated to reference both
  transports.
- Callers (api/rpc/network.rs, api/rpc/peers.rs, server health
  loop): look up fips_npub from federation storage by onion and
  pass it.
- mesh::send_typed_wire_via_federation: the spawned background POST
  for the /archipelago/mesh-typed endpoint now uses PeerRequest with
  federation-resolved fips_npub. Signature domain unchanged.
- api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL
  rebuilt as (base_url, path_with_query) so PeerRequest can append
  the query string after swapping the host. Cap/exp/peer
  parameters are still signed over the content ref itself, so
  transport choice is invisible to the signature.
- network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup
  before sync_single_peer; health/pull/push each dial through
  PeerRequest, so any DWN peer known to federation gets FIPS.

Left Tor-only on purpose:
- api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion —
  resolving TO a DID, no anchor yet.
- content.browse / preview calls to non-federated peers fall
  through to Tor naturally inside PeerRequest (no fips_npub → skip
  FIPS branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
Dorian
c8defc9bf1 feat(content): route peer content fetch via FIPS first
All four content-over-peer handlers prefer FIPS when the peer is in
our federation and has advertised a FIPS npub; fall back to Tor
otherwise (unknown peers, FIPS daemon down, transient failure).

- content.handle_content_download_peer / _paid: DID-authenticated
  fetch, payment token header threaded through both transports.
- content.handle_content_browse_peer / _preview: no DID header by
  design (anonymous browse) — still benefits from FIPS when the
  peer happens to be federated.
- federation::fips_npub_for_onion: storage helper that looks up a
  peer's FIPS npub from the federation nodes file given their onion
  address. Suffix-tolerant (`abc` matches `abc.onion`).

Preserves the Tor-only path for truly unknown peers: PeerRequest
returns Err from the Tor branch instead of silently succeeding,
matching the previous behavior when the peer was unreachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:29:13 -04:00
Dorian
17ad45cab7 feat(federation): route state-sync / invites / notifications via FIPS first
Every federation peer-to-peer call now prefers FIPS (direct ULA dial
over `fips0`, ~LAN latency) and falls back to Tor only on network
failure. Per-method ed25519 signatures are preserved on both
transports so authenticity doesn't change.

- fips::dial::PeerRequest — fluent builder that owns transport
  selection. Returns the Response plus the TransportKind that carried
  it, so handlers can log or expose which path was used.
- fips::dial::is_service_active — free-standing async probe used by
  migration sites (the transport::fips::is_available cache is keyed
  to a `&self`, not usable from static contexts).
- federation/sync.rs: sync_with_peer + deploy_to_peer drop the
  hand-rolled reqwest::Proxy dance, call PeerRequest instead.
- federation/invites.rs: notify_join takes the remote's fips_npub
  (already parsed out of the invite code since v1.4) and dials over
  FIPS when available. The "peer-joined" signature domain is
  unchanged.
- api/rpc/federation/handlers.rs: DID rotation broadcast loops over
  federated peers through PeerRequest; the per-peer result payload
  gains a `transport` field so the UI can surface mesh vs. onion.
- api/rpc/tor/mod.rs: onion-address-change propagation is now the
  most useful FIPS-first call — fips_npub is stable across onion
  rotation, so peers get the new address even when the old onion
  is already dead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:20:44 -04:00
Dorian
5479e225d7 feat(fips): peer dialing + dedicated fips0 listener with path whitelist
Wires the FIPS transport end-to-end so peer-to-peer calls can reach
other nodes over the mesh without going through Tor:

- fips::dial — raw RFC 1035 DNS client (zero new deps) that queries the
  FIPS daemon's local resolver at 127.0.0.1:5354 for `<npub>.fips` AAAA
  records. Exposes peer_base_url(npub) → "http://[fd9d:…]:5679" plus a
  reqwest client factory for call-site migrations.
- fips::iface — parses /proc/net/if_inet6 to find the ULA address on
  `fips0`. Runs under the archipelago service user without extra caps.
- FipsTransport::is_available() — live probe of archipelago-fips and
  upstream fips.service via `systemctl is-active`, cached 10s so the
  send hot path doesn't thrash DBus.
- FipsTransport::send() — resolve npub, POST TransportMessage JSON to
  the peer's /transport/inbox. Today /transport/inbox isn't wired on
  the receive side, so call-site migrations use dial::peer_base_url
  directly against the already-signed endpoints (/rpc/v1,
  /archipelago/node-message, /content/*). The inbox handler lands as
  part of the Settings/transport work.
- server::serve_with_shutdown — takes an optional peer_addr and spawns
  a second listener bound specifically to the fips0 ULA on port 5679.
  The peer listener applies is_peer_allowed_path() — a whitelist of
  endpoints that already do per-request signature auth — and returns
  404 for everything else. Shutdown cascades to both listeners via a
  watch channel; 5s drain window preserved.
- main.rs — if fips0 has a ULA at startup, pass the peer SocketAddr to
  serve_with_shutdown; otherwise run the main listener only.

Security: the peer listener is bound to the fips0 ULA directly, not
wildcard, so it's unreachable from WAN IPv6. The path whitelist limits
exposure to endpoints whose handlers verify ed25519 signatures or
federation DID headers server-side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:12:39 -04:00
Dorian
becdb1af5a fix(fips): fall back to upstream daemon npub on legacy/dev nodes
Nodes without a seed-derived FIPS key (legacy deploys, fresh pre-onboarding
installs) were reporting "Awaiting seed" in the dashboard even when the
upstream fips.service was running — status.npub was None unless
/data/identity/fips_key.pub existed.

- fips/service.rs: new read_upstream_npub() reads /etc/fips/fips.pub
  (bech32 text or raw 32 bytes) from the debian package.
- fips/mod.rs: FipsStatus::current() prefers the seed-derived npub,
  falls back to the upstream key. service_active is now TRUE if either
  archipelago-fips.service OR upstream fips.service is active; adds
  upstream_service_state to the status payload.
- fips/update.rs: resolve the upstream default branch from the GitHub
  repo API (jmcorgan/fips is on `master`, not `main`) instead of
  hardcoding — future repo rename just works.
- network/router.rs + api/rpc/router.rs: diagnostics gain wifi_ssid from
  `nmcli -t device` so the Network card can show the connected SSID.
- UI: Home.vue adds a FIPS row to the Local Network card; Server.vue
  mounts the new FipsNetworkCard and shows SSID + FIPS Mesh rows;
  HomeNetworkCard.vue removed (superseded by the inline rows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:42:56 -04:00
Dorian
df0736e2e0 release: publish v1.4.0 artifacts + manifest
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 1s
Commits the musl-static backend binary and frontend tarball under
releases/v1.4.0/ so the raw-content URLs referenced by manifest.json
actually resolve. Without this commit update.check returns "available"
but update.download 404s, which is why self-updates have been silently
broken since 1.3.5.

Size budget note: this is ~110 MiB committed per release (binary + PWA
assets). Established pattern in the existing manifest; once CI publishes
reliably we can move to a GitHub release or FileBrowser share instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:08:51 -04:00
Dorian
c1cfca6212 feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 2s
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into
the node stack, supervised by archipelago alongside Tor. Runs as a
system service, identity derives from the same BIP-39 master seed, and
user-triggered updates track upstream main.

Identity
  seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated
  secp256k1 key, distinct from the Nostr-node key for crypto isolation
  but still seed-recoverable
  identity.rs: writes fips_key[.pub] to /data/identity on onboarding,
  chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors

Transport
  TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4)
  → router prefers FIPS over Tor for all peer traffic
  PeerRecord gains fips_npub + last_fips fields (serde(default) for
  backward-compat with older nodes)
  transport/fips.rs: NodeTransport stub, reports unavailable until the
  daemon is live so router falls through to Tor cleanly

Federation invites
  FederatedNode and FederationInvite carry optional fips_npub
  create_invite / accept_invite / peer-joined callback thread it end
  to end; signature domain deliberately unchanged — FIPS Noise does
  its own session auth, so the unsigned hint only affects path
  selection

crate::fips
  config.rs: renders /etc/fips/fips.yaml and sudo-installs key material
  service.rs: systemctl status/activate/restart/mask wrappers
  update.rs: GitHub API check against upstream main; apply stubbed
  until per-commit .deb artefact source is decided

RPC + dashboard
  fips.status / fips.check-update / fips.apply-update / fips.install /
  fips.restart registered in dispatcher
  HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue
  when ready); shows state pill, version, FIPS npub, update button,
  activate button when key is present but service is down

ISO + systemd
  archipelago-fips.service: conditional on key presence, masked by
  default — backend unmasks after onboarding writes the key
  build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS
  .deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt
  installs it so trixie resolves deps; unit copied + masked

Version bump: 1.3.5 → 1.4.0

Tests: 33 new/updated passing (seed, identity, transport, federation,
fips module, transport::fips).

Known gaps: fips.apply-update returns a clear stub error until
upstream publishes per-commit .deb artefacts; HomeNetworkCard is not
mounted in Home.vue by default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:51 -04:00
Dorian
f04804ae25 chore(fmt): rustfmt drift cleanup across misc crates
Pure formatter output — no semantic changes. Sweeping these into their
own commit so the FIPS integration diff that follows stays scoped to
the actual feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:14 -04:00
Dorian
58e9754cf2 fix(ci): QEMU boot test ignores trailing numeric arg + enforces timeout
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 35m12s
The CI workflow calls `test-iso-qemu.sh "$ISO" 120`. The old arg parser
had a `case *) ISO=...` fallthrough that silently let the second
positional `120` overwrite ISO, so QEMU went looking for a file literally
named "120". That's the "failed step" the user was seeing on recent ISO
runs — the rest of the job succeeded because the QEMU step has
`continue-on-error: true`.

Changes:
- Treat `--timeout=N` or a bare numeric first-match as a CI timeout in
  seconds; the original ISO path still wins the positional.
- When a timeout is set, force `--nographic` (CI has no DISPLAY anyway)
  and wrap the QEMU invocation in coreutils' `timeout` so the script
  always returns instead of hanging.
- After termination (or timeout), grep the serial log for well-known
  systemd/live-boot markers. Pass if the kernel reached userspace, fail
  if no marker appeared within the window — useful signal rather than
  the previous "did the VM shut itself off" proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:36:20 -04:00
Dorian
9e6639e88f ci: bump build marker to kick off ISO workflow
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
2026-04-18 18:32:02 -04:00
Dorian
cc626c269d ci: trigger ISO build on .160 runner with rustfmt + clippy fixes baked in
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 14m12s
2026-04-18 18:17:04 -04:00
Dorian
7ff8f8748c chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 10m37s
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:

- Applies rustfmt across the tree (the bulk of the diff — untouched
  since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
    container/bitcoin_simulator.rs wildcard-in-or-pattern
    container/manifest.rs from_str rename to parse (reserved name)
    container/podman_client.rs .get(0) -> .first()
    container/runtime.rs manual += collapse
    archipelago/src/constants.rs doc-comment → module-doc
    api/rpc/package/install.rs stray /// comment above a non-item
    container/docker_packages.rs redundant field init
    streaming/advertisement.rs missing Metric import in tests
    tests/orchestration_tests.rs `vec!` in non-Vec contexts
    mesh/listener/dispatch.rs unused store_plain_message import
    api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
  stylistic lints (too_many_arguments, type_complexity, doc indent,
  enum variant prefix, wildcard-in-or, assertions-on-constants,
  drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
  of places with no correctness payoff and have been churning every
  toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
  are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
  rollback compatibility, vpn::get_nostr_vpn_status is surface-area
  for a not-yet-landed RPC.

cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
Dorian
902e730bd2 fix(mesh): single-flight send + spinner + async federation POST
Root cause of the "every bubble shows twice" complaint after the prior
dedup fix: the frontend was firing mesh.send twice per user action. A
held/repeating Enter key on the input fires a keydown per repeat, and
handleSendMessage didn't guard on mesh.sending, so both calls queued
through the store's sendQueue and both executed against the same
contact_id (backend logs show two mesh.send RPCs 13ms apart, same text).
That's why sender and receiver both saw doubles — the envelope actually
was transmitted twice.

Mesh.vue: handleSendMessage now early-returns if mesh.sending or
sendingArch is already set. Send button replaces the `...` placeholder
with a proper spinning ring (`.mesh-send-spinner`) so the held-Enter case
stops looking like the app is ignoring the user.

mesh/mod.rs: send_typed_wire_via_federation no longer blocks on the Tor
POST. Sent MeshMessage is recorded synchronously (UI bubble appears
instantly); the HTTP goes in tokio::spawn. Tor circuit setup was the
1–5s lag the user was seeing on every send to a federation peer. Delivery
failure still shows as `delivered: false` via the read-receipt path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:57:11 -04:00