kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.

Potentially problematic release.


This version of kubiya-control-plane-api might be problematic. Click here for more details.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,95 @@
1
+ from pydantic_settings import BaseSettings
2
+ from pydantic import model_validator
3
+ from typing import Optional
4
+ import os
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ """Application settings loaded from environment variables"""
9
+
10
+ # API Settings
11
+ API_HOST: str = "0.0.0.0"
12
+ API_PORT: int = 7777
13
+ API_TITLE: str = "Agent Control Plane"
14
+ API_VERSION: str = "0.1.0"
15
+ API_DESCRIPTION: str = "Multi-tenant agent orchestration with Temporal workflows"
16
+
17
+ # Supabase Settings (replaces DATABASE_URL for serverless)
18
+ SUPABASE_URL: str = "" # Required: Set via environment variable
19
+ SUPABASE_ANON_KEY: Optional[str] = None
20
+ SUPABASE_SERVICE_KEY: str = "" # Required: Set via environment variable for admin operations
21
+
22
+ # Legacy Database URL (kept for backward compatibility)
23
+ DATABASE_URL: Optional[str] = None
24
+
25
+ @model_validator(mode='after')
26
+ def set_database_url_fallback(self):
27
+ """Set DATABASE_URL from SUPABASE_POSTGRES_URL if not already set (for Vercel compatibility)"""
28
+ if not self.DATABASE_URL:
29
+ # Check for Vercel's SUPABASE_POSTGRES_URL or other variants
30
+ supabase_db_url = (
31
+ os.environ.get("SUPABASE_POSTGRES_URL") or
32
+ os.environ.get("SUPABASE_POSTGRES_PRISMA_URL") or
33
+ os.environ.get("SUPABASE_DB_URL")
34
+ )
35
+ if supabase_db_url:
36
+ # Fix URL format for SQLAlchemy 2.0+ (postgres:// -> postgresql://)
37
+ if supabase_db_url.startswith("postgres://"):
38
+ supabase_db_url = supabase_db_url.replace("postgres://", "postgresql://", 1)
39
+ # Remove invalid Supabase pooler parameters that SQLAlchemy doesn't understand
40
+ supabase_db_url = supabase_db_url.replace("&supa=base-pooler.x", "")
41
+ self.DATABASE_URL = supabase_db_url
42
+
43
+ # Also fix DATABASE_URL if it was already set with old postgres:// scheme
44
+ if self.DATABASE_URL and self.DATABASE_URL.startswith("postgres://"):
45
+ self.DATABASE_URL = self.DATABASE_URL.replace("postgres://", "postgresql://", 1)
46
+
47
+ # Remove invalid Supabase pooler parameters from DATABASE_URL
48
+ if self.DATABASE_URL:
49
+ self.DATABASE_URL = self.DATABASE_URL.replace("&supa=base-pooler.x", "")
50
+
51
+ return self
52
+
53
+ # Redis Settings (from Composer)
54
+ REDIS_URL: str = "redis://localhost:6379/0"
55
+ REDIS_HOST: Optional[str] = None
56
+ REDIS_PORT: int = 6379
57
+ REDIS_PASSWORD: Optional[str] = None
58
+ REDIS_DB: int = 0
59
+
60
+ # Temporal Settings
61
+ TEMPORAL_HOST: str = "localhost:7233"
62
+ TEMPORAL_NAMESPACE: str = "default"
63
+ TEMPORAL_CLIENT_CERT_PATH: Optional[str] = None
64
+ TEMPORAL_CLIENT_KEY_PATH: Optional[str] = None
65
+
66
+ # Security Settings
67
+ SECRET_KEY: str = "change-this-secret-key-in-production"
68
+ ALGORITHM: str = "HS256"
69
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
70
+
71
+ # Logging
72
+ LOG_LEVEL: str = "info"
73
+
74
+ # CORS
75
+ CORS_ORIGINS: list[str] = ["*"]
76
+
77
+ # LiteLLM Settings
78
+ LITELLM_API_BASE: str = "https://llm-proxy.kubiya.ai"
79
+ LITELLM_API_KEY: str = "" # Required: Set via environment variable
80
+ LITELLM_DEFAULT_MODEL: str = "kubiya/claude-sonnet-4"
81
+ LITELLM_TIMEOUT: int = 300
82
+
83
+ # Kubiya API Settings
84
+ KUBIYA_API_BASE: str = "https://api.kubiya.ai"
85
+
86
+ # Environment
87
+ ENVIRONMENT: str = "development" # development, staging, production
88
+
89
+ class Config:
90
+ env_file = ".env"
91
+ case_sensitive = True
92
+ extra = "ignore" # Allow extra environment variables (for worker-specific vars)
93
+
94
+
95
+ settings = Settings()
@@ -0,0 +1,135 @@
1
+ from sqlalchemy import create_engine, event, text
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+ from sqlalchemy.pool import NullPool
5
+ from control_plane_api.app.config import settings
6
+ import structlog
7
+ import os
8
+
9
+ logger = structlog.get_logger()
10
+
11
+ # Lazy-load engine and session to avoid crashing on import if DATABASE_URL not set
12
+ _engine = None
13
+ _SessionLocal = None
14
+
15
+ # Create base class for models
16
+ Base = declarative_base()
17
+
18
+ # Detect if running in serverless environment
19
+ # Only enable serverless mode in actual Vercel/Lambda, not in local development
20
+ IS_SERVERLESS = bool(os.getenv("VERCEL") or os.getenv("AWS_LAMBDA_FUNCTION_NAME"))
21
+
22
+
23
+ def get_engine():
24
+ """Get or create the database engine optimized for serverless"""
25
+ global _engine
26
+ if _engine is None:
27
+ if not settings.database_url:
28
+ raise RuntimeError(
29
+ "DATABASE_URL not configured. SQLAlchemy-based endpoints are not available. "
30
+ "Please use Supabase-based endpoints instead."
31
+ )
32
+
33
+ # Serverless-optimized configuration
34
+ if IS_SERVERLESS:
35
+ logger.info("creating_serverless_database_engine")
36
+ _engine = create_engine(
37
+ settings.database_url,
38
+ # Use NullPool for serverless - no connection pooling, create fresh connections
39
+ poolclass=NullPool,
40
+ # Verify connections are alive before using them
41
+ pool_pre_ping=True,
42
+ # Short connection timeout for serverless
43
+ connect_args={
44
+ "connect_timeout": 5,
45
+ "options": "-c statement_timeout=30000", # 30s query timeout
46
+ "keepalives": 1,
47
+ "keepalives_idle": 30,
48
+ "keepalives_interval": 10,
49
+ "keepalives_count": 5,
50
+ }
51
+ )
52
+ else:
53
+ # Traditional server configuration with connection pooling
54
+ logger.info("creating_traditional_database_engine")
55
+ _engine = create_engine(
56
+ settings.database_url,
57
+ pool_pre_ping=True,
58
+ pool_size=2, # Reduced for better resource management
59
+ max_overflow=3, # Reduced overflow
60
+ pool_recycle=300, # Recycle connections after 5 minutes
61
+ pool_timeout=10, # Reduced timeout
62
+ connect_args={
63
+ "connect_timeout": 10,
64
+ "options": "-c statement_timeout=30000"
65
+ }
66
+ )
67
+
68
+ # Add connection event listeners for better debugging
69
+ @event.listens_for(_engine, "connect")
70
+ def receive_connect(dbapi_conn, connection_record):
71
+ logger.debug("database_connection_established")
72
+
73
+ @event.listens_for(_engine, "close")
74
+ def receive_close(dbapi_conn, connection_record):
75
+ logger.debug("database_connection_closed")
76
+
77
+ logger.info("database_engine_created", serverless=IS_SERVERLESS)
78
+ return _engine
79
+
80
+
81
+ def get_session_local():
82
+ """Get or create the session factory"""
83
+ global _SessionLocal
84
+ if _SessionLocal is None:
85
+ _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=get_engine())
86
+ return _SessionLocal
87
+
88
+
89
+ def get_db():
90
+ """Dependency for getting database sessions"""
91
+ SessionLocal = get_session_local()
92
+ db = SessionLocal()
93
+ try:
94
+ yield db
95
+ except Exception:
96
+ # Rollback on any error during request processing
97
+ db.rollback()
98
+ raise
99
+ finally:
100
+ db.close()
101
+
102
+
103
+ def dispose_engine():
104
+ """
105
+ Dispose of the database engine to release all connections.
106
+ Should be called at the end of serverless function invocations.
107
+ """
108
+ global _engine
109
+ if _engine is not None:
110
+ logger.info("disposing_database_engine")
111
+ _engine.dispose()
112
+ _engine = None
113
+
114
+
115
+ def init_db():
116
+ """Initialize database tables"""
117
+ Base.metadata.create_all(bind=get_engine())
118
+
119
+
120
+ def health_check_db():
121
+ """
122
+ Health check for database connectivity.
123
+ Returns True if database is accessible, False otherwise.
124
+ """
125
+ from sqlalchemy.exc import OperationalError, DisconnectionError
126
+
127
+ try:
128
+ engine = get_engine()
129
+ with engine.connect() as conn:
130
+ conn.execute(text("SELECT 1"))
131
+ logger.info("database_health_check_passed")
132
+ return True
133
+ except (OperationalError, DisconnectionError) as e:
134
+ logger.error("database_health_check_failed", error=str(e))
135
+ return False
@@ -0,0 +1,408 @@
1
+ """
2
+ Centralized exception hierarchy for Control Plane API.
3
+
4
+ Provides standardized exceptions with consistent error codes and status codes.
5
+ """
6
+
7
+ from typing import Optional, Dict, Any
8
+ from enum import Enum
9
+
10
+
11
+ class ErrorCode(str, Enum):
12
+ """Standard error codes for API responses."""
13
+
14
+ # Validation errors (400)
15
+ VALIDATION_ERROR = "VALIDATION_ERROR"
16
+ INVALID_INPUT = "INVALID_INPUT"
17
+ MISSING_PARAMETER = "MISSING_PARAMETER"
18
+ INVALID_FORMAT = "INVALID_FORMAT"
19
+
20
+ # Authentication/Authorization (401/403)
21
+ AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
22
+ INVALID_CREDENTIALS = "INVALID_CREDENTIALS"
23
+ TOKEN_EXPIRED = "TOKEN_EXPIRED"
24
+ AUTHORIZATION_ERROR = "AUTHORIZATION_ERROR"
25
+ INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS"
26
+
27
+ # Resource errors (404/409)
28
+ RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
29
+ RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
30
+ RESOURCE_CONFLICT = "RESOURCE_CONFLICT"
31
+
32
+ # Rate limiting (429)
33
+ RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
34
+
35
+ # External service errors (502/503)
36
+ EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR"
37
+ SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
38
+ TIMEOUT_ERROR = "TIMEOUT_ERROR"
39
+
40
+ # Internal errors (500)
41
+ INTERNAL_ERROR = "INTERNAL_ERROR"
42
+ DATABASE_ERROR = "DATABASE_ERROR"
43
+ WORKFLOW_ERROR = "WORKFLOW_ERROR"
44
+ CONFIGURATION_ERROR = "CONFIGURATION_ERROR"
45
+
46
+
47
+ class ControlPlaneException(Exception):
48
+ """
49
+ Base exception for all Control Plane errors.
50
+
51
+ Attributes:
52
+ message: Human-readable error message
53
+ error_code: Machine-readable error code
54
+ status_code: HTTP status code
55
+ details: Additional error details
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ message: str,
61
+ error_code: Optional[ErrorCode] = None,
62
+ status_code: int = 500,
63
+ details: Optional[Dict[str, Any]] = None,
64
+ ):
65
+ super().__init__(message)
66
+ self.message = message
67
+ self.error_code = error_code or ErrorCode.INTERNAL_ERROR
68
+ self.status_code = status_code
69
+ self.details = details or {}
70
+
71
+ def to_dict(self) -> Dict[str, Any]:
72
+ """Convert exception to dictionary for API response."""
73
+ return {
74
+ "code": self.error_code,
75
+ "message": self.message,
76
+ "details": self.details,
77
+ }
78
+
79
+
80
+ # 400 - Bad Request Errors
81
+
82
+ class ValidationError(ControlPlaneException):
83
+ """Raised when input validation fails."""
84
+
85
+ def __init__(
86
+ self,
87
+ message: str,
88
+ field: Optional[str] = None,
89
+ value: Optional[Any] = None,
90
+ details: Optional[Dict[str, Any]] = None,
91
+ ):
92
+ if field:
93
+ details = details or {}
94
+ details["field"] = field
95
+ if value is not None:
96
+ details["value"] = str(value)
97
+
98
+ super().__init__(
99
+ message=message,
100
+ error_code=ErrorCode.VALIDATION_ERROR,
101
+ status_code=400,
102
+ details=details,
103
+ )
104
+
105
+
106
+ class InvalidInputError(ControlPlaneException):
107
+ """Raised when input is invalid but passes basic validation."""
108
+
109
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
110
+ super().__init__(
111
+ message=message,
112
+ error_code=ErrorCode.INVALID_INPUT,
113
+ status_code=400,
114
+ details=details,
115
+ )
116
+
117
+
118
+ # 401 - Authentication Errors
119
+
120
+ class AuthenticationError(ControlPlaneException):
121
+ """Raised when authentication fails."""
122
+
123
+ def __init__(self, message: str = "Authentication required"):
124
+ super().__init__(
125
+ message=message,
126
+ error_code=ErrorCode.AUTHENTICATION_ERROR,
127
+ status_code=401,
128
+ )
129
+
130
+
131
+ class InvalidCredentialsError(ControlPlaneException):
132
+ """Raised when credentials are invalid."""
133
+
134
+ def __init__(self, message: str = "Invalid credentials"):
135
+ super().__init__(
136
+ message=message,
137
+ error_code=ErrorCode.INVALID_CREDENTIALS,
138
+ status_code=401,
139
+ )
140
+
141
+
142
+ class TokenExpiredError(ControlPlaneException):
143
+ """Raised when authentication token has expired."""
144
+
145
+ def __init__(self, message: str = "Token has expired"):
146
+ super().__init__(
147
+ message=message,
148
+ error_code=ErrorCode.TOKEN_EXPIRED,
149
+ status_code=401,
150
+ )
151
+
152
+
153
+ # 403 - Authorization Errors
154
+
155
+ class AuthorizationError(ControlPlaneException):
156
+ """Raised when user lacks required permissions."""
157
+
158
+ def __init__(
159
+ self,
160
+ message: str = "Insufficient permissions",
161
+ required_permission: Optional[str] = None,
162
+ ):
163
+ details = {}
164
+ if required_permission:
165
+ details["required_permission"] = required_permission
166
+
167
+ super().__init__(
168
+ message=message,
169
+ error_code=ErrorCode.AUTHORIZATION_ERROR,
170
+ status_code=403,
171
+ details=details,
172
+ )
173
+
174
+
175
+ # 404 - Not Found Errors
176
+
177
+ class ResourceNotFoundError(ControlPlaneException):
178
+ """Raised when a requested resource doesn't exist."""
179
+
180
+ def __init__(
181
+ self,
182
+ resource_type: str,
183
+ resource_id: str,
184
+ message: Optional[str] = None,
185
+ ):
186
+ if not message:
187
+ message = f"{resource_type} with ID '{resource_id}' not found"
188
+
189
+ super().__init__(
190
+ message=message,
191
+ error_code=ErrorCode.RESOURCE_NOT_FOUND,
192
+ status_code=404,
193
+ details={
194
+ "resource_type": resource_type,
195
+ "resource_id": resource_id,
196
+ },
197
+ )
198
+
199
+
200
+ # 409 - Conflict Errors
201
+
202
+ class ResourceAlreadyExistsError(ControlPlaneException):
203
+ """Raised when attempting to create a resource that already exists."""
204
+
205
+ def __init__(
206
+ self,
207
+ resource_type: str,
208
+ identifier: str,
209
+ message: Optional[str] = None,
210
+ ):
211
+ if not message:
212
+ message = f"{resource_type} already exists: {identifier}"
213
+
214
+ super().__init__(
215
+ message=message,
216
+ error_code=ErrorCode.RESOURCE_ALREADY_EXISTS,
217
+ status_code=409,
218
+ details={
219
+ "resource_type": resource_type,
220
+ "identifier": identifier,
221
+ },
222
+ )
223
+
224
+
225
+ class ResourceConflictError(ControlPlaneException):
226
+ """Raised when a resource operation conflicts with current state."""
227
+
228
+ def __init__(
229
+ self,
230
+ message: str,
231
+ resource_type: Optional[str] = None,
232
+ resource_id: Optional[str] = None,
233
+ ):
234
+ details = {}
235
+ if resource_type:
236
+ details["resource_type"] = resource_type
237
+ if resource_id:
238
+ details["resource_id"] = resource_id
239
+
240
+ super().__init__(
241
+ message=message,
242
+ error_code=ErrorCode.RESOURCE_CONFLICT,
243
+ status_code=409,
244
+ details=details,
245
+ )
246
+
247
+
248
+ # 429 - Rate Limit Errors
249
+
250
+ class RateLimitError(ControlPlaneException):
251
+ """Raised when rate limit is exceeded."""
252
+
253
+ def __init__(
254
+ self,
255
+ limit: int,
256
+ window: str,
257
+ retry_after: Optional[int] = None,
258
+ ):
259
+ details = {
260
+ "limit": limit,
261
+ "window": window,
262
+ }
263
+ if retry_after:
264
+ details["retry_after_seconds"] = retry_after
265
+
266
+ super().__init__(
267
+ message=f"Rate limit exceeded: {limit} requests per {window}",
268
+ error_code=ErrorCode.RATE_LIMIT_EXCEEDED,
269
+ status_code=429,
270
+ details=details,
271
+ )
272
+
273
+
274
+ # 502/503 - Service Errors
275
+
276
+ class ExternalServiceError(ControlPlaneException):
277
+ """Raised when an external service call fails."""
278
+
279
+ def __init__(
280
+ self,
281
+ service: str,
282
+ message: str,
283
+ status_code: int = 502,
284
+ details: Optional[Dict[str, Any]] = None,
285
+ ):
286
+ full_details = {"service": service}
287
+ if details:
288
+ full_details.update(details)
289
+
290
+ super().__init__(
291
+ message=f"{service}: {message}",
292
+ error_code=ErrorCode.EXTERNAL_SERVICE_ERROR,
293
+ status_code=status_code,
294
+ details=full_details,
295
+ )
296
+
297
+
298
+ class ServiceUnavailableError(ControlPlaneException):
299
+ """Raised when a service is temporarily unavailable."""
300
+
301
+ def __init__(
302
+ self,
303
+ service: str,
304
+ message: Optional[str] = None,
305
+ retry_after: Optional[int] = None,
306
+ ):
307
+ if not message:
308
+ message = f"{service} is temporarily unavailable"
309
+
310
+ details = {"service": service}
311
+ if retry_after:
312
+ details["retry_after_seconds"] = retry_after
313
+
314
+ super().__init__(
315
+ message=message,
316
+ error_code=ErrorCode.SERVICE_UNAVAILABLE,
317
+ status_code=503,
318
+ details=details,
319
+ )
320
+
321
+
322
+ class TimeoutError(ControlPlaneException):
323
+ """Raised when an operation times out."""
324
+
325
+ def __init__(
326
+ self,
327
+ operation: str,
328
+ timeout_seconds: Optional[float] = None,
329
+ ):
330
+ details = {"operation": operation}
331
+ if timeout_seconds:
332
+ details["timeout_seconds"] = timeout_seconds
333
+
334
+ super().__init__(
335
+ message=f"Operation '{operation}' timed out",
336
+ error_code=ErrorCode.TIMEOUT_ERROR,
337
+ status_code=504,
338
+ details=details,
339
+ )
340
+
341
+
342
+ # 500 - Internal Errors
343
+
344
+ class DatabaseError(ControlPlaneException):
345
+ """Raised when a database operation fails."""
346
+
347
+ def __init__(
348
+ self,
349
+ message: str,
350
+ operation: Optional[str] = None,
351
+ details: Optional[Dict[str, Any]] = None,
352
+ ):
353
+ full_details = details or {}
354
+ if operation:
355
+ full_details["operation"] = operation
356
+
357
+ super().__init__(
358
+ message=message,
359
+ error_code=ErrorCode.DATABASE_ERROR,
360
+ status_code=500,
361
+ details=full_details,
362
+ )
363
+
364
+
365
+ class WorkflowExecutionError(ControlPlaneException):
366
+ """Raised when a Temporal workflow execution fails."""
367
+
368
+ def __init__(
369
+ self,
370
+ workflow_id: str,
371
+ message: str,
372
+ workflow_type: Optional[str] = None,
373
+ details: Optional[Dict[str, Any]] = None,
374
+ ):
375
+ full_details = {
376
+ "workflow_id": workflow_id,
377
+ }
378
+ if workflow_type:
379
+ full_details["workflow_type"] = workflow_type
380
+ if details:
381
+ full_details.update(details)
382
+
383
+ super().__init__(
384
+ message=f"Workflow {workflow_id}: {message}",
385
+ error_code=ErrorCode.WORKFLOW_ERROR,
386
+ status_code=500,
387
+ details=full_details,
388
+ )
389
+
390
+
391
+ class ConfigurationError(ControlPlaneException):
392
+ """Raised when configuration is invalid or missing."""
393
+
394
+ def __init__(
395
+ self,
396
+ message: str,
397
+ config_key: Optional[str] = None,
398
+ ):
399
+ details = {}
400
+ if config_key:
401
+ details["config_key"] = config_key
402
+
403
+ super().__init__(
404
+ message=message,
405
+ error_code=ErrorCode.CONFIGURATION_ERROR,
406
+ status_code=500,
407
+ details=details,
408
+ )
@@ -0,0 +1,11 @@
1
+ """Shared library modules"""
2
+
3
+ from control_plane_api.app.lib.supabase import get_supabase, execute_with_org_context
4
+ from control_plane_api.app.lib.temporal_client import get_temporal_client, close_temporal_client
5
+
6
+ __all__ = [
7
+ "get_supabase",
8
+ "execute_with_org_context",
9
+ "get_temporal_client",
10
+ "close_temporal_client",
11
+ ]