agno 2.3.12__py3-none-any.whl → 2.3.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. agno/agent/agent.py +1125 -1401
  2. agno/eval/__init__.py +21 -8
  3. agno/knowledge/embedder/azure_openai.py +0 -1
  4. agno/knowledge/embedder/google.py +1 -1
  5. agno/models/anthropic/claude.py +4 -1
  6. agno/models/azure/openai_chat.py +11 -5
  7. agno/models/base.py +8 -4
  8. agno/models/openai/chat.py +0 -2
  9. agno/models/openai/responses.py +2 -2
  10. agno/os/app.py +112 -5
  11. agno/os/auth.py +190 -3
  12. agno/os/config.py +9 -0
  13. agno/os/interfaces/a2a/router.py +619 -9
  14. agno/os/interfaces/a2a/utils.py +31 -32
  15. agno/os/middleware/__init__.py +2 -0
  16. agno/os/middleware/jwt.py +670 -108
  17. agno/os/router.py +0 -1
  18. agno/os/routers/agents/router.py +22 -4
  19. agno/os/routers/agents/schema.py +14 -1
  20. agno/os/routers/teams/router.py +20 -4
  21. agno/os/routers/teams/schema.py +14 -1
  22. agno/os/routers/workflows/router.py +88 -9
  23. agno/os/scopes.py +469 -0
  24. agno/os/utils.py +86 -53
  25. agno/reasoning/anthropic.py +85 -1
  26. agno/reasoning/azure_ai_foundry.py +93 -1
  27. agno/reasoning/deepseek.py +91 -1
  28. agno/reasoning/gemini.py +81 -1
  29. agno/reasoning/groq.py +103 -1
  30. agno/reasoning/manager.py +1244 -0
  31. agno/reasoning/ollama.py +93 -1
  32. agno/reasoning/openai.py +113 -1
  33. agno/reasoning/vertexai.py +85 -1
  34. agno/run/agent.py +11 -0
  35. agno/run/base.py +1 -1
  36. agno/run/team.py +11 -0
  37. agno/session/team.py +0 -3
  38. agno/team/team.py +1204 -1452
  39. agno/tools/postgres.py +1 -1
  40. agno/utils/cryptography.py +22 -0
  41. agno/utils/events.py +69 -2
  42. agno/utils/hooks.py +4 -10
  43. agno/utils/print_response/agent.py +52 -2
  44. agno/utils/print_response/team.py +141 -10
  45. agno/utils/prompts.py +8 -6
  46. agno/utils/string.py +46 -0
  47. agno/utils/team.py +1 -1
  48. agno/vectordb/chroma/chromadb.py +1 -0
  49. agno/vectordb/milvus/milvus.py +32 -3
  50. agno/vectordb/redis/redisdb.py +16 -2
  51. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/METADATA +3 -2
  52. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/RECORD +55 -52
  53. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/WHEEL +0 -0
  54. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/licenses/LICENSE +0 -0
  55. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/top_level.txt +0 -0
agno/eval/__init__.py CHANGED
@@ -1,12 +1,4 @@
1
- from agno.eval.accuracy import AccuracyAgentResponse, AccuracyEval, AccuracyEvaluation, AccuracyResult
2
- from agno.eval.agent_as_judge import (
3
- AgentAsJudgeEval,
4
- AgentAsJudgeEvaluation,
5
- AgentAsJudgeResult,
6
- )
7
1
  from agno.eval.base import BaseEval
8
- from agno.eval.performance import PerformanceEval, PerformanceResult
9
- from agno.eval.reliability import ReliabilityEval, ReliabilityResult
10
2
 
