13 Commits

Author SHA1 Message Date
archipelago
8f83b37d51 feat(orchestrator): complete container migration and release hardening 2026-04-28 15:00:58 -04:00
archipelago
4bf35f95e6 test: repair stale test fixtures across identity, mesh, update, wallet, fips
Several tests had drifted from the current production behavior:

- identity_manager: create() already auto-provisions a Nostr key, so the
  explicit create_nostr_key() call failed with "already exists". Rewrite
  the test to assert on record.nostr_npub from create() directly.
- mesh/protocol: test_build_app_start read the app name from frame[4..]
  but the v2 layout is [0:marker][1-2:len][3:cmd][4:version][5..:name].
  test_identity_broadcast_roundtrip expected input DID = output DID but
  the v2 decoder derives DID from the ed25519 pubkey, so the roundtrip
  compares against did_key_from_pubkey_hex(&pub) now.
- mesh/bitcoin_relay: test_build_block_header_announcement asserted
  sig.is_some(), but the builder intentionally emits an unsigned envelope
  to fit the 160-byte LoRa limit; assert sig.is_none(). Also widen
  placeholder hashes to the required 64 hex chars (32 bytes).
- update: load_mirrors() now merges default mirrors post-migration, so
  the roundtrip test must assert the custom mirror survives alongside
  the defaults rather than strict equality.
- wallet/cashu: test_proof_c_as_pubkey used hex that is not on the curve;
  replace with the secp256k1 generator point G so parsing succeeds.
- fips: test_status_reports_no_key_pre_onboarding asserted npub.is_none(),
  which fails on dev boxes where the fips daemon is already running. Keep
  the !key_present assertion and drop the npub one.
2026-04-23 13:02:45 -04:00
Dorian
1c1416cc1a release(v1.7.25-alpha): TCP transport for public FIPS mesh + modal cleanup
Re-adds the TCP transport (`0.0.0.0:8443`) to the rendered fips.yaml
alongside UDP. Upstream factory default enables both; we had
inadvertently narrowed to UDP-only when the yaml rewriter was last
touched, which left nodes unable to reach fips.v0l.io (the public
anchor only answers on TCP right now) or talk across networks that
block UDP.

Backend startup now compares the installed yaml against the current
rendered schema and restarts whichever fips unit is active when they
differ — so OTA-upgrading nodes pick up the new transport without
anyone having to click Reconnect.

Dropped the earlier plan to auto-add federated peers as seed anchors:
invites don't carry a FIPS-reachable IP:port, and once TCP reconnects
the public mesh, federated peers become npub-routable without needing
a seed entry.

Seed Anchors modal cleanup: replaced malformed header icon with a
three-arc broadcast glyph, and the close button now matches the
What's New modal (embedded in the card header, same icon + hover
style) instead of the earlier floating off-design placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:25:53 -04:00
Dorian
4b6a088e38 release(v1.7.22-alpha): honest anchor status + Reconnect works on all nodes
- fips::service::active_unit() picks whichever fips unit is running
  (archipelago-fips.service vs upstream fips.service) so
  handle_fips_restart and handle_fips_reconnect don't silently no-op
  on hosts where the archipelago-managed unit was never created.
- peer_connectivity_summary(anchor_candidates) replaces the old
  identity-cache check. anchor_connected is now true when at least
  one authenticated peer's npub matches the public anchor OR any
  entry in seed-anchors.json, which matches what the user actually
  cares about ("am I in the mesh?") rather than what the card used
  to claim ("is this one specific public anchor reachable?").
- FipsStatus::query takes data_dir now (so it can read seed-anchors)
  rather than identity_dir. All call-sites updated.
- handle_fips_reconnect re-pushes seed anchors after restart so the
  new daemon gets dialed without waiting for the 5-min apply loop.
- FipsNetworkCard label drops "(fips.v0l.io)" — misleading now that
  multiple anchors may be configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 07:08:26 -04:00
Dorian
f8304aed90 release(v1.7.21-alpha): operator-editable FIPS seed anchors
Adds a local seed-anchor list at <data_dir>/seed-anchors.json. Each
entry is {npub, address, transport, label}. On archipelago startup
and every 5 minutes the list is pushed into the running fips daemon
via `fipsctl connect <npub> <addr> <transport>`, so a cluster can
anchor itself independently of the global fips.v0l.io. A flaky or
unreachable public anchor no longer strands a fresh install.

New RPCs:
- fips.list-seed-anchors
- fips.add-seed-anchor (validates npub1… + host:port)
- fips.remove-seed-anchor
- fips.apply-seed-anchors (on-demand re-dial)

New standalone UI card at views/server/FipsSeedAnchorsCard.vue. Not
wired into Home.vue / Server.vue — operator places it per the
entry-point convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 06:21:37 -04:00
Dorian
be8e5ee46b release(v1.7.14-alpha): install overlay + FIPS real fix + AIUI restore
Install UX
  SystemUpdate.vue now shows a full-screen overlay after apply: the
  BitcoinFaceAscii logo, a target-version label, an indeterminate
  progress stripe (solid orange; solid green on ready), and an
  elapsed-time readout. Polls /health every 1.5s and auto-reloads
  once the backend reports the new version. 3-min stall → "Reload
  now" button. Download UI also shows a spinner + "Finishing
  download — verifying checksum…" while the fake bar sits at 95%.

FIPS reconnect — for real this time
  New fips.reconnect RPC does stop → start → wait 20s → re-poll →
  classify. Classification buckets: connected / daemon_down /
  no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor,
  each with a plain-language hint surfaced verbatim by the Reconnect
  button. The real reason nodes like .198/.253 couldn't reach the
  anchor: identity::write_fips_key_from_seed was writing fips_key.pub
  as a bech32 npub TEXT file, but upstream fips expects 32 raw
  bytes. The daemon silently authenticated with garbage. Fix:
  PublicKey::to_bytes() → raw 32 bytes, and new
  fips::config::normalize_pub_file migrates legacy files by decoding
  the npub and rewriting in place. fips.reconnect also re-installs
  the config + healed keys to /etc/fips before restarting.

AIUI preservation + restore
  apply_update was wiping /opt/archipelago/web-ui/aiui because the
  Vue build doesn't include it — every OTA lost the Claude sidebar.
  The preserve block now copies aiui/ + archipelago-companion.apk
  from the old web-ui into the staging dir before the swap, and
  prefers new-tar versions if present. To restore it on the three
  nodes that already lost it (.116/.198/.253), this release bundles
  the 85 MB aiui build into the frontend tarball. Frontend component
  size is now ~155 MB.

Download / install timeouts
  Backend download client timeout 1800s → 3600s (1 h). Larger
  tarball + slow gitea raw throughput put us above the old cap.
  Frontend update.download rpc timeout 30 min → 65 min to match.
  package.install rpc timeout 15 min → 45 min — IndeedHub pulls
  6 images and was timing out mid-install.

UI nit
  "Rollback to Previous" → "Rollback Available".

App-catalog proxy already landed in v1.7.13.

Artefacts:
  archipelago                                      725e18e6…3c525e6   40462288
  archipelago-frontend-1.7.14-alpha.tar.gz         c35284be…ff2c16   162077052 (+aiui)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:40:25 -04:00
Dorian
fe963a1a8b 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
3ce7bb6c18 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
8b88c45262 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
1fdb5e5cf2 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
274ed008fe 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
6b42bfd503 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
30a7f73ead feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
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