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,645 @@
|
|
|
1
|
+
"""Authentication middleware for multi-tenant API with Kubiya integration"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import jwt
|
|
6
|
+
import hashlib
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
from fastapi import Request, HTTPException, status
|
|
10
|
+
from fastapi.security import HTTPBearer
|
|
11
|
+
import httpx
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
from control_plane_api.app.lib.redis_client import get_redis_client
|
|
15
|
+
from control_plane_api.app.database import get_session_local
|
|
16
|
+
from sqlalchemy.orm import Session
|
|
17
|
+
from control_plane_api.app.models.worker import WorkerHeartbeat
|
|
18
|
+
|
|
19
|
+
# Import OpenTelemetry for span enrichment (optional)
|
|
20
|
+
from control_plane_api.app.observability.optional import get_current_span
|
|
21
|
+
|
|
22
|
+
logger = structlog.get_logger()
|
|
23
|
+
|
|
24
|
+
security = HTTPBearer(auto_error=False)
|
|
25
|
+
|
|
26
|
+
# Cache TTL settings
|
|
27
|
+
DEFAULT_CACHE_TTL = 3600 # 1 hour default
|
|
28
|
+
MAX_CACHE_TTL = 86400 # 24 hours max
|
|
29
|
+
|
|
30
|
+
# Shared httpx client for auth validation (reuse connections)
|
|
31
|
+
_auth_http_client: Optional[httpx.AsyncClient] = None
|
|
32
|
+
|
|
33
|
+
def get_auth_http_client() -> httpx.AsyncClient:
|
|
34
|
+
"""Get or create shared httpx client for auth validation"""
|
|
35
|
+
global _auth_http_client
|
|
36
|
+
if _auth_http_client is None:
|
|
37
|
+
_auth_http_client = httpx.AsyncClient(
|
|
38
|
+
timeout=3.0,
|
|
39
|
+
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
|
|
40
|
+
)
|
|
41
|
+
return _auth_http_client
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_token_cache_key(token: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Generate a unique cache key for a token using SHA256 hash.
|
|
47
|
+
|
|
48
|
+
This ensures each unique token gets its own cache entry, preventing
|
|
49
|
+
collisions and ensuring proper isolation between different tokens/orgs.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
token: Authentication token
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Cache key in format: auth:token:sha256:{hash}
|
|
56
|
+
"""
|
|
57
|
+
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
|
58
|
+
return f"auth:token:sha256:{token_hash}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def extract_token_from_headers(request: Request) -> Optional[str]:
|
|
62
|
+
"""
|
|
63
|
+
Extract authentication token from request headers.
|
|
64
|
+
|
|
65
|
+
Supports multiple header formats for compatibility:
|
|
66
|
+
- Authorization: Bearer <token>
|
|
67
|
+
- Authorization: UserKey <token>
|
|
68
|
+
- Authorization: Baerer <token> (typo compatibility)
|
|
69
|
+
- Authorization: <token> (raw token)
|
|
70
|
+
- UserKey: <token>
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
request: FastAPI request object
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Extracted token or None
|
|
77
|
+
"""
|
|
78
|
+
# Check Authorization header
|
|
79
|
+
auth_header = request.headers.get("authorization")
|
|
80
|
+
if auth_header:
|
|
81
|
+
# Handle "Bearer <token>"
|
|
82
|
+
if auth_header.startswith("Bearer "):
|
|
83
|
+
return auth_header[7:]
|
|
84
|
+
# Handle "UserKey <token>"
|
|
85
|
+
elif auth_header.startswith("UserKey "):
|
|
86
|
+
return auth_header[8:]
|
|
87
|
+
# Handle "Baerer <token>" (common typo)
|
|
88
|
+
elif auth_header.startswith("Baerer "):
|
|
89
|
+
logger.warning("api_key_typo_detected", message="'Baerer' should be 'Bearer'")
|
|
90
|
+
return auth_header[7:]
|
|
91
|
+
# Handle raw token without prefix
|
|
92
|
+
elif " " not in auth_header:
|
|
93
|
+
return auth_header
|
|
94
|
+
|
|
95
|
+
# Check UserKey header (alternative)
|
|
96
|
+
userkey_header = request.headers.get("userkey")
|
|
97
|
+
if userkey_header:
|
|
98
|
+
return userkey_header
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decode_jwt_token(token: str) -> Optional[Dict[str, Any]]:
|
|
104
|
+
"""
|
|
105
|
+
Decode JWT token without verification to extract expiry and metadata.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
token: JWT token string
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Decoded token payload or None
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
# Decode without verification (we just need exp and other metadata)
|
|
115
|
+
decoded = jwt.decode(token, options={"verify_signature": False})
|
|
116
|
+
return decoded
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.warning("jwt_decode_failed", error=str(e))
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_jwt_algorithm(token: str) -> Optional[str]:
|
|
123
|
+
"""
|
|
124
|
+
Extract the algorithm from a JWT token's header.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
token: JWT token string
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Algorithm string (e.g., "RS256", "HS256") or None
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
header = jwt.get_unverified_header(token)
|
|
134
|
+
return header.get("alg")
|
|
135
|
+
except Exception:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def is_kubiya_api_key_jwt(token: str) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Check if a JWT token is a Kubiya API key (not an Auth0 JWT).
|
|
142
|
+
|
|
143
|
+
Kubiya API keys use HS256 signing, while Auth0 uses RS256.
|
|
144
|
+
Only Kubiya API keys should be validated locally.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
token: JWT token string
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if this is a Kubiya API key JWT, False otherwise
|
|
151
|
+
"""
|
|
152
|
+
alg = get_jwt_algorithm(token)
|
|
153
|
+
# Auth0 uses RS256 (RSA), Kubiya API keys use HS256 or similar symmetric algorithms
|
|
154
|
+
# If it's RS256, it's an Auth0 token - don't validate locally
|
|
155
|
+
if alg == "RS256":
|
|
156
|
+
return False
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_cache_ttl_from_token(token: str) -> int:
|
|
161
|
+
"""
|
|
162
|
+
Extract expiry from JWT token and calculate appropriate cache TTL.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
token: JWT token string
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
TTL in seconds (minimum 60s, maximum MAX_CACHE_TTL)
|
|
169
|
+
"""
|
|
170
|
+
decoded = decode_jwt_token(token)
|
|
171
|
+
if not decoded or "exp" not in decoded:
|
|
172
|
+
return DEFAULT_CACHE_TTL
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
exp_timestamp = decoded["exp"]
|
|
176
|
+
exp_datetime = datetime.fromtimestamp(exp_timestamp)
|
|
177
|
+
now = datetime.now()
|
|
178
|
+
|
|
179
|
+
# Calculate time until expiry
|
|
180
|
+
ttl = int((exp_datetime - now).total_seconds())
|
|
181
|
+
|
|
182
|
+
# Ensure TTL is reasonable (at least 60s, max MAX_CACHE_TTL)
|
|
183
|
+
ttl = max(60, min(ttl, MAX_CACHE_TTL))
|
|
184
|
+
|
|
185
|
+
logger.debug("calculated_cache_ttl", ttl=ttl, exp=exp_timestamp)
|
|
186
|
+
return ttl
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.warning("ttl_calculation_failed", error=str(e))
|
|
189
|
+
return DEFAULT_CACHE_TTL
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def validate_kubiya_api_key(auth_header: str, use_userkey: bool = False) -> Optional[Dict[str, Any]]:
|
|
193
|
+
"""
|
|
194
|
+
Validate API key with Kubiya API by calling /api/v1/users/self or /api/v1/users/current.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
auth_header: Full Authorization header value (e.g., "Bearer xyz123" or "UserKey xyz123")
|
|
198
|
+
use_userkey: If True, use "UserKey" prefix instead of "Bearer" for worker authentication
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
User object from Kubiya API or None if invalid
|
|
202
|
+
"""
|
|
203
|
+
base_url = os.getenv("KUBIYA_API_BASE") or os.getenv("KUBIYA_API_URL") or "https://api.kubiya.ai"
|
|
204
|
+
endpoints = ["/api/v1/users/self", "/api/v1/users/current"]
|
|
205
|
+
|
|
206
|
+
# If use_userkey is True, ensure the header uses "UserKey" prefix
|
|
207
|
+
if use_userkey and not auth_header.startswith("UserKey "):
|
|
208
|
+
# Extract token and reformat with UserKey prefix
|
|
209
|
+
token = auth_header.replace("Bearer ", "").replace("UserKey ", "").strip()
|
|
210
|
+
auth_header = f"UserKey {token}"
|
|
211
|
+
logger.debug("reformatted_auth_header_for_worker", prefix="UserKey")
|
|
212
|
+
|
|
213
|
+
client = get_auth_http_client()
|
|
214
|
+
|
|
215
|
+
# Get current span to suppress error status for expected 401s
|
|
216
|
+
current_span = get_current_span()
|
|
217
|
+
|
|
218
|
+
for endpoint in endpoints:
|
|
219
|
+
try:
|
|
220
|
+
url = f"{base_url}{endpoint}"
|
|
221
|
+
logger.debug("validating_token_with_kubiya", url=url, use_userkey=use_userkey)
|
|
222
|
+
|
|
223
|
+
response = await client.get(
|
|
224
|
+
url,
|
|
225
|
+
headers={"Authorization": auth_header}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if response.status_code == 200:
|
|
229
|
+
user_data = response.json()
|
|
230
|
+
logger.info(
|
|
231
|
+
"token_validated_with_kubiya",
|
|
232
|
+
endpoint=endpoint,
|
|
233
|
+
user_id=user_data.get("id"),
|
|
234
|
+
org_id=user_data.get("organization_id"),
|
|
235
|
+
use_userkey=use_userkey
|
|
236
|
+
)
|
|
237
|
+
return user_data
|
|
238
|
+
|
|
239
|
+
# 401 is expected when token format doesn't match - not an error
|
|
240
|
+
# Log at debug level and continue to next auth method
|
|
241
|
+
if response.status_code == 401:
|
|
242
|
+
logger.debug(
|
|
243
|
+
"kubiya_auth_attempt_unauthorized",
|
|
244
|
+
endpoint=endpoint,
|
|
245
|
+
use_userkey=use_userkey,
|
|
246
|
+
note="Expected when trying different auth methods"
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
logger.debug(
|
|
250
|
+
"kubiya_validation_attempt_failed",
|
|
251
|
+
endpoint=endpoint,
|
|
252
|
+
status=response.status_code,
|
|
253
|
+
use_userkey=use_userkey
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.debug("kubiya_validation_error", endpoint=endpoint, error=str(e))
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# Only log as debug since this is expected when using cached auth
|
|
261
|
+
logger.debug("token_validation_skipped", use_userkey=use_userkey, note="Relying on cached authentication")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def get_cached_user_data(token: str) -> Optional[Dict[str, Any]]:
|
|
266
|
+
"""
|
|
267
|
+
Get cached user data from Redis using SHA256 hash of token.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
token: Authentication token (full token will be hashed)
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Cached user data or None
|
|
274
|
+
"""
|
|
275
|
+
redis = get_redis_client()
|
|
276
|
+
if not redis:
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
cache_key = get_token_cache_key(token)
|
|
281
|
+
cached_data = await redis.get(cache_key)
|
|
282
|
+
|
|
283
|
+
if cached_data:
|
|
284
|
+
logger.debug("cache_hit", cache_key=cache_key[:40] + "...")
|
|
285
|
+
if isinstance(cached_data, bytes):
|
|
286
|
+
cached_data = cached_data.decode('utf-8')
|
|
287
|
+
data = json.loads(cached_data)
|
|
288
|
+
|
|
289
|
+
# Invalidate cache if RS256 token was incorrectly cached as JWT auth_type
|
|
290
|
+
# RS256 tokens are Auth0 JWTs and should use Bearer auth, not JWT/UserKey
|
|
291
|
+
cached_auth_type = data.get("_auth_type")
|
|
292
|
+
if cached_auth_type == "JWT":
|
|
293
|
+
alg = get_jwt_algorithm(token)
|
|
294
|
+
if alg == "RS256":
|
|
295
|
+
logger.info("cache_invalidated_rs256_jwt", cache_key=cache_key[:40] + "...", reason="RS256 token incorrectly cached as JWT")
|
|
296
|
+
await redis.delete(cache_key)
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
return data
|
|
300
|
+
|
|
301
|
+
logger.debug("cache_miss", cache_key=cache_key[:40] + "...")
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.warning("cache_read_failed", error=str(e))
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def cache_user_data(token: str, user_data: Dict[str, Any]) -> None:
|
|
310
|
+
"""
|
|
311
|
+
Cache user data in Redis with TTL based on token expiry.
|
|
312
|
+
Uses SHA256 hash of the full token as cache key for uniqueness.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
token: Authentication token (full token will be hashed)
|
|
316
|
+
user_data: User data to cache
|
|
317
|
+
"""
|
|
318
|
+
redis = get_redis_client()
|
|
319
|
+
if not redis:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
cache_key = get_token_cache_key(token)
|
|
324
|
+
ttl = get_cache_ttl_from_token(token)
|
|
325
|
+
|
|
326
|
+
await redis.set(
|
|
327
|
+
cache_key,
|
|
328
|
+
json.dumps(user_data),
|
|
329
|
+
ex=ttl # Set expiry in seconds
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
logger.info("cache_write_success", cache_key=cache_key[:40] + "...", ttl=ttl)
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.warning("cache_write_failed", error=str(e))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def get_organization_from_worker_token(token: str) -> Optional[dict]:
|
|
339
|
+
"""
|
|
340
|
+
Validate worker token and return organization data.
|
|
341
|
+
|
|
342
|
+
Worker tokens are in format: worker_{uuid} and stored in worker_heartbeats.worker_metadata
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
token: Worker authentication token
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Organization dict or None if invalid
|
|
349
|
+
"""
|
|
350
|
+
if not token.startswith("worker_"):
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
SessionLocal = get_session_local()
|
|
354
|
+
db = SessionLocal()
|
|
355
|
+
try:
|
|
356
|
+
# Query worker_heartbeats for active workers
|
|
357
|
+
workers = db.query(WorkerHeartbeat).filter(
|
|
358
|
+
WorkerHeartbeat.status == "active"
|
|
359
|
+
).all()
|
|
360
|
+
|
|
361
|
+
# Find worker with matching token in metadata
|
|
362
|
+
for worker in workers:
|
|
363
|
+
worker_metadata = worker.worker_metadata or {}
|
|
364
|
+
if worker_metadata.get("worker_token") == token:
|
|
365
|
+
# Return minimal org data for worker
|
|
366
|
+
return {
|
|
367
|
+
"id": worker.organization_id,
|
|
368
|
+
"name": "Worker", # Workers don't need full org details
|
|
369
|
+
"slug": "worker",
|
|
370
|
+
"user_id": "worker",
|
|
371
|
+
"user_email": "worker@system",
|
|
372
|
+
"user_name": "Worker Process"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
logger.warning("worker_token_not_found", token_prefix=token[:15])
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error("worker_token_validation_failed", error=str(e))
|
|
380
|
+
return None
|
|
381
|
+
finally:
|
|
382
|
+
db.close()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
async def get_organization_allow_worker(request: Request) -> dict:
|
|
386
|
+
"""
|
|
387
|
+
Dependency that accepts both user tokens and worker tokens.
|
|
388
|
+
|
|
389
|
+
This is used by endpoints that workers need to call (like execution updates).
|
|
390
|
+
|
|
391
|
+
Flow:
|
|
392
|
+
1. Extract token from Authorization header
|
|
393
|
+
2. If token starts with "worker_", validate as worker token
|
|
394
|
+
3. Otherwise, validate as user token (with caching)
|
|
395
|
+
4. Return organization data
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
request: FastAPI request object
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Organization dict with id, name, slug, user_id, etc.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
HTTPException: 401 if authentication fails
|
|
405
|
+
"""
|
|
406
|
+
# Extract token from headers
|
|
407
|
+
token = await extract_token_from_headers(request)
|
|
408
|
+
|
|
409
|
+
if not token:
|
|
410
|
+
logger.warning("auth_token_missing", path=request.url.path)
|
|
411
|
+
raise HTTPException(
|
|
412
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
413
|
+
detail={
|
|
414
|
+
"error": "Unauthorized",
|
|
415
|
+
"message": "Authorization header is required",
|
|
416
|
+
"hint": "Include 'Authorization: Bearer <token>' header"
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Check if this is a worker token (fast path)
|
|
421
|
+
if token.startswith("worker_"):
|
|
422
|
+
logger.debug("validating_worker_token", path=request.url.path)
|
|
423
|
+
org = await get_organization_from_worker_token(token)
|
|
424
|
+
if org:
|
|
425
|
+
# Store in request state for later use
|
|
426
|
+
request.state.organization = org
|
|
427
|
+
request.state.worker_token = token
|
|
428
|
+
|
|
429
|
+
# Enrich span with worker organizational context
|
|
430
|
+
span = get_current_span()
|
|
431
|
+
if span and span.is_recording():
|
|
432
|
+
try:
|
|
433
|
+
span.set_attribute("organization.id", org["id"])
|
|
434
|
+
span.set_attribute("organization.name", org.get("name", ""))
|
|
435
|
+
span.set_attribute("auth.type", "worker_token")
|
|
436
|
+
if org.get("worker_id"):
|
|
437
|
+
span.set_attribute("worker.id", org["worker_id"])
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.warning("worker_span_enrichment_failed", error=str(e))
|
|
440
|
+
|
|
441
|
+
logger.info(
|
|
442
|
+
"worker_authenticated",
|
|
443
|
+
org_id=org["id"],
|
|
444
|
+
path=request.url.path,
|
|
445
|
+
method=request.method,
|
|
446
|
+
)
|
|
447
|
+
return org
|
|
448
|
+
|
|
449
|
+
# Worker token invalid
|
|
450
|
+
logger.warning("worker_token_invalid", path=request.url.path)
|
|
451
|
+
raise HTTPException(
|
|
452
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
453
|
+
detail={
|
|
454
|
+
"error": "Unauthorized",
|
|
455
|
+
"message": "Invalid or expired worker token",
|
|
456
|
+
"hint": "Worker token not found in active workers"
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Fall back to regular user authentication
|
|
461
|
+
return await get_current_organization(request)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
async def get_current_organization(request: Request) -> dict:
|
|
465
|
+
"""
|
|
466
|
+
Dependency to get current organization and validate authentication.
|
|
467
|
+
|
|
468
|
+
Flow:
|
|
469
|
+
1. Extract token from Authorization header
|
|
470
|
+
2. Check Redis cache for user data
|
|
471
|
+
3. If not cached, validate with Kubiya API
|
|
472
|
+
4. Cache the user data with TTL based on JWT expiry
|
|
473
|
+
5. Return organization data
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
request: FastAPI request object
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Organization dict:
|
|
480
|
+
{
|
|
481
|
+
"id": "org-uuid",
|
|
482
|
+
"name": "Organization Name",
|
|
483
|
+
"slug": "org-slug",
|
|
484
|
+
"user_id": "user-uuid",
|
|
485
|
+
"user_email": "user@example.com",
|
|
486
|
+
"user_name": "User Name"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
Raises:
|
|
490
|
+
HTTPException: 401 if authentication fails
|
|
491
|
+
"""
|
|
492
|
+
# Extract token from headers
|
|
493
|
+
token = await extract_token_from_headers(request)
|
|
494
|
+
|
|
495
|
+
if not token:
|
|
496
|
+
logger.warning("auth_token_missing", path=request.url.path)
|
|
497
|
+
raise HTTPException(
|
|
498
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
499
|
+
detail={
|
|
500
|
+
"error": "Unauthorized",
|
|
501
|
+
"message": "Authorization header is required",
|
|
502
|
+
"hint": "Include 'Authorization: Bearer <token>' header"
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Try to get cached user data
|
|
507
|
+
user_data = await get_cached_user_data(token)
|
|
508
|
+
|
|
509
|
+
# Track which auth type succeeded
|
|
510
|
+
auth_type = None
|
|
511
|
+
|
|
512
|
+
# If not cached, validate with Kubiya API or fallback to JWT validation
|
|
513
|
+
if not user_data:
|
|
514
|
+
# First, try to validate JWT structure locally - but only for Kubiya API keys (HS256)
|
|
515
|
+
# Auth0 tokens (RS256) should be validated via Kubiya API to ensure proper authorization
|
|
516
|
+
jwt_data = decode_jwt_token(token)
|
|
517
|
+
|
|
518
|
+
# If JWT is valid, has required fields, AND is a Kubiya API key (not Auth0), use it directly
|
|
519
|
+
if jwt_data and jwt_data.get("organization") and jwt_data.get("email") and is_kubiya_api_key_jwt(token):
|
|
520
|
+
logger.info("using_jwt_local_validation", org=jwt_data.get("organization"), email=jwt_data.get("email"))
|
|
521
|
+
user_data = {
|
|
522
|
+
"org": jwt_data.get("organization"),
|
|
523
|
+
"email": jwt_data.get("email"),
|
|
524
|
+
"uuid": jwt_data.get("token_id") or jwt_data.get("user_id"),
|
|
525
|
+
"name": jwt_data.get("token_name", {}).get("name") if isinstance(jwt_data.get("token_name"), dict) else jwt_data.get("name"),
|
|
526
|
+
}
|
|
527
|
+
auth_type = "JWT"
|
|
528
|
+
# Also support management API keys that have 'organization' but no 'email'
|
|
529
|
+
# These are service-level tokens with scopes like 'manage:agents'
|
|
530
|
+
elif jwt_data and jwt_data.get("organization") and jwt_data.get("scopes"):
|
|
531
|
+
logger.info("using_management_api_key", org=jwt_data.get("organization"), scopes=jwt_data.get("scopes"))
|
|
532
|
+
user_data = {
|
|
533
|
+
"org": jwt_data.get("organization"),
|
|
534
|
+
"email": f"management-api@{jwt_data.get('organization')}.kubiya.ai", # Synthetic email for management keys
|
|
535
|
+
"uuid": jwt_data.get("jti") or jwt_data.get("token_id"),
|
|
536
|
+
"name": "Management API Key",
|
|
537
|
+
}
|
|
538
|
+
auth_type = "ManagementKey"
|
|
539
|
+
else:
|
|
540
|
+
# Fallback to external API validation
|
|
541
|
+
logger.debug("validating_with_kubiya_api", path=request.url.path)
|
|
542
|
+
auth_header = f"Bearer {token}" if not token.startswith("Bearer ") else token
|
|
543
|
+
|
|
544
|
+
# Try Bearer first (for regular user tokens)
|
|
545
|
+
user_data = await validate_kubiya_api_key(auth_header, use_userkey=False)
|
|
546
|
+
if user_data:
|
|
547
|
+
auth_type = "Bearer"
|
|
548
|
+
|
|
549
|
+
# If Bearer fails, try UserKey (for API keys/worker tokens)
|
|
550
|
+
if not user_data:
|
|
551
|
+
logger.debug("bearer_auth_failed_trying_userkey", path=request.url.path)
|
|
552
|
+
user_data = await validate_kubiya_api_key(auth_header, use_userkey=True)
|
|
553
|
+
if user_data:
|
|
554
|
+
auth_type = "UserKey"
|
|
555
|
+
|
|
556
|
+
if not user_data:
|
|
557
|
+
logger.warning("authentication_failed", path=request.url.path)
|
|
558
|
+
raise HTTPException(
|
|
559
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
560
|
+
detail={
|
|
561
|
+
"error": "Unauthorized",
|
|
562
|
+
"message": "Invalid or expired authentication token",
|
|
563
|
+
"hint": "Ensure you're using a valid Kubiya API token"
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# Cache the validated user data with auth type
|
|
568
|
+
await cache_user_data(token, {**user_data, "_auth_type": auth_type})
|
|
569
|
+
else:
|
|
570
|
+
# Retrieve auth type from cache
|
|
571
|
+
auth_type = user_data.get("_auth_type", "Bearer")
|
|
572
|
+
|
|
573
|
+
# Extract organization slug from Kubiya API response
|
|
574
|
+
# Kubiya API returns the org slug in the "org" field (e.g., "kubiya-ai")
|
|
575
|
+
# We use this slug as the primary organization identifier throughout the system
|
|
576
|
+
org_slug = user_data.get("org")
|
|
577
|
+
|
|
578
|
+
if not org_slug:
|
|
579
|
+
logger.error("org_slug_missing_in_user_data", user_data=user_data)
|
|
580
|
+
raise HTTPException(
|
|
581
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
582
|
+
detail={
|
|
583
|
+
"error": "Unauthorized",
|
|
584
|
+
"message": "No organization slug found in user data",
|
|
585
|
+
"hint": "User data must contain 'org' field from Kubiya API"
|
|
586
|
+
}
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Build organization object
|
|
590
|
+
# We use the org slug as the primary identifier (id field)
|
|
591
|
+
# This slug is used for all database operations (agents, teams, executions, etc.)
|
|
592
|
+
user_email = user_data.get("email")
|
|
593
|
+
user_name = user_data.get("name") or (user_email.split("@")[0] if user_email else None) # Fallback to email username
|
|
594
|
+
|
|
595
|
+
organization = {
|
|
596
|
+
"id": org_slug, # Use slug as ID (e.g., "kubiya-ai")
|
|
597
|
+
"name": user_data.get("org"), # Also use slug as display name
|
|
598
|
+
"slug": org_slug, # The slug itself
|
|
599
|
+
"user_id": user_data.get("uuid"), # User UUID from Kubiya
|
|
600
|
+
"user_email": user_email,
|
|
601
|
+
"user_name": user_name,
|
|
602
|
+
"user_avatar": user_data.get("picture") or user_data.get("avatar") or user_data.get("image"), # Avatar from Auth0/Kubiya
|
|
603
|
+
"user_status": user_data.get("user_status") or user_data.get("status"),
|
|
604
|
+
"user_groups": user_data.get("groups") or user_data.get("user_groups"),
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
# Store in request state for later use
|
|
608
|
+
request.state.organization = organization
|
|
609
|
+
request.state.kubiya_token = token
|
|
610
|
+
request.state.kubiya_auth_type = auth_type # Store whether to use Bearer or UserKey
|
|
611
|
+
request.state.user_data = user_data
|
|
612
|
+
|
|
613
|
+
# Enrich current span with organizational context for distributed tracing
|
|
614
|
+
span = get_current_span()
|
|
615
|
+
if span and span.is_recording():
|
|
616
|
+
try:
|
|
617
|
+
span.set_attribute("organization.id", organization["id"])
|
|
618
|
+
span.set_attribute("organization.name", organization.get("name", ""))
|
|
619
|
+
span.set_attribute("organization.slug", organization.get("slug", ""))
|
|
620
|
+
|
|
621
|
+
if organization.get("user_id"):
|
|
622
|
+
span.set_attribute("user.id", organization["user_id"])
|
|
623
|
+
if organization.get("user_email"):
|
|
624
|
+
span.set_attribute("user.email", organization["user_email"])
|
|
625
|
+
if organization.get("user_name"):
|
|
626
|
+
span.set_attribute("user.name", organization["user_name"])
|
|
627
|
+
|
|
628
|
+
logger.debug(
|
|
629
|
+
"span_enriched_in_auth",
|
|
630
|
+
org_id=organization["id"],
|
|
631
|
+
trace_id=format(span.get_span_context().trace_id, '032x')
|
|
632
|
+
)
|
|
633
|
+
except Exception as e:
|
|
634
|
+
logger.warning("span_enrichment_failed_in_auth", error=str(e))
|
|
635
|
+
|
|
636
|
+
logger.info(
|
|
637
|
+
"request_authenticated",
|
|
638
|
+
org_id=organization["id"],
|
|
639
|
+
user_id=organization.get("user_id"),
|
|
640
|
+
path=request.url.path,
|
|
641
|
+
method=request.method,
|
|
642
|
+
cached=user_data is not None
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return organization
|