11
3
  __all__ = [
12
4
  "AccuracyAgentResponse",
@@ -22,3 +14,24 @@ __all__ = [
22
14
  "ReliabilityEval",
23
15
  "ReliabilityResult",
24
16
  ]
17
+
18
+
19
+ def __getattr__(name: str):
20
+ """Lazy import for eval implementations to avoid circular imports with Agent."""
21
+ if name in ("AccuracyAgentResponse", "AccuracyEval", "AccuracyEvaluation", "AccuracyResult"):
22
+ from agno.eval import accuracy
23
+
24
+ return getattr(accuracy, name)
25
+ elif name in ("AgentAsJudgeEval", "AgentAsJudgeEvaluation", "AgentAsJudgeResult"):
26
+ from agno.eval import agent_as_judge
27
+
28
+ return getattr(agent_as_judge, name)
29
+ elif name in ("PerformanceEval", "PerformanceResult"):
30
+ from agno.eval import performance
31
+
32
+ return getattr(performance, name)
33
+ elif name in ("ReliabilityEval", "ReliabilityResult"):
34
+ from agno.eval import reliability
35
+
36
+ return getattr(reliability, name)
37
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -18,7 +18,6 @@ except ImportError:
18
18
  @dataclass
19
19
  class AzureOpenAIEmbedder(Embedder):
20
20
  id: str = "text-embedding-3-small" # This has to match the model that you deployed at the provided URL
21
-
22
21
  dimensions: int = 1536
23
22
  encoding_format: Literal["float", "base64"] = "float"
24
23
  user: Optional[str] = None
@@ -15,7 +15,7 @@ except ImportError:
15
15
 
16
16
  @dataclass
17
17
  class GeminiEmbedder(Embedder):
18
- id: str = "gemini-embedding-exp-03-07"
18
+ id: str = "gemini-embedding-001"
19
19
  task_type: str = "RETRIEVAL_QUERY"
20
20
  title: Optional[str] = None
21
21
  dimensions: Optional[int] = 1536
@@ -320,8 +320,11 @@ class Claude(Model):
320
320
 
321
321
  return {"type": "json_schema", "schema": schema}
322
322
 
323
- # Handle dict format (already in correct structure)
323
+ # Handle dict format
324
324
  elif isinstance(response_format, dict):
325
+ # Claude only supports json_schema, not json_object
326
+ if response_format.get("type") == "json_object":
327
+ return None
325
328
  return response_format
326
329
 
327
330
  return None
@@ -65,11 +65,17 @@ class AzureOpenAI(OpenAILike):
65
65
  self.azure_endpoint = self.azure_endpoint or getenv("AZURE_OPENAI_ENDPOINT")
66
66
  self.azure_deployment = self.azure_deployment or getenv("AZURE_OPENAI_DEPLOYMENT")
67
67
 
68
- if not self.api_key:
69
- raise ModelAuthenticationError(
70
- message="AZURE_OPENAI_API_KEY not set. Please set the AZURE_OPENAI_API_KEY environment variable.",
71
- model_name=self.name,
72
- )
68
+ if not (self.api_key or self.azure_ad_token):
69
+ if not self.api_key:
70
+ raise ModelAuthenticationError(
71
+ message="AZURE_OPENAI_API_KEY not set. Please set the AZURE_OPENAI_API_KEY environment variable.",
72
+ model_name=self.name,
73
+ )
74
+ if not self.azure_ad_token:
75
+ raise ModelAuthenticationError(
76
+ message="AZURE_AD_TOKEN not set. Please set the AZURE_AD_TOKEN environment variable.",
77
+ model_name=self.name,
78
+ )
73
79
 
74
80
  params_mapping = {
75
81
  "api_key": self.api_key,
agno/models/base.py CHANGED
@@ -196,7 +196,8 @@ class Model(ABC):
196
196
  )
197
197
  sleep(delay)
198
198
  else:
199
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
199
+ if self.retries > 0:
200
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
200
201
  except RetryableModelProviderError as e:
201
202
  current_count = retries_with_guidance_count
202
203
  if current_count >= self.retry_with_guidance_limit:
@@ -238,7 +239,8 @@ class Model(ABC):
238
239
  )
239
240
  await asyncio.sleep(delay)
240
241
  else:
241
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
242
+ if self.retries > 0:
243
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
242
244
  except RetryableModelProviderError as e:
243
245
  current_count = retries_with_guidance_count
244
246
  if current_count >= self.retry_with_guidance_limit:
@@ -283,7 +285,8 @@ class Model(ABC):
283
285
  )
284
286
  sleep(delay)
285
287
  else:
