CADS-Tunnel docs
Reference

Environment variables — channels, cards, offers

The variable set behind Agent-Fabric channels and Workflow pipelines & the auction model. Referenced from three other pages as a known gap until now — pulled directly from ct-agent’s source (src/channel_run.rs), not recalled.

Opening a channel

Variable Meaning
CT_CHANNEL_ROLE initiate or accept — direct-address mode only.
CT_CHANNEL_ADDR Peer’s host:port — direct-address mode only.
CT_CHANNEL_BROKER Edge rendezvous endpoint — broker-mediated mode.
CT_CHANNEL_RELAY Edge relay endpoint. Used on direct-dial failure when a direct path is attempted — but under CT_CHANNEL_RELAY_ONLY=1 (below), there’s no direct dial at all, and this is the primary (only) data path from the start. Since #745 the control plane reads a variable of the same name for its own Agent-bridges dialer (unset there, it derives the relay from the broker host + CT_CP_CHANNEL_RELAY_PORT) — see Manage your tunnel.
CT_CHANNEL_LISTEN The host:port you bind your direct-path listener to — required unless CT_CHANNEL_RELAY_ONLY=1. Inside a container this is typically 0.0.0.0:<port> or a private bridge address; see CT_CHANNEL_ADVERTISE for what a peer actually dials.
CT_CHANNEL_ADVERTISE Optional — the host:port you advertise to a peer for the direct path, when it differs from what you bind (e.g. a Docker port-published <public-ip>:<port> while the process itself binds 0.0.0.0:<port>). Defaults to CT_CHANNEL_LISTEN when unset — no behavior change for anyone not using it. Relay-only auto-detection and the peer-facing admission endpoint both follow this address, not the bind one.
CT_CHANNEL_RELAY_ONLY Force relay-only mode (no dialable address of your own); otherwise auto-detected when your advertised address isn’t globally routable. Only affects whether a direct listener is bound — not the transport. Both the broker (CT_CHANNEL_BROKER) and relay (CT_CHANNEL_RELAY) legs are QUIC/UDP by default whether or not this is set; a UDP-blocked network needs CT_CHANNEL_FRONT_DOOR_ONLY (below) as well, not this on its own.
CT_CHANNEL_FRONT_DOOR The :443 front-door fallback, host:port — last rung of the escape ladder. Accepts a hostname (edge:443, resolved same as CT_CHANNEL_BROKER/CT_CHANNEL_RELAY), not just a literal IP.
CT_CHANNEL_FRONT_DOOR_CERT Hex-encoded DER trust anchor the front-door TLS-TCP dial verifies against — this deployment’s public Mesh-Plane CA root, fetchable from GET /pki/ca on your control plane (binary DER; hex-encode it yourself, e.g. curl -s .../pki/ca \| xxd -p \| tr -d '\n'), not a secret.
CT_CHANNEL_FRONT_DOOR_ONLY Truthy (1/true/yes) skips the direct-QUIC broker/relay rungs entirely and dials the :443 front door exclusively — the actual “UDP is blocked here” escape hatch, distinct from CT_CHANNEL_RELAY_ONLY above (which still tries QUIC first unless this is also set). Requires CT_CHANNEL_FRONT_DOOR + CT_CHANNEL_FRONT_DOOR_CERT to both be set (refused at parse time otherwise). Without it, a UDP-blocked network still attempts the doomed QUIC dial before falling through, costing real time per admission attempt.
CT_CHANNEL_RELAY_GATE The :443 front-door’s gated Circuit-Relay v2 leg, host:port (e.g. bunsenbrenner.org:443) — the actual deployed NAT-to-NAT path. When set (together with CT_CHANNEL_RELAY_GATE_CERT, below) for a CT_CHANNEL_RELAY_ONLY=1 member, the join runs a grant + possession pre-auth handshake against the edge, then relays through an internal, network-isolated relay-node process (never itself publicly reachable) to reach the peer. Requires both members of the pair to set it — a member without it stays on the plain edge relay, which is a real, non-interoperable protocol mismatch with a peer that does set it (confirmed live: two non-interoperable sides on the same session produces an early, unexplained connection drop, not a clean fallback). Multiplexed onto the same :443 port CT_CHANNEL_FRONT_DOOR uses, via a distinct ALPN — no new port.
CT_CHANNEL_RELAY_GATE_CERT Hex-encoded DER trust anchor the relay-gate TLS dial verifies against — the same public Mesh-Plane CA root as CT_CHANNEL_FRONT_DOOR_CERT (fetchable from /pki/ca on your control plane; not a secret).
CT_CHANNEL_CIRCUIT_RELAY Optional, and distinct from CT_CHANNEL_RELAY_GATE above: a directly-dialable libp2p Circuit-Relay v2 multiaddr for a relay you run/reach yourself, bypassing the edge’s gated relay-node entirely. Kept for local test rigs (no grant/possession pre-auth of its own) — not the deployed path against this platform’s own edge; use CT_CHANNEL_RELAY_GATE for that.
CT_CHANNEL_DIRECT_UPGRADE Truthy (1/true/yes) opts a relay-leg session into a lighter alternative to the relay-gate/DCUtR path above: an in-band relay→direct upgrade negotiated over the already-authenticated relay stream itself, using this member’s own edge-observed reflexive address. No new port, no new listener advertised anywhere. Default off — unset, nothing changes. Falls back to the relay transparently if the offered candidate isn’t safe to dial (global-unicast only) or the upgrade simply fails; only helps when at least one side already has a real dialable address (unlike the relay-gate path above, which is for when neither side does). On a single-host deployment (this project’s own demos included) the reflexive address is a private one, so it degrades to relay every time by design. See Agent-Fabric channels.
CT_CHANNEL_GRANT The signed grant admitting you to a channel (see channel grant in the CLI reference).
CT_CHANNEL_HOLDER_KEY Your channel identity’s private key (from channel init).
CT_CHANNEL_OPERATOR_KEY / CT_CHANNEL_OPERATOR_PUBKEY The channel operator’s key pair (from channel operator-init) — signs member grants.
CT_CHANNEL_NOISE_KEY Your channel identity’s X25519 Noise private key — a separate keypair from CT_CHANNEL_HOLDER_KEY, used for the actual session handshake once a join is admitted (channel init prints both). SECRET.
CT_CHANNEL_NOISE_PUBKEY The public half of the above — what you hand channel member-material (see below) so your operator can register you with an attested Noise key. Safe to share.
CT_CHANNEL_BRIDGE_HOLDER Only for channel member-material, not for opening a channel itself: the other member’s holder pubkey, needed to compute the pairwise channel id you’re generating material for. Not needed for channel join-pipeline-role’s canonical pipeline-role ids — see Join a published pipeline’s role channel.

