Agent-Fabric channels
The browser tunnel (Zero-knowledge architecture)
gets your service to a browser. Agent-Fabric channels are the separate mechanism for connecting your
agent directly to another agent — what powers MCP tool-calling and workflow-pipeline coordination. This
page covers how a channel actually gets established, grounded in ct-agent’s own source
(src/channel_run.rs), not just the concept.
Two ways to open a channel
Direct address (CT_CHANNEL_ROLE=initiate|accept, CT_CHANNEL_ADDR=host:port): the simplest case —
you already know the other agent’s reachable address. No broker involved at all.
Broker-mediated (CT_CHANNEL_BROKER, CT_CHANNEL_RELAY, plus either CT_CHANNEL_LISTEN or
CT_CHANNEL_RELAY_ONLY=1): for the common case where you don’t have a stable, dialable address for the
peer — most agents behind NAT or a firewall, and what persistent --serve actually uses in production.
The edge’s broker helps two agents that have never directly communicated find and verify each other.
/internal/channel/authorize, whose durable channel_members
table is the sole source of truth (the edge itself holds no membership state). A correctly-signed
CT_CHANNEL_GRANT is necessary but not sufficient on this path; skipping
channel register + the membership registration is the single most common reason a
broker-mediated join is refused despite a valid grant. See
[How the edge decides whether to admit a channel join](/explanation/channel-admission/)
for the full picture, and [Serve your own service, solo](/how-to/serve-your-own-service-solo/)
if you're setting up persistent serve without a pipeline or a known second party yet.
The fallback ladder
Whichever mode, the connection attempt follows a strict order, same encrypted session throughout — only the transport route changes:
- Direct dial — try to reach the peer’s own address straight away.
- Relay ports — if direct fails, route through the edge’s dedicated relay.
:443front door (CT_CHANNEL_FRONT_DOOR,CT_CHANNEL_FRONT_DOOR_CERT) — if even the relay ports are blocked, fall all the way to the same port every browser’s HTTPS traffic already uses. One of the few outbound ports almost no restrictive network blocks.
At every rung, the broker/relay only ever handles ciphertext, or — during initial rendezvous — an attested public key the two agents need to verify each other. It never sees plaintext application data, and it’s never a permanent hub sitting in the middle of an established session: once two agents connect, subsequent traffic isn’t routed through a third party unless the relay rung is what succeeded.
Upgrading from relay to direct, in-band
The fallback ladder above describes the initial connection attempt. There’s a separate, later
opportunity: once a session has landed on the relay rung, CT_CHANNEL_DIRECT_UPGRADE=1 opts it into
trying to cut over to a real direct link mid-session, negotiated over the already-authenticated
relay stream itself — no new port, no new listener, no restart of the handshake. Each side offers the
other its own edge-observed reflexive (post-NAT) address; the peer attempts one real UDP dial to it. If
that succeeds, the session cuts over to the direct link; if not, it stays on relay, transparently, with
no dropped bytes either way.
#104 upgrade — direct dial to 89.56.48.254:1024 failed (Unreachable) —
staying on relay) and the session continued cleanly on relay. In **neither** case did the direct
dial actually succeed. That's not a bug or an incomplete result — a bare UDP dial to a NAT'd address,
with no coordinated hole-punching on the other side, has essentially no chance of connecting. This
mechanism proves the negotiation and fallback are correct and safe in production; it is not, by itself,
a NAT-traversal solution. Real direct connectivity across NATs would need an actual hole-punch
coordinator (see CT_CHANNEL_CIRCUIT_RELAY below) — a distinct, larger piece of work, not
attempted by this mechanism.
Falls back automatically if the upgrade simply fails, or if a candidate isn’t safe to dial — checked
symmetrically on both sides (global-unicast only, so a private or loopback address can never be
smuggled in as a “direct” target): the responder refuses to dial an unsafe address the peer offered
(upgrade_safe_endpoint, #137), and — since 2026-08-01 — the initiator applies the identical check to
its own reflexive address before ever offering it at all (build_upgrade_candidate, ct-agent
883e20f). Default off — unset, nothing about the fallback ladder above changes.
172.18.0.19) would still *offer* that
address as a direct-upgrade candidate. The responder's own guard correctly refused to dial it, but the
initiator had no timeout on that reply wait — so instead of a clean, fast fallback to relay, the whole
session hung for the full outer session timeout. Symptom looked like a generic stall, not an obviously
address-related bug. The fix (the symmetric check described above) makes the initiator skip offering the
doomed candidate in the first place, so a single-host deployment now degrades to relay-only
immediately, same as if the edge had reported no reflexive address at all — not after a
wasted round trip. Full trace: #248.
Admitting someone else’s agent
Everything above assumes you’re connecting your own agents to each other. The same channel mechanism
also supports admitting an agent that belongs to a different account: a redeemable invitation
(POST /channel/invite/challenge + POST /channel/invite/redeem — see
API endpoints for the exact shapes), proven by an
ed25519 signature over the invitation, single-use and expiring
(consume_invitation — replay is rejected, an expired invitation is rejected, confirmed directly against
the control plane’s storage layer). Both endpoints are public and unauthenticated — proof-gated by the
signatures themselves, not a bearer token — so this is unaffected by whether /me/* is up. This is what
makes a pipeline able to span accounts, not just your own devices — see
One service, several devices on the landing page for the
composition side of that.
Related
- How the edge decides whether to admit a channel join — what actually happens at the admission step this page’s fallback ladder leads into, including a real reliability fix (fail-static vs. fail-closed) that shipped the same day this page was last touched.
- Set up an Agent-Fabric channel — establish one for real: identity, admission, a live connection.
- Serve a callable service over a channel — what an established channel is actually for: exposing a tool a peer can call.
- Publish an agent card — the discoverability layer this page’s “Admitting someone else’s agent” section assumes, made concrete.
- Every
CT_CHANNEL_*/CT_AGENT_CARD_*/CT_AGENT_OFFER_*variable is documented in Environment variables (channels, cards, offers).