Skip to content

mcpx: reach a daemon by identity, not by address — iroh, and the Go-binding question it forces #1967

Closed

Description

@dezren39

Relocated from dezren39/darwin-nix#297 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #174

Summary

Reaching a daemon today means reaching an address. daemon.endpoint
(internal/settings/registry.go:356) takes a URL, the server listens on a unix
socket and a TCP port (internal/daemon/server.go:205,220), and #85 covers
getting to that port over ssh with tunnels. All of it assumes you can name a
route to the machine.

The thing that is actually wanted is to reach a daemon by identity — "my
laptop's mcpx", from a phone, from a different network, with no port forwarded,
no public IP, no dynamic DNS and no ssh key copied anywhere. iroh does exactly
this: QUIC between nodes named by an ed25519 public key, hole-punched directly
where possible and relayed where not, end-to-end encrypted either way.

The crux is not whether that is desirable. It is that iroh is Rust and mcpx is
Go
, and this issue exists mostly to force that question into the open before
anyone starts, because the honest answer may be that a Go-native transport wins
on merit.

Original notes

add another for iroh support

you can imagine what i might want build out your dreams

Current state

Two listeners, both address-shaped. Server.Start binds a unix socket at
s.paths.Socket and a TCP listener on a host/port
(internal/daemon/server.go:205,220). daemon.endpoint lets a client point at
a remote daemon, with a documented fallback to a local one when the remote is
unreachable (internal/settings/registry.go:356-364).

Remote reach is already a theme, already scoped to ssh. #85 is "remote
access over ssh -- TUI, RPC, reverse tunnels, and why not a built-in ssh
server", under the #174 surfaces tracker. ssh solves authentication and
encryption and solves routing only if you can already reach the host. The
laptop-behind-NAT case is precisely where it does not help, and that is the case
worth solving.

Content addressing is also already a theme. #187 asks for "one
content-addressed cache for prepared dependencies, import graphs and flake
resolution". Artifacts shipped in #57. Both are BLAKE3-shaped problems that
iroh-blobs already solves as a library, with verified streaming and resumable
transfer.

No Go implementation of iroh exists. iroh is a Rust project (n0-computer).
iroh-ffi provides C bindings. There is no maintained Go port and writing one
is not a weekend.

Approaches

Approach A — least recommended: cgo against iroh-ffi

Link the Rust library into the Go binary through its C FFI.

This gets real iroh with its real guarantees and poisons the build. mcpx is
currently a pure-Go binary that nix build cross-compiles without ceremony;
cgo plus a Rust toolchain means per-platform native artefacts, a much larger
package.nix, cross-compilation that stops being free, and a release matrix
(#158) that multiplies. It also drags Rust's async runtime into a Go process,
which is a category of debugging nobody in this repository has signed up for.
The capability is not worth that unless everything else has failed.

Approach B — middle: iroh as a sidecar, tunnelling to the existing socket

Run an iroh node as a separate process. It accepts an incoming connection by
NodeId, and proxies the stream to the daemon's existing unix socket. mcpx
learns one new thing: how to supervise and address a sidecar.

Honest and incremental. The daemon needs no new transport at all — it already
speaks HTTP over a unix socket, and the sidecar is a tunnel endpoint. It keeps
Go pure, it is testable, and it can be an optional component people install
rather than something in every binary.

Its costs are distribution and truth-in-advertising: a second binary from a
second toolchain in the release pipeline, and "mcpx supports iroh" meaning "mcpx
supports a thing you install next to mcpx".

Approach C — recommended: write down the requirement, then pick the transport on Go-native merit

The property wanted is a short list, and iroh is one implementation of it:

  1. a daemon reachable by stable cryptographic identity, not by address
  2. NAT traversal without port forwarding, with a relay fallback
  3. end-to-end encryption and mutual authentication from that identity

Who may then do what is a separate question, tracked separately (#298/#253),
and explicitly not a dependency of this issue.

Stated that way, the Go ecosystem has candidates that do not cost a Rust
toolchain, and they should be evaluated against iroh rather than assumed
inferior:

  • tsnet (Tailscale) embeds a full Tailscale node in a Go program. It
    gives 1–3 outright, with mature hole-punching, a working relay network
    (DERP), identity tied to an existing account, and ACLs that are a real answer
    to 4. It is pure Go and already in this project's orbit — Update github.com/charmbracelet/promwish digest to 6c16e8f - autoclosed #115 proposes a
    Tailscale server, which means the dependency may be arriving anyway. The
    cost is a dependency on Tailscale's coordination service unless self-hosting
    Headscale, which is a genuine centralisation objection iroh does not have.
  • go-libp2p gives 1–3 with no central coordinator, pure Go, at the price
    of being a large dependency with sharp operational edges around relays and
    peer discovery.
  • iroh, via B, if the decentralisation and the blob story outweigh the
    sidecar.

Where iroh wins on something the others do not have: iroh-blobs is
BLAKE3-verified, resumable, content-addressed transfer, and #187 is already
asking for a content-addressed cache. If prepared dependencies, import graphs
and artefacts became iroh blobs, then "fetch this script's dependency closure
from whichever of my machines has it" is the same mechanism as "reach my
daemon", and that is a genuinely better story than tsnet or libp2p offer. If
that convergence is the goal, iroh's case is much stronger and B becomes worth
its cost.

So: do not start with a transport. Start with a short research issue that
answers "which of tsnet, libp2p and iroh-as-sidecar meets 1–4 for a pure-Go
binary, and does #187 want iroh-blobs badly enough to pay for a sidecar?" Then
implement the winner behind an interface the daemon can hold more than one of,
because the authorisation work in 4 is required regardless of who wins and is
probably the larger half of the job.

Open questions

  • Does the daemon hold more than one transport at once, or is it one at a time
    by configuration? Holding several is friendlier and multiplies the surface
    each has to be correct on.
  • Does the TUI (chore(deps): bump openssl from 0.10.54 to 0.10.60 in /sources/pavex/examples/realworld #85's reverse-tunnel motivation) work acceptably over a relayed
    connection, or does the latency make it unpleasant precisely when hole
    punching fails?
  • Is the real requirement "reach my daemon" or "reach my daemon from a phone"?
    If a browser is in the picture at all, none of iroh, tsnet or libp2p is
    directly reachable from a web page, and the answer is a hosted relay, which is
    a different project.
  • Does daemon.endpoint grow a scheme (iroh://<nodeid>, ts://<host>), or
    does identity-based reach get its own setting? A scheme keeps one setting and
    one mental model.
  • If a sidecar is acceptable here, is it acceptable for the Roc platform host
    too? Two issues wanting a Rust artefact is a different decision from one.

Ease · Impact · Related

Ease: S for the research comparison; M for a sidecar tunnel (B); L–XL for
cgo (A). Impact: high — "reach your own daemon from anywhere without
configuring a network" is the difference between mcpx as a local tool and mcpx
as personal infrastructure.

Depends on / related: #85 (remote access over ssh — the incumbent answer,
and this issue is the case ssh cannot reach), #174 (parent tracker: surfaces),
#187 (content-addressed cache — the strongest argument for iroh specifically,
via iroh-blobs), #115 (proposes a Tailscale server; if that lands, tsnet is
already a dependency and Approach C's calculus changes), #158 (release
artefacts, which a sidecar multiplies), #84 (embedding and the C ABI — the same
cgo question from the other direction), #113 (the operation surface where a
"who may call this" policy would be administered). Authorisation is tracked
separately in #253/#298/#300 and is not a dependency of this issue.

Acceptance criteria

  • A written comparison of iroh-from-Go options (bindings, sidecar, protocol reimplementation) against a Go-native identity transport, with a recommendation — the Go-binding question is answered, not deferred.
  • If a transport is built: a client reaches a daemon by node identity from a different network with no port forwarding, no public IP, and no ssh key copying, authenticated by the identity key.
  • Address-based endpoints (unix socket, TCP, ssh per chore(deps): bump openssl from 0.10.54 to 0.10.60 in /sources/pavex/examples/realworld #85) keep working unchanged.
  • Failure modes (relay-only path, identity mismatch) produce clear errors, never a silent fallback to an unauthenticated route.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#297 (verbatim, oldest first):


    @dezren39 · 2026-10-27T14:29:42Z

    Intake review

    🤖 Agent note · filed

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-10-27T14:14Z
    • Found via: monitor reconciliation (intake enrichment, protocol §5)
    • Task: intake pass — liveness check, labels normalized, acceptance criteria appended to the body
    • Expectation: see the effort label on this issue
    • Links: n/a

    Related: Related: parent #174; #85 already scopes ssh remote access and does not cover the behind-NAT identity case. No merged PR resolves it.

    Risks / notes: The crux is the Rust/Go boundary, not desirability: an honest answer may be a Go-native transport. Binding or sidecar approaches carry packaging costs this repo has rejected before — price them in the comparison, not after a prototype.

    Suggested approach: Write the transport comparison first (iroh bindings / sidecar / Go-native), get the decision recorded on this issue, then prototype only the winning route.

    Liveness: searched merged PRs and recent fleet records this run — no merged PR explicitly resolves this issue, so it stays open. Marked meta/enriched.


    @dezren39 · 2026-10-28T19:31:22Z

    🤖 Agent note · triage confirmed


    @dezren39 · 2026-10-28T20:16:14Z

    Recommendation summary (task 2654, decision doc PR #1052): adopt tsnet as mcpx's first identity transport, behind a transport interface that can hold more than one. Iroh-as-sidecar is the sanctioned second transport if the decentralisation or iroh-blobs requirement is ever written down. Iroh-via-cgo is rejected outright.

    Full comparison: PR #1052 adds docs/mcpx-identity-transport-decision.md — the four options (iroh-cgo, iroh-sidecar, tsnet, go-libp2p) scored against this issue's three properties plus pure-Go/release cost (#158), coordinator centralisation, and operational edges, with current state re-verified at developing-today/code e91098f1 (registry.go:429; server.go:253–280 binds unix + TCP only; zero iroh/tsnet/libp2p in the tree).

    Why tsnet, briefly: it is the only option that meets properties 1–3 today with zero build damage (pure Go, most proven NAT/relay story, no release-matrix change); its one real cost — coordination centralisation — is a cost #115 is already deciding to pay, and if #115 is rejected for centralisation, that same decision promotes the sidecar instead, which the interface makes a substitution rather than a rewrite. Its weakest point is named in the doc: identity is delegated to the tailnet rather than being a bare keypair mcpx owns; if "no third party can revoke it" becomes a hard requirement, the answer flips to the sidecar.

    On the question this issue says the comparison must answer — does #187 want iroh-blobs badly enough to pay for a sidecar? No, not on what #187 asks for today. Read at HEAD, #187's problem is local cache policy fragmentation (three features designing caches alone; its current-state section is entirely local pruners and eviction rules). Nothing in #187 asks to move cache entries between machines. iroh-blobs solves verified transfer, which becomes #187's problem only when someone writes the "fetch the closure from whichever machine has it" requirement — that day, the interface admits the sidecar without rework.

    Open questions, as answered in the doc: multi-transport — interface yes, one enabled at a time by configuration; relay latency/TUI — measure in the tsnet spike, v1 promises RPC/CLI with TUI best-effort on relayed paths; phone/browser — out of scope for v1 (none of the three reaches a web page; a browser requirement is a separate hosted-relay project); daemon.endpoint — grow the scheme (tsnet://…, later iroh://…), keeping the empty = local-socket default and all address-based endpoints unchanged; Roc sidecar precedent — informational, not binding, each sidecar decided on its own merits. Nothing here touches authorisation (#253/#298/#300), per this issue's own scoping.

    Task 2654 is parked on PR #1052's CI; the resume brief is on the task.


    @dezren39 · 2026-10-28T20:52:02Z

    Decision doc landed (lane A, task 2654): PR #1053 merged — planning/plans/mcpx-identity-transport.md (merge commit 8c42c2c).

    Recommendation: adopt tsnet as mcpx's first identity transport, behind a small transport interface in the daemon (transports produce a net.Listener; Server.Listen already serves a slice of them, so unix/TCP are untouched). All four options were scored in a full criteria matrix in the doc; the short version:

    • iroh via cgo (iroh-ffi) — rejected, and the Go-binding question is answered, not deferred. It breaks the pure-Go build (mcpx package.nix sets CGO_ENABLED=0, verified at developing-today/code 528886e), needs a Rust toolchain per platform, and rests on Go bindings that iroh's own documentation lists as community FFI bindings that may lag releases.
    • iroh as sidecar — not now. The daemon stays pure, but the release gains a second toolchain's binary and a supervision problem, paid for a blobs benefit that does not currently exist (next point). Kept as the named fallback if a cross-machine content-transfer requirement is ever filed.
    • go-libp2p — the decentralised fallback. Meets the identity/encryption properties in pure Go, but relay capacity and peer discovery for "my two machines" are its sharp edges, and the dependency is very large.
    • tsnet — recommended. Properties 1–3 with the most mature relay network (DERP), a first-party Go library returning a plain net.Listener, and it is already in this project's orbit (Update github.com/charmbracelet/promwish digest to 6c16e8f - autoclosed #115, the fleet tailnet). Its real cost — coordination-service centralisation — is named in the doc, with Headscale as the mitigation and the transport interface as the reversibility guarantee.

    #187 / iroh-blobs: no, it does not want blobs badly enough to pay for a sidecar. #187's recommended design is a local cache package (per-kind namespaces, SHA-256 keys, lease pinning, byte/age eviction); its flake tenant's problem is GC roots for store paths nix already transfers, and its dependency tenant may shrink to a manifest. Cross-machine blob fetch is an attractive but unfiled requirement — an option, not a need.

    Open questions answered in the doc: multiple transports yes-by-design (enable one identity transport first); relay-path TUI latency is an acceptance measurement, and status must show direct vs relayed; endpoint grows a scheme on daemon.endpoint (e.g. tsnet://…) rather than a second setting; the Roc sidecar precedent is moot (no sidecar adopted).

    Escalated to Drew (labelled user/escalated): browser reachability. None of the four candidates is reachable from a web page; that would be a hosted-gateway project. The doc proceeds on phone-yes (tailnet app) / browser-no — if browser reach is actually in scope, that is a scope decision only Drew can make.

    Next step when picked up: a follow-up implementation issue (tsnet listener + tsnet:// scheme + direct/relayed status + relay-latency measurement) filed against the doc. No code or dependency was added here.

  3. added
    source/humanFiled directly by a human
    priority/lowNice to have; do when convenient
    user/escalatedQueue or user escalation state
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  4. 118 remaining items

  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (resume)

  6. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked — merge queue stall (#1599)

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 29, 2026
  8. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · blocked

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-inskind/enhancementmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/lowNice to have; do when convenientqueue/blockedQueue or user escalation statesource/humanFiled directly by a humanstatus:doneuser/escalatedQueue or user escalation statework/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions