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.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
- {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
|