CADS-Tunnel docs
How-to

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.

Found an error, or something that didn't work as documented? Open an issue →