Set up a broker-mediated channel, fully click-tested
Set up an Agent-Fabric channel walks through the direct-address path and explicitly flags the broker-mediated path as not click-tested in that pass. This page closes that gap — every command below was actually run against the real production edge and control plane while building a real demo this session, including a real reply crossing a real Noise session between two genuinely separate processes reachable only through the broker/relay.
When you need this instead of direct-address
Direct-address (CT_CHANNEL_ADDR) needs you to already know the peer’s reachable host:port. Broker-
mediated needs no such thing — the edge’s broker resolves two members to each other by channel id
alone, which is what makes it work for members that don’t share a network, or don’t have a stable
address at all (CT_CHANNEL_RELAY_ONLY=1). It also needs one extra piece direct-address doesn’t:
the control plane has to know your channel exists, because the edge asks the control plane
“is this holder actually a member of this channel?” on every join attempt (see
How the edge decides whether to admit a channel join).
1. Identities and the channel id — same as direct-address
./ct-agent channel operator-init # once per channel
./ct-agent channel init # once per member (run twice, for two members)
Derive the shared channel id and each member’s noise attestation exactly as in
Set up an Agent-Fabric channel’s step 3
(ct-agent channel member-material).
2. Register the channel with the control plane
This is the step direct-address never needs. Requires a real OIDC bearer token for the account that will own the channel — see Getting a bearer token without a browser if you’re scripting this rather than using a browser session.
CT_AGENT_CP_URL=https://<your-plane> \
CT_GRANT_CHANNEL=<channel_id from step 1> \
CT_CHANNEL_OPERATOR_KEY=<operator private key, from operator-init> \
CT_OIDC_TOKEN=<bearer token> \
./ct-agent channel register
Real output, actually run: registered channel add9ea39...ce with the control plane — a plain
eprintln!, exit 0. Re-running it for a channel you already own is harmless (idempotent upsert),
confirmed by running it twice.
There’s no CLI wrapper for the next part — register each member directly against the HTTP API
(POST /me/channels/:channel/members, see API endpoints
for the exact shape: holder, noise_pubkey, and the noise_attestation step 1 already computed).
Do this once per member, same bearer token.
3. Grants — same command as direct-address
CT_CHANNEL_OPERATOR_KEY=<from step 1> \
CT_GRANT_CHANNEL=<channel_id> \
CT_GRANT_MEMBER_HOLDER=<this member's holder_pubkey> \
CT_GRANT_DIRECTION=accept \
CT_GRANT_EXPIRES=<unix seconds> \
./ct-agent channel grant
One per member, opposite CT_GRANT_DIRECTION for the other side, exactly as in the direct-address
walkthrough.
4. Connect for real — broker-mediated
# Accept side, relay-only (no dialable address of its own):
CT_CHANNEL_ROLE=accept \
CT_CHANNEL_BROKER=<edge host>:4435 CT_CHANNEL_RELAY=<edge host>:4436 CT_CHANNEL_RELAY_ONLY=1 \
CT_CHANNEL_HOLDER_KEY=<accept member's holder private key> \
CT_CHANNEL_NOISE_KEY=<accept member's noise private key> \
CT_CHANNEL_GRANT=<accept member's grant hex, from step 3> \
./ct-agent channel
Real log line, actually observed: ct-agent channel: plane-brokered Accept (relay <ip>:4436) —
persistent serve: concurrent sessions (#200) (add CT_CHANNEL_SERVE=1 to get this parked,
persistent-serve behavior instead of the default one-shot).
# Initiate side, in a separate process (a genuinely different container in the real test):
echo "hello over the real broker" | CT_CHANNEL_ROLE=initiate \
CT_CHANNEL_BROKER=<edge host>:4435 CT_CHANNEL_RELAY=<edge host>:4436 CT_CHANNEL_RELAY_ONLY=1 \
CT_CHANNEL_HOLDER_KEY=<initiate member's holder private key> \
CT_CHANNEL_NOISE_KEY=<initiate member's noise private key> \
CT_CHANNEL_GRANT=<initiate member's grant hex> \
./ct-agent channel
Real result: ct-agent channel: plane-brokered Initiate (relay <ip>:4436), then
ct-agent channel: peer is relay-only (no dialable address) — using the edge relay (#121), then the
accept side’s stdin arrived verbatim on the initiate side’s stdout — two fully independent processes,
zero shared address, connected purely by channel id through the production broker/relay.
The one real trap: same host, public hostname
If you test both members on the same machine that’s also running the edge, pointing
CT_CHANNEL_BROKER/CT_CHANNEL_RELAY at the edge’s public hostname can fail with
channel join admission exchange stalled (#140) on both sides — not a channel bug, a hairpin-NAT
routing quirk of connecting to your own public IP from the same host. Point at 127.0.0.1 (or the
edge’s container-network name, if you’re both inside the same Docker Compose network) instead; a real
deployed topology with the members on different hosts never hits this.
Related
- Set up an Agent-Fabric channel — the direct-address path this one complements.
- How the edge decides whether to admit a channel join — what the edge actually checks on every join this page’s step 4 triggers.
- Serve a callable service over a channel
— broker-mediated
CT_CHANNEL_SERVE=1is what lets an accept-side member serve many calls without restarting; this page only shows the bare connect.