kubiya-control-plane-api 0.9.15__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 (479) hide show
  1. control_plane_api/LICENSE +676 -0
  2. control_plane_api/README.md +350 -0
  3. control_plane_api/__init__.py +4 -0
  4. control_plane_api/__version__.py +8 -0
  5. control_plane_api/alembic/README +1 -0
  6. control_plane_api/alembic/env.py +121 -0
  7. control_plane_api/alembic/script.py.mako +28 -0
  8. control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
  9. control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
  10. control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
  11. control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
  12. control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
  13. control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
  14. control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
  15. control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
  16. control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
  17. control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
  18. control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
  19. control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
  20. control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
  21. control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
  22. control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
  23. control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
  24. control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
  25. control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -0
  26. control_plane_api/alembic.ini +148 -0
  27. control_plane_api/api/index.py +12 -0
  28. control_plane_api/app/__init__.py +11 -0
  29. control_plane_api/app/activities/__init__.py +20 -0
  30. control_plane_api/app/activities/agent_activities.py +384 -0
  31. control_plane_api/app/activities/plan_generation_activities.py +499 -0
  32. control_plane_api/app/activities/team_activities.py +424 -0
  33. control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
  34. control_plane_api/app/config/__init__.py +35 -0
  35. control_plane_api/app/config/api_config.py +469 -0
  36. control_plane_api/app/config/config_loader.py +224 -0
  37. control_plane_api/app/config/model_pricing.py +323 -0
  38. control_plane_api/app/config/storage_config.py +159 -0
  39. control_plane_api/app/config.py +115 -0
  40. control_plane_api/app/controllers/__init__.py +0 -0
  41. control_plane_api/app/controllers/execution_environment_controller.py +1315 -0
  42. control_plane_api/app/database.py +135 -0
  43. control_plane_api/app/exceptions.py +408 -0
  44. control_plane_api/app/lib/__init__.py +11 -0
  45. control_plane_api/app/lib/environment.py +65 -0
  46. control_plane_api/app/lib/event_bus/__init__.py +17 -0
  47. control_plane_api/app/lib/event_bus/base.py +136 -0
  48. control_plane_api/app/lib/event_bus/manager.py +335 -0
  49. control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
  50. control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
  51. control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
  52. control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
  53. control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
  54. control_plane_api/app/lib/job_executor.py +330 -0
  55. control_plane_api/app/lib/kubiya_client.py +293 -0
  56. control_plane_api/app/lib/litellm_pricing.py +166 -0
  57. control_plane_api/app/lib/mcp_validation.py +163 -0
  58. control_plane_api/app/lib/nats/__init__.py +13 -0
  59. control_plane_api/app/lib/nats/credentials_manager.py +288 -0
  60. control_plane_api/app/lib/nats/listener.py +374 -0
  61. control_plane_api/app/lib/planning_prompt_builder.py +153 -0
  62. control_plane_api/app/lib/planning_tools/__init__.py +41 -0
  63. control_plane_api/app/lib/planning_tools/agents.py +409 -0
  64. control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
  65. control_plane_api/app/lib/planning_tools/base.py +119 -0
  66. control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
  67. control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
  68. control_plane_api/app/lib/planning_tools/environments.py +218 -0
  69. control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
  70. control_plane_api/app/lib/planning_tools/models.py +93 -0
  71. control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
  72. control_plane_api/app/lib/planning_tools/resources.py +242 -0
  73. control_plane_api/app/lib/planning_tools/teams.py +334 -0
  74. control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
  75. control_plane_api/app/lib/redis_client.py +803 -0
  76. control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
  77. control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
  78. control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
  79. control_plane_api/app/lib/storage/__init__.py +20 -0
  80. control_plane_api/app/lib/storage/base_provider.py +274 -0
  81. control_plane_api/app/lib/storage/provider_factory.py +157 -0
  82. control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
  83. control_plane_api/app/lib/supabase.py +71 -0
  84. control_plane_api/app/lib/supabase_utils.py +138 -0
  85. control_plane_api/app/lib/task_planning/__init__.py +138 -0
  86. control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
  87. control_plane_api/app/lib/task_planning/agents.py +389 -0
  88. control_plane_api/app/lib/task_planning/cache.py +218 -0
  89. control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
  90. control_plane_api/app/lib/task_planning/helpers.py +293 -0
  91. control_plane_api/app/lib/task_planning/hooks.py +474 -0
  92. control_plane_api/app/lib/task_planning/models.py +503 -0
  93. control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
  94. control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
  95. control_plane_api/app/lib/task_planning/runner.py +656 -0
  96. control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
  97. control_plane_api/app/lib/task_planning/workflow.py +424 -0
  98. control_plane_api/app/lib/templating/__init__.py +88 -0
  99. control_plane_api/app/lib/templating/compiler.py +278 -0
  100. control_plane_api/app/lib/templating/engine.py +178 -0
  101. control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
  102. control_plane_api/app/lib/templating/parsers/base.py +96 -0
  103. control_plane_api/app/lib/templating/parsers/env.py +85 -0
  104. control_plane_api/app/lib/templating/parsers/graph.py +112 -0
  105. control_plane_api/app/lib/templating/parsers/secret.py +87 -0
  106. control_plane_api/app/lib/templating/parsers/simple.py +81 -0
  107. control_plane_api/app/lib/templating/resolver.py +366 -0
  108. control_plane_api/app/lib/templating/types.py +214 -0
  109. control_plane_api/app/lib/templating/validator.py +201 -0
  110. control_plane_api/app/lib/temporal_client.py +232 -0
  111. control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
  112. control_plane_api/app/lib/temporal_credentials_service.py +203 -0
  113. control_plane_api/app/lib/validation/__init__.py +24 -0
  114. control_plane_api/app/lib/validation/runtime_validation.py +388 -0
  115. control_plane_api/app/main.py +531 -0
  116. control_plane_api/app/middleware/__init__.py +10 -0
  117. control_plane_api/app/middleware/auth.py +645 -0
  118. control_plane_api/app/middleware/exception_handler.py +267 -0
  119. control_plane_api/app/middleware/prometheus_middleware.py +173 -0
  120. control_plane_api/app/middleware/rate_limiting.py +384 -0
  121. control_plane_api/app/middleware/request_id.py +202 -0
  122. control_plane_api/app/models/__init__.py +40 -0
  123. control_plane_api/app/models/agent.py +90 -0
  124. control_plane_api/app/models/analytics.py +206 -0
  125. control_plane_api/app/models/associations.py +107 -0
  126. control_plane_api/app/models/auth_user.py +73 -0
  127. control_plane_api/app/models/context.py +161 -0
  128. control_plane_api/app/models/custom_integration.py +99 -0
  129. control_plane_api/app/models/environment.py +64 -0
  130. control_plane_api/app/models/execution.py +125 -0
  131. control_plane_api/app/models/execution_transition.py +50 -0
  132. control_plane_api/app/models/job.py +159 -0
  133. control_plane_api/app/models/llm_model.py +78 -0
  134. control_plane_api/app/models/orchestration.py +66 -0
  135. control_plane_api/app/models/plan_execution.py +102 -0
  136. control_plane_api/app/models/presence.py +49 -0
  137. control_plane_api/app/models/project.py +61 -0
  138. control_plane_api/app/models/project_management.py +85 -0
  139. control_plane_api/app/models/session.py +29 -0
  140. control_plane_api/app/models/skill.py +155 -0
  141. control_plane_api/app/models/system_tables.py +43 -0
  142. control_plane_api/app/models/task_planning.py +372 -0
  143. control_plane_api/app/models/team.py +86 -0
  144. control_plane_api/app/models/trace.py +257 -0
  145. control_plane_api/app/models/user_profile.py +54 -0
  146. control_plane_api/app/models/worker.py +221 -0
  147. control_plane_api/app/models/workflow.py +161 -0
  148. control_plane_api/app/models/workspace.py +50 -0
  149. control_plane_api/app/observability/__init__.py +177 -0
  150. control_plane_api/app/observability/context_logging.py +475 -0
  151. control_plane_api/app/observability/decorators.py +337 -0
  152. control_plane_api/app/observability/local_span_processor.py +702 -0
  153. control_plane_api/app/observability/metrics.py +303 -0
  154. control_plane_api/app/observability/middleware.py +246 -0
  155. control_plane_api/app/observability/optional.py +115 -0
  156. control_plane_api/app/observability/tracing.py +382 -0
  157. control_plane_api/app/policies/README.md +149 -0
  158. control_plane_api/app/policies/approved_users.rego +62 -0
  159. control_plane_api/app/policies/business_hours.rego +51 -0
  160. control_plane_api/app/policies/rate_limiting.rego +100 -0
  161. control_plane_api/app/policies/tool_enforcement/README.md +336 -0
  162. control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
  163. control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
  164. control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
  165. control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
  166. control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -0
  167. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  168. control_plane_api/app/routers/__init__.py +4 -0
  169. control_plane_api/app/routers/agents.py +382 -0
  170. control_plane_api/app/routers/agents_v2.py +1598 -0
  171. control_plane_api/app/routers/analytics.py +1310 -0
  172. control_plane_api/app/routers/auth.py +59 -0
  173. control_plane_api/app/routers/client_config.py +57 -0
  174. control_plane_api/app/routers/context_graph.py +561 -0
  175. control_plane_api/app/routers/context_manager.py +577 -0
  176. control_plane_api/app/routers/custom_integrations.py +490 -0
  177. control_plane_api/app/routers/enforcer.py +132 -0
  178. control_plane_api/app/routers/environment_context.py +252 -0
  179. control_plane_api/app/routers/environments.py +761 -0
  180. control_plane_api/app/routers/execution_environment.py +847 -0
  181. control_plane_api/app/routers/executions/__init__.py +28 -0
  182. control_plane_api/app/routers/executions/router.py +286 -0
  183. control_plane_api/app/routers/executions/services/__init__.py +22 -0
  184. control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
  185. control_plane_api/app/routers/executions/services/status_service.py +420 -0
  186. control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
  187. control_plane_api/app/routers/executions/services/worker_health.py +514 -0
  188. control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
  189. control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
  190. control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
  191. control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
  192. control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
  193. control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
  194. control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
  195. control_plane_api/app/routers/executions.py +4888 -0
  196. control_plane_api/app/routers/health.py +165 -0
  197. control_plane_api/app/routers/health_v2.py +394 -0
  198. control_plane_api/app/routers/integration_templates.py +496 -0
  199. control_plane_api/app/routers/integrations.py +287 -0
  200. control_plane_api/app/routers/jobs.py +1809 -0
  201. control_plane_api/app/routers/metrics.py +517 -0
  202. control_plane_api/app/routers/models.py +82 -0
  203. control_plane_api/app/routers/models_v2.py +628 -0
  204. control_plane_api/app/routers/plan_executions.py +1481 -0
  205. control_plane_api/app/routers/plan_generation_async.py +304 -0
  206. control_plane_api/app/routers/policies.py +669 -0
  207. control_plane_api/app/routers/presence.py +234 -0
  208. control_plane_api/app/routers/projects.py +987 -0
  209. control_plane_api/app/routers/runners.py +379 -0
  210. control_plane_api/app/routers/runtimes.py +172 -0
  211. control_plane_api/app/routers/secrets.py +171 -0
  212. control_plane_api/app/routers/skills.py +1010 -0
  213. control_plane_api/app/routers/skills_definitions.py +140 -0
  214. control_plane_api/app/routers/storage.py +456 -0
  215. control_plane_api/app/routers/task_planning.py +611 -0
  216. control_plane_api/app/routers/task_queues.py +650 -0
  217. control_plane_api/app/routers/team_context.py +274 -0
  218. control_plane_api/app/routers/teams.py +1747 -0
  219. control_plane_api/app/routers/templates.py +248 -0
  220. control_plane_api/app/routers/traces.py +571 -0
  221. control_plane_api/app/routers/websocket_client.py +479 -0
  222. control_plane_api/app/routers/websocket_executions_status.py +437 -0
  223. control_plane_api/app/routers/websocket_gateway.py +323 -0
  224. control_plane_api/app/routers/websocket_traces.py +576 -0
  225. control_plane_api/app/routers/worker_queues.py +2555 -0
  226. control_plane_api/app/routers/worker_websocket.py +419 -0
  227. control_plane_api/app/routers/workers.py +1004 -0
  228. control_plane_api/app/routers/workflows.py +204 -0
  229. control_plane_api/app/runtimes/__init__.py +6 -0
  230. control_plane_api/app/runtimes/validation.py +344 -0
  231. control_plane_api/app/schemas/__init__.py +1 -0
  232. control_plane_api/app/schemas/job_schemas.py +302 -0
  233. control_plane_api/app/schemas/mcp_schemas.py +311 -0
  234. control_plane_api/app/schemas/template_schemas.py +133 -0
  235. control_plane_api/app/schemas/trace_schemas.py +168 -0
  236. control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
  237. control_plane_api/app/services/__init__.py +1 -0
  238. control_plane_api/app/services/agno_planning_strategy.py +233 -0
  239. control_plane_api/app/services/agno_service.py +838 -0
  240. control_plane_api/app/services/claude_code_planning_service.py +203 -0
  241. control_plane_api/app/services/context_graph_client.py +224 -0
  242. control_plane_api/app/services/custom_integration_service.py +415 -0
  243. control_plane_api/app/services/integration_resolution_service.py +345 -0
  244. control_plane_api/app/services/litellm_service.py +394 -0
  245. control_plane_api/app/services/plan_generator.py +79 -0
  246. control_plane_api/app/services/planning_strategy.py +66 -0
  247. control_plane_api/app/services/planning_strategy_factory.py +118 -0
  248. control_plane_api/app/services/policy_service.py +615 -0
  249. control_plane_api/app/services/state_transition_service.py +755 -0
  250. control_plane_api/app/services/storage_service.py +593 -0
  251. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  252. control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
  253. control_plane_api/app/services/trace_retention.py +354 -0
  254. control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
  255. control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
  256. control_plane_api/app/services/workflow_operations_service.py +611 -0
  257. control_plane_api/app/skills/__init__.py +100 -0
  258. control_plane_api/app/skills/base.py +239 -0
  259. control_plane_api/app/skills/builtin/__init__.py +37 -0
  260. control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
  261. control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
  262. control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
  263. control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
  264. control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
  265. control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
  266. control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
  267. control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
  268. control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
  269. control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
  270. control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
  271. control_plane_api/app/skills/builtin/docker/skill.py +104 -0
  272. control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
  273. control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
  274. control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
  275. control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
  276. control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
  277. control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
  278. control_plane_api/app/skills/builtin/python/__init__.py +4 -0
  279. control_plane_api/app/skills/builtin/python/skill.py +92 -0
  280. control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
  281. control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
  282. control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
  283. control_plane_api/app/skills/builtin/shell/skill.py +161 -0
  284. control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
  285. control_plane_api/app/skills/builtin/slack/skill.py +302 -0
  286. control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
  287. control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
  288. control_plane_api/app/skills/business_intelligence.py +189 -0
  289. control_plane_api/app/skills/config.py +63 -0
  290. control_plane_api/app/skills/loaders/__init__.py +14 -0
  291. control_plane_api/app/skills/loaders/base.py +73 -0
  292. control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
  293. control_plane_api/app/skills/registry.py +125 -0
  294. control_plane_api/app/utils/helpers.py +12 -0
  295. control_plane_api/app/utils/workflow_executor.py +354 -0
  296. control_plane_api/app/workflows/__init__.py +11 -0
  297. control_plane_api/app/workflows/agent_execution.py +520 -0
  298. control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
  299. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  300. control_plane_api/app/workflows/plan_generation.py +254 -0
  301. control_plane_api/app/workflows/team_execution.py +442 -0
  302. control_plane_api/scripts/seed_models.py +240 -0
  303. control_plane_api/scripts/validate_existing_tool_names.py +492 -0
  304. control_plane_api/shared/__init__.py +8 -0
  305. control_plane_api/shared/version.py +17 -0
  306. control_plane_api/test_deduplication.py +274 -0
  307. control_plane_api/test_executor_deduplication_e2e.py +309 -0
  308. control_plane_api/test_job_execution_e2e.py +283 -0
  309. control_plane_api/test_real_integration.py +193 -0
  310. control_plane_api/version.py +38 -0
  311. control_plane_api/worker/__init__.py +0 -0
  312. control_plane_api/worker/activities/__init__.py +0 -0
  313. control_plane_api/worker/activities/agent_activities.py +1585 -0
  314. control_plane_api/worker/activities/approval_activities.py +234 -0
  315. control_plane_api/worker/activities/job_activities.py +199 -0
  316. control_plane_api/worker/activities/runtime_activities.py +1167 -0
  317. control_plane_api/worker/activities/skill_activities.py +282 -0
  318. control_plane_api/worker/activities/team_activities.py +479 -0
  319. control_plane_api/worker/agent_runtime_server.py +370 -0
  320. control_plane_api/worker/binary_manager.py +333 -0
  321. control_plane_api/worker/config/__init__.py +31 -0
  322. control_plane_api/worker/config/worker_config.py +273 -0
  323. control_plane_api/worker/control_plane_client.py +1491 -0
  324. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  325. control_plane_api/worker/health_monitor.py +159 -0
  326. control_plane_api/worker/metrics.py +237 -0
  327. control_plane_api/worker/models/__init__.py +1 -0
  328. control_plane_api/worker/models/error_events.py +105 -0
  329. control_plane_api/worker/models/inputs.py +89 -0
  330. control_plane_api/worker/runtimes/__init__.py +35 -0
  331. control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
  332. control_plane_api/worker/runtimes/agno/__init__.py +34 -0
  333. control_plane_api/worker/runtimes/agno/config.py +248 -0
  334. control_plane_api/worker/runtimes/agno/hooks.py +385 -0
  335. control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
  336. control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
  337. control_plane_api/worker/runtimes/agno/utils.py +163 -0
  338. control_plane_api/worker/runtimes/base.py +979 -0
  339. control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
  340. control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
  341. control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
  342. control_plane_api/worker/runtimes/claude_code/config.py +829 -0
  343. control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
  344. control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
  345. control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
  346. control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
  347. control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
  348. control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
  349. control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
  350. control_plane_api/worker/runtimes/factory.py +173 -0
  351. control_plane_api/worker/runtimes/model_utils.py +107 -0
  352. control_plane_api/worker/runtimes/validation.py +93 -0
  353. control_plane_api/worker/services/__init__.py +1 -0
  354. control_plane_api/worker/services/agent_communication_tools.py +908 -0
  355. control_plane_api/worker/services/agent_executor.py +485 -0
  356. control_plane_api/worker/services/agent_executor_v2.py +793 -0
  357. control_plane_api/worker/services/analytics_collector.py +457 -0
  358. control_plane_api/worker/services/analytics_service.py +464 -0
  359. control_plane_api/worker/services/approval_tools.py +310 -0
  360. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  361. control_plane_api/worker/services/cancellation_manager.py +177 -0
  362. control_plane_api/worker/services/code_ingestion_tools.py +465 -0
  363. control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
  364. control_plane_api/worker/services/data_visualization.py +834 -0
  365. control_plane_api/worker/services/event_publisher.py +531 -0
  366. control_plane_api/worker/services/jira_tools.py +257 -0
  367. control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
  368. control_plane_api/worker/services/runtime_analytics.py +328 -0
  369. control_plane_api/worker/services/session_service.py +365 -0
  370. control_plane_api/worker/services/skill_context_enhancement.py +181 -0
  371. control_plane_api/worker/services/skill_factory.py +471 -0
  372. control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
  373. control_plane_api/worker/services/team_executor.py +715 -0
  374. control_plane_api/worker/services/team_executor_v2.py +1866 -0
  375. control_plane_api/worker/services/tool_enforcement.py +254 -0
  376. control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
  377. control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
  378. control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
  379. control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
  380. control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
  381. control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
  382. control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
  383. control_plane_api/worker/services/workflow_executor/models.py +142 -0
  384. control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
  385. control_plane_api/worker/skills/__init__.py +12 -0
  386. control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
  387. control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
  388. control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
  389. control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
  390. control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
  391. control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
  392. control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
  393. control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
  394. control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
  395. control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
  396. control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
  397. control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
  398. control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
  399. control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
  400. control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
  401. control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
  402. control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
  403. control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
  404. control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
  405. control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
  406. control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
  407. control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
  408. control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
  409. control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
  410. control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
  411. control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
  412. control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
  413. control_plane_api/worker/skills/loaders/__init__.py +5 -0
  414. control_plane_api/worker/skills/loaders/base.py +23 -0
  415. control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
  416. control_plane_api/worker/skills/registry.py +208 -0
  417. control_plane_api/worker/tests/__init__.py +1 -0
  418. control_plane_api/worker/tests/conftest.py +12 -0
  419. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  420. control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
  421. control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
  422. control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
  423. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  424. control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
  425. control_plane_api/worker/tests/integration/__init__.py +0 -0
  426. control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
  427. control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
  428. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  429. control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
  430. control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
  431. control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
  432. control_plane_api/worker/tests/unit/__init__.py +0 -0
  433. control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
  434. control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
  435. control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
  436. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  437. control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
  438. control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
  439. control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
  440. control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
  441. control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
  442. control_plane_api/worker/utils/__init__.py +1 -0
  443. control_plane_api/worker/utils/chunk_batcher.py +330 -0
  444. control_plane_api/worker/utils/environment.py +65 -0
  445. control_plane_api/worker/utils/error_publisher.py +260 -0
  446. control_plane_api/worker/utils/event_batcher.py +256 -0
  447. control_plane_api/worker/utils/logging_config.py +335 -0
  448. control_plane_api/worker/utils/logging_helper.py +326 -0
  449. control_plane_api/worker/utils/parameter_validator.py +120 -0
  450. control_plane_api/worker/utils/retry_utils.py +60 -0
  451. control_plane_api/worker/utils/streaming_utils.py +665 -0
  452. control_plane_api/worker/utils/tool_validation.py +332 -0
  453. control_plane_api/worker/utils/workspace_manager.py +163 -0
  454. control_plane_api/worker/websocket_client.py +393 -0
  455. control_plane_api/worker/worker.py +1297 -0
  456. control_plane_api/worker/workflows/__init__.py +0 -0
  457. control_plane_api/worker/workflows/agent_execution.py +909 -0
  458. control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
  459. control_plane_api/worker/workflows/team_execution.py +611 -0
  460. kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
  461. kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
  462. kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
  463. kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
  464. kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
  465. kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
  466. scripts/__init__.py +1 -0
  467. scripts/migrations.py +39 -0
  468. scripts/seed_worker_queues.py +128 -0
  469. scripts/setup_agent_runtime.py +142 -0
  470. worker_internal/__init__.py +1 -0
  471. worker_internal/planner/__init__.py +1 -0
  472. worker_internal/planner/activities.py +1499 -0
  473. worker_internal/planner/agent_tools.py +197 -0
  474. worker_internal/planner/event_models.py +148 -0
  475. worker_internal/planner/event_publisher.py +67 -0
  476. worker_internal/planner/models.py +199 -0
  477. worker_internal/planner/retry_logic.py +134 -0
  478. worker_internal/planner/worker.py +300 -0
  479. worker_internal/planner/workflows.py +970 -0
