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,987 @@
1
+ """
2
+ Projects router - Jira-style multi-project management.
3
+
4
+ This router handles project CRUD operations and manages associations
5
+ between projects, agents, and teams.
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
+ import structlog
13
+ import uuid
14
+
15
+ from control_plane_api.app.middleware.auth import get_current_organization
16
+ from control_plane_api.app.database import get_db
17
+ from sqlalchemy.orm import Session, joinedload
18
+ from sqlalchemy import desc, func
19
+ from control_plane_api.app.models.project import Project
20
+ from control_plane_api.app.models.project_management import ProjectAgent, ProjectTeam
21
+ from control_plane_api.app.models.agent import Agent
22
+ from control_plane_api.app.models.team import Team
23
+
24
+ logger = structlog.get_logger()
25
+
26
+ router = APIRouter()
27
+
28
+
29
+ # Pydantic schemas
30
+ class ProjectCreate(BaseModel):
31
+ name: str = Field(..., description="Project name")
32
+ key: str = Field(..., description="Short project key (e.g., JIRA, PROJ)", min_length=2, max_length=50)
33
+ description: str | None = Field(None, description="Project description")
34
+ goals: str | None = Field(None, description="Project goals and objectives")
35
+ settings: dict = Field(default_factory=dict, description="Project settings")
36
+ visibility: str = Field("private", description="Project visibility: private or org")
37
+ restrict_to_environment: bool = Field(False, description="Restrict to specific runners/environment")
38
+ policy_ids: List[str] = Field(default_factory=list, description="List of OPA policy IDs for access control")
39
+ default_model: str | None = Field(None, description="Default LLM model for this project")
40
+
41
+
42
+ class ProjectUpdate(BaseModel):
43
+ name: str | None = None
44
+ key: str | None = None
45
+ description: str | None = None
46
+ goals: str | None = None
47
+ settings: dict | None = None
48
+ status: str | None = None
49
+ visibility: str | None = None
50
+ restrict_to_environment: bool | None = None
51
+ policy_ids: List[str] | None = None
52
+ default_model: str | None = None
53
+
54
+
55
+ class ProjectResponse(BaseModel):
56
+ id: str
57
+ organization_id: str
58
+ name: str
59
+ key: str
60
+ description: str | None
61
+ goals: str | None
62
+ settings: dict
63
+ status: str
64
+ visibility: str
65
+ owner_id: str | None
66
+ owner_email: str | None
67
+ restrict_to_environment: bool = False
68
+ policy_ids: List[str] = []
69
+ default_model: str | None = None
70
+ created_at: str
71
+ updated_at: str
72
+ archived_at: str | None
73
+
74
+ # Counts
75
+ agent_count: int = 0
76
+ team_count: int = 0
77
+
78
+
79
+ class ProjectAgentAdd(BaseModel):
80
+ agent_id: str = Field(..., description="Agent UUID to add to project")
81
+ role: str | None = Field(None, description="Agent role in project")
82
+
83
+
84
+ class ProjectTeamAdd(BaseModel):
85
+ team_id: str = Field(..., description="Team UUID to add to project")
86
+ role: str | None = Field(None, description="Team role in project")
87
+
88
+
89
+ def ensure_default_project(db: Session, organization: dict) -> Optional[dict]:
90
+ """
91
+ Ensure the organization has a default project.
92
+ Creates one if it doesn't exist.
93
+
94
+ Returns the default project or None if creation failed.
95
+ """
96
+ try:
97
+ # Check if default project exists
98
+ existing = db.query(Project).filter(
99
+ Project.organization_id == organization["id"],
100
+ Project.key == "DEFAULT"
101
+ ).first()
102
+
103
+ if existing:
104
+ return {
105
+ "id": str(existing.id),
106
+ "organization_id": str(existing.organization_id),
107
+ "name": existing.name,
108
+ "key": existing.key,
109
+ "description": existing.description,
110
+ "settings": existing.settings or {},
111
+ "status": existing.status,
112
+ "visibility": existing.visibility,
113
+ "owner_id": existing.owner_id,
114
+ "owner_email": existing.owner_email,
115
+ "created_at": existing.created_at.isoformat() if existing.created_at else None,
116
+ "updated_at": existing.updated_at.isoformat() if existing.updated_at else None,
117
+ "archived_at": existing.archived_at.isoformat() if existing.archived_at else None,
118
+ }
119
+
120
+ # Create default project
121
+ now = datetime.utcnow()
122
+
123
+ default_project = Project(
124
+ organization_id=organization["id"],
125
+ name="Default",
126
+ key="DEFAULT",
127
+ description="Default project for agents and teams",
128
+ settings={
129
+ "policy_ids": [],
130
+ "default_model": None,
131
+ "goals": None,
132
+ "restrict_to_environment": False
133
+ },
134
+ status="active",
135
+ visibility="org",
136
+ owner_id=organization.get("user_id"),
137
+ owner_email=organization.get("user_email"),
138
+ created_at=now,
139
+ updated_at=now,
140
+ )
141
+
142
+ db.add(default_project)
143
+ db.commit()
144
+ db.refresh(default_project)
145
+
146
+ logger.info(
147
+ "default_project_created",
148
+ project_id=str(default_project.id),
149
+ org_id=organization["id"],
150
+ )
151
+
152
+ return {
153
+ "id": str(default_project.id),
154
+ "organization_id": str(default_project.organization_id),
155
+ "name": default_project.name,
156
+ "key": default_project.key,
157
+ "description": default_project.description,
158
+ "settings": default_project.settings or {},
159
+ "status": default_project.status,
160
+ "visibility": default_project.visibility,
161
+ "owner_id": default_project.owner_id,
162
+ "owner_email": default_project.owner_email,
163
+ "created_at": default_project.created_at.isoformat() if default_project.created_at else None,
164
+ "updated_at": default_project.updated_at.isoformat() if default_project.updated_at else None,
165
+ "archived_at": default_project.archived_at.isoformat() if default_project.archived_at else None,
166
+ }
167
+
168
+ except Exception as e:
169
+ logger.error("ensure_default_project_failed", error=str(e), org_id=organization.get("id"))
170
+ db.rollback()
171
+ return None
172
+
173
+
174
+ def get_default_project_id(db: Session, organization: dict) -> Optional[str]:
175
+ """
176
+ Get the default project ID for an organization.
177
+ Creates the default project if it doesn't exist.
178
+
179
+ Returns the project ID or None if creation failed.
180
+ """
181
+ project = ensure_default_project(db, organization)
182
+ return project["id"] if project else None
183
+
184
+
185
+ @router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
186
+ async def create_project(
187
+ project_data: ProjectCreate,
188
+ request: Request,
189
+ organization: dict = Depends(get_current_organization),
190
+ db: Session = Depends(get_db),
191
+ ):
192
+ """Create a new project"""
193
+ try:
194
+ # Check if key already exists for this organization
195
+ existing = db.query(Project).filter(
196
+ Project.organization_id == organization["id"],
197
+ Project.key == project_data.key.upper()
198
+ ).first()
199
+
200
+ if existing:
201
+ raise HTTPException(
202
+ status_code=status.HTTP_409_CONFLICT,
203
+ detail=f"Project with key '{project_data.key.upper()}' already exists"
204
+ )
205
+
206
+ now = datetime.utcnow()
207
+
208
+ project = Project(
209
+ organization_id=organization["id"],
210
+ name=project_data.name,
211
+ key=project_data.key.upper(),
212
+ description=project_data.description,
213
+ # Store policy_ids, default_model, goals, and restrict_to_environment in settings JSON field
214
+ settings={
215
+ **project_data.settings,
216
+ "policy_ids": project_data.policy_ids,
217
+ "default_model": project_data.default_model,
218
+ "goals": project_data.goals,
219
+ "restrict_to_environment": project_data.restrict_to_environment
220
+ },
221
+ status="active",
222
+ visibility=project_data.visibility,
223
+ owner_id=organization.get("user_id"),
224
+ owner_email=organization.get("user_email"),
225
+ created_at=now,
226
+ updated_at=now,
227
+ )
228
+
229
+ db.add(project)
230
+ db.commit()
231
+ db.refresh(project)
232
+
233
+ logger.info(
234
+ "project_created",
235
+ project_id=str(project.id),
236
+ project_key=project.key,
237
+ org_id=organization["id"],
238
+ )
239
+
240
+ # Extract policy_ids, default_model, goals, and restrict_to_environment from settings for response
241
+ settings = project.settings or {}
242
+ policy_ids = settings.get("policy_ids", [])
243
+ default_model = settings.get("default_model")
244
+ goals = settings.get("goals")
245
+ restrict_to_environment = settings.get("restrict_to_environment", False)
246
+
247
+ return ProjectResponse(
248
+ id=str(project.id),
249
+ organization_id=str(project.organization_id),
250
+ name=project.name,
251
+ key=project.key,
252
+ description=project.description,
253
+ goals=goals,
254
+ settings=settings,
255
+ status=project.status,
256
+ visibility=project.visibility,
257
+ owner_id=project.owner_id,
258
+ owner_email=project.owner_email,
259
+ restrict_to_environment=restrict_to_environment,
260
+ policy_ids=policy_ids,
261
+ default_model=default_model,
262
+ created_at=project.created_at.isoformat() if project.created_at else None,
263
+ updated_at=project.updated_at.isoformat() if project.updated_at else None,
264
+ archived_at=project.archived_at.isoformat() if project.archived_at else None,
265
+ agent_count=0,
266
+ team_count=0,
267
+ )
268
+
269
+ except HTTPException:
270
+ raise
271
+ except Exception as e:
272
+ logger.error("project_creation_failed", error=str(e), org_id=organization["id"])
273
+ db.rollback()
274
+ raise HTTPException(
275
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
276
+ detail=f"Failed to create project: {str(e)}"
277
+ )
278
+
279
+
280
+ @router.get("/default", response_model=ProjectResponse)
281
+ async def get_default_project(
282
+ request: Request,
283
+ organization: dict = Depends(get_current_organization),
284
+ db: Session = Depends(get_db),
285
+ ):
286
+ """Get the default project for the organization (creates if doesn't exist)"""
287
+ try:
288
+ # Ensure default project exists
289
+ default_project = ensure_default_project(db, organization)
290
+
291
+ if not default_project:
292
+ raise HTTPException(
293
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
294
+ detail="Failed to get or create default project"
295
+ )
296
+
297
+ # Get counts for the default project
298
+ agent_count = db.query(func.count(ProjectAgent.id)).filter(
299
+ ProjectAgent.project_id == default_project["id"]
300
+ ).scalar() or 0
301
+
302
+ team_count = db.query(func.count(ProjectTeam.id)).filter(
303
+ ProjectTeam.project_id == default_project["id"]
304
+ ).scalar() or 0
305
+
306
+ # Extract settings fields
307
+ settings = default_project.get("settings", {})
308
+ policy_ids = settings.get("policy_ids", [])
309
+ default_model = settings.get("default_model")
310
+ goals = settings.get("goals")
311
+ restrict_to_environment = settings.get("restrict_to_environment", False)
312
+
313
+ return ProjectResponse(
314
+ id=default_project["id"],
315
+ organization_id=default_project["organization_id"],
316
+ name=default_project["name"],
317
+ key=default_project["key"],
318
+ description=default_project["description"],
319
+ goals=goals,
320
+ settings=settings,
321
+ status=default_project["status"],
322
+ visibility=default_project["visibility"],
323
+ owner_id=default_project["owner_id"],
324
+ owner_email=default_project["owner_email"],
325
+ restrict_to_environment=restrict_to_environment,
326
+ policy_ids=policy_ids,
327
+ default_model=default_model,
328
+ created_at=default_project["created_at"],
329
+ updated_at=default_project["updated_at"],
330
+ archived_at=default_project.get("archived_at"),
331
+ agent_count=agent_count,
332
+ team_count=team_count,
333
+ )
334
+
335
+ except HTTPException:
336
+ raise
337
+ except Exception as e:
338
+ logger.error("get_default_project_failed", error=str(e), org_id=organization["id"])
339
+ raise HTTPException(
340
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
341
+ detail=f"Failed to get default project: {str(e)}"
342
+ )
343
+
344
+
345
+ @router.get("", response_model=List[ProjectResponse])
346
+ async def list_projects(
347
+ request: Request,
348
+ status_filter: str | None = None,
349
+ organization: dict = Depends(get_current_organization),
350
+ db: Session = Depends(get_db),
351
+ ):
352
+ """List all projects in the organization"""
353
+ try:
354
+ # Ensure default project exists for this organization
355
+ ensure_default_project(db, organization)
356
+
357
+ # Query projects
358
+ query = db.query(Project).filter(
359
+ Project.organization_id == organization["id"]
360
+ )
361
+
362
+ if status_filter:
363
+ query = query.filter(Project.status == status_filter)
364
+
365
+ query = query.order_by(desc(Project.created_at))
366
+ projects_objs = query.all()
367
+
368
+ if not projects_objs:
369
+ return []
370
+
371
+ # Batch fetch all agent counts in one query
372
+ project_ids = [str(project.id) for project in projects_objs]
373
+ agent_counts = db.query(
374
+ ProjectAgent.project_id,
375
+ func.count(ProjectAgent.id).label("count")
376
+ ).filter(
377
+ ProjectAgent.project_id.in_(project_ids)
378
+ ).group_by(ProjectAgent.project_id).all()
379
+
380
+ # Build agent count map
381
+ agent_count_map = {str(pc.project_id): pc.count for pc in agent_counts}
382
+
383
+ # Batch fetch all team counts in one query
384
+ team_counts = db.query(
385
+ ProjectTeam.project_id,
386
+ func.count(ProjectTeam.id).label("count")
387
+ ).filter(
388
+ ProjectTeam.project_id.in_(project_ids)
389
+ ).group_by(ProjectTeam.project_id).all()
390
+
391
+ # Build team count map
392
+ team_count_map = {str(tc.project_id): tc.count for tc in team_counts}
393
+
394
+ # Build response with pre-fetched counts
395
+ projects = []
396
+ for project in projects_objs:
397
+ # Extract policy_ids, default_model, goals, and restrict_to_environment from settings for response
398
+ settings = project.settings or {}
399
+ policy_ids = settings.get("policy_ids", [])
400
+ default_model = settings.get("default_model")
401
+ goals = settings.get("goals")
402
+ restrict_to_environment = settings.get("restrict_to_environment", False)
403
+
404
+ projects.append(
405
+ ProjectResponse(
406
+ id=str(project.id),
407
+ organization_id=str(project.organization_id),
408
+ name=project.name,
409
+ key=project.key,
410
+ description=project.description,
411
+ goals=goals,
412
+ settings=settings,
413
+ status=project.status,
414
+ visibility=project.visibility,
415
+ owner_id=project.owner_id,
416
+ owner_email=project.owner_email,
417
+ restrict_to_environment=restrict_to_environment,
418
+ policy_ids=policy_ids,
419
+ default_model=default_model,
420
+ created_at=project.created_at.isoformat() if project.created_at else None,
421
+ updated_at=project.updated_at.isoformat() if project.updated_at else None,
422
+ archived_at=project.archived_at.isoformat() if project.archived_at else None,
423
+ agent_count=agent_count_map.get(str(project.id), 0),
424
+ team_count=team_count_map.get(str(project.id), 0),
425
+ )
426
+ )
427
+
428
+ logger.info(
429
+ "projects_listed",
430
+ count=len(projects),
431
+ org_id=organization["id"],
432
+ )
433
+
434
+ return projects
435
+
436
+ except Exception as e:
437
+ logger.error("projects_list_failed", error=str(e), org_id=organization["id"])
438
+ raise HTTPException(
439
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
440
+ detail=f"Failed to list projects: {str(e)}"
441
+ )
442
+
443
+
444
+ @router.get("/{project_id}", response_model=ProjectResponse)
445
+ async def get_project(
446
+ project_id: str,
447
+ request: Request,
448
+ organization: dict = Depends(get_current_organization),
449
+ db: Session = Depends(get_db),
450
+ ):
451
+ """Get a specific project by ID"""
452
+ try:
453
+ project = db.query(Project).filter(
454
+ Project.id == project_id,
455
+ Project.organization_id == organization["id"]
456
+ ).first()
457
+
458
+ if not project:
459
+ raise HTTPException(status_code=404, detail="Project not found")
460
+
461
+ # Get counts
462
+ agent_count = db.query(func.count(ProjectAgent.id)).filter(
463
+ ProjectAgent.project_id == project_id
464
+ ).scalar() or 0
465
+
466
+ team_count = db.query(func.count(ProjectTeam.id)).filter(
467
+ ProjectTeam.project_id == project_id
468
+ ).scalar() or 0
469
+
470
+ # Extract policy_ids, default_model, goals, and restrict_to_environment from settings for response
471
+ settings = project.settings or {}
472
+ policy_ids = settings.get("policy_ids", [])
473
+ default_model = settings.get("default_model")
474
+ goals = settings.get("goals")
475
+ restrict_to_environment = settings.get("restrict_to_environment", False)
476
+
477
+ return ProjectResponse(
478
+ id=str(project.id),
479
+ organization_id=str(project.organization_id),
480
+ name=project.name,
481
+ key=project.key,
482
+ description=project.description,
483
+ goals=goals,
484
+ settings=settings,
485
+ status=project.status,
486
+ visibility=project.visibility,
487
+ owner_id=project.owner_id,
488
+ owner_email=project.owner_email,
489
+ restrict_to_environment=restrict_to_environment,
490
+ policy_ids=policy_ids,
491
+ default_model=default_model,
492
+ created_at=project.created_at.isoformat() if project.created_at else None,
493
+ updated_at=project.updated_at.isoformat() if project.updated_at else None,
494
+ archived_at=project.archived_at.isoformat() if project.archived_at else None,
495
+ agent_count=agent_count,
496
+ team_count=team_count,
497
+ )
498
+
499
+ except HTTPException:
500
+ raise
501
+ except Exception as e:
502
+ logger.error("project_get_failed", error=str(e), project_id=project_id)
503
+ raise HTTPException(
504
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
505
+ detail=f"Failed to get project: {str(e)}"
506
+ )
507
+
508
+
509
+ @router.patch("/{project_id}", response_model=ProjectResponse)
510
+ async def update_project(
511
+ project_id: str,
512
+ project_data: ProjectUpdate,
513
+ request: Request,
514
+ organization: dict = Depends(get_current_organization),
515
+ db: Session = Depends(get_db),
516
+ ):
517
+ """Update a project"""
518
+ try:
519
+ # Check if project exists
520
+ project = db.query(Project).filter(
521
+ Project.id == project_id,
522
+ Project.organization_id == organization["id"]
523
+ ).first()
524
+
525
+ if not project:
526
+ raise HTTPException(status_code=404, detail="Project not found")
527
+
528
+ # Build update dict
529
+ update_data = project_data.model_dump(exclude_unset=True)
530
+
531
+ # Handle policy_ids, default_model, goals, and restrict_to_environment - store in settings if provided
532
+ settings_updates = {}
533
+ if "policy_ids" in update_data:
534
+ settings_updates["policy_ids"] = update_data.pop("policy_ids")
535
+ if "default_model" in update_data:
536
+ settings_updates["default_model"] = update_data.pop("default_model")
537
+ if "goals" in update_data:
538
+ settings_updates["goals"] = update_data.pop("goals")
539
+ if "restrict_to_environment" in update_data:
540
+ settings_updates["restrict_to_environment"] = update_data.pop("restrict_to_environment")
541
+
542
+ # Apply settings updates if any
543
+ if settings_updates:
544
+ if "settings" in update_data:
545
+ update_data["settings"].update(settings_updates)
546
+ else:
547
+ # Merge with existing settings
548
+ existing_settings = project.settings or {}
549
+ update_data["settings"] = {**existing_settings, **settings_updates}
550
+
551
+ # Uppercase key if provided
552
+ if "key" in update_data:
553
+ update_data["key"] = update_data["key"].upper()
554
+
555
+ # Apply updates to model
556
+ for key, value in update_data.items():
557
+ setattr(project, key, value)
558
+
559
+ project.updated_at = datetime.utcnow()
560
+
561
+ db.commit()
562
+ db.refresh(project)
563
+
564
+ # Get counts
565
+ agent_count = db.query(func.count(ProjectAgent.id)).filter(
566
+ ProjectAgent.project_id == project_id
567
+ ).scalar() or 0
568
+
569
+ team_count = db.query(func.count(ProjectTeam.id)).filter(
570
+ ProjectTeam.project_id == project_id
571
+ ).scalar() or 0
572
+
573
+ logger.info(
574
+ "project_updated",
575
+ project_id=project_id,
576
+ org_id=organization["id"],
577
+ )
578
+
579
+ # Extract policy_ids, default_model, goals, and restrict_to_environment from settings for response
580
+ settings = project.settings or {}
581
+ policy_ids = settings.get("policy_ids", [])
582
+ default_model = settings.get("default_model")
583
+ goals = settings.get("goals")
584
+ restrict_to_environment = settings.get("restrict_to_environment", False)
585
+
586
+ return ProjectResponse(
587
+ id=str(project.id),
588
+ organization_id=str(project.organization_id),
589
+ name=project.name,
590
+ key=project.key,
591
+ description=project.description,
592
+ goals=goals,
593
+ settings=settings,
594
+ status=project.status,
595
+ visibility=project.visibility,
596
+ owner_id=project.owner_id,
597
+ owner_email=project.owner_email,
598
+ restrict_to_environment=restrict_to_environment,
599
+ policy_ids=policy_ids,
600
+ default_model=default_model,
601
+ created_at=project.created_at.isoformat() if project.created_at else None,
602
+ updated_at=project.updated_at.isoformat() if project.updated_at else None,
603
+ archived_at=project.archived_at.isoformat() if project.archived_at else None,
604
+ agent_count=agent_count,
605
+ team_count=team_count,
606
+ )
607
+
608
+ except HTTPException:
609
+ raise
610
+ except Exception as e:
611
+ logger.error("project_update_failed", error=str(e), project_id=project_id)
612
+ db.rollback()
613
+ raise HTTPException(
614
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
615
+ detail=f"Failed to update project: {str(e)}"
616
+ )
617
+
618
+
619
+ @router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
620
+ async def delete_project(
621
+ project_id: str,
622
+ request: Request,
623
+ organization: dict = Depends(get_current_organization),
624
+ db: Session = Depends(get_db),
625
+ ):
626
+ """Delete a project (cascades to associations)"""
627
+ try:
628
+ project = db.query(Project).filter(
629
+ Project.id == project_id,
630
+ Project.organization_id == organization["id"]
631
+ ).first()
632
+
633
+ if not project:
634
+ raise HTTPException(status_code=404, detail="Project not found")
635
+
636
+ db.delete(project)
637
+ db.commit()
638
+
639
+ logger.info("project_deleted", project_id=project_id, org_id=organization["id"])
640
+
641
+ return None
642
+
643
+ except HTTPException:
644
+ raise
645
+ except Exception as e:
646
+ logger.error("project_delete_failed", error=str(e), project_id=project_id)
647
+ db.rollback()
648
+ raise HTTPException(
649
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
650
+ detail=f"Failed to delete project: {str(e)}"
651
+ )
652
+
653
+
654
+ # Agent associations
655
+ @router.post("/{project_id}/agents", status_code=status.HTTP_201_CREATED)
656
+ async def add_agent_to_project(
657
+ project_id: str,
658
+ agent_data: ProjectAgentAdd,
659
+ request: Request,
660
+ organization: dict = Depends(get_current_organization),
661
+ db: Session = Depends(get_db),
662
+ ):
663
+ """Add an agent to a project"""
664
+ try:
665
+ # Verify project exists
666
+ project = db.query(Project).filter(
667
+ Project.id == project_id,
668
+ Project.organization_id == organization["id"]
669
+ ).first()
670
+
671
+ if not project:
672
+ raise HTTPException(status_code=404, detail="Project not found")
673
+
674
+ # Verify agent exists and belongs to org
675
+ agent = db.query(Agent).filter(
676
+ Agent.id == agent_data.agent_id,
677
+ Agent.organization_id == organization["id"]
678
+ ).first()
679
+
680
+ if not agent:
681
+ raise HTTPException(status_code=404, detail="Agent not found")
682
+
683
+ # Add association
684
+ now = datetime.utcnow()
685
+ association = ProjectAgent(
686
+ project_id=project_id,
687
+ agent_id=agent_data.agent_id,
688
+ role=agent_data.role,
689
+ added_at=now,
690
+ added_by=organization.get("user_id"),
691
+ )
692
+
693
+ db.add(association)
694
+ db.commit()
695
+ db.refresh(association)
696
+
697
+ logger.info(
698
+ "agent_added_to_project",
699
+ project_id=project_id,
700
+ agent_id=agent_data.agent_id,
701
+ org_id=organization["id"],
702
+ )
703
+
704
+ return {
705
+ "id": str(association.id),
706
+ "project_id": str(association.project_id),
707
+ "agent_id": str(association.agent_id),
708
+ "role": association.role,
709
+ "added_at": association.added_at.isoformat() if association.added_at else None,
710
+ "added_by": association.added_by,
711
+ }
712
+
713
+ except HTTPException:
714
+ raise
715
+ except Exception as e:
716
+ logger.error("add_agent_to_project_failed", error=str(e))
717
+ db.rollback()
718
+ raise HTTPException(
719
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
720
+ detail=f"Failed to add agent: {str(e)}"
721
+ )
722
+
723
+
724
+ @router.get("/{project_id}/agents")
725
+ async def list_project_agents(
726
+ project_id: str,
727
+ request: Request,
728
+ organization: dict = Depends(get_current_organization),
729
+ db: Session = Depends(get_db),
730
+ ):
731
+ """List all agents in a project"""
732
+ try:
733
+ # Get project agents with agent details
734
+ project_agents = db.query(ProjectAgent).options(
735
+ joinedload(ProjectAgent.agent)
736
+ ).filter(
737
+ ProjectAgent.project_id == project_id
738
+ ).all()
739
+
740
+ # Build response with nested agent data
741
+ result = []
742
+ for pa in project_agents:
743
+ agent_data = None
744
+ if pa.agent:
745
+ agent_data = {
746
+ "id": str(pa.agent.id),
747
+ "name": pa.agent.name,
748
+ "description": pa.agent.description,
749
+ "organization_id": str(pa.agent.organization_id),
750
+ "created_at": pa.agent.created_at.isoformat() if pa.agent.created_at else None,
751
+ "updated_at": pa.agent.updated_at.isoformat() if pa.agent.updated_at else None,
752
+ }
753
+
754
+ result.append({
755
+ "id": str(pa.id),
756
+ "project_id": str(pa.project_id),
757
+ "agent_id": str(pa.agent_id),
758
+ "role": pa.role,
759
+ "added_at": pa.added_at.isoformat() if pa.added_at else None,
760
+ "added_by": pa.added_by,
761
+ "agents": agent_data,
762
+ })
763
+
764
+ logger.info(
765
+ "project_agents_listed",
766
+ project_id=project_id,
767
+ count=len(result),
768
+ org_id=organization["id"],
769
+ )
770
+
771
+ return result
772
+
773
+ except Exception as e:
774
+ logger.error("list_project_agents_failed", error=str(e))
775
+ raise HTTPException(
776
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
777
+ detail=f"Failed to list agents: {str(e)}"
778
+ )
779
+
780
+
781
+ @router.delete("/{project_id}/agents/{agent_id}", status_code=status.HTTP_204_NO_CONTENT)
782
+ async def remove_agent_from_project(
783
+ project_id: str,
784
+ agent_id: str,
785
+ request: Request,
786
+ organization: dict = Depends(get_current_organization),
787
+ db: Session = Depends(get_db),
788
+ ):
789
+ """Remove an agent from a project"""
790
+ try:
791
+ association = db.query(ProjectAgent).filter(
792
+ ProjectAgent.project_id == project_id,
793
+ ProjectAgent.agent_id == agent_id
794
+ ).first()
795
+
796
+ if not association:
797
+ raise HTTPException(status_code=404, detail="Association not found")
798
+
799
+ db.delete(association)
800
+ db.commit()
801
+
802
+ logger.info(
803
+ "agent_removed_from_project",
804
+ project_id=project_id,
805
+ agent_id=agent_id,
806
+ org_id=organization["id"],
807
+ )
808
+
809
+ return None
810
+
811
+ except HTTPException:
812
+ raise
813
+ except Exception as e:
814
+ logger.error("remove_agent_from_project_failed", error=str(e))
815
+ db.rollback()
816
+ raise HTTPException(
817
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
818
+ detail=f"Failed to remove agent: {str(e)}"
819
+ )
820
+
821
+
822
+ # Team associations (similar to agents)
823
+ @router.post("/{project_id}/teams", status_code=status.HTTP_201_CREATED)
824
+ async def add_team_to_project(
825
+ project_id: str,
826
+ team_data: ProjectTeamAdd,
827
+ request: Request,
828
+ organization: dict = Depends(get_current_organization),
829
+ db: Session = Depends(get_db),
830
+ ):
831
+ """Add a team to a project"""
832
+ try:
833
+ # Verify project exists
834
+ project = db.query(Project).filter(
835
+ Project.id == project_id,
836
+ Project.organization_id == organization["id"]
837
+ ).first()
838
+
839
+ if not project:
840
+ raise HTTPException(status_code=404, detail="Project not found")
841
+
842
+ # Verify team exists and belongs to org
843
+ team = db.query(Team).filter(
844
+ Team.id == team_data.team_id,
845
+ Team.organization_id == organization["id"]
846
+ ).first()
847
+
848
+ if not team:
849
+ raise HTTPException(status_code=404, detail="Team not found")
850
+
851
+ # Add association
852
+ now = datetime.utcnow()
853
+ association = ProjectTeam(
854
+ project_id=project_id,
855
+ team_id=team_data.team_id,
856
+ role=team_data.role,
857
+ added_at=now,
858
+ added_by=organization.get("user_id"),
859
+ )
860
+
861
+ db.add(association)
862
+ db.commit()
863
+ db.refresh(association)
864
+
865
+ logger.info(
866
+ "team_added_to_project",
867
+ project_id=project_id,
868
+ team_id=team_data.team_id,
869
+ org_id=organization["id"],
870
+ )
871
+
872
+ return {
873
+ "id": str(association.id),
874
+ "project_id": str(association.project_id),
875
+ "team_id": str(association.team_id),
876
+ "role": association.role,
877
+ "added_at": association.added_at.isoformat() if association.added_at else None,
878
+ "added_by": association.added_by,
879
+ }
880
+
881
+ except HTTPException:
882
+ raise
883
+ except Exception as e:
884
+ logger.error("add_team_to_project_failed", error=str(e))
885
+ db.rollback()
886
+ raise HTTPException(
887
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
888
+ detail=f"Failed to add team: {str(e)}"
889
+ )
890
+
891
+
892
+ @router.get("/{project_id}/teams")
893
+ async def list_project_teams(
894
+ project_id: str,
895
+ request: Request,
896
+ organization: dict = Depends(get_current_organization),
897
+ db: Session = Depends(get_db),
898
+ ):
899
+ """List all teams in a project"""
900
+ try:
901
+ # Get project teams with team details
902
+ project_teams = db.query(ProjectTeam).options(
903
+ joinedload(ProjectTeam.team)
904
+ ).filter(
905
+ ProjectTeam.project_id == project_id
906
+ ).all()
907
+
908
+ # Build response with nested team data
909
+ result = []
910
+ for pt in project_teams:
911
+ team_data = None
912
+ if pt.team:
913
+ team_data = {
914
+ "id": str(pt.team.id),
915
+ "name": pt.team.name,
916
+ "description": pt.team.description,
917
+ "organization_id": str(pt.team.organization_id),
918
+ "created_at": pt.team.created_at.isoformat() if pt.team.created_at else None,
919
+ "updated_at": pt.team.updated_at.isoformat() if pt.team.updated_at else None,
920
+ }
921
+
922
+ result.append({
923
+ "id": str(pt.id),
924
+ "project_id": str(pt.project_id),
925
+ "team_id": str(pt.team_id),
926
+ "role": pt.role,
927
+ "added_at": pt.added_at.isoformat() if pt.added_at else None,
928
+ "added_by": pt.added_by,
929
+ "teams": team_data,
930
+ })
931
+
932
+ logger.info(
933
+ "project_teams_listed",
934
+ project_id=project_id,
935
+ count=len(result),
936
+ org_id=organization["id"],
937
+ )
938
+
939
+ return result
940
+
941
+ except Exception as e:
942
+ logger.error("list_project_teams_failed", error=str(e))
943
+ raise HTTPException(
944
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
945
+ detail=f"Failed to list teams: {str(e)}"
946
+ )
947
+
948
+
949
+ @router.delete("/{project_id}/teams/{team_id}", status_code=status.HTTP_204_NO_CONTENT)
950
+ async def remove_team_from_project(
951
+ project_id: str,
952
+ team_id: str,
953
+ request: Request,
954
+ organization: dict = Depends(get_current_organization),
955
+ db: Session = Depends(get_db),
956
+ ):
957
+ """Remove a team from a project"""
958
+ try:
959
+ association = db.query(ProjectTeam).filter(
960
+ ProjectTeam.project_id == project_id,
961
+ ProjectTeam.team_id == team_id
962
+ ).first()
963
+
964
+ if not association:
965
+ raise HTTPException(status_code=404, detail="Association not found")
966
+
967
+ db.delete(association)
968
+ db.commit()
969
+
970
+ logger.info(
971
+ "team_removed_from_project",
972
+ project_id=project_id,
973
+ team_id=team_id,
974
+ org_id=organization["id"],
975
+ )
976
+
977
+ return None
978
+
979
+ except HTTPException:
980
+ raise
981
+ except Exception as e:
982
+ logger.error("remove_team_from_project_failed", error=str(e))
983
+ db.rollback()
984
+ raise HTTPException(
985
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
986
+ detail=f"Failed to remove team: {str(e)}"
987
+ )