I'm trying to connect an agent to a room created i...
# ask-ai
m
I'm trying to connect an agent to a room created in LiveKit playground so that I can easily test it. I am using the Rust Sdk and
Room::connect
method. How do I generate the token? I found this snippet in the docs:
Copy code
token = (
    api.AccessToken(os.getenv("LIVEKIT_API_KEY"), os.getenv("LIVEKIT_API_SECRET"))
    .with_identity("identity")
    .with_name("rust-agent")
    .with_grants(
        api.VideoGrants(room_join=True, room="room-name", agent=True)
    )
    .with_sip_grants(api.SIPGrants(admin=True, call=True))
    .to_jwt()
)
What's not clear: 1. How should the permissions be set for the agent 2. Should
room
parameter in
VideoGrants
be the room name of the room I created in the LK playground?
t
Thanks for reaching out, Hugo. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’ll help you with the two points you asked about: agent permissions and the
room
parameter when generating a token for connecting an agent with the Rust SDK. Acknowledgement • You’re using
Room::connect
in the Rust SDK and found the AccessToken snippet. You want to know which grants/permissions the agent needs and whether
VideoGrants.room
should match the Playground room name. Short answers • Permissions: Give the agent a grant that allows joining the room and publishing/subscribing as needed (see recommended fields below). • `VideoGrants.room`: Yes — set this to the exact room name you created in LiveKit Playground so the token is valid for that room. Recommended grants for an agent • At minimum give: ◦
room_join: true
— allows joining the room. ◦
can_publish: true
— if the agent should publish audio/video. ◦
can_subscribe: true
— if the agent should receive other participants’ tracks. ◦
can_publish_data: true
— if it will send data messages (signaling/control). • Optional: ◦
room_admin: true
— only if the agent needs admin actions (kick, mute, etc.). ◦
hidden: true
— hide the agent from UI participant lists if you want it to be invisible to regular users. Note about an
agent
field • There is no separate
agent = true
field in the Rust
VideoGrants
API. Using server-issued tokens (API key/secret) and the grants above is how an agent is authorized. Practical Rust example • Generate a token with the room name set to your Playground room:
Copy code
use livekit_server_sdk::{AccessToken, VideoGrants};

