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,639 @@
1
+ """
2
+ Policies Router - API endpoints for policy management and enforcement.
3
+
4
+ This router provides:
5
+ - Policy CRUD operations (proxy to enforcer service)
6
+ - Policy association management (linking policies to entities)
7
+ - Policy inheritance resolution
8
+ - Policy evaluation and authorization checks
9
+ """
10
+
11
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
12
+ from typing import List, Optional, Dict, Any
13
+ from pydantic import BaseModel, Field
14
+ import structlog
15
+
16
+ from control_plane_api.app.middleware.auth import get_current_organization
17
+ from control_plane_api.app.lib.policy_enforcer_client import (
18
+ create_policy_enforcer_client,
19
+ PolicyEnforcerClient,
20
+ PolicyCreate,
21
+ PolicyUpdate,
22
+ Policy,
23
+ PolicyValidationError,
24
+ PolicyNotFoundError,
25
+ EnforcerConnectionError,
26
+ )
27
+ from control_plane_api.app.services.policy_service import (
28
+ PolicyService,
29
+ PolicyAssociationCreate,
30
+ PolicyAssociationUpdate,
31
+ PolicyAssociationResponse,
32
+ ResolvedPolicy,
33
+ EntityType,
34
+ )
35
+
36
+ logger = structlog.get_logger()
37
+
38
+ router = APIRouter()
39
+
40
+
41
+ # ============================================================================
42
+ # Dependency Injection
43
+ # ============================================================================
44
+
45
+ async def get_policy_service(
46
+ request: Request,
47
+ organization: dict = Depends(get_current_organization),
48
+ ) -> PolicyService:
49
+ """
50
+ Dependency to get PolicyService with enforcer client.
51
+
52
+ Note: If ENFORCER_SERVICE_URL is not set, returns service with disabled enforcer.
53
+ The enforcer client uses the same authorization token from the incoming request.
54
+ """
55
+ # Extract the authorization token from the request state (set by auth middleware)
56
+ auth_token = getattr(request.state, "kubiya_token", None)
57
+
58
+ async with create_policy_enforcer_client(api_key=auth_token) as enforcer_client:
59
+ service = PolicyService(
60
+ organization_id=organization["id"],
61
+ enforcer_client=enforcer_client,
62
+ )
63
+ yield service
64
+
65
+
66
+ # ============================================================================
67
+ # Request/Response Models
68
+ # ============================================================================
69
+
70
+ class PolicyResponse(BaseModel):
71
+ """Extended policy response with association count"""
72
+ id: str
73
+ name: str
74
+ description: Optional[str]
75
+ policy_content: Optional[str] = "" # May be None or empty in some responses
76
+ organization_id: str
77
+ enabled: bool
78
+ tags: List[str]
79
+ version: int
80
+ created_at: Optional[str]
81
+ updated_at: Optional[str]
82
+ created_by: Optional[str] = None
83
+ updated_by: Optional[str] = None
84
+ policy_type: str = "rego"
85
+ association_count: int = 0 # Number of entities using this policy
86
+
87
+
88
+ class EvaluationRequest(BaseModel):
89
+ """Request model for policy evaluation"""
90
+ input_data: Dict[str, Any] = Field(..., description="Input data for evaluation")
91
+ policy_ids: Optional[List[str]] = Field(None, description="Specific policy IDs to evaluate")
92
+
93
+
94
+ class EvaluationResponse(BaseModel):
95
+ """Response model for policy evaluation"""
96
+ allowed: bool
97
+ violations: List[str]
98
+ policy_results: Dict[str, Dict[str, Any]] # policy_id -> result
99
+
100
+
101
+ class AuthorizationCheckRequest(BaseModel):
102
+ """Request model for authorization check"""
103
+ action: str = Field(..., description="Action to check")
104
+ resource: Optional[str] = Field(None, description="Resource identifier")
105
+ context: Optional[Dict[str, Any]] = Field(None, description="Additional context")
106
+
107
+
108
+ class AuthorizationCheckResponse(BaseModel):
109
+ """Response model for authorization check"""
110
+ authorized: bool
111
+ violations: List[str]
112
+ policies_evaluated: int
113
+
114
+
115
+ class PolicyListResponse(BaseModel):
116
+ """Paginated response for list policies"""
117
+ policies: List[PolicyResponse]
118
+ total: int
119
+ page: int
120
+ limit: int
121
+ has_more: bool
122
+
123
+
124
+ class ValidationResultResponse(BaseModel):
125
+ """Response for policy validation"""
126
+ valid: bool
127
+ errors: List[str] = []
128
+ warnings: List[str] = []
129
+
130
+
131
+ # ============================================================================
132
+ # Health Check (Must be before parameterized routes)
133
+ # ============================================================================
134
+
135
+ @router.get("/health")
136
+ async def check_policy_enforcer_health(
137
+ service: PolicyService = Depends(get_policy_service),
138
+ ):
139
+ """
140
+ Check health of the policy enforcer service.
141
+
142
+ Returns connection status and configuration information.
143
+ """
144
+ if not service.is_enabled:
145
+ return {
146
+ "enabled": False,
147
+ "healthy": False,
148
+ "message": "Policy enforcer is not configured",
149
+ }
150
+
151
+ try:
152
+ healthy = await service.enforcer_client.health_check()
153
+ return {
154
+ "enabled": True,
155
+ "healthy": healthy,
156
+ "enforcer_url": service.enforcer_client._base_url,
157
+ }
158
+ except Exception as e:
159
+ logger.error("health_check_failed", error=str(e), error_type=type(e).__name__)
160
+ return {
161
+ "enabled": True,
162
+ "healthy": False,
163
+ "error": str(e),
164
+ }
165
+
166
+
167
+ # ============================================================================
168
+ # Policy CRUD Endpoints (Proxy to Enforcer Service)
169
+ # ============================================================================
170
+
171
+ @router.post("", response_model=PolicyResponse, status_code=status.HTTP_201_CREATED)
172
+ async def create_policy(
173
+ policy: PolicyCreate,
174
+ service: PolicyService = Depends(get_policy_service),
175
+ ):
176
+ """
177
+ Create a new OPA policy in the enforcer service.
178
+
179
+ The policy will be stored in the enforcer service and can then be
180
+ associated with entities (agents, teams, environments).
181
+ """
182
+ if not service.is_enabled:
183
+ raise HTTPException(
184
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
185
+ detail="Policy enforcer is not configured. Set ENFORCER_SERVICE_URL environment variable.",
186
+ )
187
+
188
+ try:
189
+ created_policy = await service.create_policy(policy)
190
+ policy_dict = created_policy.dict()
191
+ # Convert datetime objects to ISO strings
192
+ if policy_dict.get("created_at"):
193
+ policy_dict["created_at"] = policy_dict["created_at"].isoformat() if hasattr(policy_dict["created_at"], "isoformat") else str(policy_dict["created_at"])
194
+ if policy_dict.get("updated_at"):
195
+ policy_dict["updated_at"] = policy_dict["updated_at"].isoformat() if hasattr(policy_dict["updated_at"], "isoformat") else str(policy_dict["updated_at"])
196
+ return PolicyResponse(
197
+ **policy_dict,
198
+ organization_id=service.organization_id,
199
+ association_count=0,
200
+ )
201
+ except PolicyValidationError as e:
202
+ raise HTTPException(
203
+ status_code=status.HTTP_400_BAD_REQUEST,
204
+ detail={
205
+ "error": str(e),
206
+ "code": "VALIDATION_ERROR",
207
+ "errors": getattr(e, 'errors', [])
208
+ }
209
+ )
210
+ except EnforcerConnectionError as e:
211
+ raise HTTPException(
212
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
213
+ detail={"error": str(e), "code": "SERVICE_UNAVAILABLE"}
214
+ )
215
+ except Exception as e:
216
+ logger.error("create_policy_failed", error=str(e))
217
+ raise HTTPException(
218
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
219
+ detail={"error": str(e), "code": "INTERNAL_ERROR"}
220
+ )
221
+
222
+
223
+ @router.get("/{policy_id}", response_model=PolicyResponse)
224
+ async def get_policy(
225
+ policy_id: str,
226
+ service: PolicyService = Depends(get_policy_service),
227
+ ):
228
+ """Get a specific policy by ID"""
229
+ if not service.is_enabled:
230
+ raise HTTPException(
231
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
232
+ detail="Policy enforcer is not configured",
233
+ )
234
+
235
+ try:
236
+ policy = await service.get_policy(policy_id)
237
+
238
+ # Count associations
239
+ associations = service.list_associations(policy_id=policy_id)
240
+
241
+ policy_dict = policy.dict()
242
+ # Convert datetime objects to ISO strings
243
+ if policy_dict.get("created_at"):
244
+ policy_dict["created_at"] = policy_dict["created_at"].isoformat() if hasattr(policy_dict["created_at"], "isoformat") else str(policy_dict["created_at"])
245
+ if policy_dict.get("updated_at"):
246
+ policy_dict["updated_at"] = policy_dict["updated_at"].isoformat() if hasattr(policy_dict["updated_at"], "isoformat") else str(policy_dict["updated_at"])
247
+
248
+ return PolicyResponse(
249
+ **policy_dict,
250
+ organization_id=service.organization_id,
251
+ association_count=len(associations),
252
+ )
253
+ except PolicyNotFoundError:
254
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Policy not found")
255
+ except Exception as e:
256
+ logger.error("get_policy_failed", policy_id=policy_id, error=str(e), error_type=type(e).__name__)
257
+ raise HTTPException(
258
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
259
+ detail={"error": str(e), "code": "POLICY_FETCH_ERROR"}
260
+ )
261
+
262
+
263
+ @router.get("", response_model=PolicyListResponse)
264
+ async def list_policies(
265
+ page: int = 1,
266
+ limit: int = 20,
267
+ enabled: Optional[bool] = None,
268
+ search: Optional[str] = None,
269
+ service: PolicyService = Depends(get_policy_service),
270
+ ):
271
+ """
272
+ List all policies from the enforcer service.
273
+
274
+ Supports pagination, filtering by enabled status, and search.
275
+ """
276
+ if not service.is_enabled:
277
+ return PolicyListResponse(
278
+ policies=[],
279
+ total=0,
280
+ page=page,
281
+ limit=limit,
282
+ has_more=False,
283
+ )
284
+
285
+ policies = await service.list_policies(
286
+ page=page,
287
+ limit=limit,
288
+ enabled=enabled,
289
+ search=search,
290
+ )
291
+
292
+ # Enhance with association counts
293
+ responses = []
294
+ for policy in policies:
295
+ associations = service.list_associations(policy_id=policy.id)
296
+ policy_dict = policy.dict()
297
+ # Ensure policy_content exists (list endpoint may not return it)
298
+ if not policy_dict.get("policy_content"):
299
+ policy_dict["policy_content"] = ""
300
+ # Convert datetime objects to ISO strings
301
+ if policy_dict.get("created_at"):
302
+ policy_dict["created_at"] = policy_dict["created_at"].isoformat() if hasattr(policy_dict["created_at"], "isoformat") else str(policy_dict["created_at"])
303
+ if policy_dict.get("updated_at"):
304
+ policy_dict["updated_at"] = policy_dict["updated_at"].isoformat() if hasattr(policy_dict["updated_at"], "isoformat") else str(policy_dict["updated_at"])
305
+ responses.append(
306
+ PolicyResponse(
307
+ **policy_dict,
308
+ organization_id=service.organization_id,
309
+ association_count=len(associations),
310
+ )
311
+ )
312
+
313
+ # Calculate total and has_more
314
+ # Note: The enforcer service list_policies returns all policies for now
315
+ # We'll implement proper pagination when needed
316
+ total = len(responses)
317
+ has_more = False # Since we're returning all results for now
318
+
319
+ return PolicyListResponse(
320
+ policies=responses,
321
+ total=total,
322
+ page=page,
323
+ limit=limit,
324
+ has_more=has_more,
325
+ )
326
+
327
+
328
+ @router.put("/{policy_id}", response_model=PolicyResponse)
329
+ async def update_policy(
330
+ policy_id: str,
331
+ update: PolicyUpdate,
332
+ service: PolicyService = Depends(get_policy_service),
333
+ ):
334
+ """Update an existing policy"""
335
+ if not service.is_enabled:
336
+ raise HTTPException(
337
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
338
+ detail="Policy enforcer is not configured",
339
+ )
340
+
341
+ try:
342
+ updated_policy = await service.update_policy(policy_id, update)
343
+
344
+ # Count associations
345
+ associations = service.list_associations(policy_id=policy_id)
346
+
347
+ policy_dict = updated_policy.dict()
348
+ # Convert datetime objects to ISO strings
349
+ if policy_dict.get("created_at"):
350
+ policy_dict["created_at"] = policy_dict["created_at"].isoformat() if hasattr(policy_dict["created_at"], "isoformat") else str(policy_dict["created_at"])
351
+ if policy_dict.get("updated_at"):
352
+ policy_dict["updated_at"] = policy_dict["updated_at"].isoformat() if hasattr(policy_dict["updated_at"], "isoformat") else str(policy_dict["updated_at"])
353
+
354
+ return PolicyResponse(
355
+ **policy_dict,
356
+ organization_id=service.organization_id,
357
+ association_count=len(associations),
358
+ )
359
+ except PolicyNotFoundError:
360
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Policy not found")
361
+ except PolicyValidationError as e:
362
+ raise HTTPException(
363
+ status_code=status.HTTP_400_BAD_REQUEST,
364
+ detail={
365
+ "error": str(e),
366
+ "code": "VALIDATION_ERROR",
367
+ "errors": getattr(e, 'errors', [])
368
+ }
369
+ )
370
+ except Exception as e:
371
+ logger.error("update_policy_failed", policy_id=policy_id, error=str(e))
372
+ raise HTTPException(
373
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
374
+ detail={"error": str(e), "code": "INTERNAL_ERROR"}
375
+ )
376
+
377
+
378
+ @router.delete("/{policy_id}", status_code=status.HTTP_204_NO_CONTENT)
379
+ async def delete_policy(
380
+ policy_id: str,
381
+ service: PolicyService = Depends(get_policy_service),
382
+ ):
383
+ """
384
+ Delete a policy from the enforcer service.
385
+
386
+ This will also remove all associations with entities.
387
+ """
388
+ if not service.is_enabled:
389
+ raise HTTPException(
390
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
391
+ detail="Policy enforcer is not configured",
392
+ )
393
+
394
+ try:
395
+ await service.delete_policy(policy_id)
396
+ except PolicyNotFoundError:
397
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Policy not found")
398
+
399
+
400
+ @router.post("/{policy_id}/validate", response_model=ValidationResultResponse)
401
+ async def validate_policy(
402
+ policy_id: str,
403
+ service: PolicyService = Depends(get_policy_service),
404
+ ):
405
+ """
406
+ Validate a policy's Rego syntax and structure.
407
+
408
+ Returns validation results with errors and warnings.
409
+ """
410
+ if not service.is_enabled:
411
+ raise HTTPException(
412
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
413
+ detail="Policy enforcer is not configured",
414
+ )
415
+
416
+ try:
417
+ result = await service.validate_policy(policy_id)
418
+ return {
419
+ "valid": result.valid,
420
+ "errors": result.errors,
421
+ "warnings": result.warnings,
422
+ }
423
+ except PolicyNotFoundError:
424
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Policy not found")
425
+ except Exception as e:
426
+ logger.error("validate_policy_failed", policy_id=policy_id, error=str(e))
427
+ raise HTTPException(
428
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
429
+ detail={"error": str(e), "code": "INTERNAL_ERROR"}
430
+ )
431
+
432
+
433
+ # ============================================================================
434
+ # Policy Association Endpoints
435
+ # ============================================================================
436
+
437
+ @router.post("/associations", response_model=PolicyAssociationResponse, status_code=status.HTTP_201_CREATED)
438
+ async def create_policy_association(
439
+ association: PolicyAssociationCreate,
440
+ request: Request,
441
+ service: PolicyService = Depends(get_policy_service),
442
+ organization: dict = Depends(get_current_organization),
443
+ ):
444
+ """
445
+ Create a policy association (link a policy to an entity).
446
+
447
+ Entities can be agents, teams, or environments.
448
+ Priority determines which policy wins in case of conflicts (higher wins).
449
+ """
450
+ if not service.is_enabled:
451
+ raise HTTPException(
452
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
453
+ detail="Policy enforcer is not configured",
454
+ )
455
+
456
+ # Extract user email from request if available
457
+ created_by = None
458
+ if hasattr(request.state, "user_email"):
459
+ created_by = request.state.user_email
460
+
461
+ try:
462
+ return await service.create_association(association, created_by=created_by)
463
+ except PolicyNotFoundError:
464
+ raise HTTPException(
465
+ status_code=status.HTTP_404_NOT_FOUND,
466
+ detail=f"Policy {association.policy_id} not found",
467
+ )
468
+ except Exception as e:
469
+ logger.error("create_association_failed", error=str(e))
470
+ raise HTTPException(
471
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
472
+ detail=str(e),
473
+ )
474
+
475
+
476
+ @router.get("/associations", response_model=List[PolicyAssociationResponse])
477
+ def list_policy_associations(
478
+ entity_type: Optional[EntityType] = None,
479
+ entity_id: Optional[str] = None,
480
+ policy_id: Optional[str] = None,
481
+ enabled: Optional[bool] = None,
482
+ service: PolicyService = Depends(get_policy_service),
483
+ ):
484
+ """
485
+ List policy associations with filtering.
486
+
487
+ Can filter by entity type, entity ID, policy ID, and enabled status.
488
+ """
489
+ return service.list_associations(
490
+ entity_type=entity_type,
491
+ entity_id=entity_id,
492
+ policy_id=policy_id,
493
+ enabled=enabled,
494
+ )
495
+
496
+
497
+ @router.get("/associations/{association_id}", response_model=PolicyAssociationResponse)
498
+ def get_policy_association(
499
+ association_id: str,
500
+ service: PolicyService = Depends(get_policy_service),
501
+ ):
502
+ """Get a specific policy association by ID"""
503
+ association = service.get_association(association_id)
504
+ if not association:
505
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Association not found")
506
+ return association
507
+
508
+
509
+ @router.patch("/associations/{association_id}", response_model=PolicyAssociationResponse)
510
+ def update_policy_association(
511
+ association_id: str,
512
+ update: PolicyAssociationUpdate,
513
+ service: PolicyService = Depends(get_policy_service),
514
+ ):
515
+ """Update a policy association (e.g., enable/disable, change priority)"""
516
+ association = service.update_association(association_id, update)
517
+ if not association:
518
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Association not found")
519
+ return association
520
+
521
+
522
+ @router.delete("/associations/{association_id}", status_code=status.HTTP_204_NO_CONTENT)
523
+ def delete_policy_association(
524
+ association_id: str,
525
+ service: PolicyService = Depends(get_policy_service),
526
+ ):
527
+ """Delete a policy association"""
528
+ if not service.delete_association(association_id):
529
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Association not found")
530
+
531
+
532
+ # ============================================================================
533
+ # Policy Resolution and Evaluation Endpoints
534
+ # ============================================================================
535
+
536
+ @router.get("/resolved/{entity_type}/{entity_id}", response_model=List[ResolvedPolicy])
537
+ async def resolve_entity_policies(
538
+ entity_type: EntityType,
539
+ entity_id: str,
540
+ include_details: bool = False,
541
+ service: PolicyService = Depends(get_policy_service),
542
+ ):
543
+ """
544
+ Resolve all policies applicable to an entity considering inheritance.
545
+
546
+ Inheritance order: environment > team > agent
547
+ Returns policies with source information showing where each policy comes from.
548
+
549
+ Set include_details=true to fetch full policy content from enforcer service.
550
+ """
551
+ if not service.is_enabled:
552
+ return []
553
+
554
+ return await service.resolve_entity_policies(
555
+ entity_type=entity_type,
556
+ entity_id=entity_id,
557
+ include_details=include_details,
558
+ )
559
+
560
+
561
+ @router.post("/evaluate/{entity_type}/{entity_id}", response_model=EvaluationResponse)
562
+ async def evaluate_entity_policies(
563
+ entity_type: EntityType,
564
+ entity_id: str,
565
+ request: EvaluationRequest,
566
+ service: PolicyService = Depends(get_policy_service),
567
+ ):
568
+ """
569
+ Evaluate all policies for an entity against input data.
570
+
571
+ This evaluates all inherited policies and returns aggregated results.
572
+ """
573
+ if not service.is_enabled:
574
+ return EvaluationResponse(
575
+ allowed=True,
576
+ violations=[],
577
+ policy_results={},
578
+ )
579
+
580
+ results = await service.evaluate_policies(
581
+ entity_type=entity_type,
582
+ entity_id=entity_id,
583
+ input_data=request.input_data,
584
+ )
585
+
586
+ # Aggregate results
587
+ allowed = all(result.allow for result in results.values())
588
+ all_violations = []
589
+ for result in results.values():
590
+ all_violations.extend(result.violations)
591
+
592
+ policy_results = {
593
+ policy_id: result.dict()
594
+ for policy_id, result in results.items()
595
+ }
596
+
597
+ return EvaluationResponse(
598
+ allowed=allowed,
599
+ violations=all_violations,
600
+ policy_results=policy_results,
601
+ )
602
+
603
+
604
+ @router.post("/check-authorization/{entity_type}/{entity_id}", response_model=AuthorizationCheckResponse)
605
+ async def check_entity_authorization(
606
+ entity_type: EntityType,
607
+ entity_id: str,
608
+ request: AuthorizationCheckRequest,
609
+ service: PolicyService = Depends(get_policy_service),
610
+ ):
611
+ """
612
+ Check if an entity is authorized to perform an action.
613
+
614
+ This is a convenience endpoint for common authorization checks.
615
+ It evaluates all policies and returns a simple authorized/denied response.
616
+ """
617
+ if not service.is_enabled:
618
+ return AuthorizationCheckResponse(
619
+ authorized=True,
620
+ violations=[],
621
+ policies_evaluated=0,
622
+ )
623
+
624
+ authorized, violations = await service.check_entity_authorization(
625
+ entity_type=entity_type,
626
+ entity_id=entity_id,
627
+ action=request.action,
628
+ resource=request.resource,
629
+ context=request.context,
630
+ )
631
+
632
+ # Count policies
633
+ resolved = await service.resolve_entity_policies(entity_type, entity_id)
634
+
635
+ return AuthorizationCheckResponse(
636
+ authorized=authorized,
637
+ violations=violations,
638
+ policies_evaluated=len(resolved),
639
+ )