286
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
288
+ if self.retries > 0:
289
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
287
290
  except RetryableModelProviderError as e:
288
291
  current_count = retries_with_guidance_count
289
292
  if current_count >= self.retry_with_guidance_limit:
@@ -330,7 +333,8 @@ class Model(ABC):
330
333
  )
331
334
  await asyncio.sleep(delay)
332
335
  else:
333
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
336
+ if self.retries > 0:
337
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
334
338
  except RetryableModelProviderError as e:
335
339
  current_count = retries_with_guidance_count
336
340
  if current_count >= self.retry_with_guidance_limit:
@@ -768,7 +768,6 @@ class OpenAIChat(Model):
768
768
  # Add role
769
769
  if response_message.role is not None:
770
770
  model_response.role = response_message.role
771
-
772
771
  # Add content
773
772
  if response_message.content is not None:
774
773
  model_response.content = response_message.content
@@ -846,7 +845,6 @@ class OpenAIChat(Model):
846
845
 
847
846
  if response_delta.choices and len(response_delta.choices) > 0:
848
847
  choice_delta: ChoiceDelta = response_delta.choices[0].delta
849
-
850
848
  if choice_delta:
851
849
  # Add content
852
850
  if choice_delta.content is not None:
@@ -234,8 +234,8 @@ class OpenAIResponses(Model):
234
234
  "strict": self.strict_output,
235
235
  }
236
236
  else:
237
- # JSON mode
238
- text_params["format"] = {"type": "json_object"}
237
+ # Pass through directly, user handles everything
238
+ text_params["format"] = response_format
239
239
 
240
240
  # Add text parameter if there are any text-level params
241
241
  if text_params:
agno/os/app.py CHANGED
@@ -16,6 +16,7 @@ from agno.db.base import AsyncBaseDb, BaseDb
16
16
  from agno.knowledge.knowledge import Knowledge
17
17
  from agno.os.config import (
18
18
  AgentOSConfig,
19
+ AuthorizationConfig,
19
20
  DatabaseConfig,
20
21
  EvalsConfig,
21
22
  EvalsDomainConfig,
@@ -49,11 +50,12 @@ from agno.os.utils import (
49
50
  collect_mcp_tools_from_workflow,
50
51
  find_conflicting_routes,
51
52
  load_yaml_config,
53
+ resolve_origins,
52
54
  setup_tracing_for_os,
53
55
  update_cors_middleware,
54
56
  )
55
57
  from agno.team.team import Team
56
- from agno.utils.log import log_debug, log_error, log_warning
58
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
57
59
  from agno.utils.string import generate_id, generate_id_from_name
58
60
  from agno.workflow.workflow import Workflow
59
61
 
@@ -118,17 +120,20 @@ class AgentOS:
118
120
  knowledge: Optional[List[Knowledge]] = None,
119
121
  interfaces: Optional[List[BaseInterface]] = None,
120
122
  a2a_interface: bool = False,
123
+ authorization: bool = False,
124
+ authorization_config: Optional[AuthorizationConfig] = None,
125
+ cors_allowed_origins: Optional[List[str]] = None,
121
126
  config: Optional[Union[str, AgentOSConfig]] = None,
122
127
  settings: Optional[AgnoAPISettings] = None,
123
128
  lifespan: Optional[Any] = None,
124
129
  enable_mcp_server: bool = False,
125
130
  base_app: Optional[FastAPI] = None,
126
131
  on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
127
- telemetry: bool = True,
128
132
  tracing: bool = False,
129
133
  tracing_db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
130
134
  auto_provision_dbs: bool = True,
131
135
  run_hooks_in_background: bool = False,
136
+ telemetry: bool = True,
132
137
  ):
