fix(voice): require can_manage_agent_session grant for remote session#1459
fix(voice): require can_manage_agent_session grant for remote session#1459toubatbrian wants to merge 1 commit into
Conversation
Port of livekit/agents#5487. `RoomSessionTransport` now gates inbound and outbound session messages on each remote participant's `can_manage_agent_session` permission instead of a single linked identity. Responses are routed back to the originating sender via a new `IncomingMessage` envelope, while broadcast events still fan out to every authorized peer. https://claude.ai/code/session_016YNx8jSeTRJJKNujLFy5Ut
|
|
🦋 Changeset detectedLatest commit: 04228a5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 31 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 04228a58be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!this.canManage(options.destinationIdentity)) return; | ||
| destinationIdentities = [options.destinationIdentity]; | ||
| } else { | ||
| destinationIdentities = this.authorizedIdentities(); |
There was a problem hiding this comment.
Allow RemoteSession to address agent participants
When RemoteSession.fromRoom() sends a request, sendRequest() calls transport.sendMessage(msg) without a destination, so this branch only targets remote participants whose own permission includes canManageAgentSession. In the default worker registration, agent participants are granted agent: true but not canManageAgentSession (agents/src/worker.ts), because that grant belongs to the controller/client token. As a result, a RemoteSession connected to a normal agent room has an empty destination list and every request times out (and the inbound check also ignores agent responses for the same reason).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Devin Review found 1 potential issue.
⚠️ 1 issue in files not directly in the diff
⚠️ New types IncomingMessage and SendMessageOptions not re-exported from voice/index.ts (agents/src/voice/index.ts:14-23)
The SessionTransport abstract class is exported from voice/index.ts (line 21), and its abstract method signatures now reference IncomingMessage (line 106 of remote_session.ts) and SendMessageOptions (line 104). However, neither type is included in the re-export list at agents/src/voice/index.ts:14-23. External consumers who subclass SessionTransport or call RoomSessionTransport.sendMessage with options cannot import these types by name from @livekit/agents. The CLAUDE.md notes "API Extractor: Public API surface is tracked" — API Extractor would flag these as ae-forgotten-export warnings.
View 3 additional findings in Devin Review.
Summary
Automated port of livekit/agents#5487 ("require CanManageAgentSession grant for remote session") into
agents-js. SwitchesRoomSessionTransportfrom a single-linked-identity authorization model to a permission-gated one driven by the remote participant'scan_manage_agent_sessiongrant.This was identified by the porting routine as a core runtime improvement (remote session wire protocol, not a plugin or python-specific change), so it is being ported.
Ported behavior
Before this change,
RoomSessionTransportderived "the remote peer" fromRoomIO.linkedParticipant?.identity:After this change (matching the python PR):
ParticipantPermission.canManageAgentSessionistrue. Messages from unauthorized participants are logged at debug level and dropped.destinationIdentitybroadcasts to all remote participants that hold thecan_manage_agent_sessiongrant. When called with an explicitdestinationIdentity, the transport verifies that participant still has the grant before sending (drops the message silently otherwise — matching python).IncomingMessageenvelopes that carry the sender's identity alongside the protobuf message.SessionHostuses that identity to send eachSessionResponseonly back to the requester rather than fanning out to every authorized peer. Broadcast events (AgentSessionEvent) continue to use the default (no destination) path so every authorized peer receives them.File-level changes
agents/src/voice/remote_session.tsIncomingMessageinterface ({ message, senderIdentity? }) — direct mapping of the pythonIncomingMessagedataclass.SendMessageOptions({ destinationIdentity? }) passed as an optional second arg toSessionTransport.sendMessage. Mirrors the pythondestination_identitykeyword.SessionTransport's async iterator now yieldsIncomingMessageinstead of rawpb.AgentSessionMessage.RoomSessionTransport:RoomIO— it now reads authorization directly fromroom.remoteParticipantsandparticipant.info.permission.canManageAgentSession.canManage(identity)andauthorizedIdentities()helpers.onByteStreamfilters bycanManage;readStreamthreadssenderIdentityinto the enqueuedIncomingMessage.sendMessagepicksdestinationIdentitiesfrom the option or from the authorized set; early-returns if there is nobody authorized to send to (so we don't broadcast to the whole room).SessionHost.recvLoopunwrapsIncomingMessageand dispatcheshandleRequestSafe(incoming).handleRequest*family +sendResponsenow accept theIncomingMessageso each response is targeted atincoming.senderIdentity. The error path inhandleRequestSafereusessendResponserather than building the message inline (matches the python_send_responserefactor).RemoteSession.recvLoopunwrapsIncomingMessagewhen dispatching events/responses.RemoteSession.fromRoom(room)no longer requires aRoomIOargument.agents/src/voice/agent_session.tsnew RoomSessionTransport(room)(dropped thethis._roomIOargument)..changeset/require-manage-agent-session-grant.md— patch changeset for@livekit/agents.Implementation nuances vs. python
Most of the change ports 1:1, but a few JS specifics are worth noting:
participant.permissions.can_manage_agent_session. The Node SDK doesn't surface apermissionsgetter onParticipant; the grant lives on the underlying proto atparticipant.info.permission?.canManageAgentSession(singularpermission, onParticipantInfo). The port uses that path. BecausecanManageAgentSessionis typed asboolean | undefinedin@livekit/rtc-ffi-bindings, the helper explicitly compares=== trueso an unset permission is treated as "deny", same as python's required-proto-bool semantics.roomIOdependency. The original JS implementation took aRoomIOreference solely to readlinkedParticipant. Since identity is now derived fromroom.remoteParticipantsdirectly, theRoomIOparameter is removed fromRoomSessionTransportandRemoteSession.fromRoom.RemoteSessionis marked@experimentalso this small surface change is treated as a patch-level breaking change behind the experimental flag, consistent with the python PR also reshaping its publicRoomSessionTransportconstructor (removedremote_identity)._send_responsesetsresponse.request_id = incoming.message.request.request_idand then builds the outer envelope. JS protobuf-es prefersnew pb.SessionResponse({ requestId, response, error }), so the helper extractsrequestIdfrom the incoming envelope and constructs the response in one go — including theresponseoneof only when defined (so the internal-error path produces{ requestId, error: 'internal error' }with the oneof unset, matching python).extra={"participant": ...}; the JS port uses pino's structured logging atlog().debug({ participant }, '...')— semantically identical, just the idiomatic JS form.tests/test_session_host.pyto threadIncomingMessage. agents-js doesn't currently have an analogousremote_session.test.ts, so no test edits are needed; existing voice tests still pass (pnpm test -- --run agents/src/voice→ 169/169).Validation
pnpm --filter @livekit/agents build— succeeds.pnpm --filter @livekit/agents lint— no new warnings introduced (only preexisting ones remain).pnpm test -- --run agents/src/voice— 169/169 passing.pnpm --filter @livekit/agents api:check— fails with a preexistingexport * as ___error unrelated to this PR (reproduced onmain).Test plan
RemoteSessionagainst an agent with a participant whose token does not includecanManageAgentSession— verify it cannot send or receive session messages.canManageAgentSession, verify each one independently sees broadcast events and gets responses routed only to themselves.cc @toubatbrian @livekit/agent-devs for review.
https://claude.ai/code/session_016YNx8jSeTRJJKNujLFy5Ut
Generated by Claude Code