Skip to content

Commit c8ef30b

Browse files
authored
api: add agent, agentEndpoint and agentThread grants (#859)
1 parent ad0ba6f commit c8ef30b

3 files changed

Lines changed: 106 additions & 2 deletions

File tree

‎livekit-api/livekit/api/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
from .access_token import (
4848
InferenceGrants,
4949
ObservabilityGrants,
50+
AgentGrants,
51+
AgentEndpointGrants,
52+
AgentThreadGrants,
5053
VideoGrants,
5154
SIPGrants,
5255
AccessToken,
@@ -65,6 +68,9 @@
6568
"connector_service",
6669
"InferenceGrants",
6770
"ObservabilityGrants",
71+
"AgentGrants",
72+
"AgentEndpointGrants",
73+
"AgentThreadGrants",
6874
"VideoGrants",
6975
"SIPGrants",
7076
"AccessToken",

‎livekit-api/livekit/api/access_token.py‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import jwt
2020
import dataclasses
2121
from dataclasses import dataclass
22-
from typing import Optional, List, Literal
22+
from typing import Optional, List, Literal, Type, TypeVar
2323
from google.protobuf.json_format import MessageToDict, ParseDict
2424

2525
from livekit.protocol.room import RoomConfiguration
@@ -92,6 +92,43 @@ class ObservabilityGrants:
9292
write: bool = False
9393

9494

95+
@dataclass
96+
class AgentGrants:
97+
# create/update/delete Cloud Agents
98+
admin: bool = False
99+
# manage simulations and scenarios for evaluating agents
100+
simulation_admin: bool = False
101+
# access the project's agent databases
102+
database_admin: bool = False
103+
# manage the project's agent dispatch queue
104+
dispatch_admin: bool = False
105+
106+
107+
@dataclass
108+
class AgentEndpointGrants:
109+
# invoke an agent's non-public HTTP endpoints
110+
call: bool = False
111+
# restrict to one agent; empty grants every agent in the project
112+
agent_name: str = ""
113+
# restrict to one deployment; empty grants every deployment
114+
deployment: str = ""
115+
116+
117+
@dataclass
118+
class AgentThreadGrants:
119+
# every thread and scope of the project; the action flags still apply
120+
all: bool = False
121+
# scope in which threads may be listed and created
122+
scope: str = ""
123+
# threads that may be read, written and deleted
124+
thread_ids: Optional[List[str]] = None
125+
list: bool = False
126+
create: bool = False
127+
write: bool = False
128+
read: bool = False
129+
delete: bool = False
130+
131+
95132
@dataclass
96133
class Claims:
97134
identity: str = ""
@@ -102,6 +139,9 @@ class Claims:
102139
sip: Optional[SIPGrants] = None
103140
inference: Optional[InferenceGrants] = None
104141
observability: Optional[ObservabilityGrants] = None
142+
agent: Optional[AgentGrants] = None
143+
agent_endpoint: Optional[AgentEndpointGrants] = None
144+
agent_thread: Optional[AgentThreadGrants] = None
105145
attributes: Optional[dict[str, str]] = None
106146
sha256: Optional[str] = None
107147
room_preset: Optional[str] = None
@@ -162,6 +202,18 @@ def with_inference_grants(self, grants: InferenceGrants) -> "AccessToken":
162202
self.claims.inference = grants
163203
return self
164204

205+
def with_agent_grants(self, grants: AgentGrants) -> "AccessToken":
206+
self.claims.agent = grants
207+
return self
208+
209+
def with_agent_endpoint_grants(self, grants: AgentEndpointGrants) -> "AccessToken":
210+
self.claims.agent_endpoint = grants
211+
return self
212+
213+
def with_agent_thread_grants(self, grants: AgentThreadGrants) -> "AccessToken":
214+
self.claims.agent_thread = grants
215+
return self
216+
165217
def with_identity(self, identity: str) -> "AccessToken":
166218
self.identity = identity
167219
return self
@@ -279,6 +331,9 @@ def verify(self, token: str, *, verify_signature: bool = True) -> Claims:
279331
attributes=claims.get("attributes", {}),
280332
metadata=claims.get("metadata", ""),
281333
sha256=claims.get("sha256", ""),
334+
agent=_parse_grants(claims, "agent", AgentGrants),
335+
agent_endpoint=_parse_grants(claims, "agentEndpoint", AgentEndpointGrants),
336+
agent_thread=_parse_grants(claims, "agentThread", AgentThreadGrants),
282337
)
283338

284339
if claims.get("roomPreset"):
@@ -294,6 +349,17 @@ def verify(self, token: str, *, verify_signature: bool = True) -> Claims:
294349
return grant_claims
295350

296351

352+
_G = TypeVar("_G")
353+
354+
355+
def _parse_grants(claims: dict, key: str, cls: Type[_G]) -> Optional[_G]:
356+
grants = claims.get(key)
357+
if grants is None:
358+
return None
359+
fields = cls.__dataclass_fields__ # type: ignore[attr-defined]
360+
return cls(**{camel_to_snake(k): v for k, v in grants.items() if camel_to_snake(k) in fields})
361+
362+
297363
def camel_to_snake(t: str) -> str:
298364
return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
299365

‎tests/api/test_access_token.py‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33

44
import jwt
55
import pytest
6-
from livekit.api import AccessToken, TokenVerifier, VideoGrants, SIPGrants
6+
from livekit.api import (
7+
AccessToken,
8+
AgentEndpointGrants,
9+
AgentGrants,
10+
AgentThreadGrants,
11+
SIPGrants,
12+
TokenVerifier,
13+
VideoGrants,
14+
)
715
from livekit.protocol.room import RoomConfiguration
816
from livekit.protocol.agent_dispatch import RoomAgentDispatch
917

@@ -37,6 +45,30 @@ def test_verify_token() -> None:
3745
assert claims.attributes["key2"] == "value2"
3846

3947

48+
def test_agent_grants() -> None:
49+
agent = AgentGrants(database_admin=True)
50+
endpoint = AgentEndpointGrants(call=True, agent_name="my-agent")
51+
thread = AgentThreadGrants(thread_ids=["t1"], read=True, write=True)
52+
53+
token = (
54+
AccessToken(TEST_API_KEY, TEST_API_SECRET)
55+
.with_agent_grants(agent)
56+
.with_agent_endpoint_grants(endpoint)
57+
.with_agent_thread_grants(thread)
58+
.to_jwt()
59+
)
60+
61+
decoded = jwt.decode(token, options={"verify_signature": False})
62+
assert decoded["agentThread"]["threadIds"] == ["t1"]
63+
assert decoded["agentEndpoint"]["agentName"] == "my-agent"
64+
assert decoded["agent"]["databaseAdmin"] is True
65+
66+
claims = TokenVerifier(TEST_API_KEY, TEST_API_SECRET).verify(token)
67+
assert claims.agent == agent
68+
assert claims.agent_endpoint == endpoint
69+
assert claims.agent_thread == thread
70+
71+
4072
def test_agent_config() -> None:
4173
token = (
4274
AccessToken(TEST_API_KEY, TEST_API_SECRET)

0 commit comments

Comments
 (0)