133
138
  """Initialize AgentOS.
134
139
 
@@ -149,11 +154,15 @@ class AgentOS:
149
154
  enable_mcp_server: Whether to enable MCP (Model Context Protocol)
150
155
  base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
151
156
  on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
152
- telemetry: Whether to enable telemetry
157
+ auto_provision_dbs: Whether to automatically provision databases
158
+ authorization: Whether to enable authorization
159
+ authorization_config: Configuration for the authorization middleware
160
+ cors_allowed_origins: List of allowed CORS origins (will be merged with default Agno domains)
153
161
  tracing: If True, enables OpenTelemetry tracing for all agents and teams in the OS
154
162
  tracing_db: Dedicated database for storing and reading traces. Recommended for multi-db setups.
155
163
  If not provided and tracing=True, the first available db from agents/teams/workflows is used.
156
164
  run_hooks_in_background: If True, run agent/team pre/post hooks as FastAPI background tasks (non-blocking)
165
+ telemetry: Whether to enable telemetry
157
166
 
158
167
  """
159
168
  if not agents and not workflows and not teams and not knowledge:
@@ -198,6 +207,13 @@ class AgentOS:
198
207
  self.enable_mcp_server = enable_mcp_server
199
208
  self.lifespan = lifespan
200
209
 
210
+ # RBAC
211
+ self.authorization = authorization
212
+ self.authorization_config = authorization_config
213
+
214
+ # CORS configuration - merge user-provided origins with defaults from settings
215
+ self.cors_allowed_origins = resolve_origins(cors_allowed_origins, self.settings.cors_origin_list)
216
+
201
217
  # If True, run agent/team hooks as FastAPI background tasks
202
218
  self.run_hooks_in_background = run_hooks_in_background
203
219
 
@@ -209,6 +225,9 @@ class AgentOS:
209
225
  self._initialize_teams()
210
226
  self._initialize_workflows()
211
227
 
228
+ # Check for duplicate IDs
229
+ self._raise_if_duplicate_ids()
230
+
212
231
  if self.tracing:
213
232
  self._setup_tracing()
214
233
 
@@ -250,6 +269,9 @@ class AgentOS:
250
269
  self._initialize_agents()
251
270
  self._initialize_teams()
252
271
  self._initialize_workflows()
272
+
273
+ # Check for duplicate IDs
274
+ self._raise_if_duplicate_ids()
253
275
  self._auto_discover_databases()
254
276
  self._auto_discover_knowledge_instances()
255
277
 
@@ -318,6 +340,31 @@ class AgentOS:
318
340
  self.interfaces.append(a2a_interface)
319
341
  self._add_router(app, a2a_interface.get_router())
320
342
 
343
+ def _raise_if_duplicate_ids(self) -> None:
344
+ """Check for duplicate IDs within each entity type.
345
+
346
+ Raises:
347
+ ValueError: If duplicate IDs are found within the same entity type
348
+ """
349
+ duplicate_ids: List[str] = []
350
+
351
+ for entities in [self.agents, self.teams, self.workflows]:
352
+ if not entities:
353
+ continue
354
+ seen_ids: set[str] = set()
355
+ for entity in entities:
356
+ entity_id = entity.id
357
+ if entity_id is None:
358
+ continue
359
+ if entity_id in seen_ids:
360
+ if entity_id not in duplicate_ids:
361
+ duplicate_ids.append(entity_id)
362
+ else:
363
+ seen_ids.add(entity_id)
364
+
365
+ if duplicate_ids:
366
+ raise ValueError(f"Duplicate IDs found in AgentOS: {', '.join(repr(id_) for id_ in duplicate_ids)}")
367
+
321
368
  def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
322
369
  # Adjust the FastAPI app lifespan to handle MCP connections if relevant
323
370
  app_lifespan = lifespan
@@ -558,10 +605,53 @@ class AgentOS:
558
605
  )
559
606
 
560
607
  # Update CORS middleware
561
- update_cors_middleware(fastapi_app, self.settings.cors_origin_list) # type: ignore
608
+ update_cors_middleware(fastapi_app, self.cors_allowed_origins) # type: ignore
609
+
610
+ # Set agent_os_id and cors_allowed_origins on app state
611
+ # This allows middleware (like JWT) to access these values
612
+ fastapi_app.state.agent_os_id = self.id
613
+ fastapi_app.state.cors_allowed_origins = self.cors_allowed_origins
614
+
615
+ # Add JWT middleware if authorization is enabled
616
+ if self.authorization:
617
+ self._add_jwt_middleware(fastapi_app)
562
618
 
