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,190 @@
1
+ """
2
+ LiteLLM Service
3
+
4
+ This service provides a wrapper around LiteLLM for agent execution.
5
+ """
6
+
7
+ import os
8
+ from typing import Dict, List, Optional, Any
9
+ import litellm
10
+ from litellm import completion
11
+ import logging
12
+
13
+ from control_plane_api.app.config import settings
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class LiteLLMService:
19
+ """Service for interacting with LiteLLM"""
20
+
21
+ def __init__(self):
22
+ """Initialize LiteLLM service with configuration"""
23
+ # Set LiteLLM configuration
24
+ if settings.litellm_api_key:
25
+ os.environ["LITELLM_API_KEY"] = settings.litellm_api_key
26
+ litellm.api_base = settings.litellm_api_base
27
+ litellm.drop_params = True # Drop unsupported params instead of failing
28
+
29
+ # Configure timeout
30
+ litellm.request_timeout = settings.litellm_timeout
31
+
32
+ logger.info(f"LiteLLM Service initialized with base URL: {settings.litellm_api_base}")
33
+
34
+
35
+ def execute_agent(
36
+ self,
37
+ prompt: str,
38
+ model: Optional[str] = None,
39
+ system_prompt: Optional[str] = None,
40
+ temperature: float = 0.7,
41
+ max_tokens: Optional[int] = None,
42
+ top_p: Optional[float] = None,
43
+ **kwargs: Any,
44
+ ) -> Dict[str, Any]:
45
+ """
46
+ Execute an agent with LiteLLM
47
+
48
+ Args:
49
+ prompt: The user prompt
50
+ model: Model identifier (defaults to configured default)
51
+ system_prompt: System prompt for the agent
52
+ temperature: Temperature for response generation
53
+ max_tokens: Maximum tokens to generate
54
+ top_p: Top-p sampling parameter
55
+ **kwargs: Additional parameters to pass to LiteLLM
56
+
57
+ Returns:
58
+ Dict containing the response and metadata
59
+ """
60
+ try:
61
+ # Use default model if not specified
62
+ if not model:
63
+ model = settings.litellm_default_model
64
+
65
+ # Build messages
66
+ messages = []
67
+ if system_prompt:
68
+ messages.append({"role": "system", "content": system_prompt})
69
+ messages.append({"role": "user", "content": prompt})
70
+
71
+ # Prepare completion parameters
72
+ # For custom proxies, use openai/ prefix to force OpenAI-compatible mode
73
+ # This tells LiteLLM to use the base_url as an OpenAI-compatible endpoint
74
+ completion_params = {
75
+ "model": f"openai/{model}", # Use openai/ prefix for custom proxy
76
+ "messages": messages,
77
+ "temperature": temperature,
78
+ "api_key": settings.litellm_api_key or "dummy-key", # Fallback for when key is not set
79
+ "base_url": settings.litellm_api_base,
80
+ }
81
+
82
+ if max_tokens:
83
+ completion_params["max_tokens"] = max_tokens
84
+ if top_p:
85
+ completion_params["top_p"] = top_p
86
+
87
+ # Add any additional kwargs
88
+ completion_params.update(kwargs)
89
+
90
+ logger.info(f"Executing agent with model: {model} (using openai/{model})")
91
+
92
+ # Make the completion request
93
+ response = completion(**completion_params)
94
+
95
+ # Extract response content
96
+ result = {
97
+ "success": True,
98
+ "response": response.choices[0].message.content,
99
+ "model": model,
100
+ "usage": {
101
+ "prompt_tokens": response.usage.prompt_tokens,
102
+ "completion_tokens": response.usage.completion_tokens,
103
+ "total_tokens": response.usage.total_tokens,
104
+ },
105
+ "finish_reason": response.choices[0].finish_reason,
106
+ }
107
+
108
+ logger.info(f"Agent execution successful. Tokens used: {result['usage']['total_tokens']}")
109
+ return result
110
+
111
+ except Exception as e:
112
+ logger.error(f"Error executing agent: {str(e)}")
113
+ return {
114
+ "success": False,
115
+ "error": str(e),
116
+ "model": model or settings.litellm_default_model,
117
+ }
118
+
119
+ def execute_agent_stream(
120
+ self,
121
+ prompt: str,
122
+ model: Optional[str] = None,
123
+ system_prompt: Optional[str] = None,
124
+ temperature: float = 0.7,
125
+ max_tokens: Optional[int] = None,
126
+ top_p: Optional[float] = None,
127
+ **kwargs: Any,
128
+ ):
129
+ """
130
+ Execute an agent with streaming response
131
+
132
+ Args:
133
+ prompt: The user prompt
134
+ model: Model identifier (defaults to configured default)
135
+ system_prompt: System prompt for the agent
136
+ temperature: Temperature for response generation
137
+ max_tokens: Maximum tokens to generate
138
+ top_p: Top-p sampling parameter
139
+ **kwargs: Additional parameters to pass to LiteLLM
140
+
141
+ Yields:
142
+ Response chunks as they arrive
143
+ """
144
+ try:
145
+ # Use default model if not specified
146
+ if not model:
147
+ model = settings.litellm_default_model
148
+
149
+ # Build messages
150
+ messages = []
151
+ if system_prompt:
152
+ messages.append({"role": "system", "content": system_prompt})
153
+ messages.append({"role": "user", "content": prompt})
154
+
155
+ # Prepare completion parameters
156
+ # For custom proxies, use openai/ prefix to force OpenAI-compatible mode
157
+ # This tells LiteLLM to use the base_url as an OpenAI-compatible endpoint
158
+ completion_params = {
159
+ "model": f"openai/{model}", # Use openai/ prefix for custom proxy
160
+ "messages": messages,
161
+ "temperature": temperature,
162
+ "stream": True,
163
+ "api_key": settings.litellm_api_key or "dummy-key", # Fallback for when key is not set
164
+ "base_url": settings.litellm_api_base,
165
+ }
166
+
167
+ if max_tokens:
168
+ completion_params["max_tokens"] = max_tokens
169
+ if top_p:
170
+ completion_params["top_p"] = top_p
171
+
172
+ # Add any additional kwargs
173
+ completion_params.update(kwargs)
174
+
175
+ logger.info(f"Executing agent (streaming) with model: {model} (using openai/{model})")
176
+
177
+ # Make the streaming completion request
178
+ response = completion(**completion_params)
179
+
180
+ for chunk in response:
181
+ if chunk.choices[0].delta.content:
182
+ yield chunk.choices[0].delta.content
183
+
184
+ except Exception as e:
185
+ logger.error(f"Error executing agent (streaming): {str(e)}")
186
+ yield f"Error: {str(e)}"
187
+
188
+
189
+ # Singleton instance
190
+ litellm_service = LiteLLMService()
@@ -0,0 +1,525 @@
1
+ """
2
+ Policy Service - Business logic for policy management and enforcement.
3
+
4
+ This service provides:
5
+ - Policy CRUD operations with enforcer service integration
6
+ - Policy association management (linking policies to entities)
7
+ - Policy inheritance resolution (environment > team > agent)
8
+ - Policy evaluation with pre-hook support
9
+ """
10
+
11
+ from typing import List, Optional, Dict, Any, Literal
12
+ from pydantic import BaseModel, Field
13
+ import structlog
14
+ from control_plane_api.app.lib.supabase import get_supabase
15
+ from control_plane_api.app.lib.policy_enforcer_client import (
16
+ PolicyEnforcerClient,
17
+ Policy,
18
+ PolicyCreate,
19
+ PolicyUpdate,
20
+ EvaluationResult,
21
+ PolicyNotFoundError,
22
+ PolicyValidationError,
23
+ )
24
+
25
+ logger = structlog.get_logger()
26
+
27
+ # Entity types for policy associations
28
+ EntityType = Literal["agent", "team", "environment"]
29
+
30
+ # Priority levels for inheritance
31
+ PRIORITY_LEVELS = {
32
+ "environment": 300,
33
+ "team": 200,
34
+ "agent": 100,
35
+ }
36
+
37
+
38
+ class PolicyAssociationCreate(BaseModel):
39
+ """Schema for creating a policy association"""
40
+ policy_id: str = Field(..., description="Policy UUID from enforcer service")
41
+ policy_name: str = Field(..., description="Policy name (cached)")
42
+ entity_type: EntityType = Field(..., description="Entity type")
43
+ entity_id: str = Field(..., description="Entity UUID")
44
+ enabled: bool = Field(default=True, description="Whether association is active")
45
+ priority: Optional[int] = Field(None, description="Custom priority (overrides default)")
46
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
47
+
48
+
49
+ class PolicyAssociationUpdate(BaseModel):
50
+ """Schema for updating a policy association"""
51
+ enabled: Optional[bool] = None
52
+ priority: Optional[int] = None
53
+ metadata: Optional[Dict[str, Any]] = None
54
+
55
+
56
+ class PolicyAssociationResponse(BaseModel):
57
+ """Response model for policy associations"""
58
+ id: str
59
+ organization_id: str
60
+ policy_id: str
61
+ policy_name: str
62
+ entity_type: str
63
+ entity_id: str
64
+ enabled: bool
65
+ priority: int
66
+ metadata: Dict[str, Any]
67
+ created_at: str
68
+ updated_at: str
69
+ created_by: Optional[str] = None
70
+
71
+
72
+ class ResolvedPolicy(BaseModel):
73
+ """Policy with inheritance information"""
74
+ policy_id: str
75
+ policy_name: str
76
+ source_type: str # Where the policy comes from
77
+ source_id: str
78
+ priority: int
79
+ enabled: bool
80
+ metadata: Dict[str, Any]
81
+ policy_details: Optional[Dict[str, Any]] = None # Full policy from enforcer
82
+
83
+
84
+ class PolicyService:
85
+ """Service for managing policies and their associations"""
86
+
87
+ def __init__(self, organization_id: str, enforcer_client: Optional[PolicyEnforcerClient] = None):
88
+ """
89
+ Initialize policy service.
90
+
91
+ Args:
92
+ organization_id: Organization ID for multi-tenancy
93
+ enforcer_client: Optional policy enforcer client (if None, policy features disabled)
94
+ """
95
+ self.organization_id = organization_id
96
+ self.enforcer_client = enforcer_client
97
+ self.supabase = get_supabase()
98
+
99
+ @property
100
+ def is_enabled(self) -> bool:
101
+ """Check if policy enforcement is enabled"""
102
+ return self.enforcer_client is not None
103
+
104
+ # ============================================================================
105
+ # Policy CRUD Operations (Proxy to Enforcer Service)
106
+ # ============================================================================
107
+
108
+ async def create_policy(self, policy: PolicyCreate) -> Policy:
109
+ """
110
+ Create a new policy in the enforcer service.
111
+
112
+ Args:
113
+ policy: Policy creation data
114
+
115
+ Returns:
116
+ Created Policy object
117
+
118
+ Raises:
119
+ RuntimeError: If enforcer client is not configured
120
+ PolicyValidationError: If policy is invalid
121
+ """
122
+ if not self.enforcer_client:
123
+ raise RuntimeError("Policy enforcer is not configured")
124
+
125
+ logger.info(
126
+ "creating_policy",
127
+ organization_id=self.organization_id,
128
+ policy_name=policy.name,
129
+ )
130
+
131
+ return await self.enforcer_client.policies.create(policy)
132
+
133
+ async def get_policy(self, policy_id: str) -> Policy:
134
+ """
135
+ Get a policy from the enforcer service.
136
+
137
+ Args:
138
+ policy_id: Policy UUID
139
+
140
+ Returns:
141
+ Policy object
142
+
143
+ Raises:
144
+ PolicyNotFoundError: If policy doesn't exist
145
+ """
146
+ if not self.enforcer_client:
147
+ raise RuntimeError("Policy enforcer is not configured")
148
+
149
+ return await self.enforcer_client.policies.get(policy_id)
150
+
151
+ async def list_policies(
152
+ self,
153
+ page: int = 1,
154
+ limit: int = 20,
155
+ enabled: Optional[bool] = None,
156
+ search: Optional[str] = None,
157
+ ) -> List[Policy]:
158
+ """
159
+ List policies from the enforcer service.
160
+
161
+ Args:
162
+ page: Page number
163
+ limit: Items per page
164
+ enabled: Filter by enabled status
165
+ search: Search term
166
+
167
+ Returns:
168
+ List of policies
169
+ """
170
+ if not self.enforcer_client:
171
+ return []
172
+
173
+ response = await self.enforcer_client.policies.list(
174
+ page=page,
175
+ limit=limit,
176
+ enabled=enabled,
177
+ search=search,
178
+ )
179
+ return response.policies
180
+
181
+ async def update_policy(self, policy_id: str, update: PolicyUpdate) -> Policy:
182
+ """
183
+ Update a policy in the enforcer service.
184
+
185
+ Args:
186
+ policy_id: Policy UUID
187
+ update: Update data
188
+
189
+ Returns:
190
+ Updated Policy object
191
+ """
192
+ if not self.enforcer_client:
193
+ raise RuntimeError("Policy enforcer is not configured")
194
+
195
+ return await self.enforcer_client.policies.update(policy_id, update)
196
+
197
+ async def delete_policy(self, policy_id: str) -> None:
198
+ """
199
+ Delete a policy from the enforcer service and remove all associations.
200
+
201
+ Args:
202
+ policy_id: Policy UUID
203
+ """
204
+ if not self.enforcer_client:
205
+ raise RuntimeError("Policy enforcer is not configured")
206
+
207
+ # Delete from enforcer service
208
+ await self.enforcer_client.policies.delete(policy_id)
209
+
210
+ # Delete all associations
211
+ self.supabase.table("policy_associations").delete().eq(
212
+ "organization_id", self.organization_id
213
+ ).eq("policy_id", policy_id).execute()
214
+
215
+ logger.info(
216
+ "policy_deleted_with_associations",
217
+ policy_id=policy_id,
218
+ organization_id=self.organization_id,
219
+ )
220
+
221
+ # ============================================================================
222
+ # Policy Association Management
223
+ # ============================================================================
224
+
225
+ async def create_association(
226
+ self,
227
+ association: PolicyAssociationCreate,
228
+ created_by: Optional[str] = None,
229
+ ) -> PolicyAssociationResponse:
230
+ """
231
+ Create a policy association (link policy to entity).
232
+
233
+ Args:
234
+ association: Association data
235
+ created_by: Email of creator
236
+
237
+ Returns:
238
+ Created association
239
+
240
+ Raises:
241
+ PolicyNotFoundError: If policy doesn't exist
242
+ """
243
+ if not self.enforcer_client:
244
+ raise RuntimeError("Policy enforcer is not configured")
245
+
246
+ # Verify policy exists
247
+ await self.get_policy(association.policy_id)
248
+
249
+ # Determine priority (use provided or default based on entity type)
250
+ priority = association.priority or PRIORITY_LEVELS.get(association.entity_type, 100)
251
+
252
+ # Create association
253
+ data = {
254
+ "organization_id": self.organization_id,
255
+ "policy_id": association.policy_id,
256
+ "policy_name": association.policy_name,
257
+ "entity_type": association.entity_type,
258
+ "entity_id": association.entity_id,
259
+ "enabled": association.enabled,
260
+ "priority": priority,
261
+ "metadata": association.metadata,
262
+ "created_by": created_by,
263
+ }
264
+
265
+ result = self.supabase.table("policy_associations").insert(data).execute()
266
+
267
+ logger.info(
268
+ "policy_association_created",
269
+ policy_id=association.policy_id,
270
+ entity_type=association.entity_type,
271
+ entity_id=association.entity_id[:8],
272
+ )
273
+
274
+ return PolicyAssociationResponse(**result.data[0])
275
+
276
+ def get_association(self, association_id: str) -> Optional[PolicyAssociationResponse]:
277
+ """Get a policy association by ID"""
278
+ result = (
279
+ self.supabase.table("policy_associations")
280
+ .select("*")
281
+ .eq("organization_id", self.organization_id)
282
+ .eq("id", association_id)
283
+ .execute()
284
+ )
285
+
286
+ if result.data:
287
+ return PolicyAssociationResponse(**result.data[0])
288
+ return None
289
+
290
+ def list_associations(
291
+ self,
292
+ entity_type: Optional[EntityType] = None,
293
+ entity_id: Optional[str] = None,
294
+ policy_id: Optional[str] = None,
295
+ enabled: Optional[bool] = None,
296
+ ) -> List[PolicyAssociationResponse]:
297
+ """
298
+ List policy associations with filtering.
299
+
300
+ Args:
301
+ entity_type: Filter by entity type
302
+ entity_id: Filter by entity ID
303
+ policy_id: Filter by policy ID
304
+ enabled: Filter by enabled status
305
+
306
+ Returns:
307
+ List of associations
308
+ """
309
+ query = (
310
+ self.supabase.table("policy_associations")
311
+ .select("*")
312
+ .eq("organization_id", self.organization_id)
313
+ )
314
+
315
+ if entity_type:
316
+ query = query.eq("entity_type", entity_type)
317
+ if entity_id:
318
+ query = query.eq("entity_id", entity_id)
319
+ if policy_id:
320
+ query = query.eq("policy_id", policy_id)
321
+ if enabled is not None:
322
+ query = query.eq("enabled", enabled)
323
+
324
+ result = query.order("priority", desc=True).execute()
325
+
326
+ return [PolicyAssociationResponse(**item) for item in result.data]
327
+
328
+ def update_association(
329
+ self,
330
+ association_id: str,
331
+ update: PolicyAssociationUpdate,
332
+ ) -> Optional[PolicyAssociationResponse]:
333
+ """Update a policy association"""
334
+ data = update.dict(exclude_none=True)
335
+
336
+ result = (
337
+ self.supabase.table("policy_associations")
338
+ .update(data)
339
+ .eq("organization_id", self.organization_id)
340
+ .eq("id", association_id)
341
+ .execute()
342
+ )
343
+
344
+ if result.data:
345
+ logger.info("policy_association_updated", association_id=association_id)
346
+ return PolicyAssociationResponse(**result.data[0])
347
+ return None
348
+
349
+ def delete_association(self, association_id: str) -> bool:
350
+ """Delete a policy association"""
351
+ result = (
352
+ self.supabase.table("policy_associations")
353
+ .delete()
354
+ .eq("organization_id", self.organization_id)
355
+ .eq("id", association_id)
356
+ .execute()
357
+ )
358
+
359
+ if result.data:
360
+ logger.info("policy_association_deleted", association_id=association_id)
361
+ return True
362
+ return False
363
+
364
+ # ============================================================================
365
+ # Policy Inheritance Resolution
366
+ # ============================================================================
367
+
368
+ async def resolve_entity_policies(
369
+ self,
370
+ entity_type: EntityType,
371
+ entity_id: str,
372
+ include_details: bool = False,
373
+ ) -> List[ResolvedPolicy]:
374
+ """
375
+ Resolve all policies applicable to an entity considering inheritance.
376
+
377
+ Inheritance order: environment > team > agent
378
+ Higher priority wins when same policy is defined at multiple levels.
379
+
380
+ Args:
381
+ entity_type: Entity type
382
+ entity_id: Entity UUID
383
+ include_details: Whether to fetch full policy details from enforcer
384
+
385
+ Returns:
386
+ List of resolved policies with inheritance information
387
+ """
388
+ # Call PostgreSQL function for efficient resolution
389
+ result = self.supabase.rpc(
390
+ "resolve_entity_policies",
391
+ {
392
+ "p_entity_type": entity_type,
393
+ "p_entity_id": entity_id,
394
+ "p_organization_id": self.organization_id,
395
+ },
396
+ ).execute()
397
+
398
+ policies = [ResolvedPolicy(**item) for item in result.data]
399
+
400
+ # Optionally fetch full policy details from enforcer
401
+ if include_details and self.enforcer_client:
402
+ for policy in policies:
403
+ try:
404
+ details = await self.enforcer_client.policies.get(policy.policy_id)
405
+ policy.policy_details = details.dict()
406
+ except PolicyNotFoundError:
407
+ logger.warning(
408
+ "policy_not_found_in_enforcer",
409
+ policy_id=policy.policy_id,
410
+ )
411
+
412
+ logger.info(
413
+ "policies_resolved",
414
+ entity_type=entity_type,
415
+ entity_id=entity_id[:8],
416
+ policy_count=len(policies),
417
+ )
418
+
419
+ return policies
420
+
421
+ # ============================================================================
422
+ # Policy Evaluation
423
+ # ============================================================================
424
+
425
+ async def evaluate_policies(
426
+ self,
427
+ entity_type: EntityType,
428
+ entity_id: str,
429
+ input_data: Dict[str, Any],
430
+ ) -> Dict[str, EvaluationResult]:
431
+ """
432
+ Evaluate all policies for an entity against input data.
433
+
434
+ Args:
435
+ entity_type: Entity type
436
+ entity_id: Entity UUID
437
+ input_data: Input data for evaluation
438
+
439
+ Returns:
440
+ Dict mapping policy_id to EvaluationResult
441
+ """
442
+ if not self.enforcer_client:
443
+ return {}
444
+
445
+ # Resolve policies
446
+ policies = await self.resolve_entity_policies(entity_type, entity_id)
447
+
448
+ # Evaluate each policy
449
+ results = {}
450
+ for policy in policies:
451
+ try:
452
+ result = await self.enforcer_client.evaluation.evaluate(
453
+ input_data=input_data,
454
+ policy_id=policy.policy_id,
455
+ )
456
+ results[policy.policy_id] = result
457
+ except Exception as e:
458
+ logger.warning(
459
+ "policy_evaluation_failed",
460
+ policy_id=policy.policy_id,
461
+ error=str(e),
462
+ )
463
+
464
+ return results
465
+
466
+ async def check_entity_authorization(
467
+ self,
468
+ entity_type: EntityType,
469
+ entity_id: str,
470
+ action: str,
471
+ resource: Optional[str] = None,
472
+ context: Optional[Dict[str, Any]] = None,
473
+ ) -> tuple[bool, List[str]]:
474
+ """
475
+ Check if an entity is authorized to perform an action.
476
+
477
+ Args:
478
+ entity_type: Entity type
479
+ entity_id: Entity UUID
480
+ action: Action to check (e.g., "execute", "create", "delete")
481
+ resource: Optional resource identifier
482
+ context: Additional context for evaluation
483
+
484
+ Returns:
485
+ Tuple of (is_authorized, violations)
486
+ """
487
+ if not self.enforcer_client:
488
+ # If enforcer is disabled, allow by default
489
+ return True, []
490
+
491
+ # Construct input for policy evaluation
492
+ input_data = {
493
+ "action": action,
494
+ "entity_type": entity_type,
495
+ "entity_id": entity_id,
496
+ "organization_id": self.organization_id,
497
+ }
498
+
499
+ if resource:
500
+ input_data["resource"] = resource
501
+ if context:
502
+ input_data.update(context)
503
+
504
+ # Evaluate all policies
505
+ eval_results = await self.evaluate_policies(entity_type, entity_id, input_data)
506
+
507
+ # Check if all policies allow the action
508
+ is_authorized = True
509
+ all_violations = []
510
+
511
+ for policy_id, result in eval_results.items():
512
+ if not result.allow:
513
+ is_authorized = False
514
+ all_violations.extend(result.violations)
515
+
516
+ logger.info(
517
+ "authorization_check",
518
+ entity_type=entity_type,
519
+ entity_id=entity_id[:8],
520
+ action=action,
521
+ authorized=is_authorized,
522
+ violation_count=len(all_violations),
523
+ )
524
+
525
+ return is_authorized, all_violations