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.
- control_plane_api/LICENSE +676 -0
- control_plane_api/README.md +350 -0
- control_plane_api/__init__.py +4 -0
- control_plane_api/__version__.py +8 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +121 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
- control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
- control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
- control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
- control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
- control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
- control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
- control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
- control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
- control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
- control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
- control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
- control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
- control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
- control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
- control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
- control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
- control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +384 -0
- control_plane_api/app/activities/plan_generation_activities.py +499 -0
- control_plane_api/app/activities/team_activities.py +424 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +469 -0
- control_plane_api/app/config/config_loader.py +224 -0
- control_plane_api/app/config/model_pricing.py +323 -0
- control_plane_api/app/config/storage_config.py +159 -0
- control_plane_api/app/config.py +115 -0
- control_plane_api/app/controllers/__init__.py +0 -0
- control_plane_api/app/controllers/execution_environment_controller.py +1315 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/environment.py +65 -0
- control_plane_api/app/lib/event_bus/__init__.py +17 -0
- control_plane_api/app/lib/event_bus/base.py +136 -0
- control_plane_api/app/lib/event_bus/manager.py +335 -0
- control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
- control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
- control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
- control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
- control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
- control_plane_api/app/lib/job_executor.py +330 -0
- control_plane_api/app/lib/kubiya_client.py +293 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/mcp_validation.py +163 -0
- control_plane_api/app/lib/nats/__init__.py +13 -0
- control_plane_api/app/lib/nats/credentials_manager.py +288 -0
- control_plane_api/app/lib/nats/listener.py +374 -0
- control_plane_api/app/lib/planning_prompt_builder.py +153 -0
- control_plane_api/app/lib/planning_tools/__init__.py +41 -0
- control_plane_api/app/lib/planning_tools/agents.py +409 -0
- control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
- control_plane_api/app/lib/planning_tools/base.py +119 -0
- control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
- control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
- control_plane_api/app/lib/planning_tools/environments.py +218 -0
- control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
- control_plane_api/app/lib/planning_tools/models.py +93 -0
- control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
- control_plane_api/app/lib/planning_tools/resources.py +242 -0
- control_plane_api/app/lib/planning_tools/teams.py +334 -0
- control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
- control_plane_api/app/lib/redis_client.py +803 -0
- control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
- control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
- control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
- control_plane_api/app/lib/storage/__init__.py +20 -0
- control_plane_api/app/lib/storage/base_provider.py +274 -0
- control_plane_api/app/lib/storage/provider_factory.py +157 -0
- control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/supabase_utils.py +138 -0
- control_plane_api/app/lib/task_planning/__init__.py +138 -0
- control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
- control_plane_api/app/lib/task_planning/agents.py +389 -0
- control_plane_api/app/lib/task_planning/cache.py +218 -0
- control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
- control_plane_api/app/lib/task_planning/helpers.py +293 -0
- control_plane_api/app/lib/task_planning/hooks.py +474 -0
- control_plane_api/app/lib/task_planning/models.py +503 -0
- control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
- control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
- control_plane_api/app/lib/task_planning/runner.py +656 -0
- control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
- control_plane_api/app/lib/task_planning/workflow.py +424 -0
- control_plane_api/app/lib/templating/__init__.py +88 -0
- control_plane_api/app/lib/templating/compiler.py +278 -0
- control_plane_api/app/lib/templating/engine.py +178 -0
- control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
- control_plane_api/app/lib/templating/parsers/base.py +96 -0
- control_plane_api/app/lib/templating/parsers/env.py +85 -0
- control_plane_api/app/lib/templating/parsers/graph.py +112 -0
- control_plane_api/app/lib/templating/parsers/secret.py +87 -0
- control_plane_api/app/lib/templating/parsers/simple.py +81 -0
- control_plane_api/app/lib/templating/resolver.py +366 -0
- control_plane_api/app/lib/templating/types.py +214 -0
- control_plane_api/app/lib/templating/validator.py +201 -0
- control_plane_api/app/lib/temporal_client.py +232 -0
- control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
- control_plane_api/app/lib/temporal_credentials_service.py +203 -0
- control_plane_api/app/lib/validation/__init__.py +24 -0
- control_plane_api/app/lib/validation/runtime_validation.py +388 -0
- control_plane_api/app/main.py +531 -0
- control_plane_api/app/middleware/__init__.py +10 -0
- control_plane_api/app/middleware/auth.py +645 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/prometheus_middleware.py +173 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +40 -0
- control_plane_api/app/models/agent.py +90 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +107 -0
- control_plane_api/app/models/auth_user.py +73 -0
- control_plane_api/app/models/context.py +161 -0
- control_plane_api/app/models/custom_integration.py +99 -0
- control_plane_api/app/models/environment.py +64 -0
- control_plane_api/app/models/execution.py +125 -0
- control_plane_api/app/models/execution_transition.py +50 -0
- control_plane_api/app/models/job.py +159 -0
- control_plane_api/app/models/llm_model.py +78 -0
- control_plane_api/app/models/orchestration.py +66 -0
- control_plane_api/app/models/plan_execution.py +102 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +61 -0
- control_plane_api/app/models/project_management.py +85 -0
- control_plane_api/app/models/session.py +29 -0
- control_plane_api/app/models/skill.py +155 -0
- control_plane_api/app/models/system_tables.py +43 -0
- control_plane_api/app/models/task_planning.py +372 -0
- control_plane_api/app/models/team.py +86 -0
- control_plane_api/app/models/trace.py +257 -0
- control_plane_api/app/models/user_profile.py +54 -0
- control_plane_api/app/models/worker.py +221 -0
- control_plane_api/app/models/workflow.py +161 -0
- control_plane_api/app/models/workspace.py +50 -0
- control_plane_api/app/observability/__init__.py +177 -0
- control_plane_api/app/observability/context_logging.py +475 -0
- control_plane_api/app/observability/decorators.py +337 -0
- control_plane_api/app/observability/local_span_processor.py +702 -0
- control_plane_api/app/observability/metrics.py +303 -0
- control_plane_api/app/observability/middleware.py +246 -0
- control_plane_api/app/observability/optional.py +115 -0
- control_plane_api/app/observability/tracing.py +382 -0
- control_plane_api/app/policies/README.md +149 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_enforcement/README.md +336 -0
- control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
- control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
- control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
- control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
- control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +382 -0
- control_plane_api/app/routers/agents_v2.py +1598 -0
- control_plane_api/app/routers/analytics.py +1310 -0
- control_plane_api/app/routers/auth.py +59 -0
- control_plane_api/app/routers/client_config.py +57 -0
- control_plane_api/app/routers/context_graph.py +561 -0
- control_plane_api/app/routers/context_manager.py +577 -0
- control_plane_api/app/routers/custom_integrations.py +490 -0
- control_plane_api/app/routers/enforcer.py +132 -0
- control_plane_api/app/routers/environment_context.py +252 -0
- control_plane_api/app/routers/environments.py +761 -0
- control_plane_api/app/routers/execution_environment.py +847 -0
- control_plane_api/app/routers/executions/__init__.py +28 -0
- control_plane_api/app/routers/executions/router.py +286 -0
- control_plane_api/app/routers/executions/services/__init__.py +22 -0
- control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
- control_plane_api/app/routers/executions/services/status_service.py +420 -0
- control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
- control_plane_api/app/routers/executions/services/worker_health.py +514 -0
- control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
- control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
- control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
- control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
- control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
- control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
- control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
- control_plane_api/app/routers/executions.py +4888 -0
- control_plane_api/app/routers/health.py +165 -0
- control_plane_api/app/routers/health_v2.py +394 -0
- control_plane_api/app/routers/integration_templates.py +496 -0
- control_plane_api/app/routers/integrations.py +287 -0
- control_plane_api/app/routers/jobs.py +1809 -0
- control_plane_api/app/routers/metrics.py +517 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +628 -0
- control_plane_api/app/routers/plan_executions.py +1481 -0
- control_plane_api/app/routers/plan_generation_async.py +304 -0
- control_plane_api/app/routers/policies.py +669 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +987 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +171 -0
- control_plane_api/app/routers/skills.py +1010 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/storage.py +456 -0
- control_plane_api/app/routers/task_planning.py +611 -0
- control_plane_api/app/routers/task_queues.py +650 -0
- control_plane_api/app/routers/team_context.py +274 -0
- control_plane_api/app/routers/teams.py +1747 -0
- control_plane_api/app/routers/templates.py +248 -0
- control_plane_api/app/routers/traces.py +571 -0
- control_plane_api/app/routers/websocket_client.py +479 -0
- control_plane_api/app/routers/websocket_executions_status.py +437 -0
- control_plane_api/app/routers/websocket_gateway.py +323 -0
- control_plane_api/app/routers/websocket_traces.py +576 -0
- control_plane_api/app/routers/worker_queues.py +2555 -0
- control_plane_api/app/routers/worker_websocket.py +419 -0
- control_plane_api/app/routers/workers.py +1004 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/__init__.py +1 -0
- control_plane_api/app/schemas/job_schemas.py +302 -0
- control_plane_api/app/schemas/mcp_schemas.py +311 -0
- control_plane_api/app/schemas/template_schemas.py +133 -0
- control_plane_api/app/schemas/trace_schemas.py +168 -0
- control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_planning_strategy.py +233 -0
- control_plane_api/app/services/agno_service.py +838 -0
- control_plane_api/app/services/claude_code_planning_service.py +203 -0
- control_plane_api/app/services/context_graph_client.py +224 -0
- control_plane_api/app/services/custom_integration_service.py +415 -0
- control_plane_api/app/services/integration_resolution_service.py +345 -0
- control_plane_api/app/services/litellm_service.py +394 -0
- control_plane_api/app/services/plan_generator.py +79 -0
- control_plane_api/app/services/planning_strategy.py +66 -0
- control_plane_api/app/services/planning_strategy_factory.py +118 -0
- control_plane_api/app/services/policy_service.py +615 -0
- control_plane_api/app/services/state_transition_service.py +755 -0
- control_plane_api/app/services/storage_service.py +593 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
- control_plane_api/app/services/trace_retention.py +354 -0
- control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
- control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
- control_plane_api/app/services/workflow_operations_service.py +611 -0
- control_plane_api/app/skills/__init__.py +100 -0
- control_plane_api/app/skills/base.py +239 -0
- control_plane_api/app/skills/builtin/__init__.py +37 -0
- control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
- control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
- control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
- control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
- control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
- control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
- control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
- control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
- control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
- control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
- control_plane_api/app/skills/builtin/docker/skill.py +104 -0
- control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
- control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
- control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
- control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
- control_plane_api/app/skills/builtin/python/__init__.py +4 -0
- control_plane_api/app/skills/builtin/python/skill.py +92 -0
- control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
- control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
- control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
- control_plane_api/app/skills/builtin/shell/skill.py +161 -0
- control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/app/skills/builtin/slack/skill.py +302 -0
- control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
- control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/config.py +63 -0
- control_plane_api/app/skills/loaders/__init__.py +14 -0
- control_plane_api/app/skills/loaders/base.py +73 -0
- control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
- control_plane_api/app/skills/registry.py +125 -0
- control_plane_api/app/utils/helpers.py +12 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +520 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/plan_generation.py +254 -0
- control_plane_api/app/workflows/team_execution.py +442 -0
- control_plane_api/scripts/seed_models.py +240 -0
- control_plane_api/scripts/validate_existing_tool_names.py +492 -0
- control_plane_api/shared/__init__.py +8 -0
- control_plane_api/shared/version.py +17 -0
- control_plane_api/test_deduplication.py +274 -0
- control_plane_api/test_executor_deduplication_e2e.py +309 -0
- control_plane_api/test_job_execution_e2e.py +283 -0
- control_plane_api/test_real_integration.py +193 -0
- control_plane_api/version.py +38 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1585 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/job_activities.py +199 -0
- control_plane_api/worker/activities/runtime_activities.py +1167 -0
- control_plane_api/worker/activities/skill_activities.py +282 -0
- control_plane_api/worker/activities/team_activities.py +479 -0
- control_plane_api/worker/agent_runtime_server.py +370 -0
- control_plane_api/worker/binary_manager.py +333 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +273 -0
- control_plane_api/worker/control_plane_client.py +1491 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/health_monitor.py +159 -0
- control_plane_api/worker/metrics.py +237 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/error_events.py +105 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +35 -0
- control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
- control_plane_api/worker/runtimes/agno/__init__.py +34 -0
- control_plane_api/worker/runtimes/agno/config.py +248 -0
- control_plane_api/worker/runtimes/agno/hooks.py +385 -0
- control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
- control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
- control_plane_api/worker/runtimes/agno/utils.py +163 -0
- control_plane_api/worker/runtimes/base.py +979 -0
- control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
- control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
- control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
- control_plane_api/worker/runtimes/claude_code/config.py +829 -0
- control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
- control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
- control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
- control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
- control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
- control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
- control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/model_utils.py +107 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_communication_tools.py +908 -0
- control_plane_api/worker/services/agent_executor.py +485 -0
- control_plane_api/worker/services/agent_executor_v2.py +793 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/code_ingestion_tools.py +465 -0
- control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
- control_plane_api/worker/services/data_visualization.py +834 -0
- control_plane_api/worker/services/event_publisher.py +531 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +365 -0
- control_plane_api/worker/services/skill_context_enhancement.py +181 -0
- control_plane_api/worker/services/skill_factory.py +471 -0
- control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
- control_plane_api/worker/services/team_executor.py +715 -0
- control_plane_api/worker/services/team_executor_v2.py +1866 -0
- control_plane_api/worker/services/tool_enforcement.py +254 -0
- control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
- control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
- control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
- control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
- control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
- control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/models.py +142 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
- control_plane_api/worker/skills/__init__.py +12 -0
- control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
- control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
- control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
- control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
- control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
- control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
- control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
- control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
- control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
- control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
- control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
- control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
- control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
- control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
- control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
- control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
- control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
- control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
- control_plane_api/worker/skills/loaders/__init__.py +5 -0
- control_plane_api/worker/skills/loaders/base.py +23 -0
- control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
- control_plane_api/worker/skills/registry.py +208 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/conftest.py +12 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
- control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
- control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
- control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
- control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
- control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
- control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
- control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
- control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
- control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
- control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
- control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +330 -0
- control_plane_api/worker/utils/environment.py +65 -0
- control_plane_api/worker/utils/error_publisher.py +260 -0
- control_plane_api/worker/utils/event_batcher.py +256 -0
- control_plane_api/worker/utils/logging_config.py +335 -0
- control_plane_api/worker/utils/logging_helper.py +326 -0
- control_plane_api/worker/utils/parameter_validator.py +120 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +665 -0
- control_plane_api/worker/utils/tool_validation.py +332 -0
- control_plane_api/worker/utils/workspace_manager.py +163 -0
- control_plane_api/worker/websocket_client.py +393 -0
- control_plane_api/worker/worker.py +1297 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +909 -0
- control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
- control_plane_api/worker/workflows/team_execution.py +611 -0
- kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
- kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
- kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
- kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
- scripts/__init__.py +1 -0
- scripts/migrations.py +39 -0
- scripts/seed_worker_queues.py +128 -0
- scripts/setup_agent_runtime.py +142 -0
- worker_internal/__init__.py +1 -0
- worker_internal/planner/__init__.py +1 -0
- worker_internal/planner/activities.py +1499 -0
- worker_internal/planner/agent_tools.py +197 -0
- worker_internal/planner/event_models.py +148 -0
- worker_internal/planner/event_publisher.py +67 -0
- worker_internal/planner/models.py +199 -0
- worker_internal/planner/retry_logic.py +134 -0
- worker_internal/planner/worker.py +300 -0
- worker_internal/planner/workflows.py +970 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Unit tests for tool enforcement service."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import asyncio
|
|
5
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
from control_plane_api.worker.services.tool_enforcement import ToolEnforcementService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_enforcer_client():
|
|
13
|
+
"""Mock policy enforcer client."""
|
|
14
|
+
client = AsyncMock()
|
|
15
|
+
client.evaluation = AsyncMock()
|
|
16
|
+
return client
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def enforcement_service(mock_enforcer_client):
|
|
21
|
+
"""Create enforcement service with mock client."""
|
|
22
|
+
return ToolEnforcementService(mock_enforcer_client)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def enforcement_context():
|
|
27
|
+
"""Sample enforcement context."""
|
|
28
|
+
return {
|
|
29
|
+
"user_email": "test@example.com",
|
|
30
|
+
"user_id": "user-123",
|
|
31
|
+
"user_roles": ["developer"],
|
|
32
|
+
"organization_id": "org-456",
|
|
33
|
+
"team_id": "team-789",
|
|
34
|
+
"agent_id": "agent-xyz",
|
|
35
|
+
"execution_id": "exec-abc",
|
|
36
|
+
"environment": "production"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestToolEnforcementService:
|
|
41
|
+
"""Test suite for ToolEnforcementService."""
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_enforce_tool_allowed(self, enforcement_service, enforcement_context, mock_enforcer_client):
|
|
45
|
+
"""Test enforcement check when tool is allowed."""
|
|
46
|
+
# Mock enforcer response
|
|
47
|
+
mock_enforcer_client.evaluation.enforce.return_value = {
|
|
48
|
+
"allow": True,
|
|
49
|
+
"id": "enforcement-123",
|
|
50
|
+
"policies": ["role_based_access"]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
allow, violation, metadata = await enforcement_service.enforce_tool_execution(
|
|
54
|
+
tool_name="Read",
|
|
55
|
+
tool_args={"file_path": "/tmp/test.txt"},
|
|
56
|
+
enforcement_context=enforcement_context
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert allow is True
|
|
60
|
+
assert violation is None
|
|
61
|
+
assert metadata["enforcer"] == "allowed"
|
|
62
|
+
assert "role_based_access" in metadata["policies"]
|
|
63
|
+
assert "enforcement_id" in metadata
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_enforce_tool_blocked(self, enforcement_service, enforcement_context, mock_enforcer_client):
|
|
67
|
+
"""Test enforcement check when tool is blocked."""
|
|
68
|
+
mock_enforcer_client.evaluation.enforce.return_value = {
|
|
69
|
+
"allow": False,
|
|
70
|
+
"id": "enforcement-456",
|
|
71
|
+
"policies": ["production_safeguards"]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
allow, violation, metadata = await enforcement_service.enforce_tool_execution(
|
|
75
|
+
tool_name="Bash",
|
|
76
|
+
tool_args={"command": "rm -rf /tmp/*"},
|
|
77
|
+
enforcement_context=enforcement_context
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
assert allow is False
|
|
81
|
+
assert violation is not None
|
|
82
|
+
assert "blocked by policy enforcement" in violation.lower()
|
|
83
|
+
assert "Bash" in violation
|
|
84
|
+
assert metadata["enforcer"] == "blocked"
|
|
85
|
+
assert "production_safeguards" in metadata["policies"]
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_enforce_timeout_fails_open(self, enforcement_service, enforcement_context, mock_enforcer_client):
|
|
89
|
+
"""Test that enforcement timeout fails open (allows execution)."""
|
|
90
|
+
# Mock timeout
|
|
91
|
+
async def slow_enforce(*args, **kwargs):
|
|
92
|
+
await asyncio.sleep(5) # Longer than timeout
|
|
93
|
+
return {"allow": False}
|
|
94
|
+
|
|
95
|
+
mock_enforcer_client.evaluation.enforce = slow_enforce
|
|
96
|
+
|
|
97
|
+
allow, violation, metadata = await enforcement_service.enforce_tool_execution(
|
|
98
|
+
tool_name="Bash",
|
|
99
|
+
tool_args={"command": "ls"},
|
|
100
|
+
enforcement_context=enforcement_context,
|
|
101
|
+
timeout=0.1 # Very short timeout
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
assert allow is True # Fails open
|
|
105
|
+
assert violation is None
|
|
106
|
+
assert metadata["enforcer"] == "timeout"
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_enforce_error_fails_open(self, enforcement_service, enforcement_context, mock_enforcer_client):
|
|
110
|
+
"""Test that enforcement errors fail open (allows execution)."""
|
|
111
|
+
# Mock error
|
|
112
|
+
mock_enforcer_client.evaluation.enforce.side_effect = Exception("Enforcer unavailable")
|
|
113
|
+
|
|
114
|
+
allow, violation, metadata = await enforcement_service.enforce_tool_execution(
|
|
115
|
+
tool_name="Bash",
|
|
116
|
+
tool_args={"command": "ls"},
|
|
117
|
+
enforcement_context=enforcement_context
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert allow is True # Fails open
|
|
121
|
+
assert violation is None
|
|
122
|
+
assert metadata["enforcer"] == "error"
|
|
123
|
+
assert "error" in metadata
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_disabled_enforcer(self):
|
|
127
|
+
"""Test that disabled enforcer allows all tools."""
|
|
128
|
+
service = ToolEnforcementService(None)
|
|
129
|
+
|
|
130
|
+
allow, violation, metadata = await service.enforce_tool_execution(
|
|
131
|
+
tool_name="Bash",
|
|
132
|
+
tool_args={"command": "rm -rf /"},
|
|
133
|
+
enforcement_context={}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert allow is True
|
|
137
|
+
assert violation is None
|
|
138
|
+
assert metadata["enforcer"] == "disabled"
|
|
139
|
+
|
|
140
|
+
def test_build_enforcement_payload(self, enforcement_service, enforcement_context):
|
|
141
|
+
"""Test enforcement payload construction."""
|
|
142
|
+
payload = enforcement_service._build_enforcement_payload(
|
|
143
|
+
tool_name="Bash",
|
|
144
|
+
tool_args={"command": "kubectl get pods"},
|
|
145
|
+
context=enforcement_context
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
assert payload["action"] == "tool_execution"
|
|
149
|
+
assert payload["tool"]["name"] == "Bash"
|
|
150
|
+
assert payload["tool"]["source"] == "builtin"
|
|
151
|
+
assert payload["tool"]["category"] == "command_execution"
|
|
152
|
+
assert payload["tool"]["risk_level"] == "high"
|
|
153
|
+
assert payload["user"]["email"] == "test@example.com"
|
|
154
|
+
assert payload["organization"]["id"] == "org-456"
|
|
155
|
+
assert payload["execution"]["environment"] == "production"
|
|
156
|
+
assert "timestamp" in payload["execution"]
|
|
157
|
+
|
|
158
|
+
def test_determine_tool_source(self, enforcement_service):
|
|
159
|
+
"""Test tool source detection."""
|
|
160
|
+
assert enforcement_service._determine_tool_source("mcp__github__list_repos") == "mcp"
|
|
161
|
+
assert enforcement_service._determine_tool_source("Bash") == "builtin"
|
|
162
|
+
assert enforcement_service._determine_tool_source("Read") == "builtin"
|
|
163
|
+
assert enforcement_service._determine_tool_source("custom_tool") == "skill"
|
|
164
|
+
|
|
165
|
+
def test_determine_tool_category(self, enforcement_service):
|
|
166
|
+
"""Test tool category detection."""
|
|
167
|
+
assert enforcement_service._determine_tool_category("Bash") == "command_execution"
|
|
168
|
+
assert enforcement_service._determine_tool_category("Read") == "file_operation"
|
|
169
|
+
assert enforcement_service._determine_tool_category("Write") == "file_operation"
|
|
170
|
+
assert enforcement_service._determine_tool_category("Grep") == "file_search"
|
|
171
|
+
assert enforcement_service._determine_tool_category("WebFetch") == "network"
|
|
172
|
+
assert enforcement_service._determine_tool_category("custom_tool") == "general"
|
|
173
|
+
|
|
174
|
+
def test_determine_risk_level_critical(self, enforcement_service):
|
|
175
|
+
"""Test risk level assessment for critical commands."""
|
|
176
|
+
# Critical risk - destructive commands
|
|
177
|
+
assert enforcement_service._determine_risk_level(
|
|
178
|
+
"Bash",
|
|
179
|
+
{"command": "rm -rf /"}
|
|
180
|
+
) == "critical"
|
|
181
|
+
|
|
182
|
+
assert enforcement_service._determine_risk_level(
|
|
183
|
+
"Bash",
|
|
184
|
+
{"command": "dd if=/dev/zero of=/dev/sda"}
|
|
185
|
+
) == "critical"
|
|
186
|
+
|
|
187
|
+
def test_determine_risk_level_high(self, enforcement_service):
|
|
188
|
+
"""Test risk level assessment for high-risk operations."""
|
|
189
|
+
# High risk - command execution
|
|
190
|
+
assert enforcement_service._determine_risk_level(
|
|
191
|
+
"Bash",
|
|
192
|
+
{"command": "kubectl delete deployment"}
|
|
193
|
+
) == "high"
|
|
194
|
+
|
|
195
|
+
# High risk - sensitive file access
|
|
196
|
+
assert enforcement_service._determine_risk_level(
|
|
197
|
+
"Read",
|
|
198
|
+
{"file_path": "/etc/passwd"}
|
|
199
|
+
) == "high"
|
|
200
|
+
|
|
201
|
+
assert enforcement_service._determine_risk_level(
|
|
202
|
+
"Read",
|
|
203
|
+
{"file_path": "~/.ssh/id_rsa"}
|
|
204
|
+
) == "high"
|
|
205
|
+
|
|
206
|
+
def test_determine_risk_level_medium(self, enforcement_service):
|
|
207
|
+
"""Test risk level assessment for medium-risk operations."""
|
|
208
|
+
assert enforcement_service._determine_risk_level(
|
|
209
|
+
"Write",
|
|
210
|
+
{"file_path": "/tmp/test.txt", "content": "test"}
|
|
211
|
+
) == "medium"
|
|
212
|
+
|
|
213
|
+
assert enforcement_service._determine_risk_level(
|
|
214
|
+
"Edit",
|
|
215
|
+
{"file_path": "/tmp/config.yaml"}
|
|
216
|
+
) == "medium"
|
|
217
|
+
|
|
218
|
+
def test_determine_risk_level_low(self, enforcement_service):
|
|
219
|
+
"""Test risk level assessment for low-risk operations."""
|
|
220
|
+
assert enforcement_service._determine_risk_level(
|
|
221
|
+
"Read",
|
|
222
|
+
{"file_path": "/tmp/test.txt"}
|
|
223
|
+
) == "low"
|
|
224
|
+
|
|
225
|
+
assert enforcement_service._determine_risk_level(
|
|
226
|
+
"Grep",
|
|
227
|
+
{"pattern": "error", "path": "/var/log"}
|
|
228
|
+
) == "low"
|
|
229
|
+
|
|
230
|
+
def test_format_violation_message(self, enforcement_service):
|
|
231
|
+
"""Test violation message formatting."""
|
|
232
|
+
enforcement_result = {
|
|
233
|
+
"id": "enf-123",
|
|
234
|
+
"allow": False,
|
|
235
|
+
"policies": ["policy1", "policy2"]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
message = enforcement_service._format_violation_message(
|
|
239
|
+
tool_name="Bash",
|
|
240
|
+
policies=["policy1", "policy2"],
|
|
241
|
+
enforcement_result=enforcement_result
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
assert "Tool execution blocked" in message
|
|
245
|
+
assert "Bash" in message
|
|
246
|
+
assert "policy1, policy2" in message
|
|
247
|
+
assert "enf-123" in message
|
|
248
|
+
assert "administrator" in message.lower()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestToolEnforcementPayloadValidation:
|
|
252
|
+
"""Test payload structure validation."""
|
|
253
|
+
|
|
254
|
+
def test_payload_has_all_required_fields(self, enforcement_service, enforcement_context):
|
|
255
|
+
"""Verify payload contains all required fields."""
|
|
256
|
+
payload = enforcement_service._build_enforcement_payload(
|
|
257
|
+
tool_name="Bash",
|
|
258
|
+
tool_args={"command": "ls"},
|
|
259
|
+
context=enforcement_context
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Top-level fields
|
|
263
|
+
assert "action" in payload
|
|
264
|
+
assert "tool" in payload
|
|
265
|
+
assert "user" in payload
|
|
266
|
+
assert "organization" in payload
|
|
267
|
+
assert "team" in payload
|
|
268
|
+
assert "execution" in payload
|
|
269
|
+
|
|
270
|
+
# Tool fields
|
|
271
|
+
assert "name" in payload["tool"]
|
|
272
|
+
assert "arguments" in payload["tool"]
|
|
273
|
+
assert "source" in payload["tool"]
|
|
274
|
+
assert "category" in payload["tool"]
|
|
275
|
+
assert "risk_level" in payload["tool"]
|
|
276
|
+
|
|
277
|
+
# User fields
|
|
278
|
+
assert "email" in payload["user"]
|
|
279
|
+
assert "id" in payload["user"]
|
|
280
|
+
assert "roles" in payload["user"]
|
|
281
|
+
|
|
282
|
+
# Organization fields
|
|
283
|
+
assert "id" in payload["organization"]
|
|
284
|
+
|
|
285
|
+
# Execution fields
|
|
286
|
+
assert "execution_id" in payload["execution"]
|
|
287
|
+
assert "agent_id" in payload["execution"]
|
|
288
|
+
assert "environment" in payload["execution"]
|
|
289
|
+
assert "timestamp" in payload["execution"]
|
|
290
|
+
|
|
291
|
+
def test_payload_timestamp_format(self, enforcement_service, enforcement_context):
|
|
292
|
+
"""Verify timestamp is in ISO format."""
|
|
293
|
+
payload = enforcement_service._build_enforcement_payload(
|
|
294
|
+
tool_name="Test",
|
|
295
|
+
tool_args={},
|
|
296
|
+
context=enforcement_context
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
timestamp = payload["execution"]["timestamp"]
|
|
300
|
+
# Should be able to parse back
|
|
301
|
+
parsed = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
302
|
+
assert isinstance(parsed, datetime)
|
|
303
|
+
|
|
304
|
+
def test_payload_handles_missing_context_fields(self, enforcement_service):
|
|
305
|
+
"""Test payload construction with minimal context."""
|
|
306
|
+
minimal_context = {
|
|
307
|
+
"organization_id": "org-123",
|
|
308
|
+
"agent_id": "agent-456"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
payload = enforcement_service._build_enforcement_payload(
|
|
312
|
+
tool_name="Test",
|
|
313
|
+
tool_args={},
|
|
314
|
+
context=minimal_context
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Should not crash, just have None values
|
|
318
|
+
assert payload["user"]["email"] is None
|
|
319
|
+
assert payload["user"]["roles"] == []
|
|
320
|
+
assert payload["organization"]["id"] == "org-123"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
pytest.main([__file__, "-v", "--tb=short"])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility functions and helpers"""
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart chunk batching for streaming to reduce HTTP requests.
|
|
3
|
+
|
|
4
|
+
Instead of sending one POST per chunk (50-70 requests), batch chunks
|
|
5
|
+
with configurable time/size windows (5-10 requests).
|
|
6
|
+
|
|
7
|
+
Batching Strategy:
|
|
8
|
+
- Time window: Flush after X ms (default: 100ms)
|
|
9
|
+
- Size window: Flush when batch reaches Y bytes (default: 100 bytes)
|
|
10
|
+
- Immediate flush: On tool events, errors, or completion
|
|
11
|
+
|
|
12
|
+
This provides:
|
|
13
|
+
- 90%+ reduction in HTTP requests
|
|
14
|
+
- Still feels real-time (100ms is imperceptible)
|
|
15
|
+
- Lower latency (fewer round trips)
|
|
16
|
+
- Better serverless performance (fewer cold starts)
|
|
17
|
+
- Lower costs (fewer invocations)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import time
|
|
22
|
+
from typing import Dict, Any, Optional, Callable
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
import structlog
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class BatchConfig:
|
|
31
|
+
"""Configuration for chunk batching behavior."""
|
|
32
|
+
|
|
33
|
+
# Time-based batching: flush after this many milliseconds
|
|
34
|
+
time_window_ms: int = 100
|
|
35
|
+
|
|
36
|
+
# Size-based batching: flush when accumulated content reaches this size
|
|
37
|
+
size_window_bytes: int = 100
|
|
38
|
+
|
|
39
|
+
# Maximum batch size before forced flush (safety limit)
|
|
40
|
+
max_batch_size_bytes: int = 1000
|
|
41
|
+
|
|
42
|
+
# Enable/disable batching (for testing/debugging)
|
|
43
|
+
enabled: bool = True
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_env(cls) -> "BatchConfig":
|
|
47
|
+
"""
|
|
48
|
+
Create configuration from environment variables.
|
|
49
|
+
|
|
50
|
+
Environment variables:
|
|
51
|
+
CHUNK_BATCHING_ENABLED: Enable/disable batching (default: true)
|
|
52
|
+
CHUNK_BATCHING_TIME_WINDOW_MS: Time window in ms (default: 100)
|
|
53
|
+
CHUNK_BATCHING_SIZE_WINDOW_BYTES: Size window in bytes (default: 100)
|
|
54
|
+
CHUNK_BATCHING_MAX_SIZE_BYTES: Max batch size in bytes (default: 1000)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
BatchConfig instance with values from environment
|
|
58
|
+
"""
|
|
59
|
+
import os
|
|
60
|
+
|
|
61
|
+
return cls(
|
|
62
|
+
enabled=os.getenv("CHUNK_BATCHING_ENABLED", "true").lower() == "true",
|
|
63
|
+
time_window_ms=int(os.getenv("CHUNK_BATCHING_TIME_WINDOW_MS", "100")),
|
|
64
|
+
size_window_bytes=int(os.getenv("CHUNK_BATCHING_SIZE_WINDOW_BYTES", "100")),
|
|
65
|
+
max_batch_size_bytes=int(os.getenv("CHUNK_BATCHING_MAX_SIZE_BYTES", "1000")),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ContentBatch:
|
|
71
|
+
"""Accumulated content chunks waiting to be flushed."""
|
|
72
|
+
|
|
73
|
+
chunks: list[str] = field(default_factory=list)
|
|
74
|
+
total_size: int = 0
|
|
75
|
+
first_chunk_time: Optional[float] = None
|
|
76
|
+
|
|
77
|
+
def add(self, content: str) -> None:
|
|
78
|
+
"""Add content to the batch."""
|
|
79
|
+
self.chunks.append(content)
|
|
80
|
+
self.total_size += len(content.encode('utf-8'))
|
|
81
|
+
|
|
82
|
+
if self.first_chunk_time is None:
|
|
83
|
+
self.first_chunk_time = time.time()
|
|
84
|
+
|
|
85
|
+
def get_combined_content(self) -> str:
|
|
86
|
+
"""Get all chunks combined into single string."""
|
|
87
|
+
return ''.join(self.chunks)
|
|
88
|
+
|
|
89
|
+
def clear(self) -> None:
|
|
90
|
+
"""Clear the batch after flushing."""
|
|
91
|
+
self.chunks.clear()
|
|
92
|
+
self.total_size = 0
|
|
93
|
+
self.first_chunk_time = None
|
|
94
|
+
|
|
95
|
+
def is_empty(self) -> bool:
|
|
96
|
+
"""Check if batch is empty."""
|
|
97
|
+
return len(self.chunks) == 0
|
|
98
|
+
|
|
99
|
+
def age_ms(self) -> float:
|
|
100
|
+
"""Get age of batch in milliseconds."""
|
|
101
|
+
if self.first_chunk_time is None:
|
|
102
|
+
return 0
|
|
103
|
+
return (time.time() - self.first_chunk_time) * 1000
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ChunkBatcher:
|
|
107
|
+
"""
|
|
108
|
+
Smart batching for streaming chunks to reduce HTTP requests.
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
batcher = ChunkBatcher(
|
|
112
|
+
publish_func=control_plane.publish_event,
|
|
113
|
+
execution_id=execution_id,
|
|
114
|
+
message_id=message_id,
|
|
115
|
+
config=BatchConfig(time_window_ms=100, size_window_bytes=100)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Add chunks as they arrive
|
|
119
|
+
await batcher.add_chunk("Hello")
|
|
120
|
+
await batcher.add_chunk(" world")
|
|
121
|
+
|
|
122
|
+
# Flush remaining chunks when done
|
|
123
|
+
await batcher.flush()
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
publish_func: Callable,
|
|
129
|
+
execution_id: str,
|
|
130
|
+
message_id: str,
|
|
131
|
+
config: Optional[BatchConfig] = None
|
|
132
|
+
):
|
|
133
|
+
self.publish_func = publish_func
|
|
134
|
+
self.execution_id = execution_id
|
|
135
|
+
self.message_id = message_id
|
|
136
|
+
self.config = config or BatchConfig()
|
|
137
|
+
|
|
138
|
+
self.batch = ContentBatch()
|
|
139
|
+
self._flush_task: Optional[asyncio.Task] = None
|
|
140
|
+
self._stats = {
|
|
141
|
+
"chunks_received": 0,
|
|
142
|
+
"batches_sent": 0,
|
|
143
|
+
"bytes_sent": 0,
|
|
144
|
+
"flushes_by_time": 0,
|
|
145
|
+
"flushes_by_size": 0,
|
|
146
|
+
"flushes_manual": 0,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async def add_chunk(self, content: str) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Add a chunk to the batch.
|
|
152
|
+
|
|
153
|
+
Automatically flushes if:
|
|
154
|
+
- Batch size exceeds size_window_bytes
|
|
155
|
+
- Batch age exceeds time_window_ms
|
|
156
|
+
- Max batch size is reached (safety)
|
|
157
|
+
"""
|
|
158
|
+
if not self.config.enabled:
|
|
159
|
+
# Batching disabled - send immediately
|
|
160
|
+
await self._publish_batch([content])
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
self._stats["chunks_received"] += 1
|
|
164
|
+
self.batch.add(content)
|
|
165
|
+
|
|
166
|
+
# Check if we should flush immediately due to size
|
|
167
|
+
should_flush_size = self.batch.total_size >= self.config.size_window_bytes
|
|
168
|
+
should_flush_max = self.batch.total_size >= self.config.max_batch_size_bytes
|
|
169
|
+
|
|
170
|
+
if should_flush_max:
|
|
171
|
+
# Safety: flush immediately if max size reached
|
|
172
|
+
logger.debug(
|
|
173
|
+
"Flushing batch (max size reached)",
|
|
174
|
+
execution_id=self.execution_id[:8],
|
|
175
|
+
batch_size=self.batch.total_size,
|
|
176
|
+
chunk_count=len(self.batch.chunks),
|
|
177
|
+
)
|
|
178
|
+
await self.flush(reason="max_size")
|
|
179
|
+
elif should_flush_size:
|
|
180
|
+
# Size threshold reached - flush now
|
|
181
|
+
await self.flush(reason="size")
|
|
182
|
+
else:
|
|
183
|
+
# Start/reset timer for time-based flush
|
|
184
|
+
await self._schedule_time_flush()
|
|
185
|
+
|
|
186
|
+
async def _schedule_time_flush(self) -> None:
|
|
187
|
+
"""Schedule a time-based flush if not already scheduled."""
|
|
188
|
+
if self._flush_task is not None and not self._flush_task.done():
|
|
189
|
+
# Timer already running
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
self._flush_task = asyncio.create_task(self._time_based_flush())
|
|
193
|
+
|
|
194
|
+
async def _time_based_flush(self) -> None:
|
|
195
|
+
"""Wait for time window, then flush."""
|
|
196
|
+
await asyncio.sleep(self.config.time_window_ms / 1000.0)
|
|
197
|
+
|
|
198
|
+
if not self.batch.is_empty():
|
|
199
|
+
await self.flush(reason="time")
|
|
200
|
+
|
|
201
|
+
async def flush(self, reason: str = "manual") -> None:
|
|
202
|
+
"""
|
|
203
|
+
Flush current batch immediately.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
reason: Why flush was triggered (for stats/debugging)
|
|
207
|
+
"""
|
|
208
|
+
if self.batch.is_empty():
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Cancel pending timer if any
|
|
212
|
+
if self._flush_task is not None and not self._flush_task.done():
|
|
213
|
+
self._flush_task.cancel()
|
|
214
|
+
try:
|
|
215
|
+
await self._flush_task
|
|
216
|
+
except asyncio.CancelledError:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
# Publish the batch
|
|
220
|
+
chunks = self.batch.chunks.copy()
|
|
221
|
+
await self._publish_batch(chunks)
|
|
222
|
+
|
|
223
|
+
# Update stats
|
|
224
|
+
if reason == "time":
|
|
225
|
+
self._stats["flushes_by_time"] += 1
|
|
226
|
+
elif reason == "size" or reason == "max_size":
|
|
227
|
+
self._stats["flushes_by_size"] += 1
|
|
228
|
+
else:
|
|
229
|
+
self._stats["flushes_manual"] += 1
|
|
230
|
+
|
|
231
|
+
# Clear batch
|
|
232
|
+
self.batch.clear()
|
|
233
|
+
|
|
234
|
+
async def _publish_batch(self, chunks: list[str]) -> None:
|
|
235
|
+
"""Publish a batch of chunks as single event with retry logic."""
|
|
236
|
+
combined_content = ''.join(chunks)
|
|
237
|
+
max_retries = 3
|
|
238
|
+
base_delay = 0.1 # 100ms
|
|
239
|
+
|
|
240
|
+
for attempt in range(max_retries):
|
|
241
|
+
try:
|
|
242
|
+
# CRITICAL: Always await async functions immediately
|
|
243
|
+
# publish_func is an async function, so call it with await
|
|
244
|
+
await self.publish_func(
|
|
245
|
+
execution_id=self.execution_id,
|
|
246
|
+
event_type="message_chunk",
|
|
247
|
+
data={
|
|
248
|
+
"role": "assistant",
|
|
249
|
+
"content": combined_content,
|
|
250
|
+
"is_chunk": True,
|
|
251
|
+
"message_id": self.message_id,
|
|
252
|
+
# Metadata for debugging
|
|
253
|
+
"batch_info": {
|
|
254
|
+
"chunk_count": len(chunks),
|
|
255
|
+
"batch_size": len(combined_content.encode('utf-8')),
|
|
256
|
+
} if len(chunks) > 1 else None,
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
self._stats["batches_sent"] += 1
|
|
261
|
+
self._stats["bytes_sent"] += len(combined_content.encode('utf-8'))
|
|
262
|
+
|
|
263
|
+
# Success - exit retry loop
|
|
264
|
+
if attempt > 0:
|
|
265
|
+
logger.debug(
|
|
266
|
+
"Batch published after retry",
|
|
267
|
+
execution_id=self.execution_id[:8],
|
|
268
|
+
attempt=attempt + 1,
|
|
269
|
+
chunk_count=len(chunks),
|
|
270
|
+
)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
is_last_attempt = attempt == max_retries - 1
|
|
275
|
+
|
|
276
|
+
if is_last_attempt:
|
|
277
|
+
logger.error(
|
|
278
|
+
"Failed to publish batch after all retries",
|
|
279
|
+
execution_id=self.execution_id[:8],
|
|
280
|
+
error=str(e),
|
|
281
|
+
chunk_count=len(chunks),
|
|
282
|
+
attempts=max_retries,
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
# Exponential backoff
|
|
286
|
+
delay = base_delay * (2 ** attempt)
|
|
287
|
+
logger.debug(
|
|
288
|
+
"Retrying batch publish",
|
|
289
|
+
execution_id=self.execution_id[:8],
|
|
290
|
+
error=str(e),
|
|
291
|
+
attempt=attempt + 1,
|
|
292
|
+
next_delay_ms=int(delay * 1000),
|
|
293
|
+
)
|
|
294
|
+
await asyncio.sleep(delay)
|
|
295
|
+
|
|
296
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
297
|
+
"""
|
|
298
|
+
Get batching statistics.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Dict with stats about batching performance
|
|
302
|
+
"""
|
|
303
|
+
chunks_received = self._stats["chunks_received"]
|
|
304
|
+
batches_sent = self._stats["batches_sent"]
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
**self._stats,
|
|
308
|
+
"reduction_percent": round(
|
|
309
|
+
(1 - batches_sent / max(chunks_received, 1)) * 100, 1
|
|
310
|
+
) if chunks_received > 0 else 0,
|
|
311
|
+
"avg_batch_size": round(
|
|
312
|
+
chunks_received / max(batches_sent, 1), 1
|
|
313
|
+
) if batches_sent > 0 else 0,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async def close(self) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Close the batcher and flush remaining chunks.
|
|
319
|
+
|
|
320
|
+
Call this when streaming is complete.
|
|
321
|
+
"""
|
|
322
|
+
await self.flush(reason="close")
|
|
323
|
+
|
|
324
|
+
# Log stats
|
|
325
|
+
stats = self.get_stats()
|
|
326
|
+
logger.info(
|
|
327
|
+
"Chunk batching stats",
|
|
328
|
+
execution_id=self.execution_id[:8],
|
|
329
|
+
**stats
|
|
330
|
+
)
|