563
619
  return fastapi_app
564
620
 
621
+ def _add_jwt_middleware(self, fastapi_app: FastAPI) -> None:
622
+ from agno.os.middleware.jwt import JWTMiddleware, JWTValidator
623
+
624
+ verify_audience = False
625
+ jwks_file = None
626
+ verification_keys = None
627
+ algorithm = "RS256"
628
+
629
+ if self.authorization_config:
630
+ algorithm = self.authorization_config.algorithm or "RS256"
631
+ verification_keys = self.authorization_config.verification_keys
632
+ jwks_file = self.authorization_config.jwks_file
633
+ verify_audience = self.authorization_config.verify_audience or False
634
+
635
+ log_info(f"Adding JWT middleware for authorization (algorithm: {algorithm})")
636
+
637
+ # Create validator and store on app.state for WebSocket access
638
+ jwt_validator = JWTValidator(
639
+ verification_keys=verification_keys,
640
+ jwks_file=jwks_file,
641
+ algorithm=algorithm,
642
+ )
643
+ fastapi_app.state.jwt_validator = jwt_validator
644
+
645
+ # Add middleware to stack
646
+ fastapi_app.add_middleware(
647
+ JWTMiddleware,
648
+ verification_keys=verification_keys,
649
+ jwks_file=jwks_file,
650
+ algorithm=algorithm,
651
+ authorization=self.authorization,
652
+ verify_audience=verify_audience,
653
+ )
654
+
565
655
  def get_routes(self) -> List[Any]:
566
656
  """Retrieve all routes from the FastAPI app.
567
657
 
@@ -922,6 +1012,8 @@ class AgentOS:
922
1012
  host: str = "localhost",
923
1013
  port: int = 7777,
924
1014
  reload: bool = False,
1015
+ reload_includes: Optional[List[str]] = None,
1016
+ reload_excludes: Optional[List[str]] = None,
925
1017
  workers: Optional[int] = None,
926
1018
  access_log: bool = False,
927
1019
  **kwargs,
@@ -956,4 +1048,19 @@ class AgentOS:
956
1048
  )
957
1049
  )
958
1050
 
959
- uvicorn.run(app=app, host=host, port=port, reload=reload, workers=workers, access_log=access_log, **kwargs)
1051
+ # Adding *.yaml to reload_includes to reload the app when the yaml config file changes.
1052
+ if reload and reload_includes is not None:
1053
+ reload_includes = ["*.yaml", "*.yml"]
1054
+
1055
+ uvicorn.run(
1056
+ app=app,
1057
+ host=host,
1058
+ port=port,
1059
+ reload=reload,
1060
+ reload_includes=reload_includes,
1061
+ reload_excludes=reload_excludes,
1062
+ workers=workers,
1063
+ access_log=access_log,
1064
+ lifespan="on",
1065
+ **kwargs,
1066
+ )
agno/os/auth.py CHANGED
@@ -1,6 +1,9 @@
1
- from fastapi import Depends, HTTPException
1
+ from typing import List, Set
2
+
3
+ from fastapi import Depends, HTTPException, Request
2
4
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
3
5
 
6
+ from agno.os.scopes import get_accessible_resource_ids
4
7
  from agno.os.settings import AgnoAPISettings
5
8
 
6
9
  # Create a global HTTPBearer instance
@@ -18,7 +21,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
18
21
  A dependency function that can be used with FastAPI's Depends()
19
22
  """
20
23
 
21
- def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
24
+ async def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
22
25
  # If no security key is set, skip authentication entirely
23
26
  if not settings or not settings.os_security_key:
24
27
  return True
@@ -40,7 +43,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
40
43
 
41
44
  def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
