16 Commits

Author SHA1 Message Date
archipelago
8f83b37d51 feat(orchestrator): complete container migration and release hardening 2026-04-28 15:00:58 -04:00
Dorian
4e2c6d210b release(v1.7.19-alpha): kill stale available_update + numeric version compare
load_state now drops any stored available_update whenever the running
binary version differs from what's on disk — the old migration only
cleared it when the stale entry happened to match the new version, so
skipping releases (e.g. sideloading 1.7.16 → 1.7.18 without 1.7.17)
left a pointer to an intermediate version as the "update available",
which the UI then offered as a downgrade prompt.

check_for_updates also uses a numeric version comparator so a stale or
cached manifest with an older version can't offer itself as an
update, and 1.7.10 correctly outranks 1.7.9 past the single-digit
patch boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 04:04:20 -04:00
Dorian
7d8ddcccef release(v1.7.18-alpha): transitive peers default Trusted + update-flow logs
Flip transitively-discovered federation peers to Trusted instead of
Observer. Hints are already only ingested from peers we trust and only
peers we trust are re-exported via build_local_state, so the chain of
trust is already vetted end-to-end — making the user promote each
newcomer by hand was friction with no security win.

Backend:
- federation/sync.rs: merge_transitive_peers now inserts TrustLevel::Trusted
  (doc comment updated to explain the transitive-trust rationale)
- update.rs: info! log at download start (version, components, total_bytes,
  staging path), cancel (staging wiped?, marker cleared?), and apply (backup
  path) so journalctl reveals where a stuck update actually is

Frontend:
- SystemUpdate What's New block gets a v1.7.18-alpha entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:20:36 -04:00
Dorian
f2360d570f release(v1.7.16-alpha): bidirectional + transitive federation, no self-peering
Federation join flow now notifies the inviter with the joiner's name and
immediately bumps state so the Federation UI reloads without a manual
Sync click. Accepting an invite that points back at the local node is
rejected up front (DID/pubkey/onion match). After a peer joins, we spawn
a transitive sync that pulls the new peer's federated peer hints so all
nodes in the federation learn about each other as Observer entries.
Federation.vue polls every 5s while mounted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:12:02 -04:00
Dorian
6399b6881e 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
d63cd92bee 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
2e8417e39b 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
6ddce90e45 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
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
ba825c13a5 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
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
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
Dorian
b614c5c694 chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
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
0c02d06a66 feat: deploy-to-target supports .253 + mesh/federation/VPN updates
- Add deploy_secondary() function for deploying to multiple LAN nodes
- --both now deploys to .198 and .253 (previously .198 only)
- Fleet deploy updated for 3 LAN nodes
- Mesh DM fixes: protocol frame format, DM-via-channel routing
- Federation pending requests, discover modal
- VPN status UI improvements
- Image versions and container specs updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:07:08 -04:00
Dorian
f49138270a fix: federation peer-joined updates empty onion addresses
When a node was already known (via link-node) but had an empty onion
address, the peer-joined handler returned early without updating the
onion. Now it patches missing onion/pubkey fields on existing nodes.

Also adds update_node() to federation storage and updates the
architecture comparison doc with system resources, StartOS/umbrelOS
tabs, Web5 section, and comparison view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:25:27 +01:00
Dorian
e4e0ef4f11 bug fixing and deploy and build diagnostics 2026-03-22 03:30:21 +00:00