Skip to content

feat(channels): Channels resolve their own participants; the agent object is the authorization boundary - #2497

Merged
cjol merged 4 commits into
mainfrom
feat/channels-participant-routing
Oct 6, 2026
Merged

cjol merged 4 commits into
mainfrom
feat/channels-participant-routing

Conversation

@cjol

@cjol cjol commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

This PR makes identity and authorization in agents/experimental/channels explicit: each Channel says who its senders are through a required participant callback, and the agent object (the Durable Object a route names) becomes the authorization boundary. It resolves the identity and authorization follow-ups from the Channels stack review.

Why

  • The gateway assigned identities without the application deciding them. The default web resolver let every browser in as anonymous, so separate browsers shared client tool ownership. Webhook senders became <channelKey>:<actorId>, so an app could not say that a Slack user and a web user were the same person.
  • No conversation operation was authorized. Any connection could create, fork, reset and list every conversation in an agent, and AiSdkHarness created a session for any id it was given.
  • Channels can't hold an opinion on authorization: apps have their own sharing rules. A per-action authorize hook was considered and rejected. It overlaps with tool approvals, and it would still need app bookkeeping so that a client reconnecting to a conversation it created or forked is let back in.
  • Instead, reaching an agent object grants everything inside it. Routing is the only gate, and conversation ids are navigation rather than secrets. Sharing is per object, there are no read-only roles, and fork cannot cross objects. Those limits are accepted, and finer sharing can be added later without breaking this.
  • By default, every Channel gives each participant an object of their own, Slack threads and Telegram groups included. Sharing a thread's object is then an explicit route, not an accident. Default routes are namespaced (participant:<id>) so they cannot collide with an app's own route names.
  • By default, inside a participant's object, a surface that names no conversation, such as a Slack thread or an email, joins the object's default conversation. A participant's threads therefore share one history, like one long-running agent. This is only a default: a custom route can keep threads apart, for example (event, raw, participant) => `${participant.id}/${event.thread.id}` for a private object per participant and thread, or routes.perThread for one object shared by everyone in a thread.
  • Which surfaces see a turn is a separate question. On main, webhook surfaces only have ingress, and feat(channels): Slack shows every turn of a conversation #2438 and feat(channels): Telegram shows every turn of a conversation #2439 will let each Channel say whether a surface follows the conversation or only gets replies to the turns it started.

Public API Surface

Added:

Symbol Kind Notes
web() function, agents/experimental/channels/web The Web Channel's side in the gateway: participant, optional route, optional match (default /channels[/<conversation>])
Channel.participant option Required on any Channel with ingress or emailIngress; the gateway throws at construction without it
Channel.upgrade, ChannelUpgrade, ChannelUpgradeMatch interface How a Channel takes WebSocket upgrades
ParticipantResult, ChannelParticipant types Participant | string | null; a string is shorthand for { id } and null refuses
routes.perParticipant function The default route
ChannelRouteEvent.participant field null when the sender was refused
participant on slack(), telegram(), email() option Required with webhook on Slack and Telegram; Email receives only when it is set

Changed:

  • ChannelRoute and Channel.route take (event, raw, participant) instead of (event, raw, context).
  • When no route is given, the route is now participant:<id>, where before it was the event's thread id. A participant's separate threads therefore reach one object and share its default conversation, where before each thread had its own.

Removed:

  • ChannelGatewayOptions.web, defaultRoute and findUser, and GatewayWebIdentity
  • ChannelRouteContext, routes.byIdentity and routes.byUser
  • UserIdentityStore, createUserIdentityStore, linkChannelIdentities and their types. Linking a user across Channels is the app's own lookup inside participant. ChannelIdentity and identityKey remain for keying a provider identity.

Code Changes

  • gateway.ts: webhook dispatch resolves the participant, then the route, then hands both to the agent. participantOf is gone, so the agent receives exactly the participant the app returned. Upgrades go to the first Channel whose upgrade.match claims them. A refused participant gets 401 and a refused route 403. An upgrade no Channel matches is still returned to the Worker.
  • ChannelGateway's docs name both boundaries. The gateway is the trust boundary, so a Channels agent must be reachable only through it: another fetch handler, routeAgentRequest or RPC would let a caller pick its own participant. The agent object is the authorization boundary.
  • web/ingress.ts is new. match exists so that unrelated WebSocket endpoints in the same Worker are not sent through participant and refused.
  • internal.ts: toParticipant validates what an app returns. It rejects an empty id or a wrong shape, so a typo throws rather than becoming a shared identity.
  • The comments that said authorization "would go here" in the Web Channel, #operate and AiSdkHarness now explain why there is nothing to add.
  • docs/CONTEXT.md adds Gateway and Agent object terms, says how a participant is decided, and says that surfaces naming no conversation share the default one. docs/diagrams.md, gateway.ts and the example README no longer describe a route as reaching "a conversation's agent".
  • examples/next/channels: the Worker uses web(), keeps ?as= naming and open rooms, and labels both as demo-only.

Compatibility

Everything here is still unreleased. The gateway changeset is amended rather than adding a new one. The open drafts #2438 and #2439 need participant on slack() and telegram() when they are rebased, and are where the follow/reply split for webhook surfaces will land.

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 277afd8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/agent-think Patch

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@agent-think

agent-think Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

🟢 agents import sizes: 2 entry points changed, no growth

Entry point Exports Largest gzip change Size now
🆕 agents/experimental/channels/web 1 new — 12.6 KiB
🟢 agents/experimental/channels 1 resized, 3 removed -144 B (-44.04%) 183 B
Changed exports (5)
Import Gzip change Size now
🆕 agents/experimental/channels/web#web — 12.6 KiB
🟢 agents/experimental/channels#routes -144 B (-44.04%) 183 B
➖ agents/experimental/channels#createUserIdentityStore — —
➖ agents/experimental/channels#linkChannelIdentities — —
➖ agents/experimental/channels#UserIdentityConflictError — —
How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared 0a25ca92 → 277afd89 · workflow run · reported by agent-think[bot]

@cjol
cjol marked this pull request as ready for review October 6, 2026 05:33
devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2497

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2497

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2497

hono-agents

npm i https://pkg.pr.new/hono-agents@2497

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2497

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2497

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2497

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2497

commit: d435f12

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment thread packages/agents/src/experimental/channels/identity.ts
@cjol
cjol force-pushed the feat/channels-participant-routing branch from d435f12 to 277afd8 Compare October 6, 2026 14:43
@cjol
cjol merged commit aa4dddb into main Oct 6, 2026
15 of 16 checks passed
@cjol
cjol deleted the feat/channels-participant-routing branch October 6, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant