v3/docs/adr/ADR-104-federation-wire-transport.md
ADR-097 shipped the federation plugin's security/audit/breaker layers (Phases 1, 2.a, 2.b, 3 consumer + upstream, 4 — all Implemented in @claude-flow/[email protected]). The transport layer was deliberately deferred: routing-service.ts has no fetch/WebSocket/http.request calls; every federation_send runs in-process. The plugin contract treated wire transport as an integrator concern.
For real mac↔ruvultra peering, an integrator (here: ourselves) needs to pick a transport. We surveyed three:
node:net) — ~50 LOC, encryption + identity from WireGuard, trivially works between any two tailnet hosts.agentic-flow is already a transitive dep elsewhere in the repo and advertises ./transport/quic exports (QuicClient, QuicServer, QuicConnectionPool, QuicTransport, QuicHandshakeManager). Promised 0-RTT, multiplexed streams, TLS 1.3.quinn (Rust) or @matrixai/quic (pure JS), wrap with N-API.Smoke-tested option 2 between mac (darwin/arm64) and ruvultra (linux/x64) over tailscale on 2026-05-09:
[client] connect ruvultra:9100... connected in 0ms
[client] stream.send(76B)... sent in 0ms
[server] stats: conns=0/0 streams=0/0 rx=0B
0ms for a real QUIC handshake is impossible. Server rx=0B after a successful send() is impossible. Source confirmed: loadWasmModule() returns {}, encodeHttp3Request/decodeHttp3Response are placeholders, and crates/agentic-flow-quic/src/wasm.rs carries the comment:
"This wraps the WASM stub since browsers don't support UDP/QUIC directly. For production QUIC, use native Node.js builds."
The published QUIC transport is API-only. The native build that would unstub it has 7 open Phase-1 issues in ruvnet/agentic-flow#15-21 and isn't shipped.
Adopt agentic-flow's loadQuicTransport() loader pattern with WebSocket fallback for v1; native QUIC remains the v2 target.
We:
loadQuicTransport() + WebSocketFallbackTransport from the OUTER repo's quic-loader.ts into the published inner agentic-flow package (PR ruvnet/agentic-flow#153). The loader detects native QUIC availability (today: false) and selects WebSocket. Same AgentTransport interface for both backends — federation code never branches on transport.[email protected] on the fix dist-tag so federation can consume the working transport without waiting for upstream merge. When upstream merges + cuts a release, federation re-points to the official version with no code change.agentic-flow/transport/loader, not ./transport/quic (the stub). The loader's getTransportCapabilities() answer drives the doctor surface — operators see selectedBackend: 'websocket' today and will see selectedBackend: 'quic' automatically when the native binding lands and AGENTIC_FLOW_QUIC_NATIVE=1.mac (darwin/arm64) → ruvultra:9101 (linux/x64) over tailscale, real bytes on the wire:
[srv] LISTENING on 0.0.0.0:9101
[srv] caps: {"quicAvailable":false,"webSocketFallbackAvailable":true,"selectedBackend":"websocket"}
[cli] caps: {"quicAvailable":false,"webSocketFallbackAvailable":true,"selectedBackend":"websocket"}
[cli] sending to ruvultra:9101
[cli] sent in 125ms ← real network I/O
[cli] DONE
125ms is the legit tailnet RTT for a fresh WS connect + JSON message, not the prior stub's 0ms.
AGENTIC_FLOW_QUIC_NATIVE=1 and the loader picks the native binding when it lands. No federation code changes.wss:// certs for the immediate use case.AgentTransport interface across backends — integrators write transport-agnostic code.agentic-flow/transport/quic, not just us.wss:// + cert pinning before doing so. Document in the operator runbook.agentic-flow@latest is still the stub-shipping 2.0.11. We pin agentic-flow@fix (2.0.12-fix.1) until upstream merges PR #153 and cuts 2.0.13. After that we pin ^2.0.13 and drop the fix tag dependency.| Component | Status | Where |
|---|---|---|
| Upstream fix (loader + WS fallback) | Open PR | ruvnet/agentic-flow#153 |
| Patched npm release | Published | [email protected] (fix dist-tag) |
| End-to-end mac↔ruvultra over tailscale | Verified | This ADR's "Validated" section |
Federation plugin wiring (agentic-flow/transport/loader integration) | TODO | Tracked as ADR-104 phase 2 — depends on upstream merge timing |
| 12h verification routine — QUIC check | TODO | Update trig_01DKn9PZUJfqCrugRVwac5LX with Check 8 (real-network smoke against agentic-flow@fix) |
| Native QUIC binding (real upgrade path) | Deferred | Tracked upstream at agentic-flow#15-21 |
When this is wired into the federation plugin, npx ruflo doctor --component federation will report:
✓ Federation Breaker: ADR-097 breaker loadable
✓ Federation Transport: selectedBackend=websocket (native QUIC unavailable)
When the native binding lands and is enabled:
✓ Federation Breaker: ADR-097 breaker loadable
✓ Federation Transport: selectedBackend=quic (0-RTT, multiplexed streams)
Re-open this ADR when ANY of:
isQuicAvailable() returns true)