kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.

Potentially problematic release.


This version of kubiya-control-plane-api might be problematic. Click here for more details.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,513 @@
1
+ """Authentication middleware for multi-tenant API with Kubiya integration"""
2
+
3
+ import os
4
+ import json
5
+ import jwt
6
+ import hashlib
7
+ from datetime import datetime, timedelta
8
+ from typing import Optional, Dict, Any
9
+ from fastapi import Request, HTTPException, status
10
+ from fastapi.security import HTTPBearer
11
+ import httpx
12
+ import structlog
13
+
14
+ from control_plane_api.app.lib.redis_client import get_redis_client
15
+
16
+ logger = structlog.get_logger()
17
+
18
+ security = HTTPBearer(auto_error=False)
19
+
20
+ # Cache TTL settings
21
+ DEFAULT_CACHE_TTL = 3600 # 1 hour default
22
+ MAX_CACHE_TTL = 86400 # 24 hours max
23
+
24
+ # Shared httpx client for auth validation (reuse connections)
25
+ _auth_http_client: Optional[httpx.AsyncClient] = None
26
+
27
+ def get_auth_http_client() -> httpx.AsyncClient:
28
+ """Get or create shared httpx client for auth validation"""
29
+ global _auth_http_client
30
+ if _auth_http_client is None:
31
+ _auth_http_client = httpx.AsyncClient(
32
+ timeout=3.0,
33
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
34
+ )
35
+ return _auth_http_client
36
+
37
+
38
+ def get_token_cache_key(token: str) -> str:
39
+ """
40
+ Generate a unique cache key for a token using SHA256 hash.
41
+
42
+ This ensures each unique token gets its own cache entry, preventing
43
+ collisions and ensuring proper isolation between different tokens/orgs.
44
+
45
+ Args:
46
+ token: Authentication token
47
+
48
+ Returns:
49
+ Cache key in format: auth:token:sha256:{hash}
50
+ """
51
+ token_hash = hashlib.sha256(token.encode()).hexdigest()
52
+ return f"auth:token:sha256:{token_hash}"
53
+
54
+
55
+ async def extract_token_from_headers(request: Request) -> Optional[str]:
56
+ """
57
+ Extract authentication token from request headers.
58
+
59
+ Supports multiple header formats for compatibility:
60
+ - Authorization: Bearer <token>
61
+ - Authorization: UserKey <token>
62
+ - Authorization: Baerer <token> (typo compatibility)
63
+ - Authorization: <token> (raw token)
64
+ - UserKey: <token>
65
+
66
+ Args:
67
+ request: FastAPI request object
68
+
69
+ Returns:
70
+ Extracted token or None
71
+ """
72
+ # Check Authorization header
73
+ auth_header = request.headers.get("authorization")
74
+ if auth_header:
75
+ # Handle "Bearer <token>"
76
+ if auth_header.startswith("Bearer "):
77
+ return auth_header[7:]
78
+ # Handle "UserKey <token>"
79
+ elif auth_header.startswith("UserKey "):
80
+ return auth_header[8:]
81
+ # Handle "Baerer <token>" (common typo)
82
+ elif auth_header.startswith("Baerer "):
83
+ logger.warning("api_key_typo_detected", message="'Baerer' should be 'Bearer'")
84
+ return auth_header[7:]
85
+ # Handle raw token without prefix
86
+ elif " " not in auth_header:
87
+ return auth_header
88
+
89
+ # Check UserKey header (alternative)
90
+ userkey_header = request.headers.get("userkey")
91
+ if userkey_header:
92
+ return userkey_header
93
+
94
+ return None
95
+
96
+
97
+ def decode_jwt_token(token: str) -> Optional[Dict[str, Any]]:
98
+ """
99
+ Decode JWT token without verification to extract expiry and metadata.
100
+
101
+ Args:
102
+ token: JWT token string
103
+
104
+ Returns:
105
+ Decoded token payload or None
106
+ """
107
+ try:
108
+ # Decode without verification (we just need exp and other metadata)
109
+ decoded = jwt.decode(token, options={"verify_signature": False})
110
+ return decoded
111
+ except Exception as e:
112
+ logger.warning("jwt_decode_failed", error=str(e))
113
+ return None
114
+
115
+
116
+ def get_cache_ttl_from_token(token: str) -> int:
117
+ """
118
+ Extract expiry from JWT token and calculate appropriate cache TTL.
119
+
120
+ Args:
121
+ token: JWT token string
122
+
123
+ Returns:
124
+ TTL in seconds (minimum 60s, maximum MAX_CACHE_TTL)
125
+ """
126
+ decoded = decode_jwt_token(token)
127
+ if not decoded or "exp" not in decoded:
128
+ return DEFAULT_CACHE_TTL
129
+
130
+ try:
131
+ exp_timestamp = decoded["exp"]
132
+ exp_datetime = datetime.fromtimestamp(exp_timestamp)
133
+ now = datetime.now()
134
+
135
+ # Calculate time until expiry
136
+ ttl = int((exp_datetime - now).total_seconds())
137
+
138
+ # Ensure TTL is reasonable (at least 60s, max MAX_CACHE_TTL)
139
+ ttl = max(60, min(ttl, MAX_CACHE_TTL))
140
+
141
+ logger.debug("calculated_cache_ttl", ttl=ttl, exp=exp_timestamp)
142
+ return ttl
143
+ except Exception as e:
144
+ logger.warning("ttl_calculation_failed", error=str(e))
145
+ return DEFAULT_CACHE_TTL
146
+
147
+
148
+ async def validate_kubiya_api_key(auth_header: str, use_userkey: bool = False) -> Optional[Dict[str, Any]]:
149
+ """
150
+ Validate API key with Kubiya API by calling /api/v1/users/self or /api/v1/users/current.
151
+
152
+ Args:
153
+ auth_header: Full Authorization header value (e.g., "Bearer xyz123" or "UserKey xyz123")
154
+ use_userkey: If True, use "UserKey" prefix instead of "Bearer" for worker authentication
155
+
156
+ Returns:
157
+ User object from Kubiya API or None if invalid
158
+ """
159
+ base_url = os.getenv("KUBIYA_API_BASE") or os.getenv("KUBIYA_API_URL") or "https://api.kubiya.ai"
160
+ endpoints = ["/api/v1/users/self", "/api/v1/users/current"]
161
+
162
+ # If use_userkey is True, ensure the header uses "UserKey" prefix
163
+ if use_userkey and not auth_header.startswith("UserKey "):
164
+ # Extract token and reformat with UserKey prefix
165
+ token = auth_header.replace("Bearer ", "").replace("UserKey ", "").strip()
166
+ auth_header = f"UserKey {token}"
167
+ logger.debug("reformatted_auth_header_for_worker", prefix="UserKey")
168
+
169
+ client = get_auth_http_client()
170
+ for endpoint in endpoints:
171
+ try:
172
+ url = f"{base_url}{endpoint}"
173
+ logger.debug("validating_token_with_kubiya", url=url, use_userkey=use_userkey)
174
+
175
+ response = await client.get(
176
+ url,
177
+ headers={"Authorization": auth_header}
178
+ )
179
+
180
+ if response.status_code == 200:
181
+ user_data = response.json()
182
+ logger.info(
183
+ "token_validated_with_kubiya",
184
+ endpoint=endpoint,
185
+ user_id=user_data.get("id"),
186
+ org_id=user_data.get("organization_id"),
187
+ use_userkey=use_userkey
188
+ )
189
+ return user_data
190
+
191
+ logger.debug(
192
+ "kubiya_validation_attempt_failed",
193
+ endpoint=endpoint,
194
+ status=response.status_code,
195
+ use_userkey=use_userkey
196
+ )
197
+
198
+ except Exception as e:
199
+ logger.warning("kubiya_validation_error", endpoint=endpoint, error=str(e))
200
+ continue
201
+
202
+ logger.warning("token_validation_failed_all_endpoints", use_userkey=use_userkey)
203
+ return None
204
+
205
+
206
+ async def get_cached_user_data(token: str) -> Optional[Dict[str, Any]]:
207
+ """
208
+ Get cached user data from Redis using SHA256 hash of token.
209
+
210
+ Args:
211
+ token: Authentication token (full token will be hashed)
212
+
213
+ Returns:
214
+ Cached user data or None
215
+ """
216
+ redis = get_redis_client()
217
+ if not redis:
218
+ return None
219
+
220
+ try:
221
+ cache_key = get_token_cache_key(token)
222
+ cached_data = await redis.get(cache_key)
223
+
224
+ if cached_data:
225
+ logger.debug("cache_hit", cache_key=cache_key[:40] + "...")
226
+ if isinstance(cached_data, bytes):
227
+ cached_data = cached_data.decode('utf-8')
228
+ return json.loads(cached_data)
229
+
230
+ logger.debug("cache_miss", cache_key=cache_key[:40] + "...")
231
+ return None
232
+
233
+ except Exception as e:
234
+ logger.warning("cache_read_failed", error=str(e))
235
+ return None
236
+
237
+
238
+ async def cache_user_data(token: str, user_data: Dict[str, Any]) -> None:
239
+ """
240
+ Cache user data in Redis with TTL based on token expiry.
241
+ Uses SHA256 hash of the full token as cache key for uniqueness.
242
+
243
+ Args:
244
+ token: Authentication token (full token will be hashed)
245
+ user_data: User data to cache
246
+ """
247
+ redis = get_redis_client()
248
+ if not redis:
249
+ return
250
+
251
+ try:
252
+ cache_key = get_token_cache_key(token)
253
+ ttl = get_cache_ttl_from_token(token)
254
+
255
+ await redis.set(
256
+ cache_key,
257
+ json.dumps(user_data),
258
+ ex=ttl # Set expiry in seconds
259
+ )
260
+
261
+ logger.info("cache_write_success", cache_key=cache_key[:40] + "...", ttl=ttl)
262
+
263
+ except Exception as e:
264
+ logger.warning("cache_write_failed", error=str(e))
265
+
266
+
267
+ async def get_organization_from_worker_token(token: str) -> Optional[dict]:
268
+ """
269
+ Validate worker token and return organization data.
270
+
271
+ Worker tokens are in format: worker_{uuid} and stored in worker_heartbeats.worker_metadata
272
+
273
+ Args:
274
+ token: Worker authentication token
275
+
276
+ Returns:
277
+ Organization dict or None if invalid
278
+ """
279
+ if not token.startswith("worker_"):
280
+ return None
281
+
282
+ try:
283
+ from control_plane_api.app.lib.supabase import get_supabase
284
+ client = get_supabase()
285
+
286
+ # Query worker_heartbeats for this token
287
+ result = (
288
+ client.table("worker_heartbeats")
289
+ .select("organization_id, worker_metadata")
290
+ .eq("status", "active")
291
+ .execute()
292
+ )
293
+
294
+ # Find worker with matching token in metadata
295
+ for worker in result.data:
296
+ worker_metadata = worker.get("worker_metadata", {})
297
+ if worker_metadata.get("worker_token") == token:
298
+ # Return minimal org data for worker
299
+ return {
300
+ "id": worker["organization_id"],
301
+ "name": "Worker", # Workers don't need full org details
302
+ "slug": "worker",
303
+ "user_id": "worker",
304
+ "user_email": "worker@system",
305
+ "user_name": "Worker Process"
306
+ }
307
+
308
+ logger.warning("worker_token_not_found", token_prefix=token[:15])
309
+ return None
310
+
311
+ except Exception as e:
312
+ logger.error("worker_token_validation_failed", error=str(e))
313
+ return None
314
+
315
+
316
+ async def get_organization_allow_worker(request: Request) -> dict:
317
+ """
318
+ Dependency that accepts both user tokens and worker tokens.
319
+
320
+ This is used by endpoints that workers need to call (like execution updates).
321
+
322
+ Flow:
323
+ 1. Extract token from Authorization header
324
+ 2. If token starts with "worker_", validate as worker token
325
+ 3. Otherwise, validate as user token (with caching)
326
+ 4. Return organization data
327
+
328
+ Args:
329
+ request: FastAPI request object
330
+
331
+ Returns:
332
+ Organization dict with id, name, slug, user_id, etc.
333
+
334
+ Raises:
335
+ HTTPException: 401 if authentication fails
336
+ """
337
+ # Extract token from headers
338
+ token = await extract_token_from_headers(request)
339
+
340
+ if not token:
341
+ logger.warning("auth_token_missing", path=request.url.path)
342
+ raise HTTPException(
343
+ status_code=status.HTTP_401_UNAUTHORIZED,
344
+ detail={
345
+ "error": "Unauthorized",
346
+ "message": "Authorization header is required",
347
+ "hint": "Include 'Authorization: Bearer <token>' header"
348
+ }
349
+ )
350
+
351
+ # Check if this is a worker token (fast path)
352
+ if token.startswith("worker_"):
353
+ logger.debug("validating_worker_token", path=request.url.path)
354
+ org = await get_organization_from_worker_token(token)
355
+ if org:
356
+ # Store in request state for later use
357
+ request.state.organization = org
358
+ request.state.worker_token = token
359
+ logger.info(
360
+ "worker_authenticated",
361
+ org_id=org["id"],
362
+ path=request.url.path,
363
+ method=request.method,
364
+ )
365
+ return org
366
+
367
+ # Worker token invalid
368
+ logger.warning("worker_token_invalid", path=request.url.path)
369
+ raise HTTPException(
370
+ status_code=status.HTTP_401_UNAUTHORIZED,
371
+ detail={
372
+ "error": "Unauthorized",
373
+ "message": "Invalid or expired worker token",
374
+ "hint": "Worker token not found in active workers"
375
+ }
376
+ )
377
+
378
+ # Fall back to regular user authentication
379
+ return await get_current_organization(request)
380
+
381
+
382
+ async def get_current_organization(request: Request) -> dict:
383
+ """
384
+ Dependency to get current organization and validate authentication.
385
+
386
+ Flow:
387
+ 1. Extract token from Authorization header
388
+ 2. Check Redis cache for user data
389
+ 3. If not cached, validate with Kubiya API
390
+ 4. Cache the user data with TTL based on JWT expiry
391
+ 5. Return organization data
392
+
393
+ Args:
394
+ request: FastAPI request object
395
+
396
+ Returns:
397
+ Organization dict:
398
+ {
399
+ "id": "org-uuid",
400
+ "name": "Organization Name",
401
+ "slug": "org-slug",
402
+ "user_id": "user-uuid",
403
+ "user_email": "user@example.com",
404
+ "user_name": "User Name"
405
+ }
406
+
407
+ Raises:
408
+ HTTPException: 401 if authentication fails
409
+ """
410
+ # Extract token from headers
411
+ token = await extract_token_from_headers(request)
412
+
413
+ if not token:
414
+ logger.warning("auth_token_missing", path=request.url.path)
415
+ raise HTTPException(
416
+ status_code=status.HTTP_401_UNAUTHORIZED,
417
+ detail={
418
+ "error": "Unauthorized",
419
+ "message": "Authorization header is required",
420
+ "hint": "Include 'Authorization: Bearer <token>' header"
421
+ }
422
+ )
423
+
424
+ # Try to get cached user data
425
+ user_data = await get_cached_user_data(token)
426
+
427
+ # Track which auth type succeeded
428
+ auth_type = None
429
+
430
+ # If not cached, validate with Kubiya API
431
+ if not user_data:
432
+ logger.debug("validating_with_kubiya_api", path=request.url.path)
433
+ auth_header = f"Bearer {token}" if not token.startswith("Bearer ") else token
434
+
435
+ # Try Bearer first (for regular user tokens)
436
+ user_data = await validate_kubiya_api_key(auth_header, use_userkey=False)
437
+ if user_data:
438
+ auth_type = "Bearer"
439
+
440
+ # If Bearer fails, try UserKey (for API keys/worker tokens)
441
+ if not user_data:
442
+ logger.debug("bearer_auth_failed_trying_userkey", path=request.url.path)
443
+ user_data = await validate_kubiya_api_key(auth_header, use_userkey=True)
444
+ if user_data:
445
+ auth_type = "UserKey"
446
+
447
+ if not user_data:
448
+ logger.warning("authentication_failed", path=request.url.path)
449
+ raise HTTPException(
450
+ status_code=status.HTTP_401_UNAUTHORIZED,
451
+ detail={
452
+ "error": "Unauthorized",
453
+ "message": "Invalid or expired authentication token",
454
+ "hint": "Ensure you're using a valid Kubiya API token"
455
+ }
456
+ )
457
+
458
+ # Cache the validated user data with auth type
459
+ await cache_user_data(token, {**user_data, "_auth_type": auth_type})
460
+ else:
461
+ # Retrieve auth type from cache
462
+ auth_type = user_data.get("_auth_type", "Bearer")
463
+
464
+ # Extract organization slug from Kubiya API response
465
+ # Kubiya API returns the org slug in the "org" field (e.g., "kubiya-ai")
466
+ # We use this slug as the primary organization identifier throughout the system
467
+ org_slug = user_data.get("org")
468
+
469
+ if not org_slug:
470
+ logger.error("org_slug_missing_in_user_data", user_data=user_data)
471
+ raise HTTPException(
472
+ status_code=status.HTTP_401_UNAUTHORIZED,
473
+ detail={
474
+ "error": "Unauthorized",
475
+ "message": "No organization slug found in user data",
476
+ "hint": "User data must contain 'org' field from Kubiya API"
477
+ }
478
+ )
479
+
480
+ # Build organization object
481
+ # We use the org slug as the primary identifier (id field)
482
+ # This slug is used for all database operations (agents, teams, executions, etc.)
483
+ user_email = user_data.get("email")
484
+ user_name = user_data.get("name") or (user_email.split("@")[0] if user_email else None) # Fallback to email username
485
+
486
+ organization = {
487
+ "id": org_slug, # Use slug as ID (e.g., "kubiya-ai")
488
+ "name": user_data.get("org"), # Also use slug as display name
489
+ "slug": org_slug, # The slug itself
490
+ "user_id": user_data.get("uuid"), # User UUID from Kubiya
491
+ "user_email": user_email,
492
+ "user_name": user_name,
493
+ "user_avatar": user_data.get("picture") or user_data.get("avatar") or user_data.get("image"), # Avatar from Auth0/Kubiya
494
+ "user_status": user_data.get("user_status") or user_data.get("status"),
495
+ "user_groups": user_data.get("groups") or user_data.get("user_groups"),
496
+ }
497
+
498
+ # Store in request state for later use
499
+ request.state.organization = organization
500
+ request.state.kubiya_token = token
501
+ request.state.kubiya_auth_type = auth_type # Store whether to use Bearer or UserKey
502
+ request.state.user_data = user_data
503
+
504
+ logger.info(
505
+ "request_authenticated",
506
+ org_id=organization["id"],
507
+ user_id=organization.get("user_id"),
508
+ path=request.url.path,
509
+ method=request.method,
510
+ cached=user_data is not None
511
+ )
512
+
513
+ return organization