42
45
  """
43
- Validate a bearer token for WebSocket authentication.
46
+ Validate a bearer token for WebSocket authentication (legacy os_security_key method).
44
47
 
45
48
  Args:
46
49
  token: The bearer token to validate
@@ -55,3 +58,187 @@ def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
55
58
 
56
59
  # Verify the token matches the configured security key
57
60
  return token == settings.os_security_key
61
+
62
+
63
+ def get_accessible_resources(request: Request, resource_type: str) -> Set[str]:
64
+ """
65
+ Get the set of resource IDs the user has access to based on their scopes.
66
+
67
+ This function is used to filter lists of resources (agents, teams, workflows)
68
+ based on the user's scopes from their JWT token.
69
+
70
+ Args:
71
+ request: The FastAPI request object (contains request.state.scopes)
72
+ resource_type: Type of resource ("agents", "teams", "workflows")
73
+
74
+ Returns:
75
+ Set of resource IDs the user can access. Returns {"*"} for wildcard access.
76
+
77
+ Usage:
78
+ accessible_ids = get_accessible_resources(request, "agents")
79
+ if "*" not in accessible_ids:
80
+ agents = [a for a in agents if a.id in accessible_ids]
81
+
82
+ Examples:
83
+ >>> # User with specific agent access
84
+ >>> # Token scopes: ["agent-os:my-os:agents:my-agent:read"]
85
+ >>> get_accessible_resources(request, "agents")
86
+ {'my-agent'}
87
+
88
+ >>> # User with wildcard access
89
+ >>> # Token scopes: ["agent-os:my-os:agents:*:read"] or ["admin"]
90
+ >>> get_accessible_resources(request, "agents")
91
+ {'*'}
92
+
93
+ >>> # User with agent-os level access (global resource scope)
94
+ >>> # Token scopes: ["agent-os:my-os:agents:read"]
95
+ >>> get_accessible_resources(request, "agents")
96
+ {'*'}
97
+ """
98
+ # Check if accessible_resource_ids is already cached in request state (set by JWT middleware)
99
+ # This happens when user doesn't have global scope but has specific resource scopes
100
+ cached_ids = getattr(request.state, "accessible_resource_ids", None)
101
+ if cached_ids is not None:
102
+ return cached_ids
103
+
104
+ # Get user's scopes from request state (set by JWT middleware)
105
+ user_scopes = getattr(request.state, "scopes", [])
106
+
107
+ # Get accessible resource IDs
108
+ accessible_ids = get_accessible_resource_ids(user_scopes=user_scopes, resource_type=resource_type)
109
+
110
+ return accessible_ids
111
+
112
+
113
+ def filter_resources_by_access(request: Request, resources: List, resource_type: str) -> List:
114
+ """
115
+ Filter a list of resources based on user's access permissions.
116
+
117
+ Args:
118
+ request: The FastAPI request object
119
+ resources: List of resource objects (agents, teams, or workflows) with 'id' attribute
120
+ resource_type: Type of resource ("agents", "teams", "workflows")
121
+
122
+ Returns:
123
+ Filtered list of resources the user has access to
124
+
125
+ Usage:
126
+ agents = filter_resources_by_access(request, all_agents, "agents")
127
+ teams = filter_resources_by_access(request, all_teams, "teams")
128
+ workflows = filter_resources_by_access(request, all_workflows, "workflows")
129
+
130
+ Examples:
131
+ >>> # User with specific access
132
+ >>> agents = [Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
133
+ >>> # Token scopes: ["agent-os:my-os:agents:agent-1:read", "agent-os:my-os:agents:agent-2:read"]
134
+ >>> filter_resources_by_access(request, agents, "agents")
135
+ [Agent(id="agent-1"), Agent(id="agent-2")]
136
+
137
+ >>> # User with wildcard access
138
+ >>> # Token scopes: ["admin"]
139
+ >>> filter_resources_by_access(request, agents, "agents")
140
+ [Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
141
+ """
142
+ accessible_ids = get_accessible_resources(request, resource_type)
143
+
144
+ # Wildcard access - return all resources
145
+ if "*" in accessible_ids:
146
+ return resources
147
+
148
+ # Filter to only accessible resources
149
+ return [r for r in resources if r.id in accessible_ids]
150
+
151
+
152
+ def check_resource_access(request: Request, resource_id: str, resource_type: str, action: str = "read") -> bool:
153
+ """
154
+ Check if user has access to a specific resource.
155
+
156
+ Args:
157
+ request: The FastAPI request object
158
+ resource_id: ID of the resource to check
159
+ resource_type: Type of resource ("agents", "teams", "workflows")
160
+ action: Action to check ("read", "run", etc.)
161
+
162
+ Returns:
163
+ True if user has access, False otherwise
164
+
165
+ Usage:
166
+ if not check_resource_access(request, agent_id, "agents", "run"):
167
+ raise HTTPException(status_code=403, detail="Access denied")
168
+
169
+ Examples:
170
+ >>> # Token scopes: ["agent-os:my-os:agents:my-agent:read", "agent-os:my-os:agents:my-agent:run"]
171
+ >>> check_resource_access(request, "my-agent", "agents", "run")
172
+ True
173
+
174
+ >>> check_resource_access(request, "other-agent", "agents", "run")
175
+ False
176
+ """
177
+ accessible_ids = get_accessible_resources(request, resource_type)
178
+
179
+ # Wildcard access grants all permissions
180
+ if "*" in accessible_ids:
181
+ return True
182
+
183
+ # Check if user has access to this specific resource
184
+ return resource_id in accessible_ids
185
+
186
+
187
+ def require_resource_access(resource_type: str, action: str, resource_id_param: str):
188
+ """
189
+ Create a dependency that checks if the user has access to a specific resource.
190
+
191
+ This dependency factory creates a FastAPI dependency that automatically checks
192
+ authorization when authorization is enabled. It extracts the resource ID from
193
+ the path parameters and verifies the user has the required access.
194
+
195
+ Args:
196
+ resource_type: Type of resource ("agents", "teams", "workflows")
197
+ action: Action to check ("read", "run")
198
+ resource_id_param: Name of the path parameter containing the resource ID
199
+
200
+ Returns:
201
+ A dependency function for use with FastAPI's Depends()
202
+
203
+ Usage:
204
+ @router.post("/agents/{agent_id}/runs")
205
+ async def create_agent_run(
206
+ agent_id: str,
207
+ request: Request,
208
+ _: None = Depends(require_resource_access("agents", "run", "agent_id")),
209
+ ):
210
+ ...
211
+
212
+ @router.get("/agents/{agent_id}")
213
+ async def get_agent(
214
+ agent_id: str,
215
+ request: Request,
216
+ _: None = Depends(require_resource_access("agents", "read", "agent_id")),
217
+ ):
218
+ ...
219
+
220
+ Examples:
221
+ >>> # Creates dependency for checking agent run access
222
+ >>> dep = require_resource_access("agents", "run", "agent_id")
223
+
224
+ >>> # Creates dependency for checking team read access
225
+ >>> dep = require_resource_access("teams", "read", "team_id")
226
+ """
227
+ # Map resource_type to singular form for error messages
228
+ resource_singular = {
229
+ "agents": "agent",
230
+ "teams": "team",
231
+ "workflows": "workflow",
232
+ }.get(resource_type, resource_type.rstrip("s"))
233
+
234
+ async def dependency(request: Request):
235
+ # Only check authorization if it's enabled
236
+ if not getattr(request.state, "authorization_enabled", False):
237
+ return
238
+
239
+ # Get the resource_id from path parameters
240
+ resource_id = request.path_params.get(resource_id_param)
241
+ if resource_id and not check_resource_access(request, resource_id, resource_type, action):
242
+ raise HTTPException(status_code=403, detail=f"Access denied to {action} this {resource_singular}")
243
+
244
+ return dependency
agno/os/config.py CHANGED
@@ -5,6 +5,15 @@ from typing import Generic, List, Optional, TypeVar
5
5
  from pydantic import BaseModel, field_validator
6
6
 
7
7
 
8
+ class AuthorizationConfig(BaseModel):
9
+ """Configuration for the JWT middleware"""
10
+
11
+ verification_keys: Optional[List[str]] = None
12
+ jwks_file: Optional[str] = None
13
+ algorithm: Optional[str] = None
14
+ verify_audience: Optional[bool] = None
15
+
16
+
8
17
  class EvalsDomainConfig(BaseModel):
9
18
  """Configuration for the Evals domain of the AgentOS"""
10
19