Self-serve a channel membership grant
Set up an Agent-Fabric channel’s step 4 — the operator
hand-signing a CT_CHANNEL_GRANT for every member and getting it to them out of band — works, but it’s a
manual round trip every time someone new joins, or needs to relaunch after losing their old grant. This
page is the self-service alternative: the owner allow-lists an e-mail once, and from then on anyone who
logs into the portal with that verified e-mail can claim their own membership directly — no operator
round trip needed to get added as a member.
ClaimResp
in portal_api.rs carries only claimed: bool, nothing else): it inserts a
channel_members row, it does not mint or return a CT_CHANNEL_GRANT. The edge's
broker still cryptographically verifies an operator-signed grant for both sides before pairing a channel
(channel_broker.rs's verify(operator_pubkey, ...) on each presented grant) — that
check is entirely separate from, and unaffected by, the membership row this page's claim flow creates. A
standard ct-agent channel client still needs a real CT_CHANNEL_GRANT from the
operator (ct-agent channel grant) after claiming, exactly as before. Live-reproduced
2026-08-01: a claimed member with no separate grant gets exactly as far as the edge accepting the dial and
then a connection failure once the peer looks for a grant that was never issued — see
[Recover a channel when the operator key is lost](/how-to/recover-lost-channel-operator-key/)
for the full, live-verified case this surfaced from.
1. The channel owner allow-lists an e-mail
Either from the CLI:
CT_AGENT_CP_URL=https://bunsenbrenner.org \
CT_GRANT_CHANNEL=<64 hex channel id> \
CT_OIDC_TOKEN=<your OIDC bearer token> \
./ct-agent channel allowlist add teammate@example.com
or the portal web UI — the same POST /me/channels/:channel/allowlist call either way, see
Self-service channel allow-list & claim.
This is owner-scoped: only the subject that registered the channel (POST /me/channels, see
Self-service channel registry)
can manage its allow-list.
2. The invitee logs into the portal and finds the channel
No channel id needs to be communicated out of band anymore — once allow-listed, the channel shows up automatically on Your Channels (linked in the nav on every portal page, e.g. from bunsenbrenner.org/portal/account), matched against the session’s own verified sign-in e-mail. A channel not yet claimed shows a Pending badge and a Claim link; the portal never shows another subject’s invitations — the query is keyed to exactly the logged-in session’s own e-mail, nothing else.
3. Generate key material locally
Same primitive Set up an Agent-Fabric channel’s step 1 uses — private keys never leave the machine that generates them, and the claim form only ever asks for the three public values it prints:
CT_CHANNEL_OPERATOR_PUBKEY=<channel operator's pubkey> \
CT_CHANNEL_BRIDGE_HOLDER=<the peer's holder_pubkey> \
CT_CHANNEL_HOLDER_KEY=<your own holder private key> \
CT_CHANNEL_NOISE_PUBKEY=<your own noise public key> \
./ct-agent channel member-material
holder_pubkey = ...
noise_pubkey = ...
channel_id = ...
noise_attestation = ...
The claim form doesn’t need channel_id from this output — you’re claiming the channel already listed
on /portal/channels, and the control plane checks the attestation against that channel id server-side
(a mismatched channel_id here would simply fail attestation verification, not silently claim the wrong
channel).
4. Submit the claim
Paste holder_pubkey, noise_pubkey, and noise_attestation into the form on the channel’s Claim page.
The server-side check is exactly verify_member_noise_attestation
— the same one POST /me/channels/:channel/members runs for an owner-driven grant — so a forged or
un-attested key is rejected here too; the allow-list only ever authorizes which e-mail may join, never
what key. On success the page shows “Claimed – you’re now a member of this channel,” and the
/portal/channels status flips from Pending to Claimed.
When to still use the manual grant instead
The allow-list only gates joining — it doesn’t replace
Set up an Agent-Fabric channel’s direct-address
connection step, and de-listing an e-mail (POST /me/channels/:channel/allowlist/:email/remove) stops a
future claim without revoking an already-claimed membership — that’s still
POST /me/channels/:channel/members/:holder/remove. If a member never needs to log into the portal at
all (a fully headless/automated participant with no human behind it), the manual
ct-agent channel grant flow is still the more direct path — this page is for the case where a real
person is doing the joining and would otherwise need a raw channel id and grant hex handed to them out of
band.