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.
- agno/agent/agent.py +1125 -1401
- agno/eval/__init__.py +21 -8
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/models/anthropic/claude.py +4 -1
- agno/models/azure/openai_chat.py +11 -5
- agno/models/base.py +8 -4
- agno/models/openai/chat.py +0 -2
- agno/models/openai/responses.py +2 -2
- agno/os/app.py +112 -5
- agno/os/auth.py +190 -3
- agno/os/config.py +9 -0
- agno/os/interfaces/a2a/router.py +619 -9
- agno/os/interfaces/a2a/utils.py +31 -32
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +670 -108
- agno/os/router.py +0 -1
- agno/os/routers/agents/router.py +22 -4
- agno/os/routers/agents/schema.py +14 -1
- agno/os/routers/teams/router.py +20 -4
- agno/os/routers/teams/schema.py +14 -1
- agno/os/routers/workflows/router.py +88 -9
- agno/os/scopes.py +469 -0
- agno/os/utils.py +86 -53
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +91 -1
- agno/reasoning/gemini.py +81 -1
- agno/reasoning/groq.py +103 -1
- agno/reasoning/manager.py +1244 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +113 -1
- agno/reasoning/vertexai.py +85 -1
- agno/run/agent.py +11 -0
- agno/run/base.py +1 -1
- agno/run/team.py +11 -0
- agno/session/team.py +0 -3
- agno/team/team.py +1204 -1452
- agno/tools/postgres.py +1 -1
- agno/utils/cryptography.py +22 -0
- agno/utils/events.py +69 -2
- agno/utils/hooks.py +4 -10
- agno/utils/print_response/agent.py +52 -2
- agno/utils/print_response/team.py +141 -10
- agno/utils/prompts.py +8 -6
- agno/utils/string.py +46 -0
- agno/utils/team.py +1 -1
- agno/vectordb/chroma/chromadb.py +1 -0
- agno/vectordb/milvus/milvus.py +32 -3
- agno/vectordb/redis/redisdb.py +16 -2
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/METADATA +3 -2
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/RECORD +55 -52
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/WHEEL +0 -0
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/top_level.txt +0 -0
agno/os/router.py
CHANGED
agno/os/routers/agents/router.py
CHANGED
|
@@ -18,7 +18,7 @@ from agno.agent.agent import Agent
|
|
|
18
18
|
from agno.exceptions import InputCheckError, OutputCheckError
|
|
19
19
|
from agno.media import Audio, Image, Video
|
|
20
20
|
from agno.media import File as FileMedia
|
|
21
|
-
from agno.os.auth import get_authentication_dependency
|
|
21
|
+
from agno.os.auth import get_authentication_dependency, require_resource_access
|
|
22
22
|
from agno.os.routers.agents.schema import AgentResponse
|
|
23
23
|
from agno.os.schema import (
|
|
24
24
|
BadRequestResponse,
|
|
@@ -187,6 +187,7 @@ def get_agent_router(
|
|
|
187
187
|
400: {"description": "Invalid request or unsupported file type", "model": BadRequestResponse},
|
|
188
188
|
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
189
189
|
},
|
|
190
|
+
dependencies=[Depends(require_resource_access("agents", "run", "agent_id"))],
|
|
190
191
|
)
|
|
191
192
|
async def create_agent_run(
|
|
192
193
|
agent_id: str,
|
|
@@ -372,6 +373,7 @@ def get_agent_router(
|
|
|
372
373
|
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
373
374
|
500: {"description": "Failed to cancel run", "model": InternalServerErrorResponse},
|
|
374
375
|
},
|
|
376
|
+
dependencies=[Depends(require_resource_access("agents", "run", "agent_id"))],
|
|
375
377
|
)
|
|
376
378
|
async def cancel_agent_run(
|
|
377
379
|
agent_id: str,
|
|
@@ -412,6 +414,7 @@ def get_agent_router(
|
|
|
412
414
|
400: {"description": "Invalid JSON in tools field or invalid tool structure", "model": BadRequestResponse},
|
|
413
415
|
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
414
416
|
},
|
|
417
|
+
dependencies=[Depends(require_resource_access("agents", "run", "agent_id"))],
|
|
415
418
|
)
|
|
416
419
|
async def continue_agent_run(
|
|
417
420
|
agent_id: str,
|
|
@@ -521,13 +524,27 @@ def get_agent_router(
|
|
|
521
524
|
}
|
|
522
525
|
},
|
|
523
526
|
)
|
|
524
|
-
async def get_agents() -> List[AgentResponse]:
|
|
527
|
+
async def get_agents(request: Request) -> List[AgentResponse]:
|
|
525
528
|
"""Return the list of all Agents present in the contextual OS"""
|
|
526
529
|
if os.agents is None:
|
|
527
530
|
return []
|
|
528
531
|
|
|
532
|
+
# Filter agents based on user's scopes (only if authorization is enabled)
|
|
533
|
+
if getattr(request.state, "authorization_enabled", False):
|
|
534
|
+
from agno.os.auth import filter_resources_by_access, get_accessible_resources
|
|
535
|
+
|
|
536
|
+
# Check if user has any agent scopes at all
|
|
537
|
+
accessible_ids = get_accessible_resources(request, "agents")
|
|
538
|
+
if not accessible_ids:
|
|
539
|
+
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
|
540
|
+
|
|
541
|
+
# Limit results based on the user's access/scopes
|
|
542
|
+
accessible_agents = filter_resources_by_access(request, os.agents, "agents")
|
|
543
|
+
else:
|
|
544
|
+
accessible_agents = os.agents
|
|
545
|
+
|
|
529
546
|
agents = []
|
|
530
|
-
for agent in
|
|
547
|
+
for agent in accessible_agents:
|
|
531
548
|
agent_response = await AgentResponse.from_agent(agent=agent)
|
|
532
549
|
agents.append(agent_response)
|
|
533
550
|
|
|
@@ -570,8 +587,9 @@ def get_agent_router(
|
|
|
570
587
|
},
|
|
571
588
|
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
572
589
|
},
|
|
590
|
+
dependencies=[Depends(require_resource_access("agents", "read", "agent_id"))],
|
|
573
591
|
)
|
|
574
|
-
async def get_agent(agent_id: str) -> AgentResponse:
|
|
592
|
+
async def get_agent(agent_id: str, request: Request) -> AgentResponse:
|
|
575
593
|
agent = get_agent_by_id(agent_id, os.agents)
|
|
576
594
|
if agent is None:
|
|
577
595
|
raise HTTPException(status_code=404, detail="Agent not found")
|
agno/os/routers/agents/schema.py
CHANGED
|
@@ -215,11 +215,24 @@ class AgentResponse(BaseModel):
|
|
|
215
215
|
"build_user_context": agent.build_user_context,
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
# Handle output_schema name for both Pydantic models and JSON schemas
|
|
219
|
+
output_schema_name = None
|
|
220
|
+
if agent.output_schema is not None:
|
|
221
|
+
if isinstance(agent.output_schema, dict):
|
|
222
|
+
if "json_schema" in agent.output_schema:
|
|
223
|
+
output_schema_name = agent.output_schema["json_schema"].get("name", "JSONSchema")
|
|
224
|
+
elif "schema" in agent.output_schema and isinstance(agent.output_schema["schema"], dict):
|
|
225
|
+
output_schema_name = agent.output_schema["schema"].get("title", "JSONSchema")
|
|
226
|
+
else:
|
|
227
|
+
output_schema_name = agent.output_schema.get("title", "JSONSchema")
|
|
228
|
+
elif hasattr(agent.output_schema, "__name__"):
|
|
229
|
+
output_schema_name = agent.output_schema.__name__
|
|
230
|
+
|
|
218
231
|
response_settings_info: Dict[str, Any] = {
|
|
219
232
|
"retries": agent.retries,
|
|
220
233
|
"delay_between_retries": agent.delay_between_retries,
|
|
221
234
|
"exponential_backoff": agent.exponential_backoff,
|
|
222
|
-
"output_schema_name":
|
|
235
|
+
"output_schema_name": output_schema_name,
|
|
223
236
|
"parser_model_prompt": agent.parser_model_prompt,
|
|
224
237
|
"parse_response": agent.parse_response,
|
|
225
238
|
"structured_outputs": agent.structured_outputs,
|
agno/os/routers/teams/router.py
CHANGED
|
@@ -16,7 +16,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
16
16
|
from agno.exceptions import InputCheckError, OutputCheckError
|
|
17
17
|
from agno.media import Audio, Image, Video
|
|
18
18
|
from agno.media import File as FileMedia
|
|
19
|
-
from agno.os.auth import get_authentication_dependency
|
|
19
|
+
from agno.os.auth import get_authentication_dependency, require_resource_access
|
|
20
20
|
from agno.os.routers.teams.schema import TeamResponse
|
|
21
21
|
from agno.os.schema import (
|
|
22
22
|
BadRequestResponse,
|
|
@@ -142,6 +142,7 @@ def get_team_router(
|
|
|
142
142
|
400: {"description": "Invalid request or unsupported file type", "model": BadRequestResponse},
|
|
143
143
|
404: {"description": "Team not found", "model": NotFoundResponse},
|
|
144
144
|
},
|
|
145
|
+
dependencies=[Depends(require_resource_access("teams", "run", "team_id"))],
|
|
145
146
|
)
|
|
146
147
|
async def create_team_run(
|
|
147
148
|
team_id: str,
|
|
@@ -295,6 +296,7 @@ def get_team_router(
|
|
|
295
296
|
404: {"description": "Team not found", "model": NotFoundResponse},
|
|
296
297
|
500: {"description": "Failed to cancel team run", "model": InternalServerErrorResponse},
|
|
297
298
|
},
|
|
299
|
+
dependencies=[Depends(require_resource_access("teams", "run", "team_id"))],
|
|
298
300
|
)
|
|
299
301
|
async def cancel_team_run(
|
|
300
302
|
team_id: str,
|
|
@@ -390,13 +392,26 @@ def get_team_router(
|
|
|
390
392
|
}
|
|
391
393
|
},
|
|
392
394
|
)
|
|
393
|
-
async def get_teams() -> List[TeamResponse]:
|
|
395
|
+
async def get_teams(request: Request) -> List[TeamResponse]:
|
|
394
396
|
"""Return the list of all Teams present in the contextual OS"""
|
|
395
397
|
if os.teams is None:
|
|
396
398
|
return []
|
|
397
399
|
|
|
400
|
+
# Filter teams based on user's scopes (only if authorization is enabled)
|
|
401
|
+
if getattr(request.state, "authorization_enabled", False):
|
|
402
|
+
from agno.os.auth import filter_resources_by_access, get_accessible_resources
|
|
403
|
+
|
|
404
|
+
# Check if user has any team scopes at all
|
|
405
|
+
accessible_ids = get_accessible_resources(request, "teams")
|
|
406
|
+
if not accessible_ids:
|
|
407
|
+
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
|
408
|
+
|
|
409
|
+
accessible_teams = filter_resources_by_access(request, os.teams, "teams")
|
|
410
|
+
else:
|
|
411
|
+
accessible_teams = os.teams
|
|
412
|
+
|
|
398
413
|
teams = []
|
|
399
|
-
for team in
|
|
414
|
+
for team in accessible_teams:
|
|
400
415
|
team_response = await TeamResponse.from_team(team=team)
|
|
401
416
|
teams.append(team_response)
|
|
402
417
|
|
|
@@ -485,8 +500,9 @@ def get_team_router(
|
|
|
485
500
|
},
|
|
486
501
|
404: {"description": "Team not found", "model": NotFoundResponse},
|
|
487
502
|
},
|
|
503
|
+
dependencies=[Depends(require_resource_access("teams", "read", "team_id"))],
|
|
488
504
|
)
|
|
489
|
-
async def get_team(team_id: str) -> TeamResponse:
|
|
505
|
+
async def get_team(team_id: str, request: Request) -> TeamResponse:
|
|
490
506
|
team = get_team_by_id(team_id, os.teams)
|
|
491
507
|
if team is None:
|
|
492
508
|
raise HTTPException(status_code=404, detail="Team not found")
|
agno/os/routers/teams/schema.py
CHANGED
|
@@ -197,8 +197,21 @@ class TeamResponse(BaseModel):
|
|
|
197
197
|
"resolve_in_context": team.resolve_in_context,
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
# Handle output_schema name for both Pydantic models and JSON schemas
|
|
201
|
+
output_schema_name = None
|
|
202
|
+
if team.output_schema is not None:
|
|
203
|
+
if isinstance(team.output_schema, dict):
|
|
204
|
+
if "json_schema" in team.output_schema:
|
|
205
|
+
output_schema_name = team.output_schema["json_schema"].get("name", "JSONSchema")
|
|
206
|
+
elif "schema" in team.output_schema and isinstance(team.output_schema["schema"], dict):
|
|
207
|
+
output_schema_name = team.output_schema["schema"].get("title", "JSONSchema")
|
|
208
|
+
else:
|
|
209
|
+
output_schema_name = team.output_schema.get("title", "JSONSchema")
|
|
210
|
+
elif hasattr(team.output_schema, "__name__"):
|
|
211
|
+
output_schema_name = team.output_schema.__name__
|
|
212
|
+
|
|
200
213
|
response_settings_info: Dict[str, Any] = {
|
|
201
|
-
"output_schema_name":
|
|
214
|
+
"output_schema_name": output_schema_name,
|
|
202
215
|
"parser_model_prompt": team.parser_model_prompt,
|
|
203
216
|
"parse_response": team.parse_response,
|
|
204
217
|
"use_json_mode": team.use_json_mode,
|
|
@@ -15,7 +15,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
15
15
|
from pydantic import BaseModel
|
|
16
16
|
|
|
17
17
|
from agno.exceptions import InputCheckError, OutputCheckError
|
|
18
|
-
from agno.os.auth import get_authentication_dependency, validate_websocket_token
|
|
18
|
+
from agno.os.auth import get_authentication_dependency, require_resource_access, validate_websocket_token
|
|
19
19
|
from agno.os.routers.workflows.schema import WorkflowResponse
|
|
20
20
|
from agno.os.schema import (
|
|
21
21
|
BadRequestResponse,
|
|
@@ -252,8 +252,21 @@ def get_websocket_router(
|
|
|
252
252
|
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
253
253
|
) -> APIRouter:
|
|
254
254
|
"""
|
|
255
|
-
Create WebSocket router
|
|
255
|
+
Create WebSocket router with support for both legacy (os_security_key) and JWT authentication.
|
|
256
|
+
|
|
256
257
|
WebSocket endpoints handle authentication internally via message-based auth.
|
|
258
|
+
Authentication methods (in order of precedence):
|
|
259
|
+
1. JWT tokens - if JWTMiddleware is configured (via app.state.jwt_middleware)
|
|
260
|
+
2. Legacy bearer token - if settings.os_security_key is set
|
|
261
|
+
3. No authentication - if neither is configured
|
|
262
|
+
|
|
263
|
+
The JWT middleware instance is accessed from app.state.jwt_middleware, which is set
|
|
264
|
+
by AgentOS when authorization is enabled. This allows reusing the same validation
|
|
265
|
+
logic and loaded keys as the HTTP middleware.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
os: The AgentOS instance
|
|
269
|
+
settings: API settings (includes os_security_key for legacy auth)
|
|
257
270
|
"""
|
|
258
271
|
ws_router = APIRouter()
|
|
259
272
|
|
|
@@ -263,9 +276,18 @@ def get_websocket_router(
|
|
|
263
276
|
)
|
|
264
277
|
async def workflow_websocket_endpoint(websocket: WebSocket):
|
|
265
278
|
"""WebSocket endpoint for receiving real-time workflow events"""
|
|
266
|
-
|
|
279
|
+
# Check if JWT validator is configured (set by AgentOS when authorization=True)
|
|
280
|
+
jwt_validator = getattr(websocket.app.state, "jwt_validator", None)
|
|
281
|
+
jwt_auth_enabled = jwt_validator is not None
|
|
282
|
+
|
|
283
|
+
# Determine auth requirements - JWT takes precedence over legacy
|
|
284
|
+
requires_auth = jwt_auth_enabled or bool(settings.os_security_key)
|
|
285
|
+
|
|
267
286
|
await websocket_manager.connect(websocket, requires_auth=requires_auth)
|
|
268
287
|
|
|
288
|
+
# Store user context from JWT auth
|
|
289
|
+
websocket_user_context: Dict[str, Any] = {}
|
|
290
|
+
|
|
269
291
|
try:
|
|
270
292
|
while True:
|
|
271
293
|
data = await websocket.receive_text()
|
|
@@ -279,19 +301,56 @@ def get_websocket_router(
|
|
|
279
301
|
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Token is required"}))
|
|
280
302
|
continue
|
|
281
303
|
|
|
282
|
-
if
|
|
304
|
+
if jwt_auth_enabled and jwt_validator:
|
|
305
|
+
# Use JWT validator for token validation
|
|
306
|
+
try:
|
|
307
|
+
payload = jwt_validator.validate_token(token)
|
|
308
|
+
claims = jwt_validator.extract_claims(payload)
|
|
309
|
+
await websocket_manager.authenticate_websocket(websocket)
|
|
310
|
+
|
|
311
|
+
# Store user context from JWT
|
|
312
|
+
websocket_user_context["user_id"] = claims["user_id"]
|
|
313
|
+
websocket_user_context["scopes"] = claims["scopes"]
|
|
314
|
+
websocket_user_context["payload"] = payload
|
|
315
|
+
|
|
316
|
+
# Include user info in auth success message
|
|
317
|
+
await websocket.send_text(
|
|
318
|
+
json.dumps(
|
|
319
|
+
{
|
|
320
|
+
"event": "authenticated",
|
|
321
|
+
"message": "JWT authentication successful.",
|
|
322
|
+
"user_id": claims["user_id"],
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
error_msg = str(e) if str(e) else "Invalid token"
|
|
328
|
+
error_type = "expired" if "expired" in error_msg.lower() else "invalid_token"
|
|
329
|
+
await websocket.send_text(
|
|
330
|
+
json.dumps(
|
|
331
|
+
{
|
|
332
|
+
"event": "auth_error",
|
|
333
|
+
"error": error_msg,
|
|
334
|
+
"error_type": error_type,
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
continue
|
|
339
|
+
elif validate_websocket_token(token, settings):
|
|
340
|
+
# Legacy os_security_key authentication
|
|
283
341
|
await websocket_manager.authenticate_websocket(websocket)
|
|
284
342
|
else:
|
|
285
343
|
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Invalid token"}))
|
|
286
|
-
|
|
344
|
+
continue
|
|
287
345
|
|
|
288
346
|
# Check authentication for all other actions (only when required)
|
|
289
347
|
elif requires_auth and not websocket_manager.is_authenticated(websocket):
|
|
348
|
+
auth_type = "JWT" if jwt_auth_enabled else "bearer token"
|
|
290
349
|
await websocket.send_text(
|
|
291
350
|
json.dumps(
|
|
292
351
|
{
|
|
293
352
|
"event": "auth_required",
|
|
294
|
-
"error": "Authentication required. Send authenticate action with valid
|
|
353
|
+
"error": f"Authentication required. Send authenticate action with valid {auth_type}.",
|
|
295
354
|
}
|
|
296
355
|
)
|
|
297
356
|
)
|
|
@@ -302,6 +361,10 @@ def get_websocket_router(
|
|
|
302
361
|
await websocket.send_text(json.dumps({"event": "pong"}))
|
|
303
362
|
|
|
304
363
|
elif action == "start-workflow":
|
|
364
|
+
# Add user context to message if available from JWT auth
|
|
365
|
+
if websocket_user_context:
|
|
366
|
+
if "user_id" not in message and websocket_user_context.get("user_id"):
|
|
367
|
+
message["user_id"] = websocket_user_context["user_id"]
|
|
305
368
|
# Handle workflow execution directly via WebSocket
|
|
306
369
|
await handle_workflow_via_websocket(websocket, message, os)
|
|
307
370
|
|
|
@@ -367,11 +430,24 @@ def get_workflow_router(
|
|
|
367
430
|
}
|
|
368
431
|
},
|
|
369
432
|
)
|
|
370
|
-
async def get_workflows() -> List[WorkflowSummaryResponse]:
|
|
433
|
+
async def get_workflows(request: Request) -> List[WorkflowSummaryResponse]:
|
|
371
434
|
if os.workflows is None:
|
|
372
435
|
return []
|
|
373
436
|
|
|
374
|
-
|
|
437
|
+
# Filter workflows based on user's scopes (only if authorization is enabled)
|
|
438
|
+
if getattr(request.state, "authorization_enabled", False):
|
|
439
|
+
from agno.os.auth import filter_resources_by_access, get_accessible_resources
|
|
440
|
+
|
|
441
|
+
# Check if user has any workflow scopes at all
|
|
442
|
+
accessible_ids = get_accessible_resources(request, "workflows")
|
|
443
|
+
if not accessible_ids:
|
|
444
|
+
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
|
445
|
+
|
|
446
|
+
accessible_workflows = filter_resources_by_access(request, os.workflows, "workflows")
|
|
447
|
+
else:
|
|
448
|
+
accessible_workflows = os.workflows
|
|
449
|
+
|
|
450
|
+
return [WorkflowSummaryResponse.from_workflow(workflow) for workflow in accessible_workflows]
|
|
375
451
|
|
|
376
452
|
@router.get(
|
|
377
453
|
"/workflows/{workflow_id}",
|
|
@@ -397,8 +473,9 @@ def get_workflow_router(
|
|
|
397
473
|
},
|
|
398
474
|
404: {"description": "Workflow not found", "model": NotFoundResponse},
|
|
399
475
|
},
|
|
476
|
+
dependencies=[Depends(require_resource_access("workflows", "read", "workflow_id"))],
|
|
400
477
|
)
|
|
401
|
-
async def get_workflow(workflow_id: str) -> WorkflowResponse:
|
|
478
|
+
async def get_workflow(workflow_id: str, request: Request) -> WorkflowResponse:
|
|
402
479
|
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
403
480
|
if workflow is None:
|
|
404
481
|
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
@@ -438,6 +515,7 @@ def get_workflow_router(
|
|
|
438
515
|
404: {"description": "Workflow not found", "model": NotFoundResponse},
|
|
439
516
|
500: {"description": "Workflow execution error", "model": InternalServerErrorResponse},
|
|
440
517
|
},
|
|
518
|
+
dependencies=[Depends(require_resource_access("workflows", "run", "workflow_id"))],
|
|
441
519
|
)
|
|
442
520
|
async def create_workflow_run(
|
|
443
521
|
workflow_id: str,
|
|
@@ -530,6 +608,7 @@ def get_workflow_router(
|
|
|
530
608
|
404: {"description": "Workflow or run not found", "model": NotFoundResponse},
|
|
531
609
|
500: {"description": "Failed to cancel workflow run", "model": InternalServerErrorResponse},
|
|
532
610
|
},
|
|
611
|
+
dependencies=[Depends(require_resource_access("workflows", "run", "workflow_id"))],
|
|
533
612
|
)
|
|
534
613
|
async def cancel_workflow_run(workflow_id: str, run_id: str):
|
|
535
614
|
workflow = get_workflow_by_id(workflow_id, os.workflows)
|