Serve your own service, solo (no pipeline, no known peer)
Set up a broker-mediated channel and
Set up an Agent-Fabric channel both assume two
people coordinating — an operator and a member who already know each other’s public key. This page
is for the narrower, very common case: you’re standing up a persistent --serve process for your
own service (a local model relay, a tool, anything with a stdin/stdout handler) and you’re both the
channel operator and, for now, the only member — there’s no second party’s holder_pubkey to
derive a link id against yet, and possibly never will be one you know in advance.
CT_CHANNEL_ADDR) needs no control plane at all — but broker-mediated mode
(CT_CHANNEL_BROKER/CT_CHANNEL_RELAY, what persistent --serve
actually uses) is never offline, no exception. Confirmed directly against the edge's
own source (crates/edge/src/channel_authorize.rs): every single broker-mediated join calls
POST {control-plane}/internal/channel/authorize, and the control plane's durable
channel_members table is the only source of truth for who's admitted — the edge
itself holds no membership state of its own. A correctly-signed CT_CHANNEL_GRANT alone is
necessary but not sufficient; skip channel register + the membership POST below and every
join fails with edge broker refused the channel join, however valid your grant's signature
is. See How the edge decides whether to
admit a channel join for the full picture.
1. Identities, same as any channel
./ct-agent channel operator-init # once — you're the operator
./ct-agent channel init # once — you're also the (first) member
2. Derive a channel id — self-referentially
Set up an Agent-Fabric channel’s step 3 derives the
channel id from two known holder pubkeys (channel_id_for_link(operator, holder_a, holder_b)). With
no second party yet, set CT_CHANNEL_BRIDGE_HOLDER to your own holder_pubkey:
CT_CHANNEL_OPERATOR_PUBKEY=<your operator pubkey> \
CT_CHANNEL_BRIDGE_HOLDER=<your OWN holder_pubkey, from step 1> \
CT_CHANNEL_HOLDER_KEY=<your own holder private key> \
CT_CHANNEL_NOISE_PUBKEY=<your own noise public key> \
./ct-agent channel member-material
channel_id_for_link hashes a canonically
ordered (lo, hi) pair of the two holder pubkeys — well-defined even when they're
identical (lo == hi == your holder, nothing rejects that). More importantly,
verify_member_noise_attestation — the actual server-side check on the membership POST
below — verifies only (channel, holder, noise_pubkey, signature). It never sees or checks
bridge_holder at all; that value only ever shapes which channel id you land on
client-side. There's nothing for a self-referential derivation to fail structurally — it's a
legitimate way to get a deterministic id and a real, correctly-signed attestation when there's no
second party to derive against yet.
Keep the printed channel_id — you’ll reuse it in every step below, and hand it to anyone you later
grant initiate access (they’ll need it, plus their own grant).
3. Register the channel and yourself as a member
The step Set up a broker-mediated channel’s step 2 covers for the two-party case, unchanged here:
CT_AGENT_CP_URL=https://<your-plane> \
CT_GRANT_CHANNEL=<channel_id from step 2> \
CT_CHANNEL_OPERATOR_KEY=<from step 1> \
CT_OIDC_TOKEN=<your bearer token> \
./ct-agent channel register
Then register your own holder as a member — no CLI wrapper exists for this today, it’s a raw authenticated HTTP call:
curl -X POST https://<your-plane>/me/channels/<channel_id>/members \
-H "Authorization: Bearer <your OIDC token>" -H 'content-type: application/json' \
-d '{"holder":"<your holder_pubkey>","noise_pubkey":"<your noise_pubkey>","noise_attestation":"<from step 2>"}'
4. Grant yourself accept
CT_CHANNEL_OPERATOR_KEY=<from step 1> \
CT_GRANT_CHANNEL=<channel_id> \
CT_GRANT_MEMBER_HOLDER=<your own holder_pubkey> \
CT_GRANT_DIRECTION=accept \
CT_GRANT_EXPIRES=<unix seconds> \
./ct-agent channel grant
5. Run persistent serve
CT_CHANNEL_ROLE=accept CT_CHANNEL_SERVE=1 CT_CHANNEL_RELAY_ONLY=1 \
CT_CHANNEL_BROKER=<edge host>:4435 CT_CHANNEL_RELAY=<edge host>:4436 \
CT_CHANNEL_HOLDER_KEY=<your holder private key> CT_CHANNEL_NOISE_KEY=<your noise private key> \
CT_CHANNEL_GRANT=<from step 4> \
CT_AGENT_SERVICE_HANDLER_CMD=<your handler> CT_AGENT_SERVICES=<service, snake_case — see the callout below> \
./ct-agent channel
Fetch CT_CHANNEL_BROKER/CT_CHANNEL_RELAY from GET {cp_url}/network-info rather than hardcoding
4435/4436 — see Set up an Agent-Fabric channel.
CT_AGENT_SERVICES takes the snake_case slug (text_generation,
code_generation, security_review, safety_check) — NOT the
PascalCase form (TextGeneration) that a published pipeline spec's JSON uses for the same
service. The two are easy to conflate reading across docs; only the snake_case form is valid here. See
[Environment variables (channels, cards, offers)](/reference/channel-environment-variables/).
Letting someone else call it later
Whoever you later want to admit as a caller needs three things from you: the channel_id from step 2,
a CT_GRANT_DIRECTION=initiate grant from step 4’s command (their own holder_pubkey, your operator
key), and to register their own membership the same way step 3 did for you (their own
member-material/attestation — most naturally with CT_CHANNEL_BRIDGE_HOLDER set to your holder
pubkey this time, since now there really is a second, known party). None of this requires touching a
pipeline spec or the pipeline registry — this whole page is the piece Join a workflow pipeline
role and Publish a
pipeline build on top of, not a prerequisite for
either.
Related
- Set up an Agent-Fabric channel — the two-known-parties version of steps 1–2 above.
- Set up a broker-mediated channel — the two-known-parties version of steps 3–5, fully click-tested against production.
- Serve a callable service over a channel —
the handler contract and what
CT_CHANNEL_SERVE=1actually does once you’re past admission. - How the edge decides whether to admit a channel join — why step 3 isn’t optional.