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,579 @@
1
+ """Integration tests for hook enforcement."""
2
+
3
+ import pytest
4
+ import asyncio
5
+ from unittest.mock import AsyncMock, Mock, MagicMock
6
+ from control_plane_api.worker.runtimes.agno.hooks import (
7
+ create_tool_hook_for_streaming,
8
+ create_tool_hook_with_callback,
9
+ )
10
+ from control_plane_api.worker.runtimes.claude_code.hooks import build_hooks
11
+ from control_plane_api.worker.services.tool_enforcement import ToolEnforcementService
12
+
13
+
14
+ @pytest.fixture
15
+ def mock_control_plane():
16
+ """Mock control plane client."""
17
+ client = Mock()
18
+ client.publish_event = Mock()
19
+ return client
20
+
21
+
22
+ @pytest.fixture
23
+ def mock_enforcer_client():
24
+ """Mock enforcer client for integration tests."""
25
+ client = AsyncMock()
26
+ client.evaluation = AsyncMock()
27
+ return client
28
+
29
+
30
+ @pytest.fixture
31
+ def enforcement_service(mock_enforcer_client):
32
+ """Create enforcement service."""
33
+ return ToolEnforcementService(mock_enforcer_client)
34
+
35
+
36
+ @pytest.fixture
37
+ def enforcement_context():
38
+ """Sample enforcement context."""
39
+ return {
40
+ "organization_id": "org-123",
41
+ "user_email": "test@example.com",
42
+ "user_id": "user-456",
43
+ "user_roles": ["developer"],
44
+ "team_id": "team-789",
45
+ "agent_id": "agent-abc",
46
+ "environment": "test",
47
+ }
48
+
49
+
50
+ class TestAgnoHookEnforcement:
51
+ """Test Agno hook with enforcement integration."""
52
+
53
+ def test_streaming_hook_with_allowed_tool(
54
+ self, mock_control_plane, enforcement_service, enforcement_context, mock_enforcer_client
55
+ ):
56
+ """Test streaming hook when enforcement allows tool."""
57
+ # Mock allowed response
58
+ mock_enforcer_client.evaluation.enforce.return_value = {
59
+ "allow": True,
60
+ "id": "enf-123",
61
+ "policies": ["test_policy"],
62
+ }
63
+
64
+ # Create hook
65
+ hook = create_tool_hook_for_streaming(
66
+ control_plane=mock_control_plane,
67
+ execution_id="exec-123",
68
+ message_id="msg-456",
69
+ enforcement_context=enforcement_context,
70
+ enforcement_service=enforcement_service,
71
+ )
72
+
73
+ # Mock tool function
74
+ mock_tool = Mock(return_value="tool result")
75
+
76
+ # Execute hook
77
+ result = hook(name="Read", function=mock_tool, arguments={"file_path": "/tmp/test.txt"})
78
+
79
+ # Verify tool was executed
80
+ mock_tool.assert_called_once_with(file_path="/tmp/test.txt")
81
+ assert result == "tool result"
82
+
83
+ # Verify enforcement was called
84
+ assert mock_enforcer_client.evaluation.enforce.called
85
+
86
+ # Verify events were published
87
+ assert mock_control_plane.publish_event.call_count == 2 # start + complete
88
+
89
+ # Check that enforcement metadata is in events
90
+ calls = mock_control_plane.publish_event.call_args_list
91
+ start_event = calls[0][1]["data"]
92
+ complete_event = calls[1][1]["data"]
93
+
94
+ assert "enforcement" in start_event
95
+ assert "enforcement" in complete_event
96
+ assert complete_event["enforcement"]["enforcer"] == "allowed"
97
+
98
+ def test_streaming_hook_with_blocked_tool(
99
+ self, mock_control_plane, enforcement_service, enforcement_context, mock_enforcer_client
100
+ ):
101
+ """Test streaming hook when enforcement blocks tool."""
102
+ # Mock blocked response
103
+ mock_enforcer_client.evaluation.enforce.return_value = {
104
+ "allow": False,
105
+ "id": "enf-456",
106
+ "policies": ["production_safeguards"],
107
+ }
108
+
109
+ # Create hook
110
+ hook = create_tool_hook_for_streaming(
111
+ control_plane=mock_control_plane,
112
+ execution_id="exec-123",
113
+ message_id="msg-456",
114
+ enforcement_context=enforcement_context,
115
+ enforcement_service=enforcement_service,
116
+ )
117
+
118
+ # Mock tool function
119
+ mock_tool = Mock(return_value="original result")
120
+
121
+ # Execute hook
122
+ result = hook(name="Bash", function=mock_tool, arguments={"command": "rm -rf /"})
123
+
124
+ # Verify tool was STILL executed (non-blocking)
125
+ mock_tool.assert_called_once()
126
+
127
+ # Verify result contains violation message
128
+ assert "POLICY VIOLATION" in result
129
+ assert "Bash" in result
130
+ assert "original result" in result
131
+
132
+ # Verify enforcement metadata shows blocked
133
+ calls = mock_control_plane.publish_event.call_args_list
134
+ complete_event = calls[1][1]["data"]
135
+ assert complete_event["enforcement"]["enforcer"] == "blocked"
136
+
137
+ def test_callback_hook_with_enforcement(
138
+ self, enforcement_service, enforcement_context, mock_enforcer_client
139
+ ):
140
+ """Test callback hook with enforcement."""
141
+ # Mock allowed response
142
+ mock_enforcer_client.evaluation.enforce.return_value = {
143
+ "allow": True,
144
+ "id": "enf-789",
145
+ "policies": [],
146
+ }
147
+
148
+ # Mock callback
149
+ callback_events = []
150
+ def event_callback(event):
151
+ callback_events.append(event)
152
+
153
+ # Create hook
154
+ hook = create_tool_hook_with_callback(
155
+ execution_id="exec-123",
156
+ message_id="msg-456",
157
+ event_callback=event_callback,
158
+ enforcement_context=enforcement_context,
159
+ enforcement_service=enforcement_service,
160
+ )
161
+
162
+ # Mock tool function
163
+ mock_tool = Mock(return_value="result")
164
+
165
+ # Execute hook
166
+ result = hook(name="Read", function=mock_tool, arguments={"file_path": "/tmp/test"})
167
+
168
+ # Verify events contain enforcement metadata
169
+ assert len(callback_events) == 2 # start + complete
170
+ assert "enforcement" in callback_events[0]
171
+ assert "enforcement" in callback_events[1]
172
+
173
+ def test_hook_without_enforcement_service(self, mock_control_plane):
174
+ """Test hook works without enforcement service (disabled)."""
175
+ # Create hook WITHOUT enforcement
176
+ hook = create_tool_hook_for_streaming(
177
+ control_plane=mock_control_plane,
178
+ execution_id="exec-123",
179
+ message_id="msg-456",
180
+ enforcement_context=None,
181
+ enforcement_service=None,
182
+ )
183
+
184
+ # Mock tool function
185
+ mock_tool = Mock(return_value="result")
186
+
187
+ # Execute hook
188
+ result = hook(name="Read", function=mock_tool, arguments={"file_path": "/tmp/test"})
189
+
190
+ # Should work normally
191
+ assert result == "result"
192
+ mock_tool.assert_called_once()
193
+
194
+
195
+ class TestClaudeCodeHookEnforcement:
196
+ """Test Claude Code hooks with enforcement integration."""
197
+
198
+ @pytest.mark.asyncio
199
+ async def test_claude_hooks_with_allowed_tool(
200
+ self, enforcement_service, enforcement_context, mock_enforcer_client
201
+ ):
202
+ """Test Claude Code hooks when enforcement allows tool."""
203
+ # Mock allowed response
204
+ mock_enforcer_client.evaluation.enforce.return_value = {
205
+ "allow": True,
206
+ "id": "enf-123",
207
+ "policies": ["test_policy"],
208
+ }
209
+
210
+ # Mock callback
211
+ callback_events = []
212
+ def event_callback(event):
213
+ callback_events.append(event)
214
+
215
+ # Build hooks
216
+ active_tools = {}
217
+ started_tools = set()
218
+ completed_tools = set()
219
+ hooks = build_hooks(
220
+ execution_id="exec-123",
221
+ event_callback=event_callback,
222
+ active_tools=active_tools,
223
+ completed_tools=completed_tools,
224
+ started_tools=started_tools,
225
+ enforcement_context=enforcement_context,
226
+ enforcement_service=enforcement_service,
227
+ )
228
+
229
+ # Get hooks
230
+ pre_hook = hooks["PreToolUse"][0].hooks[0]
231
+ post_hook = hooks["PostToolUse"][0].hooks[0]
232
+
233
+ # Simulate tool execution
234
+ input_data = {"tool_name": "Read", "tool_input": {"file_path": "/tmp/test"}}
235
+ tool_use_id = "tool-123"
236
+ tool_context = {}
237
+
238
+ # Call pre-hook
239
+ await pre_hook(input_data, tool_use_id, tool_context)
240
+
241
+ # Verify enforcement was called
242
+ assert mock_enforcer_client.evaluation.enforce.called
243
+
244
+ # Verify no violation stored in context
245
+ assert "enforcement_violation" not in tool_context
246
+
247
+ # Simulate tool output
248
+ output_data = {"tool_name": "Read", "output": "file contents"}
249
+
250
+ # Call post-hook
251
+ await post_hook(output_data, tool_use_id, tool_context)
252
+
253
+ # Verify output is not modified
254
+ assert output_data["output"] == "file contents"
255
+ assert "enforcement_violated" not in output_data
256
+
257
+ @pytest.mark.asyncio
258
+ async def test_claude_hooks_with_blocked_tool(
259
+ self, enforcement_service, enforcement_context, mock_enforcer_client
260
+ ):
261
+ """Test Claude Code hooks when enforcement blocks tool."""
262
+ # Mock blocked response
263
+ mock_enforcer_client.evaluation.enforce.return_value = {
264
+ "allow": False,
265
+ "id": "enf-456",
266
+ "policies": ["bash_validation"],
267
+ }
268
+
269
+ # Mock callback
270
+ callback_events = []
271
+ def event_callback(event):
272
+ callback_events.append(event)
273
+
274
+ # Build hooks
275
+ active_tools = {}
276
+ started_tools = set()
277
+ completed_tools = set()
278
+ hooks = build_hooks(
279
+ execution_id="exec-123",
280
+ event_callback=event_callback,
281
+ active_tools=active_tools,
282
+ completed_tools=completed_tools,
283
+ started_tools=started_tools,
284
+ enforcement_context=enforcement_context,
285
+ enforcement_service=enforcement_service,
286
+ )
287
+
288
+ # Get hooks
289
+ pre_hook = hooks["PreToolUse"][0].hooks[0]
290
+ post_hook = hooks["PostToolUse"][0].hooks[0]
291
+
292
+ # Simulate tool execution
293
+ input_data = {"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}}
294
+ tool_use_id = "tool-456"
295
+ tool_context = {}
296
+
297
+ # Call pre-hook
298
+ await pre_hook(input_data, tool_use_id, tool_context)
299
+
300
+ # Verify violation stored in context
301
+ assert "enforcement_violation" in tool_context
302
+ assert "enforcement_metadata" in tool_context
303
+
304
+ # Simulate tool output
305
+ output_data = {"tool_name": "Bash", "output": "command executed"}
306
+
307
+ # Call post-hook
308
+ await post_hook(output_data, tool_use_id, tool_context)
309
+
310
+ # Verify violation injected into output
311
+ assert "POLICY VIOLATION" in output_data["output"]
312
+ assert "enforcement_violated" in output_data
313
+ assert output_data["enforcement_violated"] is True
314
+ assert "command executed" in output_data["output"] # Original output preserved
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_claude_hooks_without_enforcement(self):
318
+ """Test Claude Code hooks work without enforcement service."""
319
+ # Mock callback
320
+ callback_events = []
321
+ def event_callback(event):
322
+ callback_events.append(event)
323
+
324
+ # Build hooks WITHOUT enforcement
325
+ active_tools = {}
326
+ started_tools = set()
327
+ completed_tools = set()
328
+ hooks = build_hooks(
329
+ execution_id="exec-123",
330
+ event_callback=event_callback,
331
+ active_tools=active_tools,
332
+ completed_tools=completed_tools,
333
+ started_tools=started_tools,
334
+ enforcement_context=None,
335
+ enforcement_service=None,
336
+ )
337
+
338
+ # Get hooks
339
+ pre_hook = hooks["PreToolUse"][0].hooks[0]
340
+ post_hook = hooks["PostToolUse"][0].hooks[0]
341
+
342
+ # Simulate tool execution
343
+ input_data = {"tool_name": "Read", "tool_input": {"file_path": "/tmp/test"}}
344
+ tool_use_id = "tool-123"
345
+ tool_context = {}
346
+
347
+ # Call hooks
348
+ await pre_hook(input_data, tool_use_id, tool_context)
349
+
350
+ output_data = {"tool_name": "Read", "output": "file contents"}
351
+ await post_hook(output_data, tool_use_id, tool_context)
352
+
353
+ # Should work normally without violations
354
+ assert output_data["output"] == "file contents"
355
+ assert "enforcement_violated" not in output_data
356
+
357
+
358
+ class TestEnforcementFailOpen:
359
+ """Test fail-open behavior on errors."""
360
+
361
+ def test_agno_hook_fails_open_on_enforcer_error(
362
+ self, mock_control_plane, enforcement_service, enforcement_context, mock_enforcer_client
363
+ ):
364
+ """Test Agno hook fails open when enforcer has error."""
365
+ # Mock enforcer error
366
+ mock_enforcer_client.evaluation.enforce.side_effect = Exception("Enforcer down")
367
+
368
+ # Create hook
369
+ hook = create_tool_hook_for_streaming(
370
+ control_plane=mock_control_plane,
371
+ execution_id="exec-123",
372
+ message_id="msg-456",
373
+ enforcement_context=enforcement_context,
374
+ enforcement_service=enforcement_service,
375
+ )
376
+
377
+ # Mock tool function
378
+ mock_tool = Mock(return_value="result")
379
+
380
+ # Execute hook - should NOT raise exception
381
+ result = hook(name="Read", function=mock_tool, arguments={"file_path": "/tmp/test"})
382
+
383
+ # Verify tool was executed (fail open)
384
+ mock_tool.assert_called_once()
385
+ assert result == "result"
386
+
387
+ # Verify enforcement metadata shows error
388
+ calls = mock_control_plane.publish_event.call_args_list
389
+ complete_event = calls[1][1]["data"]
390
+ assert "enforcement" in complete_event
391
+ assert complete_event["enforcement"]["enforcer"] == "error"
392
+
393
+ @pytest.mark.asyncio
394
+ async def test_claude_hook_fails_open_on_enforcer_error(
395
+ self, enforcement_service, enforcement_context, mock_enforcer_client
396
+ ):
397
+ """Test Claude Code hook fails open when enforcer has error."""
398
+ # Mock enforcer error
399
+ mock_enforcer_client.evaluation.enforce.side_effect = Exception("Enforcer down")
400
+
401
+ # Build hooks
402
+ active_tools = {}
403
+ started_tools = set()
404
+ completed_tools = set()
405
+ hooks = build_hooks(
406
+ execution_id="exec-123",
407
+ event_callback=lambda x: None,
408
+ active_tools=active_tools,
409
+ completed_tools=completed_tools,
410
+ started_tools=started_tools,
411
+ enforcement_context=enforcement_context,
412
+ enforcement_service=enforcement_service,
413
+ )
414
+
415
+ # Get pre-hook
416
+ pre_hook = hooks["PreToolUse"][0].hooks[0]
417
+
418
+ # Simulate tool execution
419
+ input_data = {"tool_name": "Bash", "tool_input": {"command": "ls"}}
420
+ tool_use_id = "tool-123"
421
+ tool_context = {}
422
+
423
+ # Call pre-hook - should NOT raise exception
424
+ await pre_hook(input_data, tool_use_id, tool_context)
425
+
426
+ # Verify no violation stored (fail open)
427
+ assert "enforcement_violation" not in tool_context
428
+
429
+
430
+ class TestToolEventDeduplication:
431
+ """Test deduplication of tool start and complete events."""
432
+
433
+ @pytest.mark.asyncio
434
+ async def test_tool_start_deduplication(self):
435
+ """Test that duplicate tool_start events are prevented."""
436
+ # Track events
437
+ callback_events = []
438
+ def event_callback(event):
439
+ callback_events.append(event)
440
+
441
+ # Build hooks
442
+ active_tools = {}
443
+ started_tools = set()
444
+ completed_tools = set()
445
+ hooks = build_hooks(
446
+ execution_id="exec-123",
447
+ event_callback=event_callback,
448
+ active_tools=active_tools,
449
+ completed_tools=completed_tools,
450
+ started_tools=started_tools,
451
+ enforcement_context=None,
452
+ enforcement_service=None,
453
+ )
454
+
455
+ # Get pre-hook
456
+ pre_hook = hooks["PreToolUse"][0].hooks[0]
457
+
458
+ # Simulate tool execution
459
+ input_data = {"tool_name": "Bash", "tool_input": {"command": "echo 'hello world'"}}
460
+ tool_use_id = "tool-123"
461
+ tool_context = {}
462
+
463
+ # Call pre-hook FIRST time
464
+ await pre_hook(input_data, tool_use_id, tool_context)
465
+
466
+ # Verify tool_start event was published
467
+ assert len(callback_events) == 1
468
+ assert callback_events[0]["type"] == "tool_start"
469
+ assert callback_events[0]["tool_name"] == "Bash"
470
+ assert callback_events[0]["tool_execution_id"] == tool_use_id
471
+
472
+ # Verify tool_use_id added to started_tools
473
+ assert tool_use_id in started_tools
474
+
475
+ # Call pre-hook SECOND time with SAME tool_use_id (simulating duplicate)
476
+ await pre_hook(input_data, tool_use_id, tool_context)
477
+
478
+ # Verify NO additional event was published (deduplication works!)
479
+ assert len(callback_events) == 1, "Duplicate tool_start event was published!"
480
+
481
+ @pytest.mark.asyncio
482
+ async def test_tool_complete_deduplication(self):
483
+ """Test that duplicate tool_complete events are prevented."""
484
+ # Track events
485
+ callback_events = []
486
+ def event_callback(event):
487
+ callback_events.append(event)
488
+
489
+ # Build hooks
490
+ active_tools = {}
491
+ started_tools = set()
492
+ completed_tools = set()
493
+ hooks = build_hooks(
494
+ execution_id="exec-123",
495
+ event_callback=event_callback,
496
+ active_tools=active_tools,
497
+ completed_tools=completed_tools,
498
+ started_tools=started_tools,
499
+ enforcement_context=None,
500
+ enforcement_service=None,
501
+ )
502
+
503
+ # Get post-hook
504
+ post_hook = hooks["PostToolUse"][0].hooks[0]
505
+
506
+ # Simulate tool output
507
+ output_data = {"tool_name": "Bash", "output": "hello world"}
508
+ tool_use_id = "tool-456"
509
+ tool_context = {}
510
+
511
+ # Call post-hook FIRST time
512
+ await post_hook(output_data, tool_use_id, tool_context)
513
+
514
+ # Verify tool_complete event was published
515
+ assert len(callback_events) == 1
516
+ assert callback_events[0]["type"] == "tool_complete"
517
+ assert callback_events[0]["tool_name"] == "Bash"
518
+ assert callback_events[0]["tool_execution_id"] == tool_use_id
519
+
520
+ # Verify tool_use_id added to completed_tools
521
+ assert tool_use_id in completed_tools
522
+
523
+ # Call post-hook SECOND time with SAME tool_use_id (simulating duplicate)
524
+ await post_hook(output_data, tool_use_id, tool_context)
525
+
526
+ # Verify NO additional event was published (deduplication works!)
527
+ assert len(callback_events) == 1, "Duplicate tool_complete event was published!"
528
+
529
+ @pytest.mark.asyncio
530
+ async def test_multiple_different_tools(self):
531
+ """Test that different tools each get their own events."""
532
+ # Track events
533
+ callback_events = []
534
+ def event_callback(event):
535
+ callback_events.append(event)
536
+
537
+ # Build hooks
538
+ active_tools = {}
539
+ started_tools = set()
540
+ completed_tools = set()
541
+ hooks = build_hooks(
542
+ execution_id="exec-123",
543
+ event_callback=event_callback,
544
+ active_tools=active_tools,
545
+ completed_tools=completed_tools,
546
+ started_tools=started_tools,
547
+ enforcement_context=None,
548
+ enforcement_service=None,
549
+ )
550
+
551
+ # Get pre-hook
552
+ pre_hook = hooks["PreToolUse"][0].hooks[0]
553
+
554
+ # Execute tool 1
555
+ input_data_1 = {"tool_name": "Bash", "tool_input": {"command": "echo 'hello'"}}
556
+ await pre_hook(input_data_1, "tool-1", {})
557
+
558
+ # Execute tool 2
559
+ input_data_2 = {"tool_name": "Read", "tool_input": {"file_path": "/tmp/test"}}
560
+ await pre_hook(input_data_2, "tool-2", {})
561
+
562
+ # Execute tool 3
563
+ input_data_3 = {"tool_name": "Write", "tool_input": {"file_path": "/tmp/out"}}
564
+ await pre_hook(input_data_3, "tool-3", {})
565
+
566
+ # Verify all 3 events were published
567
+ assert len(callback_events) == 3
568
+ assert callback_events[0]["tool_name"] == "Bash"
569
+ assert callback_events[1]["tool_name"] == "Read"
570
+ assert callback_events[2]["tool_name"] == "Write"
571
+
572
+ # Verify all 3 tool_use_ids are in started_tools
573
+ assert "tool-1" in started_tools
574
+ assert "tool-2" in started_tools
575
+ assert "tool-3" in started_tools
576
+
577
+
578
+ if __name__ == "__main__":
579
+ pytest.main([__file__, "-v", "--tb=short"])