let token = AccessToken::new(
    std::env::var("LIVEKIT_API_KEY").unwrap(),
    std::env::var("LIVEKIT_API_SECRET").unwrap(),
)
.with_identity("rust-agent")
.with_name("Rust Agent")
.with_grants(VideoGrants {
    room_join: true,
    room: Some("your-playground-room-name".to_string()),
    can_publish: true,
    can_subscribe: true,
    can_publish_data: true,
    hidden: true, // optional
    ..Default::default()
})
.to_jwt()?;
• Use that JWT when calling
Room::connect
(with the Playground WSS URL) so the agent joins the correct room. Quick checklist before connecting • LIVEKIT_API_KEY and LIVEKIT_API_SECRET are set in your environment. • The
room
string matches the room name in Playground exactly. • The WSS URL you pass to
Room::connect
is the Playground/LiveKit endpoint you’re using. If you’d like, tell me: • The exact fields available in your
VideoGrants
type (or the SDK version), and I’ll tailor the exact code snippet for your version. • The Playground room name and the WSS URL you’re using (if you want me to sanity-check them). Sources: Server-side | livekit/livekit_composite | DeepWiki | server-sdk-js | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit Documentation | server-sdk-kotlin | livekit/livekit_composite | DeepWiki | server-sdk-ruby | livekit/livekit_composite | DeepWiki
m
Interestingly, AccessToken does not need
LIVEKIT_URL
(the WSS url?) To answer the questions: 1. I'm using the newest version of the SDK 2.
playground-BwZk-BNFy
this is the room nam Also what if I am getting
Engine(Signal(Client(401, "invalid token")))
error?
t
Thanks — that’s helpful detail. I’ll cover why AccessToken doesn’t need the URL, confirm the room name usage, and walk through the most likely causes of the
Engine(Signal(Client(401, "invalid token")))
error with concrete checks you can run. Short answers first • AccessToken does not need
LIVEKIT_URL
because the token is just a JWT signed with your API key/secret. The WSS URL is only required when you actually connect (e.g.,
Room::connect
). • Use the exact Playground room name in the grant:
playground-BwZk-BNFy
. Troubleshooting the 401 "invalid token" error Check these items in order — each one is a common cause: 1) Room name must match exactly • Make sure the
VideoGrant.room
value is exactly
playground-BwZk-BNFy
(case and punctuation matter). • Example:
room: Some("playground-BwZk-BNFy".to_string())
2) API key / secret correctness • Confirm the
LIVEKIT_API_KEY
and
LIVEKIT_API_SECRET
you used to sign the token belong to the same LiveKit project that hosts the Playground room. • If you have multiple keys/projects, use the key/secret for the Playground project. 3) Token TTL / expiration • If the token expired before connect, LiveKit will reject it. Set a longer TTL while debugging: ◦ For Rust token builders, set TTL (e.g., 3600 seconds) before calling
to_jwt()
. 4) Grants / permissions included • Ensure the token includes at least: ◦
room_join: true
◦
can_publish: true
(if publishing) ◦
can_subscribe: true
(if receiving) ◦
can_publish_data: true
(if sending data) • Example VideoGrant snippet: ◦ room_join: true ◦ room: Some("playground-BwZk-BNFy".to_string()) ◦ can_publish: true ◦ can_subscribe: true ◦ can_publish_data: true 5) Correct WSS URL when connecting • When calling
Room::connect
, use the correct WSS endpoint for the Playground instance you’re targeting (e.g.,
wss://<your-playground-host>/...
). The token itself doesn’t carry the URL, but the server validating the token must match the project. • If you signed the token for a different LiveKit deployment than the WSS URL you use, the server will reject it. 6) Environment variables actually loaded at runtime • Verify the process that generates the token reads
LIVEKIT_API_KEY
and
LIVEKIT_API_SECRET
correctly (no typos, not empty). • Print or temporarily log which key ID you used (never log secrets) to ensure the correct key is used. Quick debug steps you can run right now 1. Print the generated JWT and paste it into https://jwt.io to inspect: ◦ Confirm
grants.video.room
is
playground-BwZk-BNFy
◦ Confirm
exp
is in the future ◦ Confirm `iss`/`kid` or other claims show the expected key id (if present) 1. Generate a short test token with minimal claims (identity + room_join + room) and longer TTL, then try to connect. 2. Try generating a token with another SDK (or the LiveKit CLI) to rule out a Rust SDK usage issue. 3. If possible, copy the exact code you use to build the AccessToken (redact secrets) and I’ll help inspect it. If you want, share: • The token payload (decoded, redacting secret) or the AccessToken-building code you’re using. • The exact WSS URL you pass to
Room::connect
(redact any project IDs if you prefer). I’ll look for mismatches (room name, project/key, TTL, or missing grant fields) and suggest the minimal code change to fix the 401. Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit Documentation | server-sdk-ruby | livekit/livekit_composite | DeepWiki | Server-side | livekit/livekit_composite | DeepWiki
m
Let's use the python sdk please, I don't want to include new dependencies in my Rust code just to test it.
t
Great — I’ll show you how to generate a valid access token using the Python SDK so you can test the agent connection without adding Rust dependencies. Acknowledgement • You want a Python example to generate the JWT for an agent that will join room
playground-BwZk-BNFy
. • You’re troubleshooting the 401 invalid token error, so I’ll include the recommended grants and quick checks. Python example (generate token) 1. Install the Python server SDK if you don’t already have it:
Copy code
pip install livekit-server-sdk
1. Example script to build a token for your Playground room:
Copy code
import os
from livekit_server_sdk import AccessToken, VideoGrants

# Ensure these env vars are set in your environment
# LIVEKIT_API_KEY and LIVEKIT_API_SECRET (do NOT share the secret)
api_key = os.getenv("LIVEKIT_API_KEY")
api_secret = os.getenv("LIVEKIT_API_SECRET")

token_builder = AccessToken(api_key, api_secret)
token_builder.with_identity("py-agent-1")
token_builder.with_name("Python Agent")

video_grant = VideoGrants(
    room_join=True,
    room="playground-BwZk-BNFy",
    can_publish=True,
    can_subscribe=True,
    can_publish_data=True,
)

token_builder.with_grants(video_grant)

# Optional: extend TTL while debugging (seconds)
token_builder.set_ttl(3600)

jwt = token_builder.to_jwt()
print("ACCESS TOKEN (JWT):", jwt)
How to use the token • Use the printed JWT when calling
Room::connect
in your Rust test client (or any client) as the token argument. • Make sure you connect to the correct WSS URL for the Playground instance (e.g., the Playground WSS endpoint). The token itself doesn’t include the URL — the server you connect to must match the project for which the token was issued. Quick checks for the 401 invalid token error • Room name: Verify
room
exactly equals
playground-BwZk-BNFy
. • API key/secret: Confirm
LIVEKIT_API_KEY
and
LIVEKIT_API_SECRET
are for the same project hosting that Playground room. • Token TTL: Ensure
exp
is in the future. Use
token_builder.set_ttl(...)
while debugging. • Grants: Include
room_join=True
plus
can_publish/can_subscribe/can_publish_data
if needed. • WSS URL: Connect with the correct Playground WSS URL that corresponds to the project whose key/secret you used. • Inspect the token: Paste the JWT into https://jwt.io to check
grants.video.room
,
exp
, and any `kid`/issuer fields. If you want, next steps I can help with • Verify a token you generate: paste the decoded payload (redact any secrets) and I’ll confirm the claims. • Provide the exact Rust
Room::connect
call example showing how to use the JWT and WSS URL so you can test immediately. • If you share the AccessToken-building code you used in Rust (redacting secrets), I can compare it to the Python output and spot differences. Which would you like me to do next: validate a decoded token payload you generate, or give the exact Rust connect snippet that uses the Python-generated JWT?
m
Here's the Rust code:
Copy code
//! # LiveKit WebRTC Bridge — Gemini Live (Vertex AI) Example
//!
//! Demonstrates bridging a LiveKit room with the Gemini Live API via Vertex AI
//! using the `adk-realtime` LiveKit bridge module. Shows how to:
//!
//! - Connect to a LiveKit room
//! - Wrap an event handler with [`LiveKitEventHandler`] to publish model audio
//! - Use [`bridge_input`] to feed participant audio to the AI model
//! - Run the event loop via [`RealtimeRunner`]
//!
//! ## Prerequisites
//!
//! 1. A running [LiveKit](<https://livekit.io/>) server (local or cloud).
//! 2. A Google Cloud project with the **Vertex AI API** enabled.
//! 3. Application Default Credentials configured:
//!    ```sh
//!    gcloud auth application-default login
//!
//! 4. The
livekit
and
vertex-live
features enabled for
adk-realtime
. //! //! ## Environment Variables //! //! | Variable | Required | Description | //! |------------------------|----------|---------------------------------------------------------| //! |
GOOGLE_CLOUD_PROJECT
| Yes | Your Google Cloud project ID | //! |
GOOGLE_CLOUD_REGION
| No | GCP region (defaults to
us-central1
) | //! |
LIVEKIT_URL
| Yes | LiveKit server WebSocket URL (e.g.
<ws://localhost:7880>
) | //! |
LIVEKIT_TOKEN
| Yes | LiveKit access token for the room | //! //! ## Running //! //!
Copy code
sh
//! cargo run -p adk-realtime --example livekit_bridge_gemini --features "livekit,vertex-live"
//!
//! //! ## Architecture //! //!
Copy code
text
//! ┌──────────────┐     audio frames      ┌──────────────────┐
//! │  LiveKit Room │ ──────────────────:arrow_forward:  │  bridge_input()  │
//! │  (participant) │  RemoteAudioTrack   │  PCM16 → Runner  │
//! └──────────────┘                       └────────┬─────────┘
//!                                                 │
//!                                                 ▼
//!                                        ┌──────────────────┐
//!                                        │ RealtimeRunner   │
//!                                        │ (Gemini session) │
//!                                        └────────┬─────────┘
//!                                                 │
//!                                                 ▼
//! ┌──────────────┐     audio publish     ┌──────────────────────┐
//! │  LiveKit Room │ :arrow_backward:────────────────── │ LiveKitEventHandler  │
//! │  (AI agent)   │  NativeAudioSource  │ wraps inner handler  │
//! └──────────────┘                       └──────────────────────┘
//!
//! //! ## Note //! //! This example requires a real LiveKit server and room. The
connect_to_livekit()
//! function below shows the setup pattern — you'll need to adapt it to your //! LiveKit deployment. See <https://docs.livekit.io/> for setup instructions. //! //! Gemini Live outputs audio at 24 kHz PCM16 mono and expects input at 16 kHz //! PCM16 mono. The [
bridge_input
] helper handles the input resampling automatically. use std:sync:Arc; use adk_realtime::RealtimeConfig; use adk_realtime:♊:{GeminiLiveBackend, GeminiRealtimeModel}; use adk_realtime:livekit:{LiveKitEventHandler, bridge_input}; use adk_realtime:🏃:{EventHandler, RealtimeRunner}; use rustls:crypto:CryptoProvider; use livekit:options:TrackPublishOptions; use livekit:prelude:*; use livekit:webrtcaudio sourcenative:NativeAudioSource; use livekit:webrtcaudio source:{AudioSourceOptions, RtcAudioSource}; /// Connect to a LiveKit room and return the room handle, a native audio source /// for publishing model audio, and the first remote audio track from a participant. /// /// In a production app you'd handle track subscriptions more robustly. async fn connect_to_livekit() -> Result<(Room, NativeAudioSource, livekit:track:RemoteAudioTrack), Box<dyn std:error:Error>> { let url = std:env:var("LIVEKIT_URL").expect("LIVEKIT_URL env var is required"); let token = std:env:var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN env var is required"); println!("Token: {}", token); // --- Connect to the room --- let (room, mut room_events) = Room::connect(&url, &token, RoomOptions::default()).await?; println!("Connected to LiveKit room: {}", room.name()); // --- Create a native audio source for publishing model audio --- // Gemini Live outputs audio at 24 kHz PCM16 mono. let audio_source = NativeAudioSource::new( AudioSourceOptions::default(), 24000, // 24kHz — Gemini Live output sample rate 1, // mono 100, // queue_size_ms ); // Publish the audio source as a local track let rtc_source = RtcAudioSource::Native(audio_source.clone()); let local_track = LocalAudioTrack::create_audio_track("ai-agent-audio", rtc_source); let publish_options = TrackPublishOptions::default(); room.local_participant().publish_track(LocalTrack::Audio(local_track), publish_options).await?; println!("Published AI agent audio track to room."); // --- Wait for a remote participant's audio track --- println!("Waiting for a remote participant's audio track..."); let remote_track = loop { if let Some(RoomEvent::TrackSubscribed { track: RemoteTrack::Audio(audio_track), .. }) = room_events.recv().await { println!("Subscribed to remote audio track."); break audio_track; } }; Ok((room, audio_source, remote_track)) } /// A simple event handler that prints text and transcript events. struct PrintingEventHandler; #[async_trait::async_trait] impl EventHandler for PrintingEventHandler { async fn on_text(&self, text: &str, _item_id: &str) -> adk_realtime::Result<()> { print!("{text}"); Ok(()) } async fn on_transcript(&self, transcript: &str, _item_id: &str) -> adk_realtime::Result<()> { print!("[transcript] {transcript}"); Ok(()) } async fn on_response_done(&self) -> adk_realtime::Result<()> { println!("\n--- Response complete ---"); Ok(()) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std:error:Error>> { // --- 1. Install the rustls crypto provider (required for Vertex AI TLS) --- CryptoProvider::install_default(rustls:crypto💍:default_provider()) .expect("install ring crypto provider"); // --- 2. Load credentials via Application Default Credentials (ADC) --- let credentials = google_cloud_auth:credentialsBuilder:default().build()?; // --- 3. Build the Vertex AI Live backend --- let region = std:env:var("GOOGLE_CLOUD_REGION").unwrap_or_else(|_| "us-central1".into()); let project_id = std:env:var("GOOGLE_CLOUD_PROJECT").expect("GOOGLE_CLOUD_PROJECT env var is required"); println!("Project ID: {project_id}"); let backend = GeminiLiveBackend::Vertex { credentials, region, project_id }; // --- 4. Create the Gemini realtime model --- let model = GeminiRealtimeModel::new(backend, "models/gemini-live-2.5-flash-native-audio"); // --- 5. Connect to LiveKit room --- let (_room, audio_source, remote_track) = connect_to_livekit().await?; // --- 6. Wrap event handler with LiveKit audio output --- // The LiveKitEventHandler intercepts on_audio to push model audio to the // NativeAudioSource, which publishes it to the LiveKit room. let inner_handler = PrintingEventHandler; let lk_handler = LiveKitEventHandler::new(inner_handler, audio_source, 24000, 1); // --- 7. Build the RealtimeRunner --- let config = RealtimeConfig::default() .with_instruction("You are a helpful voice assistant in a LiveKit room."); let runner = Arc::new( RealtimeRunner::builder() .model(Arc::new(model)) .config(config) .event_handler(lk_handler) .build()?, ); // --- 8. Connect the runner to the AI model --- runner.connect().await?; println!("Connected to Gemini Live (Vertex AI)."); // --- 9. Bridge participant audio to the model --- // Spawn a task that reads audio from the remote participant's track // and feeds it to the AI model via the runner. let bridge_runner = Arc::clone(&runner); let bridge_handle = tokio::spawn(async move { if let Err(e) = bridge_input(remote_track, &bridge_runner).await { eprintln!("Bridge input error: {e}"); } }); // --- 10. Run the event loop --- // This processes model responses and routes them through the // LiveKitEventHandler (which publishes audio back to the room). println!("Running event loop — speak into the LiveKit room...\n"); if let Err(e) = runner.run().await { eprintln!("Runner error: {e}"); } bridge_handle.abort(); runner.close().await?; println!("Session closed."); Ok(()) }``` I use token generated by this code:
Copy code
import os

from dotenv import load_dotenv
from livekit import api

load_dotenv()

token = (
    api.AccessToken(os.getenv("LIVEKIT_API_KEY"), os.getenv("LIVEKIT_API_SECRET"))
    .with_identity("rust-agent")
    .with_name("rust-agent")
    .with_grants(api.VideoGrants(room_join=True, room="playground-p7u3-cqNK"))
    .to_jwt()
)

print(token)
I run into this error with no extra context:
Copy code
Error: Engine(Signal(Client(401, "missing authorization header")))
t
Thanks — this is very helpful. I’ll focus on the specific error you’re seeing:
Engine(Signal(Client(401, "missing authorization header")))
. That error means the LiveKit server did not receive a valid Authorization header when your Rust client attempted the WebSocket /signal handshake. What I’ll cover: • Why the header is missing • How the Rust client should pass the JWT when calling Room::connect • Quick checks and a minimal example to test Why this happens • The AccessToken you generated in Python is fine (it only needs API key/secret and grants). The token must be sent to LiveKit as an Authorization header (Bearer <JWT>) during the signaling WebSocket connection. If your Rust client doesn’t attach the token correctly, the server responds with “missing authorization header”. Things to check first • Confirm the printed token from your Python script is non-empty and you’re copying the full JWT into the environment variable
LIVEKIT_TOKEN
. • Confirm the Rust process actually reads
LIVEKIT_TOKEN
(no typo, no different environment). Your Rust code expects
LIVEKIT_TOKEN
— you printed it in connect_to_livekit(), so check that printout shows the token. • Ensure you’re connecting to the correct WSS/WS URL (
LIVEKIT_URL
) that corresponds to the project for which the token was issued. How the token must be supplied • For signaling, LiveKit expects an HTTP Authorization header: Authorization: Bearer <token> • The SDK’s
Room::connect(url, token, ...)
helper should internally attach the token. If the token is empty or you pass the wrong variable, the header won’t be sent. Minimal checks & tests 1) Verify the env var is visible to the running Rust process • Run the Python generator and set the token into an env var, then start the Rust process from the same shell: ◦ Unix: export LIVEKIT_TOKEN="$(python3 gen_token.py)" export LIVEKIT_URL="wss://<your-playground-wss>" cargo run ... • Confirm the Rust print shows the same token value. 2) Decode the token to sanity-check claims • Copy the printed token into https://jwt.io and confirm: ◦ grants.video.room is your room (
playground-BwZk-BNFy
— note your Python example used `playground-p7u3-cqNK`; ensure this matches exactly). ◦
exp
is in the future. 3) Confirm Room::connect usage matches SDK expectations • Your code uses Room::connect(&url, &token, RoomOptions::default()). That is correct if
token
is the JWT string. • If you are building a URL with query params or doing other manual WebSocket work, do not pass token in the URL — the SDK sends it as an Authorization header. Passing the token incorrectly can result in the header missing. 4) Reproduce with a simple direct connect test • From the same environment where you run Rust, run this shell sequence to ensure the token and URL are consistent: ◦ Generate token and export: export LIVEKIT_TOKEN="$(python3 gen_token.py)" export LIVEKIT_URL="wss://<your-playground-wss>" • Quick Rust test: add a tiny program that just connects and prints the room name (you already have that — use connect_to_livekit()). Common mismatch issues I’ve seen • Token and room mismatch: your Python token example used room "playground-p7u3-cqNK" but earlier you said the room is
playground-BwZk-BNFy
. Ensure the token’s room claim matches the actual playground room name. • Running Rust in a different environment (Docker, systemd, IDE) where
LIVEKIT_TOKEN
isn’t set. The token print in your code will reveal if it’s empty. • Accidentally trimming or quoting the token when exporting it to env vars — make sure there are no surrounding quotes and no newlines. If the token shows in Rust but you still get the same error • Paste (redact secrets) the printed token header+payload (the decoded JWT fields from jwt.io). I only need the decoded JSON of the token (not the signature) so I can confirm claims (room, exp, identity). • Paste the exact
LIVEKIT_URL
you’re using (you can redact project id if you want). I’ll check that it’s the expected format for LiveKit Playground. • Tell me how you run the Rust binary (directly with cargo run, in Docker, from an IDE) so I can suggest environment-specific fixes. If you want, I can give: • A tiny reproducible Python + Rust sequence you can run locally that demonstrates generating the token, exporting it, and connecting with the Rust SDK. Which would you prefer?