docs: rewrite README to match current implementation
- Remove "Why:" line and air-gapped/QR section (those modules were deleted) - Remove stale commands: sync, bundle create/apply/info, qr-encode/decode - Add seed mode quick-start and setup instructions - Add gossip relay section with delta broadcast note - Add --lora flag to all command flag tables - Update architecture diagram: per-bridge port routing, gossip relay, 383 B MTU note - Update module list: bridge/gossip/seed/adapter (remove sync/adaptive/git_bundle/qr) - Update test count: 149 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d7b124e830
commit
aff4719910
179
README.md
179
README.md
|
|
@ -2,23 +2,18 @@
|
||||||
|
|
||||||
Bridges [Radicle](https://radicle.xyz) (decentralized Git) over [Reticulum](https://reticulum.network) mesh networking — LoRa, packet radio, serial, I2P, and more. Enables offline-first code collaboration without internet infrastructure.
|
Bridges [Radicle](https://radicle.xyz) (decentralized Git) over [Reticulum](https://reticulum.network) mesh networking — LoRa, packet radio, serial, I2P, and more. Enables offline-first code collaboration without internet infrastructure.
|
||||||
|
|
||||||
**Why:** Radicle requires publicly reachable seed nodes; Reticulum routes over any physical medium. Both use Ed25519 keys — a natural fit.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Both machines need:
|
|
||||||
- [Radicle](https://radicle.xyz/install) (`rad` CLI + `radicle-node`)
|
- [Radicle](https://radicle.xyz/install) (`rad` CLI + `radicle-node`)
|
||||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python package manager)
|
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
On **each machine**, clone this repo and install:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone rad:z4NMdcKbw2TETQ56fbQfbibFHtZqZ # via radicle
|
git clone rad:z4NMdcKbw2TETQ56fbQfbibFHtZqZ # via radicle
|
||||||
# or: git clone https://github.com/youruser/radicle-reticulum
|
# or: git clone https://github.com/youruser/radicle-reticulum
|
||||||
|
|
@ -26,176 +21,194 @@ cd radicle-reticulum
|
||||||
uv sync
|
uv sync
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional: faster push detection (inotify-based, Linux/macOS):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv sync --extra watch
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Setup: connect two machines over mesh
|
## Two modes
|
||||||
|
|
||||||
Do this once on each machine.
|
### Bridge mode — peer-to-peer
|
||||||
|
|
||||||
|
Each machine runs a bridge. They discover each other over RNS, then radicle-node syncs through the tunnel as if both nodes were on the same network.
|
||||||
|
|
||||||
|
### Seed mode — always-on relay
|
||||||
|
|
||||||
|
One machine runs a dedicated seed radicle-node plus a bridge. Other machines' bridges discover and register the seed automatically. The seed self-populates: it calls `rad seed <RID>` for any repo announced by gossip peers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start: bridge mode
|
||||||
|
|
||||||
### Step 1 — Configure radicle-node to listen on localhost
|
### Step 1 — Configure radicle-node to listen on localhost
|
||||||
|
|
||||||
Edit `~/.radicle/config.json`, find the `"node"` section, set `"listen"`:
|
Edit `~/.radicle/config.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"node": {
|
"node": {
|
||||||
"listen": ["127.0.0.1:8776"],
|
"listen": ["127.0.0.1:8776"]
|
||||||
...
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then (re)start radicle-node:
|
Restart radicle-node:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rad node start
|
rad node start
|
||||||
rad node status # confirm: "listening for inbound connections on 127.0.0.1:8776"
|
rad node status # "listening for inbound connections on 127.0.0.1:8776"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2 — Start the bridge on both machines
|
### Step 2 — Start the bridge on both machines
|
||||||
|
|
||||||
Run on each machine:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv run radicle-rns bridge
|
uv run radicle-rns bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
Within ~30 seconds the bridges discover each other via RNS announce, connect automatically, and register each other's NIDs with radicle-node. You'll see:
|
Within ~30 s the bridges discover each other via RNS announce, connect automatically, and register each other's NIDs with radicle-node:
|
||||||
|
|
||||||
```
|
```
|
||||||
[+] Discovered bridge: <hash> (NID: z6Mk...)
|
[+] Discovered bridge: <hash> (NID: z6Mk...)
|
||||||
[Status] Tunnels: 0, Remote bridges: 1, TX: 0, RX: 0
|
[Status] Tunnels: 0, Remote bridges: 1, TX: 0, RX: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
Once radicle-node connects through the bridge, tunnels open automatically:
|
Once radicle-node syncs through the bridge:
|
||||||
|
|
||||||
```
|
```
|
||||||
Tunnel 1 opened / Incoming tunnel 1 opened
|
Tunnel 1 opened
|
||||||
[Status] Tunnels: 1, Remote bridges: 1, TX: 1551, RX: 1831
|
[Status] Tunnels: 1, Remote bridges: 1, TX: 1551, RX: 1831
|
||||||
```
|
```
|
||||||
|
|
||||||
Bytes in TX/RX confirm radicle gossip is flowing over the mesh.
|
> **LoRa:** Pass `--lora` to apply duty-cycle-safe announce delays (60 s, 300 s, 900 s) instead of the WiFi defaults.
|
||||||
|
|
||||||
> **LoRa note:** The bridge re-announces at t+5s, t+15s, t+30s after startup. On LoRa,
|
### Step 3 — Use radicle normally
|
||||||
> use `--announce-retry-delays 60,300,900` to respect duty cycle limits.
|
|
||||||
|
```sh
|
||||||
|
# Machine A
|
||||||
|
rad init --name myproject --description "" --default-branch main
|
||||||
|
rad push # prints RID: rad:z3...
|
||||||
|
|
||||||
|
# Machine B
|
||||||
|
rad clone rad:z3...
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Share a repository
|
## Quick start: seed mode
|
||||||
|
|
||||||
### Machine A — init and push a repo
|
Run this once to initialise the seed identity:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mkdir myproject && cd myproject
|
uv run radicle-rns setup # checks prerequisites and prints instructions
|
||||||
git init
|
RAD_HOME=~/.radicle-seed rad auth
|
||||||
git commit --allow-empty -m "init"
|
|
||||||
rad init --name myproject --description "my project" --default-branch main
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`rad init` prints the repository ID (`rad:z3...`). Share it with Machine B.
|
Then start the seed (keeps running, restarts cleanly):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# make a commit and push
|
uv run radicle-rns seed
|
||||||
echo "hello" > hello.txt
|
|
||||||
git add . && git commit -m "hello"
|
|
||||||
rad push
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Machine B — clone it
|
Other machines running `radicle-rns bridge` (or `seed`) discover the seed automatically over RNS. The seed begins tracking any repo announced by gossip peers.
|
||||||
|
|
||||||
|
Register the seed in your local radicle-node (one-time, per machine):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rad clone rad:z3... # use the RID from Machine A
|
rad node connect <SEED_NID>@127.0.0.1:8777
|
||||||
cd myproject
|
|
||||||
cat hello.txt # hello
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Machine A — fetch updates from Machine B
|
The seed NID is printed by `radicle-rns seed` on startup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gossip relay
|
||||||
|
|
||||||
|
The gossip relay watches local Radicle storage for ref changes and sends small notifications (~100–200 B) to peer relays over RNS. On receipt, the peer calls `rad sync --fetch` against the announcing node. Seed mode enables gossip automatically; for bridge mode run it separately:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Machine B: edit, commit, push
|
uv run radicle-rns gossip rad:z3... # auto-detected from CWD if omitted
|
||||||
echo "world" >> hello.txt
|
|
||||||
git add . && git commit -m "world"
|
|
||||||
rad push
|
|
||||||
|
|
||||||
# Machine A:
|
|
||||||
rad fetch
|
|
||||||
git pull
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Gossip broadcasts only the changed refs (delta mode), so a one-commit push sends ~120 B instead of the full ref list.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
radicle-rns bridge # TCP↔RNS bridge (main command)
|
radicle-rns bridge # TCP↔RNS bridge
|
||||||
|
radicle-rns seed # dedicated seed node + bridge + gossip
|
||||||
|
radicle-rns gossip [RID ...] # standalone gossip relay
|
||||||
|
radicle-rns setup # check prerequisites, print fix instructions
|
||||||
radicle-rns node # lightweight peer-announce node
|
radicle-rns node # lightweight peer-announce node
|
||||||
radicle-rns peers # discover peers on the mesh
|
radicle-rns peers # discover peers on the mesh
|
||||||
radicle-rns ping <hash> # RTT probe to a peer
|
radicle-rns ping <hash> # RTT probe to a peer
|
||||||
radicle-rns identity generate # create/show identity
|
radicle-rns identity generate # create identity
|
||||||
radicle-rns sync <repo> # LXMF store-and-forward sync
|
radicle-rns identity info # show DID and RNS hash
|
||||||
radicle-rns bundle create <repo> # pack repo into a bundle file
|
|
||||||
radicle-rns bundle apply <bundle> <repo> # unpack a bundle into a repo
|
|
||||||
radicle-rns bundle info <bundle> # inspect bundle metadata
|
|
||||||
radicle-rns bundle qr-encode <bundle> # print ASCII QR (≤2953 bytes)
|
|
||||||
radicle-rns bundle qr-decode <image.png> # decode QR back to bundle
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Global flags: `-v` verbose logging, `--identity PATH` (default `~/.radicle-rns/identity`).
|
Global flags: `-v` verbose logging, `--identity PATH` (default `~/.radicle-rns/identity`).
|
||||||
|
|
||||||
### Bridge flags
|
### bridge flags
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
|------|---------|-------------|
|
|------|---------|-------------|
|
||||||
| `-l, --listen-port` | 8777 | TCP port radicle-node connects to |
|
| `-l, --listen-port` | 8777 | Base TCP port (first discovered bridge gets this) |
|
||||||
| `--radicle-port` | 8776 | Port radicle-node listens on |
|
| `--radicle-port` | 8776 | Port radicle-node listens on |
|
||||||
| `-c, --connect <hash>` | — | Connect to a remote bridge by RNS hash |
|
| `-c, --connect <hash>` | — | Connect to a specific remote bridge by RNS hash |
|
||||||
| `--nid <NID>` | auto-detect | Override local radicle NID |
|
| `--nid <NID>` | auto-detect | Override local radicle NID |
|
||||||
| `--no-auto-connect` | — | Disable auto-connect on discovery |
|
| `--no-auto-connect` | — | Disable auto-connect on discovery |
|
||||||
| `--no-auto-seed` | — | Disable auto-registering remote NIDs |
|
| `--no-auto-seed` | — | Disable auto-registering remote NIDs |
|
||||||
|
| `--lora` | — | LoRa-safe defaults: announce delays 60,300,900 s |
|
||||||
|
| `--announce-retry-delays` | 5,15,30 | Startup re-announce delays (seconds, comma-separated) |
|
||||||
|
|
||||||
---
|
### seed flags
|
||||||
|
|
||||||
## Air-gapped / QR transfer
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--seed-home` | `~/.radicle-seed` | RAD_HOME for the seed radicle-node |
|
||||||
|
| `--seed-port` | 8776 | TCP port for the seed radicle-node |
|
||||||
|
| `--bridge-port` | 8778 | TCP listen port for the seed bridge |
|
||||||
|
| `--poll-interval` | 30 | Seconds between gossip ref polls |
|
||||||
|
| `--lora` | — | LoRa-safe defaults |
|
||||||
|
|
||||||
For links too slow even for Reticulum, transfer tiny incremental bundles via QR code (max 2953 bytes):
|
### gossip flags
|
||||||
|
|
||||||
```sh
|
| Flag | Default | Description |
|
||||||
# Sender — create a small incremental bundle and encode it
|
|------|---------|-------------|
|
||||||
uv run radicle-rns bundle create ./myrepo --incremental --basis myrepo.refs.json
|
| `--nid` | auto-detect | Local radicle NID to advertise |
|
||||||
uv run radicle-rns bundle qr-encode myrepo-*.radicle-bundle # prints ASCII QR to terminal
|
| `--bridge-port` | 8777 | TCP port of the local bridge |
|
||||||
|
| `--poll-interval` | 30 | Seconds between ref polls |
|
||||||
# Receiver — photograph the QR, then decode and apply
|
| `--lora` | — | LoRa-safe defaults (delays 60,300,900 s; poll 120 s) |
|
||||||
uv run radicle-rns bundle qr-decode qr-photo.png -o received.radicle-bundle
|
|
||||||
uv run radicle-rns bundle apply received.radicle-bundle ./myrepo
|
|
||||||
```
|
|
||||||
|
|
||||||
QR image output (PNG) and image decoding require optional deps:
|
|
||||||
```sh
|
|
||||||
uv sync --extra qr # qrcode for PNG output
|
|
||||||
pip install pillow pyzbar # for qr-decode from image file
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
radicle-node ──TCP:8777── RadicleBridge ──RNS Link── RadicleBridge ──TCP:8776── radicle-node
|
radicle-node ─TCP─ RadicleBridge ──RNS Link── RadicleBridge ─TCP─ radicle-node
|
||||||
(Machine A) (Machine A) (Machine B) (Machine B)
|
(Machine A) (Machine A) (Machine B) (Machine B)
|
||||||
|
│ │
|
||||||
|
GossipRelay ──RNS Packet── GossipRelay
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Identity** (`identity.py`) — Ed25519 DID ↔ RNS destination; saved to `~/.radicle-rns/identity`
|
Each discovered remote bridge gets its own OS-assigned TCP listen port, so radicle-node connections always route to the correct peer. All RNS packets are chunked to ≤383 B (LoRa encrypted MTU).
|
||||||
- **Bridge** (`bridge.py`) — TCP↔RNS tunnel, announces itself, discovers peers
|
|
||||||
- **SyncManager** (`sync.py`) — LXMF store-and-forward bundles; auto-pushes on refs announce
|
- **`identity.py`** — Ed25519 DID ↔ RNS identity; saved to `~/.radicle-rns/identity`
|
||||||
- **AdaptiveSyncManager** (`adaptive.py`) — selects FULL/INCREMENTAL/MINIMAL/QR by RTT + throughput
|
- **`bridge.py`** — TCP↔RNS tunnel, per-bridge port allocation, path maintenance, reconnect
|
||||||
- **GitBundle** (`git_bundle.py`) — full and incremental Git bundles for delay-tolerant transfer
|
- **`gossip.py`** — ref-change notifications, delta broadcasts, auto-seed for unknown repos
|
||||||
- **QR** (`qr.py`) — visual air-gap transfer for tiny bundles
|
- **`seed.py`** — dedicated radicle-node process lifecycle (separate RAD_HOME)
|
||||||
|
- **`adapter.py`** — RNS peer discovery and announce filtering
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv run pytest # 158 tests
|
uv run pytest # 149 tests
|
||||||
uv run pytest -x -q # stop on first failure
|
uv run pytest -x -q # stop on first failure
|
||||||
|
mypy src/ # type check
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -214,4 +227,4 @@ On the same LAN, Reticulum auto-discovers peers via UDP multicast — no config
|
||||||
codingrate = 5
|
codingrate = 5
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Reticulum docs](https://reticulum.network/manual/) for the full interface list.
|
See the [Reticulum manual](https://reticulum.network/manual/) for the full interface list.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue