Repository navigation
Description
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:
- a daemon reachable by stable cryptographic identity, not by address
- NAT traversal without port forwarding, with a relay fallback
- 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-libp2pgives 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.endpointgrow 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
developing-today commentedon Oct 28, 2026
CODE-892
dezren39 commentedon Oct 28, 2026
History carried over from dezren39/darwin-nix#297 (verbatim, oldest first):
@dezren39 · 2026-10-27T14:29:42Z
Intake review
🤖 Agent note · filed
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
design/research,effort/l), not a code defect, so triage routes a decision doc, not a transport implementation.pkgs/mcpxin developing-today/code (laptop primary HEADbfcfc15ac2c6fbd1d0d0728b9832c89c0e798ab0):daemon.endpointis atinternal/settings/registry.go:429(moved from :356 in the issue) — KindString, empty = local unix socket, otherwise "a URL points at a daemon on another machine" orunix:///path; no identity scheme.Server.Listenatinternal/daemon/server.go:253binds onlynet.Listen("unix", s.paths.Socket)(:258) and a TCP listener (:273).grep -rn "iroh|tsnet|libp2p"overpkgs/mcpx/internal,pkgs/mcpx/cmd,go.mod: zero hits. In dezren39/nix itself (mainefd3161899dafcb7a579633b9e3ef14997bc7668) code search forirohanddaemon.endpointreturns nothing — the cited code does not live in this repo. Related fix(id): satisfy Rustfmt module ordering #174 (parent), chore(deps): bump openssl from 0.10.54 to 0.10.60 in /sources/pavex/examples/realworld #85, Phase 4: joiner with missing pre-join control messages stalls silently #187, Update github.com/charmbracelet/promwish digest to 6c16e8f - autoclosed #115 all still open; no PR found for ci(mcpx): PR #295 removes the only CI build of the mcpx Nix package — stale vendorHash on human PRs merges green #297.repo-health-nix-pilotas task 2654 (repo-health:v1:fix:dezren39/nix:issue:297, kind small_fix, docs-only): write the comparison of iroh-via-cgo / iroh-as-sidecar / tsnet / go-libp2p against ci(mcpx): PR #295 removes the only CI build of the mcpx Nix package — stale vendorHash on human PRs merges green #297's criteria 1–3 plus build/release cost, answer the Phase 4: joiner with missing pre-join control messages stalls silently #187 iroh-blobs question with evidence, record one recommendation as a PR in this repo, and post the summary here. Explicitly out of scope: implementing any transport, any new dependency, and authorisation (🎨 Palette: Add ARIA labels to icon buttons #253/mcpx: successor daemon never waits for background orphan-session DELETEs on shutdown — WaitOrphanTerminations has no production caller #298/ci(id): PR gate's hard-coded flake-check list can silently drift from pkgs/id/flake.nix — new checks get no PR coverage #300).hidden_files/packet-fix-nix-issue-297.json@dezren39 · 2026-10-28T20:16:14Z
Recommendation summary (task 2654, decision doc PR #1052): adopt
tsnetas 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/codee91098f1(registry.go:429;server.go:253–280binds 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://…, lateriroh://…), 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
tsnetas mcpx's first identity transport, behind a small transport interface in the daemon (transports produce anet.Listener;Server.Listenalready 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-ffi) — rejected, and the Go-binding question is answered, not deferred. It breaks the pure-Go build (mcpxpackage.nixsetsCGO_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.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 plainnet.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.118 remaining items
dezren39 commentedon Oct 29, 2026
🤖 Agent note · picked up (resume)
awaiting_ci. Fresh check 2026-10-29T19:57:37Z: PR feat(mcpx): daemon transport interface + tsnet identity listener (#1967) #2203 OPEN, MERGEABLE/CLEAN, head d9e4197; Codex gate clear — all 11 review threads resolved, latest Codex summaryhas_unresolved_findings: false.mainstill 5a34fa7 (stall ci: Actions check runs stuck in queued across PRs — no runner pickup since ~05:00Z 2026-10-28 #1599); PR was ejected from the queue in earlier churn. Action: re-enqueue once this shift, park if not merged.dezren39 commentedon Oct 29, 2026
🤖 Agent note · parked — merge queue stall (#1599)
mainstill 5a34fa7 (since 10:57Z). PR is OPEN/CLEAN, Codex gate clear (11/11 threads resolved, no unresolved findings).dezren39 commentedon Oct 29, 2026
🤖 Agent note · blocked
user/escalated— a prior lane already determined the remaining decision is Drew's. Per the worker runbook, an escalated item is not merged or advanced by the worker. PR feat(mcpx): daemon transport interface + tsnet identity listener (#1967) #2203 (head d9e4197, OPEN/CLEAN) was left untouched and was not re-enqueued this shift. Queue task 2902 is blocked with reason classescalated-to-userso it stops cycling through worker shifts until Drew rules.docs(mcpx): note shipped remote reachability on the as-a-server propo…