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,1010 @@
1
+ """
2
+ Multi-tenant skills router.
3
+
4
+ This router handles skill CRUD operations and associations with agents/teams/environments.
5
+ All operations are scoped to the authenticated organization.
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, field_validator
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
18
+ from sqlalchemy import desc
19
+ from sqlalchemy.inspection import inspect
20
+ from control_plane_api.app.models.skill import Skill, SkillAssociation
21
+ from control_plane_api.app.models.agent import Agent
22
+ from control_plane_api.app.models.team import Team
23
+ from control_plane_api.app.models.environment import Environment
24
+ from control_plane_api.app.models.associations import AgentEnvironment, TeamEnvironment
25
+ from control_plane_api.app.lib.kubiya_client import get_kubiya_client
26
+ from control_plane_api.app.skills import get_all_skills, get_skill, SkillType
27
+
28
+ logger = structlog.get_logger()
29
+
30
+ router = APIRouter()
31
+
32
+
33
+ # Pydantic schemas
34
+ class ToolSetConfiguration(BaseModel):
35
+ """Configuration for a skill"""
36
+ # File System
37
+ base_dir: Optional[str] = None
38
+ enable_save_file: Optional[bool] = None
39
+ enable_read_file: Optional[bool] = None
40
+ enable_list_files: Optional[bool] = None
41
+ enable_search_files: Optional[bool] = None
42
+
43
+ # Shell
44
+ allowed_commands: Optional[List[str]] = None
45
+ blocked_commands: Optional[List[str]] = None
46
+ timeout: Optional[int] = None
47
+
48
+ # Docker
49
+ enable_container_management: Optional[bool] = None
50
+ enable_image_management: Optional[bool] = None
51
+ enable_volume_management: Optional[bool] = None
52
+ enable_network_management: Optional[bool] = None
53
+
54
+ # Python
55
+ enable_code_execution: Optional[bool] = None
56
+ allowed_imports: Optional[List[str]] = None
57
+ blocked_imports: Optional[List[str]] = None
58
+
59
+ # File Generation
60
+ enable_json_generation: Optional[bool] = None
61
+ enable_csv_generation: Optional[bool] = None
62
+ enable_pdf_generation: Optional[bool] = None
63
+ enable_txt_generation: Optional[bool] = None
64
+ output_directory: Optional[str] = None
65
+
66
+ # Data Visualization
67
+ max_diagram_size: Optional[int] = None
68
+ enable_flowchart: Optional[bool] = None
69
+ enable_sequence: Optional[bool] = None
70
+ enable_class_diagram: Optional[bool] = None
71
+ enable_er_diagram: Optional[bool] = None
72
+ enable_gantt: Optional[bool] = None
73
+ enable_pie_chart: Optional[bool] = None
74
+ enable_state_diagram: Optional[bool] = None
75
+ enable_git_graph: Optional[bool] = None
76
+ enable_user_journey: Optional[bool] = None
77
+ enable_quadrant_chart: Optional[bool] = None
78
+
79
+ # Workflow Executor
80
+ workflow_type: Optional[str] = Field(None, description="Workflow type: 'json' or 'python_dsl'")
81
+ workflow_definition: Optional[str] = Field(None, description="JSON workflow definition as string")
82
+ python_dsl_code: Optional[str] = Field(None, description="Python DSL code for workflow")
83
+ validation_enabled: Optional[bool] = Field(None, description="Enable workflow validation")
84
+ default_runner: Optional[str] = Field(None, description="Default runner/environment name")
85
+
86
+ # Custom
87
+ custom_class: Optional[str] = None
88
+ custom_config: Optional[dict] = None
89
+
90
+
91
+ class ToolSetCreate(BaseModel):
92
+ name: str = Field(..., description="Skill name")
93
+ type: str = Field(..., description="Skill type (file_system, shell, docker, python, etc.)")
94
+ description: Optional[str] = Field(None, description="Skill description")
95
+ icon: Optional[str] = Field("Wrench", description="Icon name")
96
+ enabled: bool = Field(True, description="Whether skill is enabled")
97
+ configuration: ToolSetConfiguration = Field(default_factory=ToolSetConfiguration)
98
+
99
+
100
+ class ToolSetUpdate(BaseModel):
101
+ name: Optional[str] = None
102
+ description: Optional[str] = None
103
+ icon: Optional[str] = None
104
+ enabled: Optional[bool] = None
105
+ configuration: Optional[ToolSetConfiguration] = None
106
+
107
+
108
+ class ToolSetResponse(BaseModel):
109
+ id: str
110
+ organization_id: str
111
+ name: str
112
+ type: str # Aliased from skill_type in SQL query
113
+ description: Optional[str]
114
+ icon: str
115
+ enabled: bool
116
+ configuration: dict
117
+ created_at: datetime
118
+ updated_at: datetime
119
+
120
+ @field_validator("id", mode="before")
121
+ def ensure_id_is_string(cls, v):
122
+ if v is None:
123
+ return None
124
+ return str(v)
125
+
126
+
127
+ class ToolSetAssociationCreate(BaseModel):
128
+ skill_id: str = Field(..., description="Skill ID to associate")
129
+ configuration_override: Optional[ToolSetConfiguration] = Field(None, description="Entity-specific config overrides")
130
+
131
+
132
+ class ResolvedToolSet(BaseModel):
133
+ id: str
134
+ name: str
135
+ type: str
136
+ description: Optional[str]
137
+ icon: str
138
+ enabled: bool
139
+ configuration: dict
140
+ source: str # "environment", "team", "agent"
141
+ inherited: bool
142
+
143
+
144
+ # Helper functions
145
+ def get_skill_by_id(db: Session, organization_id: str, skill_id: str) -> dict:
146
+ """Get a skill by ID, scoped to organization"""
147
+ skill = db.query(Skill).filter(
148
+ Skill.organization_id == organization_id,
149
+ Skill.id == skill_id
150
+ ).first()
151
+
152
+ if not skill:
153
+ raise HTTPException(status_code=404, detail=f"Skill {skill_id} not found")
154
+
155
+ # Convert to dict and alias skill_type as type
156
+ skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
157
+ # Convert UUID fields to strings for Pydantic validation
158
+ if "id" in skill_dict and skill_dict["id"] is not None:
159
+ skill_dict["id"] = str(skill_dict["id"])
160
+ skill_dict["type"] = skill_dict.pop("skill_type")
161
+ return skill_dict
162
+
163
+
164
+ def get_entity_skills(db: Session, organization_id: str, entity_type: str, entity_id: str) -> List[dict]:
165
+ """Get skills associated with an entity"""
166
+ # Get associations with joined skills
167
+ associations = db.query(SkillAssociation).join(Skill).filter(
168
+ SkillAssociation.organization_id == organization_id,
169
+ SkillAssociation.entity_type == entity_type,
170
+ SkillAssociation.entity_id == entity_id,
171
+ Skill.enabled == True
172
+ ).all()
173
+
174
+ skills = []
175
+ for assoc in associations:
176
+ skill = assoc.skill
177
+ # Convert skill to dict and alias skill_type as type
178
+ skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
179
+ # Convert UUID fields to strings for Pydantic validation
180
+ if "id" in skill_dict and skill_dict["id"] is not None:
181
+ skill_dict["id"] = str(skill_dict["id"])
182
+ skill_dict["type"] = skill_dict.pop("skill_type")
183
+
184
+ # Merge configuration with override
185
+ config = skill_dict.get("configuration", {})
186
+ override = assoc.configuration_override
187
+ if override:
188
+ config = {**config, **override}
189
+
190
+ skill_dict["configuration"] = config
191
+ skills.append(skill_dict)
192
+
193
+ return skills
194
+
195
+
196
+ def merge_configurations(base: dict, override: dict) -> dict:
197
+ """Merge two configuration dictionaries, with override taking precedence"""
198
+ result = base.copy()
199
+ for key, value in override.items():
200
+ if value is not None:
201
+ result[key] = value
202
+ return result
203
+
204
+
205
+ async def validate_workflow_runner(config: dict, token: str, org_id: str) -> None:
206
+ """
207
+ Validate that runners specified in workflow configuration exist.
208
+
209
+ Args:
210
+ config: Workflow executor configuration
211
+ token: Kubiya API token
212
+ org_id: Organization ID
213
+
214
+ Raises:
215
+ HTTPException: If runner validation fails
216
+ """
217
+ import json as json_lib
218
+
219
+ # Extract runners to validate
220
+ runners_to_check = []
221
+
222
+ # Check default_runner
223
+ if config.get("default_runner"):
224
+ runners_to_check.append(("default_runner", config["default_runner"]))
225
+
226
+ # Check workflow-level runner in JSON workflows
227
+ if config.get("workflow_type") == "json" and config.get("workflow_definition"):
228
+ try:
229
+ workflow_data = json_lib.loads(config["workflow_definition"])
230
+ if workflow_data.get("runner"):
231
+ runners_to_check.append(("workflow.runner", workflow_data["runner"]))
232
+ except json_lib.JSONDecodeError:
233
+ # Invalid JSON - will be caught by skill validation
234
+ pass
235
+
236
+ if not runners_to_check:
237
+ # No runners specified, will use default
238
+ return
239
+
240
+ # Fetch available runners from Kubiya API
241
+ try:
242
+ kubiya_client = get_kubiya_client()
243
+ available_runners = await kubiya_client.get_runners(token, org_id)
244
+
245
+ if not available_runners:
246
+ logger.warning(
247
+ "no_runners_available_skipping_validation",
248
+ org_id=org_id
249
+ )
250
+ return
251
+
252
+ # Extract runner names/IDs from the response
253
+ runner_names = set()
254
+ for runner in available_runners:
255
+ if isinstance(runner, dict):
256
+ # Add both 'name' and 'id' to the set
257
+ if runner.get("name"):
258
+ runner_names.add(runner["name"])
259
+ if runner.get("id"):
260
+ runner_names.add(runner["id"])
261
+
262
+ # Validate each runner
263
+ for field_name, runner_value in runners_to_check:
264
+ if runner_value not in runner_names:
265
+ available_list = sorted(list(runner_names))
266
+ raise HTTPException(
267
+ status_code=400,
268
+ detail=(
269
+ f"Invalid runner '{runner_value}' specified in {field_name}. "
270
+ f"Available runners: {', '.join(available_list) if available_list else 'none'}"
271
+ )
272
+ )
273
+
274
+ logger.info(
275
+ "workflow_runners_validated",
276
+ runners_checked=[r[1] for r in runners_to_check],
277
+ available_count=len(runner_names)
278
+ )
279
+
280
+ except HTTPException:
281
+ raise
282
+ except Exception as e:
283
+ logger.error(
284
+ "runner_validation_failed",
285
+ error=str(e),
286
+ org_id=org_id
287
+ )
288
+ # Don't fail skill creation if runner validation fails
289
+ # This allows offline/testing scenarios
290
+ logger.warning("skipping_runner_validation_due_to_error")
291
+
292
+
293
+ # API Endpoints
294
+
295
+ @router.post("", response_model=ToolSetResponse, status_code=status.HTTP_201_CREATED)
296
+ async def create_skill(
297
+ skill_data: ToolSetCreate,
298
+ request: Request,
299
+ organization: dict = Depends(get_current_organization),
300
+ db: Session = Depends(get_db),
301
+ ):
302
+ """Create a new skill in the organization"""
303
+ try:
304
+ skill_id = str(uuid.uuid4())
305
+ now = datetime.utcnow()
306
+
307
+ # Validate skill type
308
+ valid_types = ["file_system", "shell", "python", "docker", "sleep", "file_generation", "data_visualization", "workflow_executor", "custom"]
309
+ if skill_data.type not in valid_types:
310
+ raise HTTPException(
311
+ status_code=400,
312
+ detail=f"Invalid skill type. Must be one of: {', '.join(valid_types)}"
313
+ )
314
+
315
+ # Validate workflow_executor runner if applicable
316
+ if skill_data.type == "workflow_executor":
317
+ config_dict = skill_data.configuration.dict(exclude_none=True)
318
+ token = request.state.kubiya_token
319
+ await validate_workflow_runner(config_dict, token, organization["id"])
320
+
321
+ skill = Skill(
322
+ id=skill_id,
323
+ organization_id=organization["id"],
324
+ name=skill_data.name,
325
+ skill_type=skill_data.type,
326
+ description=skill_data.description,
327
+ icon=skill_data.icon,
328
+ enabled=skill_data.enabled,
329
+ configuration=skill_data.configuration.dict(exclude_none=True),
330
+ created_at=now,
331
+ updated_at=now,
332
+ )
333
+
334
+ db.add(skill)
335
+ db.commit()
336
+ db.refresh(skill)
337
+
338
+ logger.info(
339
+ "skill_created",
340
+ skill_id=skill_id,
341
+ name=skill_data.name,
342
+ type=skill_data.type,
343
+ organization_id=organization["id"]
344
+ )
345
+
346
+ # Convert to dict and alias skill_type as type
347
+ skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
348
+ skill_dict["type"] = skill_dict.pop("skill_type")
349
+
350
+ return ToolSetResponse(**skill_dict)
351
+
352
+ except Exception as e:
353
+ logger.error("skill_creation_failed", error=str(e))
354
+ raise HTTPException(status_code=500, detail=str(e))
355
+
356
+
357
+ @router.get("", response_model=List[ToolSetResponse])
358
+ async def list_skills(
359
+ organization: dict = Depends(get_current_organization),
360
+ db: Session = Depends(get_db),
361
+ ):
362
+ """List all skills for the organization"""
363
+ try:
364
+ skills = db.query(Skill).filter(
365
+ Skill.organization_id == organization["id"]
366
+ ).order_by(desc(Skill.created_at)).all()
367
+
368
+ # Convert to list of dicts with aliased skill_type as type
369
+ skills_data = []
370
+ for skill in skills:
371
+ skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
372
+ skill_dict["type"] = skill_dict.pop("skill_type")
373
+ skills_data.append(ToolSetResponse(**skill_dict))
374
+
375
+ return skills_data
376
+
377
+ except Exception as e:
378
+ logger.error("skill_list_failed", error=str(e))
379
+ raise HTTPException(status_code=500, detail=str(e))
380
+
381
+
382
+ @router.get("/{skill_id}", response_model=ToolSetResponse)
383
+ async def get_skill_endpoint(
384
+ skill_id: str,
385
+ organization: dict = Depends(get_current_organization),
386
+ db: Session = Depends(get_db),
387
+ ):
388
+ """Get a specific skill"""
389
+ try:
390
+ skill = get_skill_by_id(db, organization["id"], skill_id)
391
+ return ToolSetResponse(**skill)
392
+
393
+ except HTTPException:
394
+ raise
395
+ except Exception as e:
396
+ logger.error("skill_get_failed", error=str(e), skill_id=skill_id)
397
+ raise HTTPException(status_code=500, detail=str(e))
398
+
399
+
400
+ @router.patch("/{skill_id}", response_model=ToolSetResponse)
401
+ async def update_skill(
402
+ skill_id: str,
403
+ skill_data: ToolSetUpdate,
404
+ request: Request,
405
+ organization: dict = Depends(get_current_organization),
406
+ db: Session = Depends(get_db),
407
+ ):
408
+ """Update a skill"""
409
+ try:
410
+ # Verify skill exists
411
+ skill = db.query(Skill).filter(
412
+ Skill.id == skill_id,
413
+ Skill.organization_id == organization["id"]
414
+ ).first()
415
+
416
+ if not skill:
417
+ raise HTTPException(status_code=404, detail=f"Skill {skill_id} not found")
418
+
419
+ # Build update dict
420
+ update_data = skill_data.dict(exclude_none=True)
421
+ if "configuration" in update_data:
422
+ update_data["configuration"] = update_data["configuration"]
423
+ update_data["updated_at"] = datetime.utcnow()
424
+
425
+ # Validate workflow_executor runner if updating configuration
426
+ if skill.skill_type == "workflow_executor" and "configuration" in update_data:
427
+ # Merge existing config with updates for complete validation
428
+ merged_config = {**(skill.configuration or {}), **update_data["configuration"]}
429
+ token = request.state.kubiya_token
430
+ await validate_workflow_runner(merged_config, token, organization["id"])
431
+
432
+ # Apply updates
433
+ for key, value in update_data.items():
434
+ setattr(skill, key, value)
435
+
436
+ db.commit()
437
+ db.refresh(skill)
438
+
439
+ logger.info("skill_updated", skill_id=skill_id, organization_id=organization["id"])
440
+
441
+ # Convert to dict and alias skill_type as type
442
+ skill_dict = {c.key: getattr(skill, c.key) for c in inspect(skill).mapper.column_attrs}
443
+ skill_dict["type"] = skill_dict.pop("skill_type")
444
+
445
+ return ToolSetResponse(**skill_dict)
446
+
447
+ except HTTPException:
448
+ raise
449
+ except Exception as e:
450
+ logger.error("skill_update_failed", error=str(e), skill_id=skill_id)
451
+ raise HTTPException(status_code=500, detail=str(e))
452
+
453
+
454
+ @router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT)
455
+ async def delete_skill(
456
+ skill_id: str,
457
+ organization: dict = Depends(get_current_organization),
458
+ db: Session = Depends(get_db),
459
+ ):
460
+ """Delete a skill"""
461
+ try:
462
+ # Verify skill exists
463
+ skill = db.query(Skill).filter(
464
+ Skill.id == skill_id,
465
+ Skill.organization_id == organization["id"]
466
+ ).first()
467
+
468
+ if not skill:
469
+ raise HTTPException(status_code=404, detail=f"Skill {skill_id} not found")
470
+
471
+ # Delete skill (cascade will handle associations)
472
+ db.delete(skill)
473
+ db.commit()
474
+
475
+ logger.info("skill_deleted", skill_id=skill_id, organization_id=organization["id"])
476
+
477
+ except HTTPException:
478
+ raise
479
+ except Exception as e:
480
+ logger.error("skill_delete_failed", error=str(e), skill_id=skill_id)
481
+ raise HTTPException(status_code=500, detail=str(e))
482
+
483
+
484
+ # Association endpoints for agents
485
+ @router.post("/associations/{entity_type}/{entity_id}/skills", status_code=status.HTTP_201_CREATED)
486
+ async def associate_skill(
487
+ entity_type: str,
488
+ entity_id: str,
489
+ association_data: ToolSetAssociationCreate,
490
+ organization: dict = Depends(get_current_organization),
491
+ db: Session = Depends(get_db),
492
+ ):
493
+ """Associate a skill with an entity (agent, team, environment)"""
494
+ try:
495
+ # Validate entity type
496
+ if entity_type not in ["agent", "team", "environment"]:
497
+ raise HTTPException(status_code=400, detail="Invalid entity type. Must be: agent, team, or environment")
498
+
499
+ # Verify skill exists
500
+ get_skill_by_id(db, organization["id"], association_data.skill_id)
501
+
502
+ # Verify entity exists (check appropriate table)
503
+ entity_model = None
504
+ if entity_type == "agent":
505
+ entity_model = Agent
506
+ elif entity_type == "team":
507
+ entity_model = Team
508
+ elif entity_type == "environment":
509
+ entity_model = Environment
510
+
511
+ entity = db.query(entity_model).filter(
512
+ entity_model.id == entity_id,
513
+ entity_model.organization_id == organization["id"]
514
+ ).first()
515
+
516
+ if not entity:
517
+ raise HTTPException(status_code=404, detail=f"{entity_type.capitalize()} {entity_id} not found")
518
+
519
+ # Create association
520
+ association_id = str(uuid.uuid4())
521
+ association = SkillAssociation(
522
+ id=association_id,
523
+ organization_id=organization["id"],
524
+ skill_id=association_data.skill_id,
525
+ entity_type=entity_type,
526
+ entity_id=entity_id,
527
+ configuration_override=association_data.configuration_override.dict(exclude_none=True) if association_data.configuration_override else {},
528
+ created_at=datetime.utcnow(),
529
+ )
530
+
531
+ db.add(association)
532
+
533
+ # Also update denormalized skill_ids array (only for teams)
534
+ # Agents and environments don't have a skill_ids column - they only use the skill_associations junction table
535
+ # Teams have a denormalized skill_ids array for performance
536
+ if entity_type == "team":
537
+ current_ids = entity.skill_ids or []
538
+ if association_data.skill_id not in current_ids:
539
+ updated_ids = current_ids + [association_data.skill_id]
540
+ entity.skill_ids = updated_ids
541
+
542
+ db.commit()
543
+
544
+ logger.info(
545
+ "skill_associated",
546
+ skill_id=association_data.skill_id,
547
+ entity_type=entity_type,
548
+ entity_id=entity_id,
549
+ organization_id=organization["id"]
550
+ )
551
+
552
+ return {"message": "Skill associated successfully"}
553
+
554
+ except HTTPException:
555
+ raise
556
+ except Exception as e:
557
+ logger.error("skill_association_failed", error=str(e))
558
+ raise HTTPException(status_code=500, detail=str(e))
559
+
560
+
561
+ @router.get("/associations/{entity_type}/{entity_id}/skills", response_model=List[ToolSetResponse])
562
+ async def list_entity_skills(
563
+ entity_type: str,
564
+ entity_id: str,
565
+ organization: dict = Depends(get_current_organization),
566
+ db: Session = Depends(get_db),
567
+ ):
568
+ """List skills associated with an entity"""
569
+ try:
570
+ if entity_type not in ["agent", "team", "environment"]:
571
+ raise HTTPException(status_code=400, detail="Invalid entity type")
572
+
573
+ skills = get_entity_skills(db, organization["id"], entity_type, entity_id)
574
+ return [ToolSetResponse(**skill) for skill in skills]
575
+
576
+ except HTTPException:
577
+ raise
578
+ except Exception as e:
579
+ logger.error("list_entity_skills_failed", error=str(e))
580
+ raise HTTPException(status_code=500, detail=str(e))
581
+
582
+
583
+ @router.delete("/associations/{entity_type}/{entity_id}/skills/{skill_id}", status_code=status.HTTP_204_NO_CONTENT)
584
+ async def dissociate_skill(
585
+ entity_type: str,
586
+ entity_id: str,
587
+ skill_id: str,
588
+ organization: dict = Depends(get_current_organization),
589
+ db: Session = Depends(get_db),
590
+ ):
591
+ """Remove a skill association from an entity"""
592
+ try:
593
+ if entity_type not in ["agent", "team", "environment"]:
594
+ raise HTTPException(status_code=400, detail="Invalid entity type")
595
+
596
+ # Delete association
597
+ db.query(SkillAssociation).filter(
598
+ SkillAssociation.skill_id == skill_id,
599
+ SkillAssociation.entity_type == entity_type,
600
+ SkillAssociation.entity_id == entity_id
601
+ ).delete()
602
+
603
+ # Update denormalized skill_ids array (only for teams)
604
+ # Agents and environments don't have a skill_ids column - they only use the skill_associations junction table
605
+ # Teams have a denormalized skill_ids array for performance
606
+ if entity_type == "team":
607
+ team = db.query(Team).filter(Team.id == entity_id).first()
608
+ if team:
609
+ current_ids = team.skill_ids or []
610
+ updated_ids = [tid for tid in current_ids if tid != skill_id]
611
+ team.skill_ids = updated_ids
612
+
613
+ db.commit()
614
+
615
+ logger.info(
616
+ "skill_dissociated",
617
+ skill_id=skill_id,
618
+ entity_type=entity_type,
619
+ entity_id=entity_id,
620
+ organization_id=organization["id"]
621
+ )
622
+
623
+ except Exception as e:
624
+ logger.error("skill_dissociation_failed", error=str(e))
625
+ raise HTTPException(status_code=500, detail=str(e))
626
+
627
+
628
+ @router.get("/associations/agents/{agent_id}/skills/resolved", response_model=List[ResolvedToolSet])
629
+ async def resolve_agent_skills(
630
+ agent_id: str,
631
+ organization: dict = Depends(get_current_organization),
632
+ db: Session = Depends(get_db),
633
+ ):
634
+ """
635
+ Resolve all skills for an agent (including inherited from ALL environments and team).
636
+
637
+ Inheritance order (with deduplication):
638
+ 1. All agent environments
639
+ 2. All team environments (if agent has team)
640
+ 3. Team skills
641
+ 4. Agent skills
642
+
643
+ Later layers override earlier ones if there are conflicts.
644
+ """
645
+ try:
646
+ # Get agent details
647
+ agent = db.query(Agent).filter(
648
+ Agent.id == agent_id,
649
+ Agent.organization_id == organization["id"]
650
+ ).first()
651
+
652
+ if not agent:
653
+ raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
654
+
655
+ resolved_skills = []
656
+ seen_ids = set()
657
+
658
+ # 1. Load skills from ALL agent environments (many-to-many)
659
+ agent_envs = db.query(AgentEnvironment).filter(
660
+ AgentEnvironment.agent_id == agent_id
661
+ ).all()
662
+
663
+ agent_environment_ids = [str(env.environment_id) for env in agent_envs]
664
+
665
+ for environment_id in agent_environment_ids:
666
+ env_skills = get_entity_skills(db, organization["id"], "environment", environment_id)
667
+ for skill in env_skills:
668
+ if skill["id"] not in seen_ids:
669
+ resolved_skills.append(ResolvedToolSet(
670
+ **skill,
671
+ source="environment",
672
+ inherited=True
673
+ ))
674
+ seen_ids.add(skill["id"])
675
+
676
+ # 2. Load skills from ALL team environments (if agent has team)
677
+ team_id = agent.team_id
678
+ team_environment_ids = []
679
+ if team_id:
680
+ team_envs = db.query(TeamEnvironment).filter(
681
+ TeamEnvironment.team_id == team_id
682
+ ).all()
683
+
684
+ team_environment_ids = [str(env.environment_id) for env in team_envs]
685
+
686
+ for environment_id in team_environment_ids:
687
+ env_skills = get_entity_skills(db, organization["id"], "environment", environment_id)
688
+ for skill in env_skills:
689
+ if skill["id"] not in seen_ids:
690
+ resolved_skills.append(ResolvedToolSet(
691
+ **skill,
692
+ source="environment",
693
+ inherited=True
694
+ ))
695
+ seen_ids.add(skill["id"])
696
+
697
+ # 3. Load team skills
698
+ team_skills = get_entity_skills(db, organization["id"], "team", str(team_id))
699
+ for skill in team_skills:
700
+ if skill["id"] not in seen_ids:
701
+ resolved_skills.append(ResolvedToolSet(
702
+ **skill,
703
+ source="team",
704
+ inherited=True
705
+ ))
706
+ seen_ids.add(skill["id"])
707
+
708
+ # 4. Load agent skills (highest priority)
709
+ agent_skills = get_entity_skills(db, organization["id"], "agent", agent_id)
710
+ for skill in agent_skills:
711
+ if skill["id"] not in seen_ids:
712
+ resolved_skills.append(ResolvedToolSet(
713
+ **skill,
714
+ source="agent",
715
+ inherited=False
716
+ ))
717
+ seen_ids.add(skill["id"])
718
+
719
+ logger.info(
720
+ "agent_skills_resolved",
721
+ agent_id=agent_id,
722
+ skill_count=len(resolved_skills),
723
+ agent_env_count=len(agent_environment_ids),
724
+ team_env_count=len(team_environment_ids) if team_id else 0,
725
+ organization_id=organization["id"]
726
+ )
727
+
728
+ return resolved_skills
729
+
730
+ except HTTPException:
731
+ raise
732
+ except Exception as e:
733
+ logger.error("resolve_agent_skills_failed", error=str(e), agent_id=agent_id)
734
+ raise HTTPException(status_code=500, detail=str(e))
735
+
736
+
737
+ @router.get("/associations/agents/{agent_id}/toolsets/resolved", response_model=List[ResolvedToolSet])
738
+ async def resolve_agent_toolsets_legacy(
739
+ agent_id: str,
740
+ organization: dict = Depends(get_current_organization),
741
+ db: Session = Depends(get_db),
742
+ ):
743
+ """
744
+ DEPRECATED: Legacy endpoint for backward compatibility.
745
+ Use /associations/agents/{agent_id}/skills/resolved instead.
746
+
747
+ This endpoint redirects to the new skills endpoint.
748
+ """
749
+ logger.warning(
750
+ "deprecated_toolsets_endpoint_used",
751
+ agent_id=agent_id,
752
+ endpoint="/associations/agents/{agent_id}/toolsets/resolved",
753
+ new_endpoint="/associations/agents/{agent_id}/skills/resolved"
754
+ )
755
+ return await resolve_agent_skills(agent_id, organization, db)
756
+
757
+
758
+ @router.get("/associations/teams/{team_id}/skills/resolved", response_model=List[ResolvedToolSet])
759
+ async def resolve_team_skills(
760
+ team_id: str,
761
+ organization: dict = Depends(get_current_organization),
762
+ db: Session = Depends(get_db),
763
+ ):
764
+ """
765
+ Resolve all skills for a team (including inherited from ALL environments).
766
+
767
+ Inheritance order (with deduplication):
768
+ 1. All team environments
769
+ 2. Team skills
770
+
771
+ Later layers override earlier ones if there are conflicts.
772
+ """
773
+ try:
774
+ # Get team details
775
+ team = db.query(Team).filter(
776
+ Team.id == team_id,
777
+ Team.organization_id == organization["id"]
778
+ ).first()
779
+
780
+ if not team:
781
+ raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
782
+
783
+ resolved_skills = []
784
+ seen_ids = set()
785
+
786
+ # 1. Load skills from ALL team environments (many-to-many)
787
+ team_envs = db.query(TeamEnvironment).filter(
788
+ TeamEnvironment.team_id == team_id
789
+ ).all()
790
+
791
+ team_environment_ids = [str(env.environment_id) for env in team_envs]
792
+
793
+ for environment_id in team_environment_ids:
794
+ env_skills = get_entity_skills(db, organization["id"], "environment", environment_id)
795
+ for skill in env_skills:
796
+ if skill["id"] not in seen_ids:
797
+ resolved_skills.append(ResolvedToolSet(
798
+ **skill,
799
+ source="environment",
800
+ inherited=True
801
+ ))
802
+ seen_ids.add(skill["id"])
803
+
804
+ # 2. Load team skills (highest priority)
805
+ team_skills = get_entity_skills(db, organization["id"], "team", team_id)
806
+ for skill in team_skills:
807
+ if skill["id"] not in seen_ids:
808
+ resolved_skills.append(ResolvedToolSet(
809
+ **skill,
810
+ source="team",
811
+ inherited=False
812
+ ))
813
+ seen_ids.add(skill["id"])
814
+
815
+ logger.info(
816
+ "team_skills_resolved",
817
+ team_id=team_id,
818
+ skill_count=len(resolved_skills),
819
+ team_env_count=len(team_environment_ids),
820
+ organization_id=organization["id"]
821
+ )
822
+
823
+ return resolved_skills
824
+
825
+ except HTTPException:
826
+ raise
827
+ except Exception as e:
828
+ logger.error("resolve_team_skills_failed", error=str(e), team_id=team_id)
829
+ raise HTTPException(status_code=500, detail=str(e))
830
+
831
+
832
+ @router.get("/associations/teams/{team_id}/toolsets/resolved", response_model=List[ResolvedToolSet])
833
+ async def resolve_team_toolsets_legacy(
834
+ team_id: str,
835
+ organization: dict = Depends(get_current_organization),
836
+ db: Session = Depends(get_db),
837
+ ):
838
+ """
839
+ DEPRECATED: Legacy endpoint for backward compatibility.
840
+ Use /associations/teams/{team_id}/skills/resolved instead.
841
+
842
+ This endpoint redirects to the new skills endpoint.
843
+ """
844
+ logger.warning(
845
+ "deprecated_toolsets_endpoint_used",
846
+ team_id=team_id,
847
+ endpoint="/associations/teams/{team_id}/toolsets/resolved",
848
+ new_endpoint="/associations/teams/{team_id}/skills/resolved"
849
+ )
850
+ return await resolve_team_skills(team_id, organization, db)
851
+
852
+
853
+ @router.get("/types")
854
+ async def get_skill_types():
855
+ """Get available skill types and their descriptions"""
856
+ return {
857
+ "types": [
858
+ {
859
+ "type": "file_system",
860
+ "name": "File System",
861
+ "description": "Read, write, list, and search files",
862
+ "icon": "FileText"
863
+ },
864
+ {
865
+ "type": "shell",
866
+ "name": "Shell",
867
+ "description": "Execute shell commands",
868
+ "icon": "Terminal"
869
+ },
870
+ {
871
+ "type": "docker",
872
+ "name": "Docker",
873
+ "description": "Manage containers, images, volumes, and networks",
874
+ "icon": "Container"
875
+ },
876
+ {
877
+ "type": "python",
878
+ "name": "Python",
879
+ "description": "Execute Python code",
880
+ "icon": "Code"
881
+ },
882
+ {
883
+ "type": "file_generation",
884
+ "name": "File Generation",
885
+ "description": "Generate JSON, CSV, PDF, and TXT files",
886
+ "icon": "FileOutput"
887
+ },
888
+ {
889
+ "type": "sleep",
890
+ "name": "Sleep",
891
+ "description": "Pause execution for a specified duration",
892
+ "icon": "Clock"
893
+ },
894
+ {
895
+ "type": "workflow_executor",
896
+ "name": "Workflow Executor",
897
+ "description": "Execute workflows defined via JSON or Python DSL",
898
+ "icon": "Workflow"
899
+ },
900
+ {
901
+ "type": "custom",
902
+ "name": "Custom",
903
+ "description": "User-defined custom skill",
904
+ "icon": "Wrench"
905
+ }
906
+ ]
907
+ }
908
+
909
+
910
+ @router.get("/templates")
911
+ async def get_skill_templates(
912
+ request: Request,
913
+ organization: dict = Depends(get_current_organization),
914
+ ):
915
+ """
916
+ Get detailed skill templates with variants and configuration schemas.
917
+
918
+ This endpoint returns all available skill templates including their variants,
919
+ configuration schemas, default values, and available runners for workflow executor skills.
920
+ Useful for UI forms and skill creation.
921
+ """
922
+ templates = []
923
+
924
+ # Get all registered skills from the skill system
925
+ all_skills = get_all_skills()
926
+
927
+ # Fetch available runners for workflow executor validation
928
+ runners_list = []
929
+ try:
930
+ kubiya_client = get_kubiya_client()
931
+ token = request.state.kubiya_token
932
+ available_runners = await kubiya_client.get_runners(token, organization["id"])
933
+
934
+ if available_runners:
935
+ for runner in available_runners:
936
+ if isinstance(runner, dict):
937
+ runners_list.append({
938
+ "id": runner.get("id"),
939
+ "name": runner.get("name"),
940
+ "status": runner.get("status"),
941
+ "capabilities": runner.get("capabilities", []),
942
+ })
943
+
944
+ logger.info(
945
+ "runners_fetched_for_templates",
946
+ org_id=organization["id"],
947
+ runner_count=len(runners_list)
948
+ )
949
+ except Exception as e:
950
+ logger.warning(
951
+ "failed_to_fetch_runners_for_templates",
952
+ error=str(e),
953
+ org_id=organization["id"]
954
+ )
955
+ # Continue without runners - they're optional
956
+
957
+ for skill_def in all_skills:
958
+ try:
959
+ # Get skill metadata
960
+ template = {
961
+ "type": skill_def.type.value,
962
+ "name": skill_def.name,
963
+ "description": skill_def.description,
964
+ "icon": skill_def.icon,
965
+ "icon_type": skill_def.icon_type,
966
+ "category": skill_def.get_category().value,
967
+ "default_configuration": skill_def.get_default_configuration(),
968
+ "requirements": {
969
+ "supported_os": skill_def.get_requirements().supported_os,
970
+ "min_python_version": skill_def.get_requirements().min_python_version,
971
+ "python_packages": skill_def.get_requirements().python_packages,
972
+ "required_env_vars": skill_def.get_requirements().required_env_vars,
973
+ "notes": skill_def.get_requirements().notes,
974
+ } if skill_def.get_requirements() else None,
975
+ "variants": []
976
+ }
977
+
978
+ # Add available runners for workflow executor skills
979
+ if skill_def.type.value == "workflow_executor":
980
+ template["available_runners"] = runners_list
981
+
982
+ # Get variants for this skill
983
+ variants = skill_def.get_variants()
984
+ for variant in variants:
985
+ template["variants"].append({
986
+ "id": variant.id,
987
+ "name": variant.name,
988
+ "description": variant.description,
989
+ "category": variant.category.value,
990
+ "icon": variant.icon,
991
+ "is_default": variant.is_default,
992
+ "configuration": variant.configuration,
993
+ "tags": variant.tags,
994
+ })
995
+
996
+ templates.append(template)
997
+
998
+ except Exception as e:
999
+ logger.error(
1000
+ "failed_to_build_skill_template",
1001
+ skill_type=skill_def.type.value if hasattr(skill_def, 'type') else 'unknown',
1002
+ error=str(e)
1003
+ )
1004
+ continue
1005
+
1006
+ return {
1007
+ "templates": templates,
1008
+ "count": len(templates),
1009
+ "runners": runners_list # Also return at root level for easy access
1010
+ }