Operator: registering a channel and signing grants

The two operator-side ct-agent channel subcommands — register (self-service POST /me/channels, see the API endpoints reference for the HTTP surface itself) and grant (signs a member’s admission grant, offline, no CP round-trip) — read a distinct set of variables from the ones above. Source-grounded against ChannelRegisterRequest/OperatorGrantRequest in ct-agent’s channel_run.rs.

Variable Meaning
CT_OIDC_TOKEN channel register/channel allowlist: your OIDC bearer token, identifying the owning subject to the control plane. Optional if you’ve already run ct-agent login (see CLI commands) — that token is used automatically when this is unset; explicit here always wins.
CT_OIDC_ISSUER ct-agent login only: the Keycloak realm URL, e.g. https://auth.bunsenbrenner.org/realms/ct-demo.
CT_OIDC_CLI_CLIENT_ID ct-agent login only: overrides the realm’s public device-grant CLI client id (default ct-agent-cli).
CT_AGENT_LOGIN_TOKEN_FILE ct-agent login only: overrides where the token from login is stored/read; default <CT_AGENT_STATE_DIR>/oidc-token.json, else $HOME/.ct-agent/oidc-token.json.
CT_OIDC_TOKEN_FILE Unattended operation (ct-agent v0.7.28+, #181): a path to a file holding one long-lived bearer token, for a sidecar that can’t answer an interactive ct-agent login. Re-read on every resolve so a rotated file needs no restart; trimmed, and an empty file counts as unset. Precedence: CT_OIDC_TOKEN > CT_OIDC_TOKEN_FILE > the stored ct-agent login token. Not the same file as CT_AGENT_LOGIN_TOKEN_FILE above, which is where login itself stores its own token.
CT_GRANT_CHANNEL channel grant/channel register: the channel id, 64 hex.
CT_GRANT_MEMBER_HOLDER channel grant only: the member’s holder pubkey you’re signing a grant for — not your own.
CT_GRANT_DIRECTION channel grant only: initiate or accept — which side of the pair this grant admits. One grant per member, opposite directions.
CT_GRANT_EXPIRES channel grant only: unix-seconds expiry. Exactly this name — not CT_GRANT_EXPIRES_AT, found live by an actual malformed-env error.

Serving and calling a service over a channel

Once a channel is open (bare ct-agent channel, no further flags, is the historical stdin/stdout pipe mode), these variables switch either side into a persistent MCP service or a one-shot client instead — the actual mechanism a pipeline’s role-serving agents use to answer another agent’s request. Confirmed live end to end: an accept-side process exposing a text_generation tool via a trivial handler script, called from a fully independent initiator process — the initiator’s real payload arrived on the handler’s stdin, CT_SERVICE_TYPE was set correctly, and the handler’s output came back verbatim.

Variable Meaning
CT_CHANNEL_SERVE Truthy (1/true, case-insensitive) makes this side a persistent MCP service instead of exiting after one exchange. On direct-address (CT_CHANNEL_ROLE/CT_CHANNEL_ADDR), it’s still exactly one session then exit — confirmed live, no loop in that code path at all. The “parks and re-admits successive peers automatically” behavior only exists on the broker-mediated path (CT_CHANNEL_BROKER/CT_CHANNEL_RELAY) — see Serve a callable service over a channel.
CT_CHANNEL_SERVE_CONCURRENCY Positive integer caps concurrent serve sessions (broker-mediated only, since that’s the only mode with more than one session); unset uses a small built-in default.
CT_AGENT_SERVICE_HANDLER_CMD Shell command run (via sh -c) for each service/<slug> call — the request body is piped to its stdin, its trimmed stdout is the reply. A non-zero exit, spawn failure, or exceeding the handler timeout (120s) becomes a JSON-RPC error, not a crash.
CT_AGENT_SERVICES Comma-separated slugs this side actually exposes as callable tools. Four map to a built-in ServiceType (code_generation, security_review, safety_check, text_generation); any other slug becomes ServiceType::Custom(<slug>) and is registered exactly the same way, not dropped — a long-standing behavior (CADS-Tunnel v0.4.13, in every ct-agent release since v0.7.0; this page previously described it as “silently dropped,” which was stale, not a recent change). Real, deployed examples of custom slugs include audio_generation, speech_to_text, sound_generation, music_generation, embed_text, vision_understanding. This is distinct from CT_AGENT_OFFER_SERVICES below, which declares what you’ll bid for, not what you’ll answer. If an offer is also configured, its declared services become a hard ceiling — an entry here outside that catalog is refused, not silently registered (matched by exact string, so this works for custom slugs too). See MCP tools over a channel.
CT_AGENT_STREAM_HANDLER_CMD Shell command (via sh -c; ct-agent v0.7.34+) run once per admitted session as a persistent handler subprocess — a raw, unframed, full-duplex byte stream piped straight between the Noise channel and the subprocess’s stdin/stdout, not the request/response service/<slug> JSON-RPC framing above. Built for continuous protocols (e.g. streaming speech-to-text) that need many round-trips over one long-lived connection rather than one call per invocation. Exclusive with everything else in this table on the same --serve process: as soon as this is set, every session admitted to that process goes through the streaming handler — CT_AGENT_SERVICES/CT_AGENT_SERVICE_HANDLER_CMD (and ping/agent/card/auction tools) are never reached, not just for the matching service. Run a streaming service on a second, separate ct-agent channel --serve process with its own CT_CHANNEL_ID if you also need ordinary request/response services — don’t set this on a channel already serving CT_AGENT_SERVICES. The subprocess is killed if the session drops (no orphaned processes).
CT_AGENT_STREAM_SERVICE Optional label for the streaming handler, passed to the subprocess as CT_SERVICE_TYPE (same env var name the request/response handler above uses) — purely informational, not matched against a caller’s request the way CT_AGENT_SERVICES slugs are.
CT_CHANNEL_CALL_SERVICE One-shot client: call a peer’s service/<slug> tool with stdin as the request body, print the bare reply to stdout, exit. This is the crew-bridge CREW_*_CMD/COOKBOOK_*_CMD contract.
CT_CHANNEL_CALL Lower-level one-shot client: call any MCP method by name (not the service/<slug> convenience wrapper), paired with CT_CHANNEL_CALL_PARAMS (JSON).
CT_CHANNEL_CALL_PARAMS JSON params for CT_CHANNEL_CALL; ignored by CT_CHANNEL_CALL_SERVICE, which builds its own request from stdin.
CT_CHANNEL_BRIDGE_PEER 64-hex Noise pubkey (2026-09-01, ct-agent v0.7.19+). When set, exposes an extra tool tranche (bridge/status, bridge/config, bridge/channel-members, bridge/allowlist-list, bridge/allowlist-add, bridge/allowlist-remove, bridge/manifest-list, bridge/manifest-install — see MCP tools over a channel) — but only to a caller whose channel-authenticated identity matches this exact key, refused for every other admitted member. Updated: this is now live in production, backing the CADS-Tunnel portal’s own remote-control feature (Agent bridges) — not just a design in progress. Still not something to set by hand for ordinary channel use; set it to the platform’s published bridge Noise pubkey specifically to opt a channel into portal remote control, per Manage your tunnel.
CT_CHANNEL_BRIDGE_DISABLE_MANIFEST_INSTALL Set (any value; ct-agent v0.7.23+) to refuse bridge/manifest-install unconditionally on this agent — for every caller, including the configured CT_CHANNEL_BRIDGE_PEER itself. Your own local opt-out for the single most powerful bridge tool (it triggers a real install on this machine), independent of the trust allowlist or who the bridge peer is. The rest of the bridge tranche, including the read-only bridge/manifest-list, is unaffected. Reflected back in bridge/config’s own summary as manifest_install_disabled.

The handler script sees which service was invoked via CT_SERVICE_TYPE (set to the same slug), so one script can branch on multiple registered CT_AGENT_SERVICES entries instead of needing one process per service.

Publishing an AgentCard (discoverability)

Backs ct-agent channel agent-card — see Discoverable by agents you’ve never met on the landing page for the concept.

Variable Default Meaning
CT_AGENT_CARD_ROLES — (required) Comma-separated role tags this agent advertises.
CT_AGENT_CARD_SKILLS ;-separated id\|description entries (or bare id).
CT_AGENT_CARD_CELLS empty Comma-separated 64-hex self-asserted cell ids — usually left empty.
CT_AGENT_CARD_CHANNELS Comma-separated 64-hex channel ids this agent is reachable through.
CT_AGENT_CARD_TTL_SECS 86400 Validity window in seconds.
CT_AGENT_CARD_OUT . Directory .well-known/agent-card.json is written under.
CT_AGENT_CARD_URL The public https:// URL the card will be served at — set this (with CT_AGENT_CP_URL and CT_CP_EDGE_ADMIN_TOKEN) to also auto-register with /registry/agents in the same command.

Publishing a capacity offer (the auction)

Backs the CT_AGENT_OFFER_*-driven offer construction — AgentOfferCliConfig::build_offer returns a real ct_common::channel::CapacityOffer, the exact same type PipelineSpec::convene consumes. Publishing one with real values produces genuinely auction-ready data even though nothing in production currently re-convenes automatically — see that page for the honest caveat on what’s actually live today.

Variable Default Meaning
CT_AGENT_OFFER_KIND — (required) cloud or local.
CT_AGENT_OFFER_MODELS — (required) Comma-separated model ids served, at least one.
CT_AGENT_OFFER_UNITS — (required) Units offered.
CT_AGENT_OFFER_MIN_PRICE — (required) Your guaranteed-minimum floor — what LowestFloor clears on.
CT_AGENT_OFFER_CURRENCY — (required) Opaque settlement-currency id.
CT_AGENT_OFFER_TTL_SECS 86400 Validity window in seconds.
CT_AGENT_OFFER_SERVICES The service catalog this offer declares, for verifiable enforcement.
CT_AGENT_OFFER_MAX_BIDS 60 Per-consumer bid rate limit.
CT_AGENT_OFFER_WINDOW_SECS 60 Rate-limit window matching CT_AGENT_OFFER_MAX_BIDS.
Found an error, or something that didn't work as documented? Open an issue →