@@ -0,0 +1,761 @@
1
+ """
2
+ Environments router - Clean API for environment management.
3
+
4
+ This router provides /environments endpoints that map to the environments table.
5
+ The naming "environments" is internal - externally we call them "environments".
6
+ """
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
9
+ from typing import List, Optional
10
+ from datetime import datetime
11
+ from pydantic import BaseModel, Field
12
+ from sqlalchemy.orm import Session, joinedload
13
+ from sqlalchemy.exc import IntegrityError
14
+ import structlog
15
+ import uuid
16
+ import os
17
+
18
+ from control_plane_api.app.middleware.auth import get_current_organization
19
+ from control_plane_api.app.database import get_db
20
+ from control_plane_api.app.models.environment import Environment
21
+ from control_plane_api.app.models.skill import Skill, SkillAssociation
22
+ from control_plane_api.app.lib.temporal_client import get_temporal_client
23
+
24
+ logger = structlog.get_logger()
25
+
26
+ router = APIRouter()
27
+
28
+
29
+ # Execution Environment Model (shared with agents/teams)
30
+ class ExecutionEnvironment(BaseModel):
31
+ """
32
+ Execution environment configuration - env vars, secrets, integration credentials, and MCP servers.
33
+
34
+ All string fields in mcp_servers support template syntax:
35
+ - {{variable}} - Simple variables
36
+ - {{.secret.name}} - Secrets from vault
37
+ - {{.env.VAR}} - Environment variables
38
+ """
39
+ env_vars: dict[str, str] = Field(default_factory=dict, description="Environment variables (key-value pairs)")
40
+ secrets: list[str] = Field(default_factory=list, description="Secret names from Kubiya vault")
41
+ integration_ids: list[str] = Field(default_factory=list, description="Integration UUIDs for delegated credentials")
42
+ mcp_servers: dict[str, dict] = Field(
43
+ default_factory=dict,
44
+ description="MCP (Model Context Protocol) server configurations. Supports stdio, HTTP, and SSE transports. All string values support template syntax."
45
+ )
46
+
47
+
48
+ # Pydantic schemas
49
+ class EnvironmentCreate(BaseModel):
50
+ name: str = Field(..., description="Environment name (e.g., default, production)", min_length=2, max_length=100)
51
+ display_name: str | None = Field(None, description="User-friendly display name")
52
+ description: str | None = Field(None, description="Environment description")
53
+ tags: List[str] = Field(default_factory=list, description="Tags for categorization")
54
+ settings: dict = Field(default_factory=dict, description="Environment settings")
55
+ execution_environment: ExecutionEnvironment | None = Field(None, description="Execution environment configuration")
56
+ # Note: priority and policy_ids not supported by environments table
57
+
58
+
59
+ class EnvironmentUpdate(BaseModel):
60
+ name: str | None = None
61
+ display_name: str | None = None
62
+ description: str | None = None
63
+ tags: List[str] | None = None
64
+ settings: dict | None = None
65
+ status: str | None = None
66
+ execution_environment: ExecutionEnvironment | None = None
67
+ # Note: priority and policy_ids not supported by environments table
68
+
69
+
70
+ class EnvironmentResponse(BaseModel):
71
+ id: str
72
+ organization_id: str
73
+ name: str
74
+ display_name: str | None
75
+ description: str | None
76
+ tags: List[str]
77
+ settings: dict
78
+ status: str
79
+ created_at: str
80
+ updated_at: str
81
+ created_by: str | None
82
+
83
+ # Temporal Cloud provisioning fields
84
+ worker_token: str | None = None
85
+ provisioning_workflow_id: str | None = None
86
+ provisioned_at: str | None = None
87
+ error_message: str | None = None
88
+ temporal_namespace_id: str | None = None
89
+
90
+ # Worker metrics (deprecated at environment level, use worker_queues)
91
+ active_workers: int = 0
92
+ idle_workers: int = 0
93
+ busy_workers: int = 0
94
+
95
+ # Skills (populated from associations)
96
+ skill_ids: List[str] = []
97
+ skills: List[dict] = []
98
+
99
+ # Execution environment configuration
100
+ execution_environment: dict = {}
101
+
102
+
103
+ class WorkerCommandResponse(BaseModel):
104
+ """Response with worker registration command"""
105
+ worker_token: str
106
+ environment_name: str
107
+ command: str
108
+ command_parts: dict
109
+ namespace_status: str
110
+ can_register: bool
111
+ provisioning_workflow_id: str | None = None
112
+
113
+
114
+ def ensure_default_environment(db: Session, organization: dict) -> Optional[Environment]:
115
+ """
116
+ Ensure the organization has a default environment.
117
+ Creates one if it doesn't exist.
118
+ """
119
+ try:
120
+ # Check if default environment exists
121
+ existing = db.query(Environment).filter(
122
+ Environment.organization_id == organization["id"],
123
+ Environment.name == "default"
124
+ ).first()
125
+
126
+ if existing:
127
+ return existing
128
+
129
+ # Create default environment
130
+ default_env = Environment(
131
+ id=uuid.uuid4(),
132
+ organization_id=organization["id"],
133
+ name="default",
134
+ display_name="Default Environment",
135
+ description="Default environment for all workers",
136
+ tags=[],
137
+ settings={},
138
+ status="active",
139
+ created_at=datetime.utcnow(),
140
+ updated_at=datetime.utcnow(),
141
+ created_by=organization.get("user_id"),
142
+ )
143
+
144
+ db.add(default_env)
145
+ db.commit()
146
+ db.refresh(default_env)
147
+
148
+ logger.info(
149
+ "default_environment_created",
150
+ environment_id=str(default_env.id),
151
+ org_id=organization["id"],
152
+ )
153
+ return default_env
154
+
155
+ except Exception as e:
156
+ db.rollback()
157
+ logger.error("ensure_default_environment_failed", error=str(e), org_id=organization.get("id"))
158
+ return None
159
+
160
+
161
+ def get_environment_skills(db: Session, organization_id: str, environment_id: str) -> tuple[List[str], List[dict]]:
162
+ """Get skills associated with an environment"""
163
+ try:
164
+ # Get associations with full skill data
165
+ associations = db.query(SkillAssociation).options(
166
+ joinedload(SkillAssociation.skill)
167
+ ).filter(
168
+ SkillAssociation.organization_id == organization_id,
169
+ SkillAssociation.entity_type == "environment",
170
+ SkillAssociation.entity_id == environment_id
171
+ ).all()
172
+
173
+ skill_ids = []
174
+ skills = []
175
+
176
+ for assoc in associations:
177
+ skill_data = assoc.skill
178
+ if skill_data:
179
+ skill_ids.append(str(skill_data.id))
180
+
181
+ # Merge configuration with override
182
+ config = skill_data.configuration or {}
183
+ override = assoc.configuration_override
184
+ if override:
185
+ config = {**config, **override}
186
+
187
+ skills.append({
188
+ "id": str(skill_data.id),
189
+ "name": skill_data.name,
190
+ "type": skill_data.skill_type,
191
+ "description": skill_data.description,
192
+ "enabled": skill_data.enabled if skill_data.enabled is not None else True,
193
+ "configuration": config,
194
+ })
195
+
196
+ return skill_ids, skills
197
+
198
+ except Exception as e:
199
+ logger.error("get_environment_skills_failed", error=str(e), environment_id=environment_id)
200
+ return [], []
201
+
202
+
203
+ @router.post("", response_model=EnvironmentResponse, status_code=status.HTTP_201_CREATED)
204
+ async def create_environment(
205
+ env_data: EnvironmentCreate,
206
+ request: Request,
207
+ organization: dict = Depends(get_current_organization),
208
+ db: Session = Depends(get_db),
209
+ ):
210
+ """
211
+ Create a new environment.
212
+
213
+ If this is the first environment for the organization, it will trigger
214
+ Temporal Cloud namespace provisioning workflow.
215
+ """
216
+ try:
217
+ # Check if environment name already exists
218
+ existing = db.query(Environment).filter(
219
+ Environment.organization_id == organization["id"],
220
+ Environment.name == env_data.name
221
+ ).first()
222
+
223
+ if existing:
224
+ raise HTTPException(
225
+ status_code=status.HTTP_409_CONFLICT,
226
+ detail=f"Environment with name '{env_data.name}' already exists"
227
+ )
228
+
229
+ # Check if this is the first environment
230
+ env_count = db.query(Environment).filter(
231
+ Environment.organization_id == organization["id"]
232
+ ).count()
233
+ is_first_env = env_count == 0
234
+
235
+ # Check if namespace already exists (temporal_namespaces table check)
236
+ # Note: We need to check if the table exists first
237
+ has_namespace = False
238
+ try:
239
+ from sqlalchemy import inspect
240
+ inspector = inspect(db.bind)
241
+ if 'temporal_namespaces' in inspector.get_table_names():
242
+ from sqlalchemy import text
243
+ result = db.execute(
244
+ text("SELECT COUNT(*) FROM temporal_namespaces WHERE organization_id = :org_id"),
245
+ {"org_id": organization["id"]}
246
+ )
247
+ has_namespace = result.scalar() > 0
248
+ except Exception:
249
+ pass
250
+
251
+ needs_provisioning = is_first_env and not has_namespace
252
+
253
+ # Set initial status
254
+ initial_status = "provisioning" if needs_provisioning else "ready"
255
+
256
+ env_obj = Environment(
257
+ id=uuid.uuid4(),
258
+ organization_id=organization["id"],
259
+ name=env_data.name,
260
+ display_name=env_data.display_name or env_data.name,
261
+ description=env_data.description,
262
+ tags=env_data.tags,
263
+ settings=env_data.settings,
264
+ status=initial_status,
265
+ created_at=datetime.utcnow(),
266
+ updated_at=datetime.utcnow(),
267
+ created_by=organization.get("user_id"),
268
+ worker_token=uuid.uuid4(),
269
+ execution_environment=env_data.execution_environment.model_dump() if env_data.execution_environment else {},
270
+ )
271
+
272
+ db.add(env_obj)
273
+ db.commit()
274
+ db.refresh(env_obj)
275
+
276
+ env_id = str(env_obj.id)
277
+
278
+ # Trigger namespace provisioning if needed
279
+ if needs_provisioning:
280
+ try:
281
+ from control_plane_api.app.workflows.namespace_provisioning import (
282
+ ProvisionTemporalNamespaceWorkflow,
283
+ ProvisionNamespaceInput,
284
+ )
285
+
286
+ temporal_client = await get_temporal_client()
287
+ account_id = os.environ.get("TEMPORAL_CLOUD_ACCOUNT_ID", "default-account")
288
+
289
+ workflow_input = ProvisionNamespaceInput(
290
+ organization_id=organization["id"],
291
+ organization_name=organization.get("name", organization["id"]),
292
+ task_queue_id=env_id,
293
+ account_id=account_id,
294
+ region=os.environ.get("TEMPORAL_CLOUD_REGION", "aws-us-east-1"),
295
+ )
296
+
297
+ workflow_handle = await temporal_client.start_workflow(
298
+ ProvisionTemporalNamespaceWorkflow.run,
299
+ workflow_input,
300
+ id=f"provision-namespace-{organization['id']}",
301
+ task_queue="agent-control-plane",
302
+ )
303
+
304
+ env_obj.provisioning_workflow_id = workflow_handle.id
305
+ env_obj.updated_at = datetime.utcnow()
306
+ db.commit()
307
+ db.refresh(env_obj)
308
+
309
+ logger.info(
310
+ "namespace_provisioning_workflow_started",
311
+ workflow_id=workflow_handle.id,
312
+ environment_id=env_id,
313
+ org_id=organization["id"],
314
+ )
315
+ except Exception as e:
316
+ logger.error(
317
+ "failed_to_start_provisioning_workflow",
318
+ error=str(e),
319
+ environment_id=env_id,
320
+ org_id=organization["id"],
321
+ )
322
+ env_obj.status = "error"
323
+ env_obj.error_message = f"Failed to start provisioning: {str(e)}"
324
+ env_obj.updated_at = datetime.utcnow()
325
+ db.commit()
326
+ db.refresh(env_obj)
327
+
328
+ logger.info(
329
+ "environment_created",
330
+ environment_id=env_id,
331
+ environment_name=env_obj.name,
332
+ org_id=organization["id"],
333
+ needs_provisioning=needs_provisioning,
334
+ )
335
+
336
+ # Convert SQLAlchemy model to dict
337
+ environment_dict = {
338
+ "id": str(env_obj.id),
339
+ "organization_id": env_obj.organization_id,
340
+ "name": env_obj.name,
341
+ "display_name": env_obj.display_name,
342
+ "description": env_obj.description,
343
+ "tags": env_obj.tags or [],
344
+ "settings": env_obj.settings or {},
345
+ "status": env_obj.status,
346
+ "created_at": env_obj.created_at.isoformat() if env_obj.created_at else None,
347
+ "updated_at": env_obj.updated_at.isoformat() if env_obj.updated_at else None,
348
+ "created_by": env_obj.created_by,
349
+ "worker_token": str(env_obj.worker_token) if env_obj.worker_token else None,
350
+ "provisioning_workflow_id": env_obj.provisioning_workflow_id,
351
+ "provisioned_at": env_obj.provisioned_at.isoformat() if env_obj.provisioned_at else None,
352
+ "error_message": env_obj.error_message,
353
+ "temporal_namespace_id": str(env_obj.temporal_namespace_id) if env_obj.temporal_namespace_id else None,
354
+ "execution_environment": env_obj.execution_environment or {},
355
+ }
356
+
357
+ return EnvironmentResponse(
358
+ **environment_dict,
359
+ active_workers=0,
360
+ idle_workers=0,
361
+ busy_workers=0,
362
+ skill_ids=[],
363
+ skills=[],
364
+ )
365
+
366
+ except HTTPException:
367
+ raise
368
+ except Exception as e:
369
+ db.rollback()
370
+ logger.error("environment_creation_failed", error=str(e), org_id=organization["id"])
371
+ raise HTTPException(
372
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
373
+ detail=f"Failed to create environment: {str(e)}"
374
+ )
375
+
376
+
377
+ @router.get("", response_model=List[EnvironmentResponse])
378
+ async def list_environments(
379
+ request: Request,
380
+ status_filter: str | None = None,
381
+ organization: dict = Depends(get_current_organization),
382
+ db: Session = Depends(get_db),
383
+ ):
384
+ """List all environments in the organization"""
385
+ try:
386
+ # Ensure default environment exists
387
+ ensure_default_environment(db, organization)
388
+
389
+ # Query environments
390
+ query = db.query(Environment).filter(
391
+ Environment.organization_id == organization["id"]
392
+ )
393
+
394
+ if status_filter:
395
+ query = query.filter(Environment.status == status_filter)
396
+
397
+ query = query.order_by(Environment.created_at.asc())
398
+ env_objects = query.all()
399
+
400
+ if not env_objects:
401
+ return []
402
+
403
+ # BATCH FETCH: Get all skills for all environments in one query
404
+ environment_ids = [str(env.id) for env in env_objects]
405
+ skills_associations = db.query(SkillAssociation).options(
406
+ joinedload(SkillAssociation.skill)
407
+ ).filter(
408
+ SkillAssociation.organization_id == organization["id"],
409
+ SkillAssociation.entity_type == "environment",
410
+ SkillAssociation.entity_id.in_(environment_ids)
411
+ ).all()
412
+
413
+ # Group skills by environment_id
414
+ skills_by_env = {}
415
+ for assoc in skills_associations:
416
+ env_id = str(assoc.entity_id)
417
+ skill_data = assoc.skill
418
+ if skill_data:
419
+ if env_id not in skills_by_env:
420
+ skills_by_env[env_id] = {"ids": [], "data": []}
421
+
422
+ # Merge configuration with override
423
+ config = skill_data.configuration or {}
424
+ override = assoc.configuration_override
425
+ if override:
426
+ config = {**config, **override}
427
+
428
+ skills_by_env[env_id]["ids"].append(str(skill_data.id))
429
+ skills_by_env[env_id]["data"].append({
430
+ "id": str(skill_data.id),
431
+ "name": skill_data.name,
432
+ "type": skill_data.skill_type,
433
+ "description": skill_data.description,
434
+ "enabled": skill_data.enabled if skill_data.enabled is not None else True,
435
+ "configuration": config,
436
+ })
437
+
438
+ # Build environment responses
439
+ environments = []
440
+ for env in env_objects:
441
+ env_id = str(env.id)
442
+ env_skills = skills_by_env.get(env_id, {"ids": [], "data": []})
443
+
444
+ env_dict = {
445
+ "id": env_id,
446
+ "organization_id": env.organization_id,
447
+ "name": env.name,
448
+ "display_name": env.display_name,
449
+ "description": env.description,
450
+ "tags": env.tags or [],
451
+ "settings": env.settings or {},
452
+ "status": env.status,
453
+ "created_at": env.created_at.isoformat() if env.created_at else None,
454
+ "updated_at": env.updated_at.isoformat() if env.updated_at else None,
455
+ "created_by": env.created_by,
456
+ "worker_token": str(env.worker_token) if env.worker_token else None,
457
+ "provisioning_workflow_id": env.provisioning_workflow_id,
458
+ "provisioned_at": env.provisioned_at.isoformat() if env.provisioned_at else None,
459
+ "error_message": env.error_message,
460
+ "temporal_namespace_id": str(env.temporal_namespace_id) if env.temporal_namespace_id else None,
461
+ "execution_environment": env.execution_environment or {},
462
+ }
463
+
464
+ environments.append(
465
+ EnvironmentResponse(
466
+ **env_dict,
467
+ active_workers=0,
468
+ idle_workers=0,
469
+ busy_workers=0,
470
+ skill_ids=env_skills["ids"],
471
+ skills=env_skills["data"],
472
+ )
473
+ )
474
+
475
+ logger.info(
476
+ "environments_listed",
477
+ count=len(environments),
478
+ org_id=organization["id"],
479
+ )
480
+
481
+ return environments
482
+
483
+ except Exception as e:
484
+ logger.error("environments_list_failed", error=str(e), org_id=organization["id"])
485
+ raise HTTPException(
486
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
487
+ detail=f"Failed to list environments: {str(e)}"
488
+ )
489
+
490
+
491
+ @router.get("/{environment_id}", response_model=EnvironmentResponse)
492
+ async def get_environment(
493
+ environment_id: str,
494
+ request: Request,
495
+ organization: dict = Depends(get_current_organization),
496
+ db: Session = Depends(get_db),
497
+ ):
498
+ """Get a specific environment by ID"""
499
+ try:
500
+ env = db.query(Environment).filter(
501
+ Environment.id == environment_id,
502
+ Environment.organization_id == organization["id"]
503
+ ).first()
504
+
505
+ if not env:
506
+ raise HTTPException(status_code=404, detail="Environment not found")
507
+
508
+ # Get skills
509
+ skill_ids, skills = get_environment_skills(db, organization["id"], environment_id)
510
+
511
+ env_dict = {
512
+ "id": str(env.id),
513
+ "organization_id": env.organization_id,
514
+ "name": env.name,
515
+ "display_name": env.display_name,
516
+ "description": env.description,
517
+ "tags": env.tags or [],
518
+ "settings": env.settings or {},
519
+ "status": env.status,
520
+ "created_at": env.created_at.isoformat() if env.created_at else None,
521
+ "updated_at": env.updated_at.isoformat() if env.updated_at else None,
522
+ "created_by": env.created_by,
523
+ "worker_token": str(env.worker_token) if env.worker_token else None,
524
+ "provisioning_workflow_id": env.provisioning_workflow_id,
525
+ "provisioned_at": env.provisioned_at.isoformat() if env.provisioned_at else None,
526
+ "error_message": env.error_message,
527
+ "temporal_namespace_id": str(env.temporal_namespace_id) if env.temporal_namespace_id else None,
528
+ "execution_environment": env.execution_environment or {},
529
+ }
530
+
531
+ return EnvironmentResponse(
532
+ **env_dict,
533
+ active_workers=0,
534
+ idle_workers=0,
535
+ busy_workers=0,
536
+ skill_ids=skill_ids,
537
+ skills=skills,
538
+ )
539
+
540
+ except HTTPException:
541
+ raise
542
+ except Exception as e:
543
+ logger.error("environment_get_failed", error=str(e), environment_id=environment_id)
544
+ raise HTTPException(
545
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
546
+ detail=f"Failed to get environment: {str(e)}"
547
+ )
548
+
549
+
550
+ @router.patch("/{environment_id}", response_model=EnvironmentResponse)
551
+ async def update_environment(
552
+ environment_id: str,
553
+ env_data: EnvironmentUpdate,
554
+ request: Request,
555
+ organization: dict = Depends(get_current_organization),
556
+ db: Session = Depends(get_db),
557
+ ):
558
+ """Update an environment"""
559
+ try:
560
+ # Check if environment exists
561
+ env = db.query(Environment).filter(
562
+ Environment.id == environment_id,
563
+ Environment.organization_id == organization["id"]
564
+ ).first()
565
+
566
+ if not env:
567
+ raise HTTPException(status_code=404, detail="Environment not found")
568
+
569
+ # Build update dict
570
+ update_data = env_data.model_dump(exclude_unset=True)
571
+
572
+ # Convert execution_environment Pydantic model to dict if present
573
+ if "execution_environment" in update_data and update_data["execution_environment"]:
574
+ if hasattr(update_data["execution_environment"], "model_dump"):
575
+ update_data["execution_environment"] = update_data["execution_environment"].model_dump()
576
+
577
+ # Update fields
578
+ for field, value in update_data.items():
579
+ if hasattr(env, field):
580
+ setattr(env, field, value)
581
+
582
+ env.updated_at = datetime.utcnow()
583
+
584
+ db.commit()
585
+ db.refresh(env)
586
+
587
+ # Get skills
588
+ skill_ids, skills = get_environment_skills(db, organization["id"], environment_id)
589
+
590
+ logger.info(
591
+ "environment_updated",
592
+ environment_id=environment_id,
593
+ org_id=organization["id"],
594
+ )
595
+
596
+ env_dict = {
597
+ "id": str(env.id),
598
+ "organization_id": env.organization_id,
599
+ "name": env.name,
600
+ "display_name": env.display_name,
601
+ "description": env.description,
602
+ "tags": env.tags or [],
603
+ "settings": env.settings or {},
604
+ "status": env.status,
605
+ "created_at": env.created_at.isoformat() if env.created_at else None,
606
+ "updated_at": env.updated_at.isoformat() if env.updated_at else None,
607
+ "created_by": env.created_by,
608
+ "worker_token": str(env.worker_token) if env.worker_token else None,
609
+ "provisioning_workflow_id": env.provisioning_workflow_id,
610
+ "provisioned_at": env.provisioned_at.isoformat() if env.provisioned_at else None,
611
+ "error_message": env.error_message,
612
+ "temporal_namespace_id": str(env.temporal_namespace_id) if env.temporal_namespace_id else None,
613
+ "execution_environment": env.execution_environment or {},
614
+ }
615
+
616
+ return EnvironmentResponse(
617
+ **env_dict,
618
+ active_workers=0,
619
+ idle_workers=0,
620
+ busy_workers=0,
621
+ skill_ids=skill_ids,
622
+ skills=skills,
623
+ )
624
+
625
+ except HTTPException:
626
+ raise
627
+ except Exception as e:
628
+ db.rollback()
629
+ logger.error("environment_update_failed", error=str(e), environment_id=environment_id)
630
+ raise HTTPException(
631
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
632
+ detail=f"Failed to update environment: {str(e)}"
633
+ )
634
+
635
+
636
+ @router.delete("/{environment_id}", status_code=status.HTTP_204_NO_CONTENT)
637
+ async def delete_environment(
638
+ environment_id: str,
639
+ request: Request,
640
+ organization: dict = Depends(get_current_organization),
641
+ db: Session = Depends(get_db),
642
+ ):
643
+ """Delete an environment"""
644
+ try:
645
+ # Prevent deleting default environment
646
+ env = db.query(Environment).filter(
647
+ Environment.id == environment_id,
648
+ Environment.organization_id == organization["id"]
649
+ ).first()
650
+
651
+ if not env:
652
+ raise HTTPException(status_code=404, detail="Environment not found")
653
+
654
+ if env.name == "default":
655
+ raise HTTPException(
656
+ status_code=status.HTTP_400_BAD_REQUEST,
657
+ detail="Cannot delete the default environment"
658
+ )
659
+
660
+ db.delete(env)
661
+ db.commit()
662
+
663
+ logger.info("environment_deleted", environment_id=environment_id, org_id=organization["id"])
664
+
665
+ return None
666
+
667
+ except HTTPException:
668
+ raise
669
+ except Exception as e:
670
+ db.rollback()
671
+ logger.error("environment_delete_failed", error=str(e), environment_id=environment_id)
672
+ raise HTTPException(
673
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
674
+ detail=f"Failed to delete environment: {str(e)}"
675
+ )
676
+
677
+
678
+ @router.get("/{environment_id}/worker-command", response_model=WorkerCommandResponse)
679
+ async def get_worker_registration_command(
680
+ environment_id: str,
681
+ request: Request,
682
+ organization: dict = Depends(get_current_organization),
683
+ db: Session = Depends(get_db),
684
+ ):
685
+ """
686
+ Get the worker registration command for an environment.
687
+
688
+ Returns the kubiya worker start command with the worker token.
689
+ """
690
+ try:
691
+ # Get environment
692
+ env = db.query(Environment).filter(
693
+ Environment.id == environment_id,
694
+ Environment.organization_id == organization["id"]
695
+ ).first()
696
+
697
+ if not env:
698
+ raise HTTPException(status_code=404, detail="Environment not found")
699
+
700
+ worker_token = str(env.worker_token) if env.worker_token else None
701
+
702
+ # Generate worker_token if it doesn't exist
703
+ if not worker_token:
704
+ env.worker_token = uuid.uuid4()
705
+ env.updated_at = datetime.utcnow()
706
+ db.commit()
707
+ db.refresh(env)
708
+ worker_token = str(env.worker_token)
709
+
710
+ logger.info(
711
+ "worker_token_generated",
712
+ environment_id=environment_id,
713
+ org_id=organization["id"],
714
+ )
715
+
716
+ environment_name = env.name
717
+ namespace_status = env.status or "unknown"
718
+ provisioning_workflow_id = env.provisioning_workflow_id
719
+
720
+ # Check if namespace is ready
721
+ can_register = namespace_status in ["ready", "active"]
722
+
723
+ # Build command
724
+ command = f"kubiya worker start --token {worker_token} --environment {environment_name}"
725
+
726
+ command_parts = {
727
+ "binary": "kubiya",
728
+ "subcommand": "worker start",
729
+ "flags": {
730
+ "--token": worker_token,
731
+ "--environment": environment_name,
732
+ },
733
+ }
734
+
735
+ logger.info(
736
+ "worker_command_retrieved",
737
+ environment_id=environment_id,
738
+ can_register=can_register,
739
+ status=namespace_status,
740
+ org_id=organization["id"],
741
+ )
742
+
743
+ return WorkerCommandResponse(
744
+ worker_token=worker_token,
745
+ environment_name=environment_name,
746
+ command=command,
747
+ command_parts=command_parts,
748
+ namespace_status=namespace_status,
749
+ can_register=can_register,
750
+ provisioning_workflow_id=provisioning_workflow_id,
751
+ )
752
+
753
+ except HTTPException:
754
+ raise
755
+ except Exception as e:
756
+ db.rollback()
757
+ logger.error("worker_command_get_failed", error=str(e), environment_id=environment_id)
758
+ raise HTTPException(
759
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
760
+ detail=f"Failed to get worker command: {str(e)}"
761
+ )