federation.remove-node only edited nodes.json, so a removed/renamed node
(e.g. a stale "Arch HP") lingered in the mesh chat list with its old
thread. Capture the node's pubkey before removal, then purge its
synthetic mesh peer, shared secret, messages, presence, and persisted
contact entry via the new mesh::purge_federation_peer. Combined with the
#42 name refresh, stale federation contacts can now be fully cleaned from
a node.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A peer accepted via invite is seeded into the mesh peer table with
name=None, so it shows as "Archipelago <pubkey8>" in chat. Federation
sync later learns the real name (update_node_state writes it to
nodes.json) and discovers transitive peers (merge_transitive_peers),
but nothing pushed those into the live mesh peer table — the chat list
stayed stale until the next mesh restart, and transitive peers never
appeared as contacts at all.
Add RpcHandler::refresh_federation_mesh_peers() (re-runs the idempotent,
onion-deduped seed_federation_peers_into_mesh) and call it after every
periodic sync cycle (server.rs) and after the manual federation.sync-all
RPC. Names now correct themselves and the full roster meshes within a
sync cycle, no restart needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FIPS peer content browse over the mesh was failing with "Peer returned
error: 404 Not Found" and never falling back to Tor. `is_peer_allowed_path`
only allowed `/content/<id>` (item fetches) — the catalog endpoint is
exactly `/content` (no trailing slash), so it 404'd over the FIPS peer
listener. A FIPS 404 was also treated as a successful response, so the dial
never retried Tor. Fixes: allow `/content` over the mesh; add
`fips_should_fall_back()` so a FIPS 404/5xx in Auto mode falls back to Tor
(handles version-skew peers reaching a different route). Also correct the
reconnect hint text — the public anchor is TCP/8443, not UDP/8668.
Federation: deleted nodes reappeared because transitive discovery
(`merge` of a peer's advertised trusted peers) re-added any unknown DID.
Add a tombstone store (`removed-nodes.json`): remove_node tombstones the
DID, transitive merge skips tombstoned DIDs, and a remote-triggered
peer-joined is ignored for a removed DID. Explicit local re-add (add_node)
clears the tombstone.
UI: the app credentials modal panel stretched edge-to-edge (height:100%,
max-width:none, items-stretch overlay). Constrain it to a centered card
(max-width 34rem, rounded, dimmed full-screen backdrop) matching the
AppIconGrid / wallet-receive modal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>