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,654 @@
1
+ """
2
+ Task Queues router - Worker queue management for routing work to specific workers.
3
+
4
+ This router handles task queue CRUD operations and tracks worker availability.
5
+ """
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
8
+ from typing import List, Optional
9
+ from datetime import datetime
10
+ from pydantic import BaseModel, Field
11
+ import structlog
12
+ import uuid
13
+ import os
14
+
15
+ from control_plane_api.app.middleware.auth import get_current_organization
16
+ from control_plane_api.app.lib.supabase import get_supabase
17
+ from control_plane_api.app.lib.temporal_client import get_temporal_client
18
+
19
+ logger = structlog.get_logger()
20
+
21
+ router = APIRouter()
22
+
23
+
24
+ # Pydantic schemas
25
+ class TaskQueueCreate(BaseModel):
26
+ name: str = Field(..., description="Queue name (e.g., default, high-priority)", min_length=2, max_length=100)
27
+ display_name: str | None = Field(None, description="User-friendly display name")
28
+ description: str | None = Field(None, description="Queue description")
29
+ tags: List[str] = Field(default_factory=list, description="Tags for categorization")
30
+ settings: dict = Field(default_factory=dict, description="Queue settings")
31
+ priority: int | None = Field(None, ge=1, le=10, description="Priority level (1-10)")
32
+ policy_ids: List[str] = Field(default_factory=list, description="OPA policy IDs")
33
+
34
+
35
+ class TaskQueueUpdate(BaseModel):
36
+ name: str | None = None
37
+ display_name: str | None = None
38
+ description: str | None = None
39
+ tags: List[str] | None = None
40
+ settings: dict | None = None
41
+ status: str | None = None
42
+ priority: int | None = Field(None, ge=1, le=10)
43
+ policy_ids: List[str] | None = None
44
+
45
+
46
+ class TaskQueueResponse(BaseModel):
47
+ id: str
48
+ organization_id: str
49
+ name: str
50
+ display_name: str | None
51
+ description: str | None
52
+ tags: List[str]
53
+ settings: dict
54
+ status: str
55
+ priority: int | None = None
56
+ policy_ids: List[str] = []
57
+ created_at: str
58
+ updated_at: str
59
+ created_by: str | None
60
+
61
+ # Temporal Cloud provisioning fields
62
+ worker_token: str | None = None # UUID token for worker registration
63
+ provisioning_workflow_id: str | None = None # Temporal workflow ID
64
+ provisioned_at: str | None = None
65
+ error_message: str | None = None
66
+ temporal_namespace_id: str | None = None
67
+
68
+ # Worker metrics
69
+ active_workers: int = 0
70
+ idle_workers: int = 0
71
+ busy_workers: int = 0
72
+
73
+
74
+ class WorkerHeartbeatResponse(BaseModel):
75
+ id: str
76
+ organization_id: str
77
+ task_queue_name: str
78
+ worker_id: str
79
+ hostname: str | None
80
+ worker_metadata: dict
81
+ last_heartbeat: str
82
+ status: str
83
+ tasks_processed: int
84
+ current_task_id: str | None
85
+ registered_at: str
86
+ updated_at: str
87
+
88
+
89
+ def ensure_default_queue(organization: dict) -> Optional[dict]:
90
+ """
91
+ Ensure the organization has a default task queue.
92
+ Creates one if it doesn't exist.
93
+
94
+ Returns the default queue or None if creation failed.
95
+ """
96
+ try:
97
+ client = get_supabase()
98
+
99
+ # Check if default queue exists
100
+ existing = (
101
+ client.table("environments")
102
+ .select("*")
103
+ .eq("organization_id", organization["id"])
104
+ .eq("name", "default")
105
+ .execute()
106
+ )
107
+
108
+ if existing.data:
109
+ return existing.data[0]
110
+
111
+ # Create default queue
112
+ queue_id = str(uuid.uuid4())
113
+ now = datetime.utcnow().isoformat()
114
+
115
+ default_queue = {
116
+ "id": queue_id,
117
+ "organization_id": organization["id"],
118
+ "name": "default",
119
+ "display_name": "Default Queue",
120
+ "description": "Default task queue for all workers",
121
+ "tags": [],
122
+ "settings": {},
123
+ "status": "active",
124
+ "created_at": now,
125
+ "updated_at": now,
126
+ "created_by": organization.get("user_id"),
127
+ }
128
+
129
+ result = client.table("environments").insert(default_queue).execute()
130
+
131
+ if result.data:
132
+ logger.info(
133
+ "default_queue_created",
134
+ queue_id=queue_id,
135
+ org_id=organization["id"],
136
+ )
137
+ return result.data[0]
138
+
139
+ return None
140
+
141
+ except Exception as e:
142
+ logger.error("ensure_default_queue_failed", error=str(e), org_id=organization.get("id"))
143
+ return None
144
+
145
+
146
+ @router.post("", response_model=TaskQueueResponse, status_code=status.HTTP_201_CREATED)
147
+ async def create_task_queue(
148
+ queue_data: TaskQueueCreate,
149
+ request: Request,
150
+ organization: dict = Depends(get_current_organization),
151
+ ):
152
+ """
153
+ Create a new task queue.
154
+
155
+ If this is the first task queue for the organization, it will trigger
156
+ Temporal Cloud namespace provisioning workflow.
157
+ """
158
+ try:
159
+ client = get_supabase()
160
+
161
+ # Check if queue name already exists for this organization
162
+ existing = (
163
+ client.table("environments")
164
+ .select("id")
165
+ .eq("organization_id", organization["id"])
166
+ .eq("name", queue_data.name)
167
+ .execute()
168
+ )
169
+
170
+ if existing.data:
171
+ raise HTTPException(
172
+ status_code=status.HTTP_409_CONFLICT,
173
+ detail=f"Task queue with name '{queue_data.name}' already exists"
174
+ )
175
+
176
+ # Check if this is the first task queue for this org
177
+ all_queues = (
178
+ client.table("environments")
179
+ .select("id")
180
+ .eq("organization_id", organization["id"])
181
+ .execute()
182
+ )
183
+ is_first_queue = len(all_queues.data or []) == 0
184
+
185
+ # Check if namespace already exists
186
+ namespace_result = (
187
+ client.table("temporal_namespaces")
188
+ .select("*")
189
+ .eq("organization_id", organization["id"])
190
+ .execute()
191
+ )
192
+ has_namespace = bool(namespace_result.data)
193
+ needs_provisioning = is_first_queue and not has_namespace
194
+
195
+ queue_id = str(uuid.uuid4())
196
+ now = datetime.utcnow().isoformat()
197
+
198
+ # Set initial status
199
+ initial_status = "provisioning" if needs_provisioning else "ready"
200
+
201
+ queue_record = {
202
+ "id": queue_id,
203
+ "organization_id": organization["id"],
204
+ "name": queue_data.name,
205
+ "display_name": queue_data.display_name or queue_data.name,
206
+ "description": queue_data.description,
207
+ "tags": queue_data.tags,
208
+ "settings": queue_data.settings,
209
+ "status": initial_status,
210
+ "created_at": now,
211
+ "updated_at": now,
212
+ "created_by": organization.get("user_id"),
213
+ "worker_token": str(uuid.uuid4()), # Generate worker token
214
+ }
215
+
216
+ result = client.table("environments").insert(queue_record).execute()
217
+
218
+ if not result.data:
219
+ raise HTTPException(
220
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
221
+ detail="Failed to create task queue"
222
+ )
223
+
224
+ queue = result.data[0]
225
+
226
+ # Trigger namespace provisioning workflow if needed
227
+ if needs_provisioning:
228
+ try:
229
+ from control_plane_api.app.workflows.namespace_provisioning import (
230
+ ProvisionTemporalNamespaceWorkflow,
231
+ ProvisionNamespaceInput,
232
+ )
233
+
234
+ temporal_client = await get_temporal_client()
235
+ account_id = os.environ.get("TEMPORAL_CLOUD_ACCOUNT_ID", "default-account")
236
+
237
+ workflow_input = ProvisionNamespaceInput(
238
+ organization_id=organization["id"],
239
+ organization_name=organization.get("name", organization["id"]),
240
+ task_queue_id=queue_id,
241
+ account_id=account_id,
242
+ region=os.environ.get("TEMPORAL_CLOUD_REGION", "aws-us-east-1"),
243
+ )
244
+
245
+ # Start provisioning workflow on control plane's task queue
246
+ workflow_handle = await temporal_client.start_workflow(
247
+ ProvisionTemporalNamespaceWorkflow.run,
248
+ workflow_input,
249
+ id=f"provision-namespace-{organization['id']}",
250
+ task_queue="agent-control-plane", # Control plane's task queue
251
+ )
252
+
253
+ # Update queue with workflow ID
254
+ client.table("environments").update({
255
+ "provisioning_workflow_id": workflow_handle.id,
256
+ "updated_at": datetime.utcnow().isoformat(),
257
+ }).eq("id", queue_id).execute()
258
+
259
+ queue["provisioning_workflow_id"] = workflow_handle.id
260
+
261
+ logger.info(
262
+ "namespace_provisioning_workflow_started",
263
+ workflow_id=workflow_handle.id,
264
+ queue_id=queue_id,
265
+ org_id=organization["id"],
266
+ )
267
+ except Exception as e:
268
+ logger.error(
269
+ "failed_to_start_provisioning_workflow",
270
+ error=str(e),
271
+ queue_id=queue_id,
272
+ org_id=organization["id"],
273
+ )
274
+ # Update queue status to error
275
+ client.table("environments").update({
276
+ "status": "error",
277
+ "error_message": f"Failed to start provisioning: {str(e)}",
278
+ "updated_at": datetime.utcnow().isoformat(),
279
+ }).eq("id", queue_id).execute()
280
+ queue["status"] = "error"
281
+ queue["error_message"] = f"Failed to start provisioning: {str(e)}"
282
+
283
+ logger.info(
284
+ "task_queue_created",
285
+ queue_id=queue_id,
286
+ queue_name=queue["name"],
287
+ org_id=organization["id"],
288
+ needs_provisioning=needs_provisioning,
289
+ )
290
+
291
+ return TaskQueueResponse(
292
+ **queue,
293
+ active_workers=0,
294
+ idle_workers=0,
295
+ busy_workers=0,
296
+ )
297
+
298
+ except HTTPException:
299
+ raise
300
+ except Exception as e:
301
+ logger.error("task_queue_creation_failed", error=str(e), org_id=organization["id"])
302
+ raise HTTPException(
303
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
304
+ detail=f"Failed to create task queue: {str(e)}"
305
+ )
306
+
307
+
308
+ @router.get("", response_model=List[TaskQueueResponse])
309
+ async def list_task_queues(
310
+ request: Request,
311
+ status_filter: str | None = None,
312
+ organization: dict = Depends(get_current_organization),
313
+ ):
314
+ """List all task queues in the organization"""
315
+ try:
316
+ client = get_supabase()
317
+
318
+ # Ensure default queue exists
319
+ ensure_default_queue(organization)
320
+
321
+ # Query queues
322
+ query = client.table("environments").select("*").eq("organization_id", organization["id"])
323
+
324
+ if status_filter:
325
+ query = query.eq("status", status_filter)
326
+
327
+ query = query.order("created_at", desc=False)
328
+ result = query.execute()
329
+
330
+ if not result.data:
331
+ return []
332
+
333
+ # Note: Worker stats are now tracked at worker_queue level, not environment level
334
+ # For backward compatibility, we return 0 for environment-level worker counts
335
+ # Use worker_queues endpoints for detailed worker information
336
+ queues = []
337
+ for queue in result.data:
338
+ queues.append(
339
+ TaskQueueResponse(
340
+ **queue,
341
+ active_workers=0,
342
+ idle_workers=0,
343
+ busy_workers=0,
344
+ )
345
+ )
346
+
347
+ logger.info(
348
+ "task_queues_listed",
349
+ count=len(queues),
350
+ org_id=organization["id"],
351
+ )
352
+
353
+ return queues
354
+
355
+ except Exception as e:
356
+ logger.error("task_queues_list_failed", error=str(e), org_id=organization["id"])
357
+ raise HTTPException(
358
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
359
+ detail=f"Failed to list task queues: {str(e)}"
360
+ )
361
+
362
+
363
+ @router.get("/{queue_id}", response_model=TaskQueueResponse)
364
+ async def get_task_queue(
365
+ queue_id: str,
366
+ request: Request,
367
+ organization: dict = Depends(get_current_organization),
368
+ ):
369
+ """Get a specific task queue by ID"""
370
+ try:
371
+ client = get_supabase()
372
+
373
+ result = (
374
+ client.table("environments")
375
+ .select("*")
376
+ .eq("id", queue_id)
377
+ .eq("organization_id", organization["id"])
378
+ .single()
379
+ .execute()
380
+ )
381
+
382
+ if not result.data:
383
+ raise HTTPException(status_code=404, detail="Task queue not found")
384
+
385
+ queue = result.data
386
+
387
+ # Note: Worker stats are now tracked at worker_queue level
388
+ # Return 0 for environment-level worker counts
389
+ return TaskQueueResponse(
390
+ **queue,
391
+ active_workers=0,
392
+ idle_workers=0,
393
+ busy_workers=0,
394
+ )
395
+
396
+ except HTTPException:
397
+ raise
398
+ except Exception as e:
399
+ logger.error("task_queue_get_failed", error=str(e), queue_id=queue_id)
400
+ raise HTTPException(
401
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
402
+ detail=f"Failed to get task queue: {str(e)}"
403
+ )
404
+
405
+
406
+ @router.patch("/{queue_id}", response_model=TaskQueueResponse)
407
+ async def update_task_queue(
408
+ queue_id: str,
409
+ queue_data: TaskQueueUpdate,
410
+ request: Request,
411
+ organization: dict = Depends(get_current_organization),
412
+ ):
413
+ """Update a task queue"""
414
+ try:
415
+ client = get_supabase()
416
+
417
+ # Check if queue exists
418
+ existing = (
419
+ client.table("environments")
420
+ .select("id")
421
+ .eq("id", queue_id)
422
+ .eq("organization_id", organization["id"])
423
+ .execute()
424
+ )
425
+
426
+ if not existing.data:
427
+ raise HTTPException(status_code=404, detail="Task queue not found")
428
+
429
+ # Build update dict
430
+ update_data = queue_data.model_dump(exclude_unset=True)
431
+ update_data["updated_at"] = datetime.utcnow().isoformat()
432
+
433
+ # Update queue
434
+ result = (
435
+ client.table("environments")
436
+ .update(update_data)
437
+ .eq("id", queue_id)
438
+ .eq("organization_id", organization["id"])
439
+ .execute()
440
+ )
441
+
442
+ if not result.data:
443
+ raise HTTPException(
444
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
445
+ detail="Failed to update task queue"
446
+ )
447
+
448
+ queue = result.data[0]
449
+
450
+ logger.info(
451
+ "task_queue_updated",
452
+ queue_id=queue_id,
453
+ org_id=organization["id"],
454
+ )
455
+
456
+ # Note: Worker stats are now tracked at worker_queue level
457
+ return TaskQueueResponse(
458
+ **queue,
459
+ active_workers=0,
460
+ idle_workers=0,
461
+ busy_workers=0,
462
+ )
463
+
464
+ except HTTPException:
465
+ raise
466
+ except Exception as e:
467
+ logger.error("task_queue_update_failed", error=str(e), queue_id=queue_id)
468
+ raise HTTPException(
469
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
470
+ detail=f"Failed to update task queue: {str(e)}"
471
+ )
472
+
473
+
474
+ @router.delete("/{queue_id}", status_code=status.HTTP_204_NO_CONTENT)
475
+ async def delete_task_queue(
476
+ queue_id: str,
477
+ request: Request,
478
+ organization: dict = Depends(get_current_organization),
479
+ ):
480
+ """Delete a task queue"""
481
+ try:
482
+ client = get_supabase()
483
+
484
+ # Prevent deleting default queue
485
+ queue_check = (
486
+ client.table("environments")
487
+ .select("name")
488
+ .eq("id", queue_id)
489
+ .eq("organization_id", organization["id"])
490
+ .single()
491
+ .execute()
492
+ )
493
+
494
+ if queue_check.data and queue_check.data.get("name") == "default":
495
+ raise HTTPException(
496
+ status_code=status.HTTP_400_BAD_REQUEST,
497
+ detail="Cannot delete the default queue"
498
+ )
499
+
500
+ result = (
501
+ client.table("environments")
502
+ .delete()
503
+ .eq("id", queue_id)
504
+ .eq("organization_id", organization["id"])
505
+ .execute()
506
+ )
507
+
508
+ if not result.data:
509
+ raise HTTPException(status_code=404, detail="Task queue not found")
510
+
511
+ logger.info("task_queue_deleted", queue_id=queue_id, org_id=organization["id"])
512
+
513
+ return None
514
+
515
+ except HTTPException:
516
+ raise
517
+ except Exception as e:
518
+ logger.error("task_queue_delete_failed", error=str(e), queue_id=queue_id)
519
+ raise HTTPException(
520
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
521
+ detail=f"Failed to delete task queue: {str(e)}"
522
+ )
523
+
524
+
525
+ @router.get("/{queue_name}/workers", response_model=List[WorkerHeartbeatResponse])
526
+ async def list_queue_workers(
527
+ queue_name: str,
528
+ request: Request,
529
+ organization: dict = Depends(get_current_organization),
530
+ ):
531
+ """
532
+ List all workers for a specific queue.
533
+
534
+ NOTE: This endpoint is deprecated. Workers are now organized by worker_queues.
535
+ Use GET /environments/{env_id}/worker-queues and worker_queues endpoints instead.
536
+ """
537
+ logger.warning(
538
+ "deprecated_endpoint_called",
539
+ endpoint="/task-queues/{queue_name}/workers",
540
+ queue_name=queue_name,
541
+ org_id=organization["id"],
542
+ )
543
+
544
+ # Return empty list for backward compatibility
545
+ return []
546
+
547
+
548
+ # Worker Registration
549
+
550
+ class WorkerCommandResponse(BaseModel):
551
+ """Response with worker registration command"""
552
+ worker_token: str
553
+ task_queue_name: str
554
+ command: str
555
+ command_parts: dict # Broken down for UI display
556
+ namespace_status: str # pending, provisioning, ready, error
557
+ can_register: bool
558
+ provisioning_workflow_id: str | None = None
559
+
560
+
561
+ @router.get("/{queue_id}/worker-command", response_model=WorkerCommandResponse)
562
+ async def get_worker_registration_command(
563
+ queue_id: str,
564
+ request: Request,
565
+ organization: dict = Depends(get_current_organization),
566
+ ):
567
+ """
568
+ Get the worker registration command for a task queue.
569
+
570
+ Returns the kubiya worker start command with the worker token that users
571
+ should run to start a worker for this task queue.
572
+
573
+ The UI should display this in a "Register Worker" dialog when the queue
574
+ is ready, and show provisioning status while the namespace is being created.
575
+ """
576
+ try:
577
+ client = get_supabase()
578
+
579
+ # Get task queue
580
+ result = (
581
+ client.table("environments")
582
+ .select("*")
583
+ .eq("id", queue_id)
584
+ .eq("organization_id", organization["id"])
585
+ .single()
586
+ .execute()
587
+ )
588
+
589
+ if not result.data:
590
+ raise HTTPException(status_code=404, detail="Task queue not found")
591
+
592
+ queue = result.data
593
+ worker_token = queue.get("worker_token")
594
+
595
+ # Generate worker_token if it doesn't exist (for existing queues)
596
+ if not worker_token:
597
+ worker_token = str(uuid.uuid4())
598
+ # Update the queue with the generated token
599
+ client.table("environments").update({
600
+ "worker_token": worker_token,
601
+ "updated_at": datetime.utcnow().isoformat(),
602
+ }).eq("id", queue_id).execute()
603
+
604
+ logger.info(
605
+ "worker_token_generated",
606
+ queue_id=queue_id,
607
+ org_id=organization["id"],
608
+ )
609
+
610
+ task_queue_name = queue["name"]
611
+ namespace_status = queue.get("status", "unknown")
612
+ provisioning_workflow_id = queue.get("provisioning_workflow_id")
613
+
614
+ # Check if namespace is ready
615
+ can_register = namespace_status in ["ready", "active"]
616
+
617
+ # Build command
618
+ command = f"kubiya worker start --token {worker_token} --task-queue {task_queue_name}"
619
+
620
+ command_parts = {
621
+ "binary": "kubiya",
622
+ "subcommand": "worker start",
623
+ "flags": {
624
+ "--token": worker_token,
625
+ "--task-queue": task_queue_name,
626
+ },
627
+ }
628
+
629
+ logger.info(
630
+ "worker_command_retrieved",
631
+ queue_id=queue_id,
632
+ can_register=can_register,
633
+ status=namespace_status,
634
+ org_id=organization["id"],
635
+ )
636
+
637
+ return WorkerCommandResponse(
638
+ worker_token=worker_token,
639
+ task_queue_name=task_queue_name,
640
+ command=command,
641
+ command_parts=command_parts,
642
+ namespace_status=namespace_status,
643
+ can_register=can_register,
644
+ provisioning_workflow_id=provisioning_workflow_id,
645
+ )
646
+
647
+ except HTTPException:
648
+ raise
649
+ except Exception as e:
650
+ logger.error("worker_command_get_failed", error=str(e), queue_id=queue_id)
651
+ raise HTTPException(
652
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
653
+ detail=f"Failed to get worker command: {str(e)}"
654
+ )