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,517 @@
1
+ """
2
+ Execution Environment Router - Resolve execution environment for agents/teams
3
+
4
+ This router provides workers with resolved execution environment configuration:
5
+ - Fetches agent/team execution_environment from database
6
+ - Resolves secret names to actual values from Kubiya API
7
+ - Resolves integration IDs to actual tokens from Kubiya API
8
+ - Maps integration tokens to specific env var names (GH_TOKEN, JIRA_TOKEN, etc.)
9
+ - Returns complete env var dict ready for worker to inject into execution
10
+ """
11
+
12
+ import httpx
13
+ from fastapi import APIRouter, Depends, HTTPException, Request
14
+ from typing import Dict, Any
15
+ import structlog
16
+
17
+ from control_plane_api.app.middleware.auth import get_current_organization
18
+ from control_plane_api.app.lib.supabase import get_supabase
19
+ from control_plane_api.app.lib.kubiya_client import KUBIYA_API_BASE
20
+
21
+ logger = structlog.get_logger()
22
+
23
+ router = APIRouter(prefix="/execution-environment", tags=["execution-environment"])
24
+
25
+
26
+ # Integration type to environment variable name mapping
27
+ INTEGRATION_ENV_VAR_MAP = {
28
+ "github": "GH_TOKEN",
29
+ "github_app": "GITHUB_TOKEN",
30
+ "jira": "JIRA_TOKEN",
31
+ "slack": "SLACK_TOKEN",
32
+ "aws": "AWS_ACCESS_KEY_ID", # Note: AWS might need multiple vars
33
+ "aws-serviceaccount": "AWS_ROLE_ARN",
34
+ "kubernetes": "KUBECONFIG",
35
+ }
36
+
37
+
38
+ async def resolve_secret_value(
39
+ secret_name: str,
40
+ token: str,
41
+ org_id: str,
42
+ ) -> str:
43
+ """
44
+ Resolve a secret name to its actual value from Kubiya API.
45
+
46
+ Args:
47
+ secret_name: Name of the secret to resolve
48
+ token: Kubiya API token
49
+ org_id: Organization ID
50
+
51
+ Returns:
52
+ Secret value as string
53
+ """
54
+ headers = {
55
+ "Authorization": f"UserKey {token}",
56
+ "Accept": "application/json",
57
+ "Content-Type": "application/json",
58
+ "X-Kubiya-Client": "agent-control-plane",
59
+ "X-Organization-ID": org_id,
60
+ }
61
+
62
+ async with httpx.AsyncClient(timeout=30.0) as client:
63
+ response = await client.get(
64
+ f"{KUBIYA_API_BASE}/api/v2/secrets/get_value/{secret_name}",
65
+ headers=headers,
66
+ )
67
+
68
+ if response.status_code == 200:
69
+ return response.text
70
+ else:
71
+ logger.warning(
72
+ "secret_resolution_failed",
73
+ secret_name=secret_name[:20],
74
+ status=response.status_code,
75
+ )
76
+ raise HTTPException(
77
+ status_code=response.status_code,
78
+ detail=f"Failed to resolve secret '{secret_name}': {response.text[:200]}",
79
+ )
80
+
81
+
82
+ async def resolve_integration_token(
83
+ integration_id: str,
84
+ integration_type: str,
85
+ token: str,
86
+ org_id: str,
87
+ ) -> Dict[str, str]:
88
+ """
89
+ Resolve an integration ID to its actual token from Kubiya API.
90
+
91
+ Args:
92
+ integration_id: Integration UUID
93
+ integration_type: Type of integration (github, jira, etc.)
94
+ token: Kubiya API token
95
+ org_id: Organization ID
96
+
97
+ Returns:
98
+ Dict with env_var_name and token value
99
+ """
100
+ headers = {
101
+ "Authorization": f"UserKey {token}",
102
+ "Accept": "application/json",
103
+ "Content-Type": "application/json",
104
+ "X-Kubiya-Client": "agent-control-plane",
105
+ "X-Organization-ID": org_id,
106
+ }
107
+
108
+ # Build token URL based on integration type
109
+ integration_type_lower = integration_type.lower()
110
+
111
+ if integration_type_lower == "github":
112
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integration/github/token/{integration_id}"
113
+ elif integration_type_lower == "github_app":
114
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integration/github_app/token/{integration_id}"
115
+ elif integration_type_lower == "jira":
116
+ token_url = f"{KUBIYA_API_BASE}/api/v1/integration/jira/token/{integration_id}"
117
+ else:
118
+ logger.warning(
119
+ "unsupported_integration_type",
120
+ integration_type=integration_type,
121
+ integration_id=integration_id[:8],
122
+ )
123
+ # For unsupported types, skip
124
+ return {}
125
+
126
+ async with httpx.AsyncClient(timeout=30.0) as client:
127
+ response = await client.get(token_url, headers=headers)
128
+
129
+ if response.status_code == 200:
130
+ # Try to parse as JSON first
131
+ try:
132
+ token_data = response.json()
133
+ token_value = token_data.get("token", response.text)
134
+ except:
135
+ # If not JSON, use plain text
136
+ token_value = response.text
137
+
138
+ # Map to env var name
139
+ env_var_name = INTEGRATION_ENV_VAR_MAP.get(integration_type_lower, f"{integration_type.upper()}_TOKEN")
140
+
141
+ return {env_var_name: token_value}
142
+ else:
143
+ logger.warning(
144
+ "integration_token_resolution_failed",
145
+ integration_id=integration_id[:8],
146
+ integration_type=integration_type,
147
+ status=response.status_code,
148
+ )
149
+ # Don't fail the entire request for one integration
150
+ return {}
151
+
152
+
153
+ async def resolve_environment_configs(
154
+ environment_ids: list[str],
155
+ org_id: str,
156
+ ) -> Dict[str, Any]:
157
+ """
158
+ Resolve execution environment configs from a list of environment IDs.
159
+ Merges configs from all environments.
160
+
161
+ Args:
162
+ environment_ids: List of environment IDs
163
+ org_id: Organization ID
164
+
165
+ Returns:
166
+ Merged execution environment dict with env_vars, secrets, integration_ids
167
+ """
168
+ if not environment_ids:
169
+ return {"env_vars": {}, "secrets": [], "integration_ids": []}
170
+
171
+ supabase = get_supabase()
172
+
173
+ # Fetch all environments
174
+ result = (
175
+ supabase.table("environments")
176
+ .select("execution_environment")
177
+ .in_("id", environment_ids)
178
+ .eq("organization_id", org_id)
179
+ .execute()
180
+ )
181
+
182
+ # Merge all environment configs
183
+ merged_env_vars = {}
184
+ merged_secrets = set()
185
+ merged_integration_ids = set()
186
+
187
+ for env in result.data:
188
+ env_config = env.get("execution_environment", {})
189
+
190
+ # Merge env vars (later environments override earlier ones)
191
+ merged_env_vars.update(env_config.get("env_vars", {}))
192
+
193
+ # Collect secrets (union)
194
+ merged_secrets.update(env_config.get("secrets", []))
195
+
196
+ # Collect integration IDs (union)
197
+ merged_integration_ids.update(env_config.get("integration_ids", []))
198
+
199
+ return {
200
+ "env_vars": merged_env_vars,
201
+ "secrets": list(merged_secrets),
202
+ "integration_ids": list(merged_integration_ids),
203
+ }
204
+
205
+
206
+ @router.get("/agents/{agent_id}/resolved")
207
+ async def get_agent_execution_environment(
208
+ agent_id: str,
209
+ request: Request,
210
+ organization: dict = Depends(get_current_organization),
211
+ ) -> Dict[str, str]:
212
+ """
213
+ Get resolved execution environment for an agent.
214
+
215
+ This endpoint:
216
+ 1. Fetches agent's execution_environment and environment_ids from database
217
+ 2. Fetches and merges execution configs from all associated environments
218
+ 3. Merges agent's own execution_environment (agent config overrides environment)
219
+ 4. Resolves all secret names to actual values
220
+ 5. Resolves all integration IDs to actual tokens
221
+ 6. Maps integration tokens to specific env var names
222
+ 7. Returns merged env var dict
223
+
224
+ Inheritance order (later overrides earlier):
225
+ - Environment 1 execution_environment
226
+ - Environment 2 execution_environment
227
+ - ...
228
+ - Agent execution_environment
229
+
230
+ Returns:
231
+ Dict of environment variables ready to inject into agent execution
232
+ """
233
+ try:
234
+ token = request.state.kubiya_token
235
+ org_id = organization["id"]
236
+ supabase = get_supabase()
237
+
238
+ # Fetch agent with environment associations
239
+ result = supabase.table("agents").select("execution_environment, environment_ids").eq("id", agent_id).eq("organization_id", org_id).execute()
240
+
241
+ if not result.data:
242
+ raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
243
+
244
+ agent = result.data[0]
245
+
246
+ # Get environment-level configs first
247
+ environment_ids = agent.get("environment_ids", [])
248
+ env_config = await resolve_environment_configs(environment_ids, org_id)
249
+
250
+ # Get agent-level config
251
+ agent_config = agent.get("execution_environment", {})
252
+
253
+ # Merge: environment config + agent config (agent overrides environment)
254
+ execution_environment = {
255
+ "env_vars": {**env_config.get("env_vars", {}), **agent_config.get("env_vars", {})},
256
+ "secrets": list(set(env_config.get("secrets", []) + agent_config.get("secrets", []))),
257
+ "integration_ids": list(set(env_config.get("integration_ids", []) + agent_config.get("integration_ids", []))),
258
+ }
259
+
260
+ # Start with custom env vars
261
+ resolved_env_vars = dict(execution_environment.get("env_vars", {}))
262
+
263
+ # Resolve secrets
264
+ secrets = execution_environment.get("secrets", [])
265
+ for secret_name in secrets:
266
+ try:
267
+ secret_value = await resolve_secret_value(secret_name, token, org_id)
268
+ resolved_env_vars[secret_name] = secret_value
269
+ logger.info(
270
+ "secret_resolved",
271
+ agent_id=agent_id[:8],
272
+ secret_name=secret_name[:20],
273
+ )
274
+ except Exception as e:
275
+ logger.error(
276
+ "secret_resolution_error",
277
+ agent_id=agent_id[:8],
278
+ secret_name=secret_name[:20],
279
+ error=str(e),
280
+ )
281
+ # Continue with other secrets even if one fails
282
+
283
+ # Resolve integrations
284
+ integration_ids = execution_environment.get("integration_ids", [])
285
+ if integration_ids:
286
+ # First, fetch integration details to get types
287
+ headers = {
288
+ "Authorization": f"UserKey {token}",
289
+ "Accept": "application/json",
290
+ "Content-Type": "application/json",
291
+ "X-Kubiya-Client": "agent-control-plane",
292
+ "X-Organization-ID": org_id,
293
+ }
294
+
295
+ async with httpx.AsyncClient(timeout=30.0) as client:
296
+ response = await client.get(
297
+ f"{KUBIYA_API_BASE}/api/v2/integrations?full=true",
298
+ headers=headers,
299
+ )
300
+
301
+ if response.status_code == 200:
302
+ all_integrations = response.json()
303
+
304
+ for integration_id in integration_ids:
305
+ # Find integration by UUID
306
+ integration = next(
307
+ (i for i in all_integrations if i.get("uuid") == integration_id),
308
+ None
309
+ )
310
+
311
+ if integration:
312
+ integration_type = integration.get("integration_type", "")
313
+ try:
314
+ token_env_vars = await resolve_integration_token(
315
+ integration_id,
316
+ integration_type,
317
+ token,
318
+ org_id,
319
+ )
320
+ resolved_env_vars.update(token_env_vars)
321
+ logger.info(
322
+ "integration_resolved",
323
+ agent_id=agent_id[:8],
324
+ integration_id=integration_id[:8],
325
+ integration_type=integration_type,
326
+ env_vars=list(token_env_vars.keys()),
327
+ )
328
+ except Exception as e:
329
+ logger.error(
330
+ "integration_resolution_error",
331
+ agent_id=agent_id[:8],
332
+ integration_id=integration_id[:8],
333
+ error=str(e),
334
+ )
335
+ else:
336
+ logger.warning(
337
+ "integration_not_found",
338
+ agent_id=agent_id[:8],
339
+ integration_id=integration_id[:8],
340
+ )
341
+
342
+ logger.info(
343
+ "execution_environment_resolved",
344
+ agent_id=agent_id[:8],
345
+ env_var_count=len(resolved_env_vars),
346
+ env_var_keys=list(resolved_env_vars.keys()),
347
+ )
348
+
349
+ return resolved_env_vars
350
+
351
+ except HTTPException:
352
+ raise
353
+ except Exception as e:
354
+ logger.error(
355
+ "execution_environment_resolution_error",
356
+ agent_id=agent_id[:8],
357
+ error=str(e),
358
+ error_type=type(e).__name__,
359
+ )
360
+ raise HTTPException(status_code=500, detail=f"Failed to resolve execution environment: {str(e)}")
361
+
362
+
363
+ @router.get("/teams/{team_id}/resolved")
364
+ async def get_team_execution_environment(
365
+ team_id: str,
366
+ request: Request,
367
+ organization: dict = Depends(get_current_organization),
368
+ ) -> Dict[str, str]:
369
+ """
370
+ Get resolved execution environment for a team.
371
+
372
+ This endpoint:
373
+ 1. Fetches team's execution_environment and environment_ids from database
374
+ 2. Fetches and merges execution configs from all associated environments
375
+ 3. Merges team's own execution_environment (team config overrides environment)
376
+ 4. Resolves all secret names to actual values
377
+ 5. Resolves all integration IDs to actual tokens
378
+ 6. Maps integration tokens to specific env var names
379
+ 7. Returns merged env var dict
380
+
381
+ Inheritance order (later overrides earlier):
382
+ - Environment 1 execution_environment
383
+ - Environment 2 execution_environment
384
+ - ...
385
+ - Team execution_environment
386
+
387
+ Returns:
388
+ Dict of environment variables ready to inject into team execution
389
+ """
390
+ try:
391
+ token = request.state.kubiya_token
392
+ org_id = organization["id"]
393
+ supabase = get_supabase()
394
+
395
+ # Fetch team with environment associations
396
+ result = supabase.table("teams").select("execution_environment, environment_ids").eq("id", team_id).eq("organization_id", org_id).execute()
397
+
398
+ if not result.data:
399
+ raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
400
+
401
+ team = result.data[0]
402
+
403
+ # Get environment-level configs first
404
+ environment_ids = team.get("environment_ids", [])
405
+ env_config = await resolve_environment_configs(environment_ids, org_id)
406
+
407
+ # Get team-level config
408
+ team_config = team.get("execution_environment", {})
409
+
410
+ # Merge: environment config + team config (team overrides environment)
411
+ execution_environment = {
412
+ "env_vars": {**env_config.get("env_vars", {}), **team_config.get("env_vars", {})},
413
+ "secrets": list(set(env_config.get("secrets", []) + team_config.get("secrets", []))),
414
+ "integration_ids": list(set(env_config.get("integration_ids", []) + team_config.get("integration_ids", []))),
415
+ }
416
+
417
+ # Start with custom env vars
418
+ resolved_env_vars = dict(execution_environment.get("env_vars", {}))
419
+
420
+ # Resolve secrets
421
+ secrets = execution_environment.get("secrets", [])
422
+ for secret_name in secrets:
423
+ try:
424
+ secret_value = await resolve_secret_value(secret_name, token, org_id)
425
+ resolved_env_vars[secret_name] = secret_value
426
+ logger.info(
427
+ "secret_resolved",
428
+ team_id=team_id[:8],
429
+ secret_name=secret_name[:20],
430
+ )
431
+ except Exception as e:
432
+ logger.error(
433
+ "secret_resolution_error",
434
+ team_id=team_id[:8],
435
+ secret_name=secret_name[:20],
436
+ error=str(e),
437
+ )
438
+ # Continue with other secrets even if one fails
439
+
440
+ # Resolve integrations
441
+ integration_ids = execution_environment.get("integration_ids", [])
442
+ if integration_ids:
443
+ # First, fetch integration details to get types
444
+ headers = {
445
+ "Authorization": f"UserKey {token}",
446
+ "Accept": "application/json",
447
+ "Content-Type": "application/json",
448
+ "X-Kubiya-Client": "agent-control-plane",
449
+ "X-Organization-ID": org_id,
450
+ }
451
+
452
+ async with httpx.AsyncClient(timeout=30.0) as client:
453
+ response = await client.get(
454
+ f"{KUBIYA_API_BASE}/api/v2/integrations?full=true",
455
+ headers=headers,
456
+ )
457
+
458
+ if response.status_code == 200:
459
+ all_integrations = response.json()
460
+
461
+ for integration_id in integration_ids:
462
+ # Find integration by UUID
463
+ integration = next(
464
+ (i for i in all_integrations if i.get("uuid") == integration_id),
465
+ None
466
+ )
467
+
468
+ if integration:
469
+ integration_type = integration.get("integration_type", "")
470
+ try:
471
+ token_env_vars = await resolve_integration_token(
472
+ integration_id,
473
+ integration_type,
474
+ token,
475
+ org_id,
476
+ )
477
+ resolved_env_vars.update(token_env_vars)
478
+ logger.info(
479
+ "integration_resolved",
480
+ team_id=team_id[:8],
481
+ integration_id=integration_id[:8],
482
+ integration_type=integration_type,
483
+ env_vars=list(token_env_vars.keys()),
484
+ )
485
+ except Exception as e:
486
+ logger.error(
487
+ "integration_resolution_error",
488
+ team_id=team_id[:8],
489
+ integration_id=integration_id[:8],
490
+ error=str(e),
491
+ )
492
+ else:
493
+ logger.warning(
494
+ "integration_not_found",
495
+ team_id=team_id[:8],
496
+ integration_id=integration_id[:8],
497
+ )
498
+
499
+ logger.info(
500
+ "execution_environment_resolved",
501
+ team_id=team_id[:8],
502
+ env_var_count=len(resolved_env_vars),
503
+ env_var_keys=list(resolved_env_vars.keys()),
504
+ )
505
+
506
+ return resolved_env_vars
507
+
508
+ except HTTPException:
509
+ raise
510
+ except Exception as e:
511
+ logger.error(
512
+ "execution_environment_resolution_error",
513
+ team_id=team_id[:8],
514
+ error=str(e),
515
+ error_type=type(e).__name__,
516
+ )
517
+ raise HTTPException(status_code=500, detail=f"Failed to resolve execution environment: {str(e)}")