archy/reticulum-daemon
archipelago 0eb5c258f5 fix(mesh): Meshtastic 3ccc pkc_capable pill + Sideband image interop + critical CBOR wire-bloat fix
Merges in the meshtastic agent's now-finished work alongside this session's
continuation: stock-peer (3ccc) PKI-capability is now stamped through
get_contacts -> refresh_contacts -> MeshPeer.pkc_capable, so a directed DM to/from
a PKC-capable stock Meshtastic peer correctly shows the E2E pill on the Sent row,
not just received messages. Confirmed live: .198 sees "Meshtastic 3ccc" with
pkc_capable=true.

Also fixes two real interop/correctness bugs found while live-testing the
Reticulum <-> Sideband link:
  - Receive: the daemon only ever read LXMF's plain-text content, silently
    dropping native FIELD_IMAGE/FIELD_FILE_ATTACHMENTS fields — a stock
    Sideband/NomadNet photo vanished into a blank-space message. Now decoded
    into the same ContentInline typed envelope our own attachments use.
  - Send: images to a non-archy (stock) peer now use native LXMF FIELD_IMAGE
    instead of our own opaque CBOR wire format, which Sideband can't decode.
  - Root cause of a garbled MC-chunk-fragment bug: TypedEnvelope.v/.sig (the
    OUTER wrapper every message type uses) serialized raw bytes as a CBOR
    array-of-integers instead of a native byte string, bloating every
    message on the wire ~2-3.5x — enough to push even a tiny ReadReceipt
    over the 140-byte single-frame chunking threshold. Root-caused by
    reading ciborium's deserializer source directly (deserialize_bytes only
    works within its internal scratch buffer; deserialize_byte_buf streams
    unbounded).

Frontend: consolidated the attach/record buttons into a single animated "+"
menu (was overflowing the compose row).

857/857 tests pass. Verified live across all 5 deploy-roster nodes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 22:07:45 -04:00
..

reticulum-daemon

Host-supervised Reticulum (RNS) + LXMF bridge for Archipelago's Mesh tab. This is the Python side of the Reticulum transport plan: archipelago spawns one of these per active Reticulum (RNode) radio, it owns the serial port, and the Rust mesh subsystem drives it over a Unix-socket JSON-RPC.

Why a daemon (not the Rust reticulum-rs crate): the canonical Python rns/lxmf guarantees interop with Sideband / NomadNet / MeshChat, and lets us derive the RNS identity from the existing Archy key (proven in spike_identity.py).

Layout

  • archy_rns_identity.py — derive a deterministic RNS Identity from the 32-byte Archy Ed25519 seed (identity_dir/node_key) via domain-separated HKDF. The node's LXMF destination hash is a stable function of the Archy identity.
  • spike_identity.pyPhase-0 gate #1 (no radio): proves that determinism.
  • reticulum_daemon.py — the daemon: RNS bring-up, LXMF router, announce handler, and the Unix-socket RPC. See its module docstring for the wire protocol.
  • requirements.txt — pinned rns==1.3.5, lxmf==1.0.1 (validated on Python 3.13).

Dev setup

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Run the spike / smoke tests (no hardware)

.venv/bin/python spike_identity.py                     # gate #1: identity determinism
.venv/bin/python reticulum_daemon.py --check \
    --identity-key /path/to/node_key                   # print this node's dest hash
.venv/bin/python reticulum_daemon.py --selftest \
    --identity-key /path/to/node_key                   # bring up RNS+LXMF, no radio

Run against a real RNode (Phase-0 hardware gate, on .116 / .228)

.venv/bin/python reticulum_daemon.py \
    --identity-key /var/lib/archipelago/identity/node_key \
    --serial-port /dev/reticulum-radio \
    --socket /run/archy/reticulum.sock \
    --display-name "archy-228"

Then verify a two-node LXMF DM over LoRa and interop with a stock Sideband/MeshChat client (Phase-0 gates #2 and #3).

Packaging (Phase 1)

Ship as a PyInstaller single binary in the OTA next to /usr/local/bin/archipelago (no provision-time pip install). archipelago supervises it: start on RNode detect, kill on unplug/disable. The RPC socket and RNS config dir are archipelago-owned, 0600.

./build.sh   # → dist/archy-reticulum-daemon (~16M, fully standalone)

-d noarchive is required, not optional — see the comment in build.sh: RNS computes RNS.Interfaces.__all__ via a glob() against its own __file__ directory at import time, which only works when PyInstaller keeps modules as loose files instead of zipping them into the binary.

Status

Phase-0 gate #1 (identity determinism) passes, verified in both the dev venv and the packaged binary (same dest hash). The signed-identity announce (ARCHY:2:{ed}:{x25519} in _announce_app_data, via --archy-ed-pubkey-hex/--archy-x25519-pubkey-hex) is wired and the Rust side (reticulum.rs) already passes the node's real keys through. Packaging is done and verified standalone. What's left is entirely hardware-dependent: the live LoRa message path (Phase-0 gates #2/#3) needs a real RNode-flashed board.