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,1315 @@
1
+ """
2
+ Execution Environment Controller - Centralized logic for resolving execution environments
3
+
4
+ This controller provides reusable logic for resolving execution environments for agents/teams.
5
+ It can be called from:
6
+ - API routes (for HTTP requests)
7
+ - Workers (for direct execution)
8
+ - Other internal services
9
+
10
+ The controller handles:
11
+ - Fetching execution environment configs from database
12
+ - Resolving secret names to actual values from Kubiya API
13
+ - Resolving integration IDs to actual tokens from Kubiya API
14
+ - Merging configs from environments + agent/team
15
+ - Template resolution in config fields
16
+ """
17
+
18
+ import httpx
19
+ import os
20
+ import tempfile
21
+ from typing import Dict, Any, List, Optional, Tuple
22
+ from sqlalchemy.orm import Session
23
+ import structlog
24
+
25
+ from control_plane_api.app.models import (
26
+ Agent,
27
+ Team,
28
+ Environment,
29
+ AgentEnvironment,
30
+ TeamEnvironment,
31
+ )
32
+ from control_plane_api.app.models.custom_integration import CustomIntegration
33
+ from control_plane_api.app.lib.kubiya_client import KUBIYA_API_BASE
34
+ from control_plane_api.app.lib.templating import TemplateContext, resolve_templates
35
+
36
+ logger = structlog.get_logger(__name__)
37
+
38
+
39
+ # Integration type to environment variable name mapping
40
+ # Each integration can map to multiple env vars (e.g., AWS needs key + secret)
41
+ INTEGRATION_ENV_VAR_MAP = {
42
+ "github": ["GH_TOKEN", "GITHUB_TOKEN"],
43
+ "github_app": ["GITHUB_TOKEN"],
44
+ "jira": ["JIRA_TOKEN"],
45
+ "slack": ["SLACK_TOKEN", "SLACK_BOT_TOKEN"],
46
+ "aws": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_REGION"],
47
+ "aws-serviceaccount": ["AWS_ROLE_ARN", "AWS_REGION"],
48
+ "kubernetes": ["KUBECONFIG"],
49
+ "gcp": ["GCP_SERVICE_ACCOUNT_KEY", "GOOGLE_APPLICATION_CREDENTIALS"],
50
+ "azure": ["AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID"],
51
+ "datadog": ["DD_API_KEY", "DD_APP_KEY", "DD_SITE"],
52
+ "pagerduty": ["PD_TOKEN"],
53
+ "gitlab": ["GITLAB_TOKEN"],
54
+ "bitbucket": ["BITBUCKET_TOKEN"],
55
+ "linear": ["LINEAR_API_KEY"],
56
+ "notion": ["NOTION_API_KEY"],
57
+ "openai": ["OPENAI_API_KEY"],
58
+ "anthropic": ["ANTHROPIC_API_KEY"],
59
+ }
60
+
61
+
62
+ class ExecutionEnvironmentResolutionError(Exception):
63
+ """Raised when execution environment resolution fails"""
64
+ pass
65
+
66
+
67
+ def create_aws_credentials_file(
68
+ access_key_id: str,
69
+ secret_access_key: str,
70
+ session_token: Optional[str] = None,
71
+ region: Optional[str] = None,
72
+ profile: str = "default",
73
+ ) -> Tuple[str, str]:
74
+ """
75
+ Create AWS credentials and config files for use with AWS SDKs/CLI.
76
+
77
+ Args:
78
+ access_key_id: AWS access key ID
79
+ secret_access_key: AWS secret access key
80
+ session_token: Optional session token for temporary credentials
81
+ region: Optional AWS region
82
+ profile: Profile name (default: "default")
83
+
84
+ Returns:
85
+ Tuple of (credentials_file_path, config_file_path)
86
+ """
87
+ # Create temp directory for AWS config
88
+ temp_dir = tempfile.mkdtemp(prefix="aws_")
89
+
90
+ # Create credentials file
91
+ credentials_path = os.path.join(temp_dir, "credentials")
92
+ credentials_content = f"""[{profile}]
93
+ aws_access_key_id = {access_key_id}
94
+ aws_secret_access_key = {secret_access_key}
95
+ """
96
+ if session_token:
97
+ credentials_content += f"aws_session_token = {session_token}\n"
98
+
99
+ with open(credentials_path, "w") as f:
100
+ f.write(credentials_content)
101
+
102
+ # Create config file
103
+ config_path = os.path.join(temp_dir, "config")
104
+ config_content = f"""[{profile}]
105
+ """
106
+ if region:
107
+ config_content += f"region = {region}\n"
108
+ else:
109
+ config_content += "region = us-east-1\n"
110
+
111
+ with open(config_path, "w") as f:
112
+ f.write(config_content)
113
+
114
+ logger.debug(
115
+ "aws_credentials_files_created",
116
+ credentials_path=credentials_path,
117
+ config_path=config_path,
118
+ profile=profile,
119
+ )
120
+
121
+ return credentials_path, config_path
122
+
123
+
124
+ def create_kubeconfig_file(kubeconfig_content: str) -> str:
125
+ """
126
+ Create a kubeconfig file from content.
127
+
128
+ Args:
129
+ kubeconfig_content: YAML content of kubeconfig
130
+
131
+ Returns:
132
+ Path to kubeconfig file
133
+ """
134
+ temp_dir = tempfile.mkdtemp(prefix="kube_")
135
+ kubeconfig_path = os.path.join(temp_dir, "config")
136
+
137
+ with open(kubeconfig_path, "w") as f:
138
+ f.write(kubeconfig_content)
139
+
140
+ logger.debug("kubeconfig_file_created", kubeconfig_path=kubeconfig_path)
141
+
142
+ return kubeconfig_path
143
+
144
+
145
+ async def resolve_custom_integration(
146
+ custom_integration_id: str,
147
+ org_id: str,
148
+ db: Session,
149
+ kubiya_token: str,
150
+ auth_type: str = "UserKey",
151
+ ) -> Dict[str, Any]:
152
+ """
153
+ Resolve a custom integration to environment variables and files.
154
+
155
+ Args:
156
+ custom_integration_id: Custom integration UUID
157
+ org_id: Organization ID
158
+ db: Database session
159
+ kubiya_token: Kubiya API token for secrets resolution
160
+ auth_type: Authorization type ("UserKey" for API keys, "Bearer" for JWT tokens)
161
+
162
+ Returns:
163
+ Dict with:
164
+ - env_vars: Dict of environment variables
165
+ - files: List of files to create
166
+ - context_prompt: Optional contextual prompt
167
+ """
168
+ # Fetch custom integration from database
169
+ custom_int = db.query(CustomIntegration).filter(
170
+ CustomIntegration.id == custom_integration_id,
171
+ CustomIntegration.organization_id == org_id
172
+ ).first()
173
+
174
+ if not custom_int:
175
+ logger.warning(
176
+ "custom_integration_not_found",
177
+ integration_id=custom_integration_id[:8],
178
+ org_id=org_id
179
+ )
180
+ return {"env_vars": {}, "files": [], "context_prompt": None}
181
+
182
+ config = custom_int.config or {}
183
+ resolved_env_vars = {}
184
+ files_to_create = []
185
+
186
+ # Add direct env vars
187
+ env_vars = config.get("env_vars", {})
188
+ resolved_env_vars.update(env_vars)
189
+
190
+ # Resolve secrets
191
+ secrets = config.get("secrets", [])
192
+ for secret_name in secrets:
193
+ try:
194
+ secret_value = await resolve_secret_value(secret_name, kubiya_token, org_id, auth_type)
195
+ resolved_env_vars[secret_name] = secret_value
196
+ logger.debug(
197
+ "custom_integration_secret_resolved",
198
+ integration_id=custom_integration_id[:8],
199
+ secret_name=secret_name[:20]
200
+ )
201
+ except Exception as e:
202
+ logger.error(
203
+ "custom_integration_secret_resolution_error",
204
+ integration_id=custom_integration_id[:8],
205
+ secret_name=secret_name[:20],
206
+ error=str(e)
207
+ )
208
+
209
+ # Prepare files
210
+ files_config = config.get("files", [])
211
+ for file_config in files_config:
212
+ file_path = file_config.get("path")
213
+ content = file_config.get("content")
214
+ secret_ref = file_config.get("secret_ref")
215
+ mode = file_config.get("mode", "0644")
216
+
217
+ if not file_path:
218
+ continue
219
+
220
+ # Resolve content from secret if needed
221
+ if secret_ref and not content:
222
+ try:
223
+ content = await resolve_secret_value(secret_ref, kubiya_token, org_id, auth_type)
224
+ logger.debug(
225
+ "custom_integration_file_secret_resolved",
226
+ integration_id=custom_integration_id[:8],
227
+ file_path=file_path,
228
+ secret_ref=secret_ref[:20]
229
+ )
230
+ except Exception as e:
231
+ logger.error(
232
+ "custom_integration_file_secret_error",
233
+ integration_id=custom_integration_id[:8],
234
+ file_path=file_path,
235
+ error=str(e)
236
+ )
237
+ continue
238
+
239
+ if content:
240
+ files_to_create.append({
241
+ "path": file_path,
242
+ "content": content,
243
+ "mode": mode,
244
+ "description": file_config.get("description")
245
+ })
246
+
247
+ context_prompt = config.get("context_prompt")
248
+
249
+ logger.info(
250
+ "custom_integration_resolved",
251
+ integration_id=custom_integration_id[:8],
252
+ integration_name=custom_int.name,
253
+ env_var_count=len(resolved_env_vars),
254
+ file_count=len(files_to_create),
255
+ has_context=bool(context_prompt)
256
+ )
257
+
258
+ return {
259
+ "env_vars": resolved_env_vars,
260
+ "files": files_to_create,
261
+ "context_prompt": context_prompt,
262
+ "name": custom_int.name,
263
+ "integration_type": custom_int.integration_type
264
+ }
265
+
266
+
267
+ def build_integration_context(
268
+ resolved_integrations: List[Dict[str, Any]],
269
+ ) -> str:
270
+ """
271
+ Build integration context information for injection into system prompt.
272
+
273
+ This provides the agent with awareness of available integrations and their
274
+ configuration without exposing credentials.
275
+
276
+ Args:
277
+ resolved_integrations: List of resolved integration metadata
278
+
279
+ Returns:
280
+ Markdown-formatted context string for system prompt
281
+ """
282
+ if not resolved_integrations:
283
+ return ""
284
+
285
+ context_parts = ["## Available Integrations\n"]
286
+ context_parts.append("The following integrations are configured and available for use:\n")
287
+
288
+ for integration in resolved_integrations:
289
+ integration_type = integration.get("integration_type", "unknown")
290
+ integration_name = integration.get("name", integration_type)
291
+ env_vars = integration.get("env_vars", [])
292
+ custom_context = integration.get("custom_context")
293
+
294
+ context_parts.append(f"\n### {integration_name} ({integration_type})")
295
+
296
+ if env_vars:
297
+ context_parts.append(f"- Available environment variables: {', '.join(env_vars)}")
298
+
299
+ # Add custom context if provided
300
+ if custom_context:
301
+ context_parts.append(f"- {custom_context}")
302
+
303
+ # Add integration-specific guidance
304
+ if integration_type in ["aws", "aws-serviceaccount"]:
305
+ context_parts.append("- AWS SDK and CLI are pre-configured with credentials")
306
+ context_parts.append("- Use environment variables or ~/.aws/credentials file")
307
+ elif integration_type == "kubernetes":
308
+ context_parts.append("- Kubernetes kubectl is pre-configured")
309
+ context_parts.append("- Use KUBECONFIG environment variable or ~/.kube/config")
310
+ elif integration_type in ["github", "github_app"]:
311
+ context_parts.append("- GitHub API access is available via GH_TOKEN or GITHUB_TOKEN")
312
+ elif integration_type == "jira":
313
+ context_parts.append("- Jira API access is available via JIRA_TOKEN")
314
+ elif integration_type == "slack":
315
+ context_parts.append("- Slack API access is available via SLACK_TOKEN")
316
+
317
+ return "\n".join(context_parts)
318
+
319
+
320
+ async def resolve_secret_value(
321
+ secret_name: str,
322
+ token: str,
323
+ org_id: str,
324
+ auth_type: str = "UserKey",
325
+ ) -> str:
326
+ """
327
+ Resolve a secret name to its actual value from Kubiya API.
328
+
329
+ Args:
330
+ secret_name: Name of the secret to resolve
331
+ token: Kubiya API token
332
+ org_id: Organization ID
333
+ auth_type: Authorization type ("UserKey" for API keys, "Bearer" for JWT tokens)
334
+
335
+ Returns:
336
+ Secret value as string
337
+
338
+ Raises:
339
+ ExecutionEnvironmentResolutionError: If secret resolution fails
340
+ """
341
+ # Map auth types to what Kubiya API accepts
342
+ # "JWT" is used internally for Kubiya API keys that are JWTs - Kubiya API expects "UserKey" for these
343
+ kubiya_auth_type = "UserKey" if auth_type in ("JWT", "UserKey") else auth_type
344
+ headers = {
345
+ "Authorization": f"{kubiya_auth_type} {token}",
346
+ "Accept": "application/json",
347
+ "Content-Type": "application/json",
348
+ "X-Kubiya-Client": "agent-control-plane",
349
+ "X-Organization-ID": org_id,
350
+ }
351
+
352
+ async with httpx.AsyncClient(timeout=30.0) as client:
353
+ response = await client.get(
354
+ f"{KUBIYA_API_BASE}/api/v2/secrets/get_value/{secret_name}",
355
+ headers=headers,
356
+ )
357
+
358
+ if response.status_code == 200:
359
+ return response.text
360
+ else:
361
+ logger.warning(
362
+ "secret_resolution_failed",
363
+ secret_name=secret_name[:20],
364
+ status=response.status_code,
365
+ )
366
+ raise ExecutionEnvironmentResolutionError(
367
+ f"Failed to resolve secret '{secret_name}': {response.text[:200]}"
368
+ )
369
+
370
+
371
+ async def resolve_integration_token(
372
+ integration_id: str,
373
+ integration_type: str,
374
+ token: str,
375
+ org_id: str,
376
+ auth_type: str = "UserKey",
377
+ ) -> Dict[str, str]:
378
+ """
379
+ Resolve an integration ID to its credentials from Kubiya API.
380
+
381
+ Args:
382
+ integration_id: Integration UUID
383
+ integration_type: Type of integration (github, jira, aws, etc.)
384
+ token: Kubiya API token
385
+ org_id: Organization ID
386
+ auth_type: Authorization type ("UserKey" for API keys, "Bearer" for JWT tokens)
387
+
388
+ Returns:
389
+ Dict with environment variable names mapped to their values.
390
+ May return multiple env vars for integrations like AWS (key + secret).
391
+ """
392
+ # Map auth types to what Kubiya API accepts
393
+ kubiya_auth_type = "UserKey" if auth_type in ("JWT", "UserKey") else auth_type
394
+ headers = {
395
+ "Authorization": f"{kubiya_auth_type} {token}",
396
+ "Accept": "application/json",
397
+ "Content-Type": "application/json",
398
+ "X-Kubiya-Client": "agent-control-plane",
399
+ "X-Organization-ID": org_id,
400
+ }
401
+
402
+ # Build token URL based on integration type
403
+ integration_type_lower = integration_type.lower()
404
+
405
+ # Map integration type to API endpoint
406
+ # NOTE: Kubiya API currently supports github, github_app, jira via /token endpoint
407
+ # For other types, we try the generic /creds endpoint used by SDK
408
+
409
+ if integration_type_lower == "github":
410
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integration/github/token/{integration_id}"
411
+ elif integration_type_lower == "github_app":
412
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integration/github_app/token/{integration_id}"
413
+ elif integration_type_lower == "jira":
414
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integration/jira/token/{integration_id}"
415
+ elif integration_type_lower == "slack":
416
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integration/slack/token/{integration_id}"
417
+ else:
418
+ # Use SDK-style generic endpoint: /api/v1/integrations/{vendor}/creds/{id}
419
+ # This supports AWS, Kubernetes, Azure, and other integrations
420
+ # Special integrations (jira, github_app) use "0" as ID per SDK convention
421
+ SPECIAL_INTEGRATIONS = {"jira", "github_app"}
422
+ actual_id = "0" if integration_type_lower in SPECIAL_INTEGRATIONS else integration_id
423
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integrations/{integration_type_lower}/creds/{actual_id}"
424
+ logger.debug(
425
+ "using_sdk_creds_endpoint",
426
+ integration_type=integration_type,
427
+ integration_id=integration_id[:8],
428
+ actual_id=actual_id if actual_id == "0" else actual_id[:8],
429
+ )
430
+
431
+ async with httpx.AsyncClient(timeout=30.0) as client:
432
+ response = await client.get(token_url, headers=headers)
433
+
434
+ if response.status_code == 200:
435
+ try:
436
+ # Try to parse as JSON first
437
+ credential_data = response.json()
438
+
439
+ # Handle different response formats based on integration type
440
+ if integration_type_lower in ["aws", "aws-serviceaccount"]:
441
+ # AWS returns structured credentials
442
+ env_vars = {}
443
+ if "access_key_id" in credential_data:
444
+ env_vars["AWS_ACCESS_KEY_ID"] = credential_data["access_key_id"]
445
+ if "secret_access_key" in credential_data:
446
+ env_vars["AWS_SECRET_ACCESS_KEY"] = credential_data["secret_access_key"]
447
+ if "session_token" in credential_data:
448
+ env_vars["AWS_SESSION_TOKEN"] = credential_data["session_token"]
449
+ if "region" in credential_data:
450
+ env_vars["AWS_REGION"] = credential_data["region"]
451
+ if "role_arn" in credential_data:
452
+ env_vars["AWS_ROLE_ARN"] = credential_data["role_arn"]
453
+ return env_vars
454
+
455
+ elif integration_type_lower == "kubernetes":
456
+ # Kubernetes returns kubeconfig content
457
+ kubeconfig_content = credential_data.get("kubeconfig", response.text)
458
+ return {"KUBECONFIG_CONTENT": kubeconfig_content}
459
+
460
+ elif integration_type_lower == "azure":
461
+ # Azure returns structured credentials
462
+ env_vars = {}
463
+ if "client_id" in credential_data:
464
+ env_vars["AZURE_CLIENT_ID"] = credential_data["client_id"]
465
+ if "client_secret" in credential_data:
466
+ env_vars["AZURE_CLIENT_SECRET"] = credential_data["client_secret"]
467
+ if "tenant_id" in credential_data:
468
+ env_vars["AZURE_TENANT_ID"] = credential_data["tenant_id"]
469
+ return env_vars
470
+
471
+ else:
472
+ # Generic token-based integration
473
+ token_value = credential_data.get("token", response.text)
474
+
475
+ # Map to standard env var names
476
+ env_var_names = INTEGRATION_ENV_VAR_MAP.get(
477
+ integration_type_lower, [f"{integration_type.upper()}_TOKEN"]
478
+ )
479
+
480
+ # Return token for first (primary) env var name
481
+ return {env_var_names[0]: token_value}
482
+
483
+ except Exception as e:
484
+ logger.debug(
485
+ "credential_json_parse_failed",
486
+ integration_id=integration_id[:8],
487
+ error=str(e),
488
+ note="Falling back to plain text"
489
+ )
490
+ # If not JSON, use plain text
491
+ token_value = response.text.strip()
492
+ env_var_names = INTEGRATION_ENV_VAR_MAP.get(
493
+ integration_type_lower, [f"{integration_type.upper()}_TOKEN"]
494
+ )
495
+ return {env_var_names[0]: token_value}
496
+ else:
497
+ logger.warning(
498
+ "integration_token_resolution_failed",
499
+ integration_id=integration_id[:8],
500
+ integration_type=integration_type,
501
+ status=response.status_code,
502
+ response_preview=response.text[:200],
503
+ )
504
+ # Don't fail the entire request for one integration
505
+ return {}
506
+
507
+
508
+ async def resolve_environment_configs(
509
+ environment_ids: List[str],
510
+ org_id: str,
511
+ db: Session,
512
+ ) -> Dict[str, Any]:
513
+ """
514
+ Resolve execution environment configs from a list of environment IDs.
515
+ Merges configs from all environments.
516
+
517
+ Args:
518
+ environment_ids: List of environment IDs
519
+ org_id: Organization ID
520
+ db: Database session
521
+
522
+ Returns:
523
+ Merged execution environment dict with env_vars, secrets, integration_ids, mcp_servers
524
+ """
525
+ if not environment_ids:
526
+ return {
527
+ "env_vars": {},
528
+ "secrets": [],
529
+ "integration_ids": [],
530
+ "mcp_servers": {},
531
+ }
532
+
533
+ # Fetch all environments
534
+ environments = (
535
+ db.query(Environment)
536
+ .filter(Environment.id.in_(environment_ids), Environment.organization_id == org_id)
537
+ .all()
538
+ )
539
+
540
+ # Merge all environment configs
541
+ merged_env_vars = {}
542
+ merged_secrets = set()
543
+ merged_integration_ids = set()
544
+ merged_mcp_servers = {}
545
+
546
+ for env in environments:
547
+ env_config = env.execution_environment or {}
548
+
549
+ # Merge env vars (later environments override earlier ones)
550
+ merged_env_vars.update(env_config.get("env_vars", {}))
551
+
552
+ # Collect secrets (union)
553
+ merged_secrets.update(env_config.get("secrets", []))
554
+
555
+ # Collect integration IDs (union)
556
+ merged_integration_ids.update(env_config.get("integration_ids", []))
557
+
558
+ # Merge MCP servers (later environments override earlier ones)
559
+ merged_mcp_servers.update(env_config.get("mcp_servers", {}))
560
+
561
+ return {
562
+ "env_vars": merged_env_vars,
563
+ "secrets": list(merged_secrets),
564
+ "integration_ids": list(merged_integration_ids),
565
+ "mcp_servers": merged_mcp_servers,
566
+ }
567
+
568
+
569
+ def apply_template_resolution(
570
+ config: Dict[str, Any],
571
+ resolved_secrets: Dict[str, str],
572
+ resolved_env_vars: Dict[str, str],
573
+ ) -> Dict[str, Any]:
574
+ """
575
+ Apply template resolution to a configuration object.
576
+
577
+ Resolves all templates in the config using resolved secrets and env vars.
578
+ Templates are resolved recursively in all string fields.
579
+
580
+ Args:
581
+ config: Configuration dict with potential templates
582
+ resolved_secrets: Map of secret names to resolved values
583
+ resolved_env_vars: Map of env var names to values
584
+
585
+ Returns:
586
+ Configuration with all templates resolved
587
+ """
588
+ try:
589
+ # Build template context
590
+ # Include env_vars in variables for simple {{VAR}} syntax support
591
+ # This allows both {{.env.VAR}} and {{VAR}} to work for environment variables
592
+ context = TemplateContext(
593
+ variables=dict(resolved_env_vars), # Also expose env vars as simple variables
594
+ secrets=resolved_secrets,
595
+ env_vars=resolved_env_vars,
596
+ )
597
+
598
+ # Apply template resolution recursively to entire config
599
+ resolved_config = resolve_templates(config, context, skip_on_error=True)
600
+
601
+ logger.debug(
602
+ "template_resolution_applied",
603
+ config_keys=list(config.keys()),
604
+ secrets_count=len(resolved_secrets),
605
+ env_vars_count=len(resolved_env_vars),
606
+ )
607
+
608
+ return resolved_config
609
+
610
+ except Exception as e:
611
+ logger.error(
612
+ "template_resolution_failed",
613
+ error=str(e),
614
+ config_keys=list(config.keys()),
615
+ )
616
+ # Return original config on error to avoid breaking execution
617
+ return config
618
+
619
+
620
+ async def resolve_agent_execution_environment(
621
+ agent_id: str,
622
+ org_id: str,
623
+ db: Session,
624
+ kubiya_token: str = None,
625
+ ) -> Dict[str, Any]:
626
+ """
627
+ Resolve complete execution environment for an agent.
628
+
629
+ This is the main controller function that:
630
+ 1. Fetches agent config and associated environments from database
631
+ 2. Merges environment configs (env vars, secrets, integrations, MCP servers)
632
+ 3. Resolves secret names to actual values from Kubiya API
633
+ 4. Resolves integration IDs to actual tokens from Kubiya API
634
+ 5. Applies template resolution to all config fields
635
+ 6. Returns complete resolved execution environment
636
+
637
+ Args:
638
+ agent_id: Agent UUID
639
+ org_id: Organization ID
640
+ db: Database session
641
+ kubiya_token: Kubiya API token for secret/integration resolution (optional, uses env var if not provided)
642
+
643
+ Returns:
644
+ Dict with:
645
+ - env_vars: Resolved environment variables (dict)
646
+ - mcp_servers: MCP server configs with templates resolved (dict)
647
+ - system_prompt: Resolved system prompt (str)
648
+ - description: Resolved description (str)
649
+ - configuration: Resolved agent configuration (dict)
650
+
651
+ Raises:
652
+ ExecutionEnvironmentResolutionError: If agent not found or resolution fails
653
+ """
654
+ try:
655
+ # Use environment KUBIYA_API_KEY if token not provided
656
+ # This is needed because the JWT bearer token from requests doesn't work with Kubiya secrets API
657
+ import os
658
+ if not kubiya_token:
659
+ kubiya_token = os.environ.get("KUBIYA_API_KEY")
660
+ if not kubiya_token:
661
+ logger.warning(
662
+ "kubiya_api_key_not_available",
663
+ agent_id=agent_id[:8],
664
+ note="Secrets and integrations will not be resolved"
665
+ )
666
+ # Continue without secret resolution
667
+ # Fetch agent with configuration fields
668
+ agent = (
669
+ db.query(Agent)
670
+ .filter(Agent.id == agent_id, Agent.organization_id == org_id)
671
+ .first()
672
+ )
673
+
674
+ if not agent:
675
+ raise ExecutionEnvironmentResolutionError(
676
+ f"Agent {agent_id} not found in organization {org_id}"
677
+ )
678
+
679
+ # Get environment associations from join table
680
+ env_associations = (
681
+ db.query(AgentEnvironment)
682
+ .filter(AgentEnvironment.agent_id == agent_id)
683
+ .all()
684
+ )
685
+ environment_ids = [str(assoc.environment_id) for assoc in env_associations]
686
+
687
+ # Fetch environment names for dataset scoping
688
+ environment_names = []
689
+ if environment_ids:
690
+ environments = (
691
+ db.query(Environment)
692
+ .filter(Environment.id.in_(environment_ids))
693
+ .all()
694
+ )
695
+ environment_names = [env.name for env in environments]
696
+
697
+ # Resolve and merge environment configs
698
+ env_config = await resolve_environment_configs(environment_ids, org_id, db)
699
+
700
+ # Get agent-level config
701
+ agent_exec_env = agent.execution_environment or {}
702
+ agent_configuration = agent.configuration or {}
703
+
704
+ # Merge: environment config + agent config (agent overrides environment)
705
+ execution_environment = {
706
+ "env_vars": {
707
+ **env_config.get("env_vars", {}),
708
+ **agent_exec_env.get("env_vars", {}),
709
+ },
710
+ "secrets": list(
711
+ set(env_config.get("secrets", []) + agent_exec_env.get("secrets", []))
712
+ ),
713
+ "integration_ids": list(
714
+ set(
715
+ env_config.get("integration_ids", [])
716
+ + agent_exec_env.get("integration_ids", [])
717
+ )
718
+ ),
719
+ "custom_integration_ids": list(
720
+ set(
721
+ env_config.get("custom_integration_ids", [])
722
+ + agent_exec_env.get("custom_integration_ids", [])
723
+ )
724
+ ),
725
+ "mcp_servers": {
726
+ **env_config.get("mcp_servers", {}),
727
+ **agent_exec_env.get("mcp_servers", {}),
728
+ },
729
+ }
730
+
731
+ # Start with custom env vars
732
+ resolved_env_vars = dict(execution_environment.get("env_vars", {}))
733
+ resolved_secrets = {}
734
+ resolved_integrations = [] # Track resolved integrations for context
735
+
736
+ # Add KUBIYA_API_KEY to resolved_env_vars for template resolution
737
+ # This allows MCP server configs to use {{KUBIYA_API_KEY}} templates
738
+ # Use the kubiya_token parameter (from request) since Control Plane doesn't have KUBIYA_API_KEY in env
739
+ if kubiya_token:
740
+ resolved_env_vars["KUBIYA_API_KEY"] = kubiya_token
741
+
742
+ # Resolve secrets
743
+ secrets = execution_environment.get("secrets", [])
744
+ for secret_name in secrets:
745
+ try:
746
+ secret_value = await resolve_secret_value(
747
+ secret_name, kubiya_token, org_id
748
+ )
749
+ resolved_env_vars[secret_name] = secret_value
750
+ resolved_secrets[secret_name] = secret_value
751
+ logger.debug(
752
+ "secret_resolved",
753
+ agent_id=agent_id[:8],
754
+ secret_name=secret_name[:20],
755
+ )
756
+ except Exception as e:
757
+ logger.error(
758
+ "secret_resolution_error",
759
+ agent_id=agent_id[:8],
760
+ secret_name=secret_name[:20],
761
+ error=str(e),
762
+ )
763
+ # Continue with other secrets even if one fails
764
+
765
+ # Resolve integrations
766
+ integration_ids = execution_environment.get("integration_ids", [])
767
+ if integration_ids:
768
+ headers = {
769
+ "Authorization": f"UserKey {kubiya_token}",
770
+ "Accept": "application/json",
771
+ "Content-Type": "application/json",
772
+ "X-Kubiya-Client": "agent-control-plane",
773
+ "X-Organization-ID": org_id,
774
+ }
775
+
776
+ async with httpx.AsyncClient(timeout=30.0) as client:
777
+ response = await client.get(
778
+ f"{KUBIYA_API_BASE}/api/v2/integrations?full=true",
779
+ headers=headers,
780
+ )
781
+
782
+ if response.status_code == 200:
783
+ all_integrations = response.json()
784
+
785
+ for integration_id in integration_ids:
786
+ integration = next(
787
+ (
788
+ i
789
+ for i in all_integrations
790
+ if i.get("uuid") == integration_id
791
+ ),
792
+ None,
793
+ )
794
+
795
+ if integration:
796
+ integration_type = integration.get("integration_type", "")
797
+ integration_name = integration.get("name", integration_type)
798
+ try:
799
+ token_env_vars = await resolve_integration_token(
800
+ integration_id,
801
+ integration_type,
802
+ kubiya_token,
803
+ org_id,
804
+ )
805
+
806
+ # Handle AWS credentials file creation
807
+ if integration_type in ["aws", "aws-serviceaccount"] and token_env_vars:
808
+ if "AWS_ACCESS_KEY_ID" in token_env_vars and "AWS_SECRET_ACCESS_KEY" in token_env_vars:
809
+ try:
810
+ creds_path, config_path = create_aws_credentials_file(
811
+ access_key_id=token_env_vars["AWS_ACCESS_KEY_ID"],
812
+ secret_access_key=token_env_vars["AWS_SECRET_ACCESS_KEY"],
813
+ session_token=token_env_vars.get("AWS_SESSION_TOKEN"),
814
+ region=token_env_vars.get("AWS_REGION"),
815
+ )
816
+ # Set AWS file locations
817
+ resolved_env_vars["AWS_SHARED_CREDENTIALS_FILE"] = creds_path
818
+ resolved_env_vars["AWS_CONFIG_FILE"] = config_path
819
+ except Exception as e:
820
+ logger.warning(
821
+ "aws_credentials_file_creation_failed",
822
+ error=str(e),
823
+ )
824
+
825
+ # Handle Kubernetes kubeconfig file creation
826
+ elif integration_type == "kubernetes" and "KUBECONFIG_CONTENT" in token_env_vars:
827
+ try:
828
+ kubeconfig_path = create_kubeconfig_file(
829
+ token_env_vars["KUBECONFIG_CONTENT"]
830
+ )
831
+ resolved_env_vars["KUBECONFIG"] = kubeconfig_path
832
+ # Remove content from env vars, keep only path
833
+ del token_env_vars["KUBECONFIG_CONTENT"]
834
+ except Exception as e:
835
+ logger.warning(
836
+ "kubeconfig_file_creation_failed",
837
+ error=str(e),
838
+ )
839
+
840
+ # Add all resolved env vars
841
+ resolved_env_vars.update(token_env_vars)
842
+
843
+ # Track integration for context
844
+ resolved_integrations.append({
845
+ "integration_type": integration_type,
846
+ "name": integration_name,
847
+ "env_vars": list(token_env_vars.keys()),
848
+ })
849
+
850
+ logger.debug(
851
+ "integration_resolved",
852
+ agent_id=agent_id[:8],
853
+ integration_id=integration_id[:8],
854
+ integration_type=integration_type,
855
+ env_var_count=len(token_env_vars),
856
+ )
857
+ except Exception as e:
858
+ logger.error(
859
+ "integration_resolution_error",
860
+ agent_id=agent_id[:8],
861
+ integration_id=integration_id[:8],
862
+ error=str(e),
863
+ )
864
+
865
+ # Resolve custom integrations
866
+ custom_integration_ids = execution_environment.get("custom_integration_ids", [])
867
+ custom_integration_files = [] # Track files to be created
868
+
869
+ for custom_integration_id in custom_integration_ids:
870
+ try:
871
+ result = await resolve_custom_integration(
872
+ custom_integration_id=custom_integration_id,
873
+ org_id=org_id,
874
+ db=db,
875
+ kubiya_token=kubiya_token,
876
+ )
877
+
878
+ # Add resolved env vars
879
+ resolved_env_vars.update(result["env_vars"])
880
+
881
+ # Track files to be created
882
+ custom_integration_files.extend(result["files"])
883
+
884
+ # Track integration for context
885
+ resolved_integrations.append({
886
+ "integration_type": result.get("integration_type", "custom"),
887
+ "name": result.get("name", "Custom Integration"),
888
+ "env_vars": list(result["env_vars"].keys()),
889
+ "custom_context": result.get("context_prompt"),
890
+ })
891
+
892
+ logger.debug(
893
+ "custom_integration_resolved",
894
+ agent_id=agent_id[:8],
895
+ custom_integration_id=custom_integration_id[:8],
896
+ env_var_count=len(result["env_vars"]),
897
+ file_count=len(result["files"]),
898
+ )
899
+ except Exception as e:
900
+ logger.error(
901
+ "custom_integration_resolution_error",
902
+ agent_id=agent_id[:8],
903
+ custom_integration_id=custom_integration_id[:8],
904
+ error=str(e),
905
+ )
906
+ # Continue with other custom integrations even if one fails
907
+
908
+ # Build complete config to resolve templates
909
+ complete_config = {
910
+ "system_prompt": agent_configuration.get("system_prompt"),
911
+ "description": agent.description,
912
+ "configuration": agent_configuration,
913
+ "mcp_servers": execution_environment.get("mcp_servers", {}),
914
+ "env_vars": execution_environment.get("env_vars", {}),
915
+ }
916
+
917
+ # Apply template resolution to ENTIRE config
918
+ resolved_config = apply_template_resolution(
919
+ complete_config, resolved_secrets, resolved_env_vars
920
+ )
921
+
922
+ mcp_servers_resolved = resolved_config.get("mcp_servers", {})
923
+
924
+ # Build integration context and inject into system prompt
925
+ integration_context = build_integration_context(resolved_integrations)
926
+ system_prompt = resolved_config.get("system_prompt", "")
927
+
928
+ if integration_context and system_prompt:
929
+ # Append integration context to system prompt
930
+ system_prompt = f"{system_prompt}\n\n{integration_context}"
931
+
932
+ # Add memory and context graph guidance to system prompt
933
+ memory_guidance = """
934
+
935
+ ## Memory & Context Graph Tools
936
+
937
+ You have built-in persistent memory and organizational context awareness:
938
+
939
+ **Memory Tools** (use proactively):
940
+ - **store_memory(content, metadata)**: Store important facts, decisions, preferences, or context
941
+ - **recall_memory(query, limit)**: Retrieve stored memories using semantic search
942
+
943
+ **Context Graph** (discover organizational data):
944
+ - **search_nodes(label, property_name, property_value)**: Find resources by type and properties
945
+ - **search_by_text(property_name, search_text)**: Text search across the graph
946
+ - **get_node(node_id)**: Get detailed information about a specific resource
947
+
948
+ **Best Practices**:
949
+ - Store user preferences, task decisions, and important context as you learn them
950
+ - Use recall_memory at the start of tasks to check for relevant past context
951
+ - Search the graph to discover infrastructure, services, and relationships
952
+ - Add descriptive metadata when storing memories for better retrieval"""
953
+
954
+ if system_prompt:
955
+ system_prompt = f"{system_prompt}\n{memory_guidance}"
956
+ else:
957
+ system_prompt = memory_guidance.strip()
958
+
959
+ # Get context graph API URL from settings for memory tools
960
+ from control_plane_api.app.config import settings
961
+ graph_api_url = settings.context_graph_api_base
962
+ dataset_name = environment_names[0] if environment_names else "default"
963
+
964
+ # Inject memory dataset name into env vars
965
+ # Worker skills will fetch graph URL from control plane's /api/v1/client/config endpoint
966
+ resolved_env_vars["MEMORY_DATASET_NAME"] = dataset_name
967
+
968
+ logger.info(
969
+ "agent_execution_environment_resolved",
970
+ agent_id=agent_id[:8],
971
+ env_var_count=len(resolved_env_vars),
972
+ mcp_server_count=len(mcp_servers_resolved),
973
+ mcp_server_names=list(mcp_servers_resolved.keys()),
974
+ secrets_count=len(resolved_secrets),
975
+ integrations_count=len(resolved_integrations),
976
+ custom_integration_files_count=len(custom_integration_files),
977
+ graph_api_url=graph_api_url,
978
+ dataset_name=dataset_name,
979
+ )
980
+
981
+ return {
982
+ "env_vars": resolved_env_vars,
983
+ "mcp_servers": mcp_servers_resolved,
984
+ "system_prompt": system_prompt,
985
+ "description": resolved_config.get("description"),
986
+ "configuration": resolved_config.get("configuration", {}),
987
+ "files": custom_integration_files,
988
+ # Context graph configuration for memory tools
989
+ "graph_api_url": graph_api_url,
990
+ "dataset_name": dataset_name,
991
+ }
992
+
993
+ except ExecutionEnvironmentResolutionError:
994
+ raise
995
+ except Exception as e:
996
+ logger.error(
997
+ "agent_execution_environment_resolution_error",
998
+ agent_id=agent_id[:8],
999
+ error=str(e),
1000
+ exc_info=True,
1001
+ )
1002
+ raise ExecutionEnvironmentResolutionError(
1003
+ f"Failed to resolve execution environment for agent {agent_id}: {str(e)}"
1004
+ )
1005
+
1006
+
1007
+ async def resolve_team_execution_environment(
1008
+ team_id: str,
1009
+ org_id: str,
1010
+ db: Session,
1011
+ kubiya_token: str = None,
1012
+ ) -> Dict[str, Any]:
1013
+ """
1014
+ Resolve complete execution environment for a team.
1015
+
1016
+ Similar to resolve_agent_execution_environment but for teams.
1017
+
1018
+ Args:
1019
+ team_id: Team UUID
1020
+ org_id: Organization ID
1021
+ db: Database session
1022
+ kubiya_token: Kubiya API token for secret/integration resolution (optional, uses env var if not provided)
1023
+
1024
+ Returns:
1025
+ Dict with resolved execution environment
1026
+
1027
+ Raises:
1028
+ ExecutionEnvironmentResolutionError: If team not found or resolution fails
1029
+ """
1030
+ try:
1031
+ # Use environment KUBIYA_API_KEY if token not provided
1032
+ import os
1033
+ if not kubiya_token:
1034
+ kubiya_token = os.environ.get("KUBIYA_API_KEY")
1035
+ if not kubiya_token:
1036
+ logger.warning(
1037
+ "kubiya_api_key_not_available",
1038
+ team_id=team_id[:8],
1039
+ note="Secrets and integrations will not be resolved"
1040
+ )
1041
+ # Fetch team with configuration fields
1042
+ team = (
1043
+ db.query(Team)
1044
+ .filter(Team.id == team_id, Team.organization_id == org_id)
1045
+ .first()
1046
+ )
1047
+
1048
+ if not team:
1049
+ raise ExecutionEnvironmentResolutionError(
1050
+ f"Team {team_id} not found in organization {org_id}"
1051
+ )
1052
+
1053
+ # Get environment-level configs
1054
+ environment_ids = team.environment_ids or []
1055
+
1056
+ # Fetch environment names for dataset scoping
1057
+ environment_names = []
1058
+ if environment_ids:
1059
+ environments = (
1060
+ db.query(Environment)
1061
+ .filter(Environment.id.in_(environment_ids))
1062
+ .all()
1063
+ )
1064
+ environment_names = [env.name for env in environments]
1065
+
1066
+ env_config = await resolve_environment_configs(environment_ids, org_id, db)
1067
+
1068
+ # Get team-level config
1069
+ team_exec_env = team.execution_environment or {}
1070
+
1071
+ # Merge: environment config + team config (team overrides environment)
1072
+ execution_environment = {
1073
+ "env_vars": {
1074
+ **env_config.get("env_vars", {}),
1075
+ **team_exec_env.get("env_vars", {}),
1076
+ },
1077
+ "secrets": list(
1078
+ set(env_config.get("secrets", []) + team_exec_env.get("secrets", []))
1079
+ ),
1080
+ "integration_ids": list(
1081
+ set(
1082
+ env_config.get("integration_ids", [])
1083
+ + team_exec_env.get("integration_ids", [])
1084
+ )
1085
+ ),
1086
+ "mcp_servers": {
1087
+ **env_config.get("mcp_servers", {}),
1088
+ **team_exec_env.get("mcp_servers", {}),
1089
+ },
1090
+ }
1091
+
1092
+ # Start with custom env vars
1093
+ resolved_env_vars = dict(execution_environment.get("env_vars", {}))
1094
+ resolved_secrets = {}
1095
+ resolved_integrations = [] # Track resolved integrations for context
1096
+
1097
+ # Add KUBIYA_API_KEY to resolved_env_vars for template resolution
1098
+ # This allows MCP server configs to use {{KUBIYA_API_KEY}} templates
1099
+ # Use the kubiya_token parameter (from request) since Control Plane doesn't have KUBIYA_API_KEY in env
1100
+ if kubiya_token:
1101
+ resolved_env_vars["KUBIYA_API_KEY"] = kubiya_token
1102
+
1103
+ # Resolve secrets
1104
+ secrets = execution_environment.get("secrets", [])
1105
+ for secret_name in secrets:
1106
+ try:
1107
+ secret_value = await resolve_secret_value(
1108
+ secret_name, kubiya_token, org_id
1109
+ )
1110
+ resolved_env_vars[secret_name] = secret_value
1111
+ resolved_secrets[secret_name] = secret_value
1112
+ logger.debug(
1113
+ "secret_resolved",
1114
+ team_id=team_id[:8],
1115
+ secret_name=secret_name[:20],
1116
+ )
1117
+ except Exception as e:
1118
+ logger.error(
1119
+ "secret_resolution_error",
1120
+ team_id=team_id[:8],
1121
+ secret_name=secret_name[:20],
1122
+ error=str(e),
1123
+ )
1124
+
1125
+ # Resolve integrations
1126
+ integration_ids = execution_environment.get("integration_ids", [])
1127
+ if integration_ids:
1128
+ headers = {
1129
+ "Authorization": f"UserKey {kubiya_token}",
1130
+ "Accept": "application/json",
1131
+ "Content-Type": "application/json",
1132
+ "X-Kubiya-Client": "agent-control-plane",
1133
+ "X-Organization-ID": org_id,
1134
+ }
1135
+
1136
+ async with httpx.AsyncClient(timeout=30.0) as client:
1137
+ response = await client.get(
1138
+ f"{KUBIYA_API_BASE}/api/v2/integrations?full=true",
1139
+ headers=headers,
1140
+ )
1141
+
1142
+ if response.status_code == 200:
1143
+ all_integrations = response.json()
1144
+
1145
+ for integration_id in integration_ids:
1146
+ integration = next(
1147
+ (
1148
+ i
1149
+ for i in all_integrations
1150
+ if i.get("uuid") == integration_id
1151
+ ),
1152
+ None,
1153
+ )
1154
+
1155
+ if integration:
1156
+ integration_type = integration.get("integration_type", "")
1157
+ integration_name = integration.get("name", integration_type)
1158
+ try:
1159
+ token_env_vars = await resolve_integration_token(
1160
+ integration_id,
1161
+ integration_type,
1162
+ kubiya_token,
1163
+ org_id,
1164
+ )
1165
+
1166
+ # Handle AWS credentials file creation
1167
+ if integration_type in ["aws", "aws-serviceaccount"] and token_env_vars:
1168
+ if "AWS_ACCESS_KEY_ID" in token_env_vars and "AWS_SECRET_ACCESS_KEY" in token_env_vars:
1169
+ try:
1170
+ creds_path, config_path = create_aws_credentials_file(
1171
+ access_key_id=token_env_vars["AWS_ACCESS_KEY_ID"],
1172
+ secret_access_key=token_env_vars["AWS_SECRET_ACCESS_KEY"],
1173
+ session_token=token_env_vars.get("AWS_SESSION_TOKEN"),
1174
+ region=token_env_vars.get("AWS_REGION"),
1175
+ )
1176
+ resolved_env_vars["AWS_SHARED_CREDENTIALS_FILE"] = creds_path
1177
+ resolved_env_vars["AWS_CONFIG_FILE"] = config_path
1178
+ except Exception as e:
1179
+ logger.warning(
1180
+ "aws_credentials_file_creation_failed",
1181
+ error=str(e),
1182
+ )
1183
+
1184
+ # Handle Kubernetes kubeconfig file creation
1185
+ elif integration_type == "kubernetes" and "KUBECONFIG_CONTENT" in token_env_vars:
1186
+ try:
1187
+ kubeconfig_path = create_kubeconfig_file(
1188
+ token_env_vars["KUBECONFIG_CONTENT"]
1189
+ )
1190
+ resolved_env_vars["KUBECONFIG"] = kubeconfig_path
1191
+ del token_env_vars["KUBECONFIG_CONTENT"]
1192
+ except Exception as e:
1193
+ logger.warning(
1194
+ "kubeconfig_file_creation_failed",
1195
+ error=str(e),
1196
+ )
1197
+
1198
+ resolved_env_vars.update(token_env_vars)
1199
+
1200
+ # Track integration for context
1201
+ resolved_integrations.append({
1202
+ "integration_type": integration_type,
1203
+ "name": integration_name,
1204
+ "env_vars": list(token_env_vars.keys()),
1205
+ })
1206
+
1207
+ logger.debug(
1208
+ "integration_resolved",
1209
+ team_id=team_id[:8],
1210
+ integration_id=integration_id[:8],
1211
+ integration_type=integration_type,
1212
+ env_var_count=len(token_env_vars),
1213
+ )
1214
+ except Exception as e:
1215
+ logger.error(
1216
+ "integration_resolution_error",
1217
+ team_id=team_id[:8],
1218
+ integration_id=integration_id[:8],
1219
+ error=str(e),
1220
+ )
1221
+
1222
+ # Build complete config to resolve templates
1223
+ complete_config = {
1224
+ "instructions": (
1225
+ team.configuration.get("instructions") if team.configuration else None
1226
+ ),
1227
+ "description": team.description,
1228
+ "configuration": team.configuration or {},
1229
+ "mcp_servers": execution_environment.get("mcp_servers", {}),
1230
+ "env_vars": execution_environment.get("env_vars", {}),
1231
+ }
1232
+
1233
+ # Apply template resolution to ENTIRE config
1234
+ resolved_config = apply_template_resolution(
1235
+ complete_config, resolved_secrets, resolved_env_vars
1236
+ )
1237
+
1238
+ # Build integration context and inject into instructions
1239
+ integration_context = build_integration_context(resolved_integrations)
1240
+ instructions = resolved_config.get("instructions", "")
1241
+
1242
+ if integration_context and instructions:
1243
+ # Append integration context to instructions
1244
+ instructions = f"{instructions}\n\n{integration_context}"
1245
+
1246
+ # Add memory and context graph guidance to instructions
1247
+ memory_guidance = """
1248
+
1249
+ ## Memory & Context Graph Tools
1250
+
1251
+ You have built-in persistent memory and organizational context awareness:
1252
+
1253
+ **Memory Tools** (use proactively):
1254
+ - **store_memory(content, metadata)**: Store important facts, decisions, preferences, or context
1255
+ - **recall_memory(query, limit)**: Retrieve stored memories using semantic search
1256
+
1257
+ **Context Graph** (discover organizational data):
1258
+ - **search_nodes(label, property_name, property_value)**: Find resources by type and properties
1259
+ - **search_by_text(property_name, search_text)**: Text search across the graph
1260
+ - **get_node(node_id)**: Get detailed information about a specific resource
1261
+
1262
+ **Best Practices**:
1263
+ - Store user preferences, task decisions, and important context as you learn them
1264
+ - Use recall_memory at the start of tasks to check for relevant past context
1265
+ - Search the graph to discover infrastructure, services, and relationships
1266
+ - Add descriptive metadata when storing memories for better retrieval"""
1267
+
1268
+ if instructions:
1269
+ instructions = f"{instructions}\n{memory_guidance}"
1270
+ else:
1271
+ instructions = memory_guidance.strip()
1272
+
1273
+ # Get context graph API URL from settings for memory tools
1274
+ from control_plane_api.app.config import settings
1275
+ graph_api_url = settings.context_graph_api_base
1276
+ dataset_name = environment_names[0] if environment_names else "default"
1277
+
1278
+ # Inject memory dataset name into env vars
1279
+ # Worker skills will fetch graph URL from control plane's /api/v1/client/config endpoint
1280
+ resolved_env_vars["MEMORY_DATASET_NAME"] = dataset_name
1281
+
1282
+ logger.info(
1283
+ "team_execution_environment_resolved",
1284
+ team_id=team_id[:8],
1285
+ env_var_count=len(resolved_env_vars),
1286
+ mcp_server_count=len(resolved_config.get("mcp_servers", {})),
1287
+ secrets_count=len(resolved_secrets),
1288
+ integrations_count=len(resolved_integrations),
1289
+ graph_api_url=graph_api_url,
1290
+ dataset_name=dataset_name,
1291
+ )
1292
+
1293
+ return {
1294
+ "env_vars": resolved_env_vars,
1295
+ "mcp_servers": resolved_config.get("mcp_servers", {}),
1296
+ "instructions": instructions,
1297
+ "description": resolved_config.get("description"),
1298
+ "configuration": resolved_config.get("configuration", {}),
1299
+ # Context graph configuration for memory tools
1300
+ "graph_api_url": graph_api_url,
1301
+ "dataset_name": dataset_name,
1302
+ }
1303
+
1304
+ except ExecutionEnvironmentResolutionError:
1305
+ raise
1306
+ except Exception as e:
1307
+ logger.error(
1308
+ "team_execution_environment_resolution_error",
1309
+ team_id=team_id[:8],
1310
+ error=str(e),
1311
+ exc_info=True,
1312
+ )
1313
+ raise ExecutionEnvironmentResolutionError(
1314
+ f"Failed to resolve execution environment for team {team_id}: {str(e)}"
1315
+ )