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,529 @@
1
+ """
2
+ Control Plane Client - Clean API for worker to communicate with Control Plane.
3
+
4
+ This centralizes all HTTP communication between worker and Control Plane,
5
+ providing a clean interface for:
6
+ - Event streaming (real-time UI updates)
7
+ - Session persistence (history storage)
8
+ - Metadata caching (execution types)
9
+ - Skill resolution
10
+
11
+ Usage:
12
+ from control_plane_client import get_control_plane_client
13
+
14
+ client = get_control_plane_client()
15
+ client.publish_event(execution_id, "message_chunk", {...})
16
+ client.persist_session(execution_id, session_id, user_id, messages)
17
+ """
18
+
19
+ import os
20
+ import httpx
21
+ from datetime import datetime, timezone
22
+ from typing import Optional, Dict, List, Any
23
+ import structlog
24
+
25
+ logger = structlog.get_logger()
26
+
27
+
28
+ class ControlPlaneClient:
29
+ """Client for communicating with the Control Plane API from workers."""
30
+
31
+ def __init__(self, base_url: str, api_key: str):
32
+ """
33
+ Initialize Control Plane client.
34
+
35
+ Args:
36
+ base_url: Control Plane URL (e.g., http://localhost:8000)
37
+ api_key: Kubiya API key for authentication
38
+ """
39
+ self.base_url = base_url.rstrip("/")
40
+ self.api_key = api_key
41
+ self.headers = {"Authorization": f"UserKey {api_key}"}
42
+
43
+ # Use BOTH sync and async clients for different use cases
44
+ # Sync client for backwards compatibility with non-async code
45
+ self._client = httpx.Client(
46
+ timeout=httpx.Timeout(30.0, connect=5.0, read=30.0, write=10.0),
47
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
48
+ )
49
+
50
+ # Async client for streaming/real-time operations
51
+ # Longer read timeout to handle streaming scenarios
52
+ self._async_client = httpx.AsyncClient(
53
+ timeout=httpx.Timeout(60.0, connect=5.0, read=60.0, write=10.0),
54
+ limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
55
+ )
56
+
57
+ def __del__(self):
58
+ """Close the HTTP clients on cleanup."""
59
+ try:
60
+ self._client.close()
61
+ except:
62
+ pass
63
+ # Async client cleanup happens via context manager or explicit close
64
+
65
+ async def aclose(self):
66
+ """Async cleanup for async client."""
67
+ try:
68
+ await self._async_client.aclose()
69
+ except:
70
+ pass
71
+
72
+ def publish_event(
73
+ self,
74
+ execution_id: str,
75
+ event_type: str,
76
+ data: Dict[str, Any],
77
+ ) -> bool:
78
+ """
79
+ Publish a streaming event for real-time UI updates (SYNC version).
80
+
81
+ NOTE: This is the BLOCKING version. For real-time streaming,
82
+ use publish_event_async() instead to avoid blocking the event loop.
83
+
84
+ Args:
85
+ execution_id: Execution ID
86
+ event_type: Event type (message_chunk, tool_started, etc.)
87
+ data: Event payload
88
+
89
+ Returns:
90
+ True if successful, False otherwise
91
+ """
92
+ try:
93
+ url = f"{self.base_url}/api/v1/executions/{execution_id}/events"
94
+ payload = {
95
+ "event_type": event_type,
96
+ "data": data,
97
+ "timestamp": datetime.now(timezone.utc).isoformat(),
98
+ }
99
+
100
+ response = self._client.post(url, json=payload, headers=self.headers)
101
+
102
+ if response.status_code not in (200, 202):
103
+ logger.warning(
104
+ "event_publish_failed",
105
+ status=response.status_code,
106
+ execution_id=execution_id[:8],
107
+ event_type=event_type,
108
+ )
109
+ return False
110
+
111
+ return True
112
+
113
+ except Exception as e:
114
+ logger.warning(
115
+ "event_publish_error",
116
+ error=str(e),
117
+ execution_id=execution_id[:8],
118
+ event_type=event_type,
119
+ )
120
+ return False
121
+
122
+ async def publish_event_async(
123
+ self,
124
+ execution_id: str,
125
+ event_type: str,
126
+ data: Dict[str, Any],
127
+ ) -> bool:
128
+ """
129
+ Publish a streaming event for real-time UI updates (ASYNC version).
130
+
131
+ This is NON-BLOCKING and should be used for streaming to avoid
132
+ blocking the event loop while waiting for HTTP responses.
133
+
134
+ Args:
135
+ execution_id: Execution ID
136
+ event_type: Event type (message_chunk, tool_started, etc.)
137
+ data: Event payload
138
+
139
+ Returns:
140
+ True if successful, False otherwise
141
+ """
142
+ try:
143
+ url = f"{self.base_url}/api/v1/executions/{execution_id}/events"
144
+ payload = {
145
+ "event_type": event_type,
146
+ "data": data,
147
+ "timestamp": datetime.now(timezone.utc).isoformat(),
148
+ }
149
+
150
+ response = await self._async_client.post(url, json=payload, headers=self.headers)
151
+
152
+ if response.status_code not in (200, 202):
153
+ logger.warning(
154
+ "event_publish_failed",
155
+ status=response.status_code,
156
+ execution_id=execution_id[:8],
157
+ event_type=event_type,
158
+ )
159
+ return False
160
+
161
+ return True
162
+
163
+ except Exception as e:
164
+ logger.warning(
165
+ "event_publish_error",
166
+ error=str(e),
167
+ execution_id=execution_id[:8],
168
+ event_type=event_type,
169
+ )
170
+ return False
171
+
172
+ def cache_metadata(
173
+ self,
174
+ execution_id: str,
175
+ execution_type: str,
176
+ ) -> bool:
177
+ """
178
+ Cache execution metadata in Redis for fast SSE lookups.
179
+
180
+ This eliminates the need for database queries on every SSE connection.
181
+
182
+ Args:
183
+ execution_id: Execution ID
184
+ execution_type: "AGENT" or "TEAM"
185
+
186
+ Returns:
187
+ True if successful, False otherwise
188
+ """
189
+ return self.publish_event(
190
+ execution_id=execution_id,
191
+ event_type="metadata",
192
+ data={"execution_type": execution_type},
193
+ )
194
+
195
+ def get_session(
196
+ self,
197
+ execution_id: str,
198
+ session_id: Optional[str] = None,
199
+ ) -> Optional[Dict[str, Any]]:
200
+ """
201
+ Retrieve session history from Control Plane database.
202
+
203
+ This loads conversation history so workers can restore context
204
+ across multiple execution turns.
205
+
206
+ Args:
207
+ execution_id: Execution ID
208
+ session_id: Session ID (defaults to execution_id if not provided)
209
+
210
+ Returns:
211
+ Dict with session data including messages, or None if not found
212
+ """
213
+ try:
214
+ session_id = session_id or execution_id
215
+ url = f"{self.base_url}/api/v1/executions/{execution_id}/session"
216
+
217
+ response = self._client.get(url, headers=self.headers)
218
+
219
+ if response.status_code == 200:
220
+ session_data = response.json()
221
+ logger.info(
222
+ "session_loaded",
223
+ execution_id=execution_id[:8],
224
+ message_count=len(session_data.get("messages", [])),
225
+ )
226
+ return session_data
227
+ elif response.status_code == 404:
228
+ logger.info(
229
+ "session_not_found",
230
+ execution_id=execution_id[:8],
231
+ )
232
+ return None
233
+ else:
234
+ logger.warning(
235
+ "session_load_failed",
236
+ status=response.status_code,
237
+ execution_id=execution_id[:8],
238
+ )
239
+ return None
240
+
241
+ except Exception as e:
242
+ logger.warning(
243
+ "session_load_error",
244
+ error=str(e),
245
+ execution_id=execution_id[:8],
246
+ )
247
+ return None
248
+
249
+ def persist_session(
250
+ self,
251
+ execution_id: str,
252
+ session_id: str,
253
+ user_id: Optional[str],
254
+ messages: List[Dict[str, Any]],
255
+ metadata: Optional[Dict[str, Any]] = None,
256
+ ) -> bool:
257
+ """
258
+ Persist session history to Control Plane database.
259
+
260
+ This ensures history is available even when worker is offline.
261
+
262
+ Args:
263
+ execution_id: Execution ID
264
+ session_id: Session ID
265
+ user_id: User ID
266
+ messages: List of session messages
267
+ metadata: Optional metadata
268
+
269
+ Returns:
270
+ True if successful, False otherwise
271
+ """
272
+ try:
273
+ url = f"{self.base_url}/api/v1/executions/{execution_id}/session"
274
+ payload = {
275
+ "session_id": session_id,
276
+ "user_id": user_id,
277
+ "messages": messages,
278
+ "metadata": metadata or {},
279
+ }
280
+
281
+ response = self._client.post(url, json=payload, headers=self.headers)
282
+
283
+ if response.status_code in (200, 201):
284
+ logger.info(
285
+ "session_persisted",
286
+ execution_id=execution_id[:8],
287
+ message_count=len(messages),
288
+ )
289
+ return True
290
+ else:
291
+ logger.warning(
292
+ "session_persistence_failed",
293
+ status=response.status_code,
294
+ execution_id=execution_id[:8],
295
+ )
296
+ return False
297
+
298
+ except Exception as e:
299
+ logger.warning(
300
+ "session_persistence_error",
301
+ error=str(e),
302
+ execution_id=execution_id[:8],
303
+ )
304
+ return False
305
+
306
+ def get_skills(
307
+ self,
308
+ agent_id: str,
309
+ ) -> List[Dict[str, Any]]:
310
+ """
311
+ Fetch resolved skills for an agent from Control Plane.
312
+
313
+ This endpoint returns skills merged from all layers:
314
+ - All agent environments (many-to-many)
315
+ - Team skills (if agent has team)
316
+ - All team environments (many-to-many)
317
+ - Agent's own skills
318
+
319
+ Args:
320
+ agent_id: Agent ID
321
+
322
+ Returns:
323
+ List of skill configurations with source and inheritance info
324
+ """
325
+ try:
326
+ url = f"{self.base_url}/api/v1/skills/associations/agents/{agent_id}/skills/resolved"
327
+ response = self._client.get(url, headers=self.headers)
328
+
329
+ if response.status_code == 200:
330
+ skills = response.json()
331
+ logger.info(
332
+ "skills_fetched",
333
+ agent_id=agent_id[:8],
334
+ skill_count=len(skills),
335
+ )
336
+ return skills
337
+ else:
338
+ logger.warning(
339
+ "skills_fetch_failed",
340
+ status=response.status_code,
341
+ agent_id=agent_id[:8],
342
+ )
343
+ return []
344
+
345
+ except Exception as e:
346
+ logger.warning(
347
+ "skills_fetch_error",
348
+ error=str(e),
349
+ agent_id=agent_id[:8],
350
+ )
351
+ return []
352
+
353
+ def get_team_skills(
354
+ self,
355
+ team_id: str,
356
+ ) -> List[Dict[str, Any]]:
357
+ """
358
+ Fetch resolved skills for a team from Control Plane.
359
+
360
+ This endpoint returns skills merged from all layers:
361
+ - All team environments (many-to-many)
362
+ - Team's own skills
363
+
364
+ Args:
365
+ team_id: Team ID
366
+
367
+ Returns:
368
+ List of skill configurations with source and inheritance info
369
+ """
370
+ try:
371
+ url = f"{self.base_url}/api/v1/skills/associations/teams/{team_id}/skills/resolved"
372
+ response = self._client.get(url, headers=self.headers)
373
+
374
+ if response.status_code == 200:
375
+ skills = response.json()
376
+ logger.info(
377
+ "team_skills_fetched",
378
+ team_id=team_id[:8],
379
+ skill_count=len(skills),
380
+ )
381
+ return skills
382
+ else:
383
+ logger.warning(
384
+ "team_skills_fetch_failed",
385
+ status=response.status_code,
386
+ team_id=team_id[:8],
387
+ )
388
+ return []
389
+
390
+ except Exception as e:
391
+ logger.warning(
392
+ "team_skills_fetch_error",
393
+ error=str(e),
394
+ team_id=team_id[:8],
395
+ )
396
+ return []
397
+
398
+ def get_agent_execution_environment(
399
+ self,
400
+ agent_id: str,
401
+ ) -> Dict[str, str]:
402
+ """
403
+ Fetch resolved execution environment for an agent from Control Plane.
404
+
405
+ This endpoint returns a fully resolved environment variable dict with:
406
+ - Custom env vars from agent configuration
407
+ - Secret values (resolved from Kubiya vault)
408
+ - Integration tokens (resolved and mapped to env var names like GH_TOKEN, JIRA_TOKEN)
409
+
410
+ Args:
411
+ agent_id: Agent ID
412
+
413
+ Returns:
414
+ Dict of environment variables ready to inject into agent execution
415
+ """
416
+ try:
417
+ url = f"{self.base_url}/api/v1/execution-environment/agents/{agent_id}/resolved"
418
+ response = self._client.get(url, headers=self.headers)
419
+
420
+ if response.status_code == 200:
421
+ env_vars = response.json()
422
+ logger.info(
423
+ "agent_execution_environment_fetched",
424
+ agent_id=agent_id[:8],
425
+ env_var_count=len(env_vars),
426
+ env_var_keys=list(env_vars.keys()),
427
+ )
428
+ return env_vars
429
+ else:
430
+ logger.warning(
431
+ "agent_execution_environment_fetch_failed",
432
+ status=response.status_code,
433
+ agent_id=agent_id[:8],
434
+ )
435
+ return {}
436
+
437
+ except Exception as e:
438
+ logger.warning(
439
+ "agent_execution_environment_fetch_error",
440
+ error=str(e),
441
+ agent_id=agent_id[:8],
442
+ )
443
+ return {}
444
+
445
+ def get_team_execution_environment(
446
+ self,
447
+ team_id: str,
448
+ ) -> Dict[str, str]:
449
+ """
450
+ Fetch resolved execution environment for a team from Control Plane.
451
+
452
+ This endpoint returns a fully resolved environment variable dict with:
453
+ - Custom env vars from team configuration
454
+ - Secret values (resolved from Kubiya vault)
455
+ - Integration tokens (resolved and mapped to env var names like GH_TOKEN, JIRA_TOKEN)
456
+
457
+ Args:
458
+ team_id: Team ID
459
+
460
+ Returns:
461
+ Dict of environment variables ready to inject into team execution
462
+ """
463
+ try:
464
+ url = f"{self.base_url}/api/v1/execution-environment/teams/{team_id}/resolved"
465
+ response = self._client.get(url, headers=self.headers)
466
+
467
+ if response.status_code == 200:
468
+ env_vars = response.json()
469
+ logger.info(
470
+ "team_execution_environment_fetched",
471
+ team_id=team_id[:8],
472
+ env_var_count=len(env_vars),
473
+ env_var_keys=list(env_vars.keys()),
474
+ )
475
+ return env_vars
476
+ else:
477
+ logger.warning(
478
+ "team_execution_environment_fetch_failed",
479
+ status=response.status_code,
480
+ team_id=team_id[:8],
481
+ )
482
+ return {}
483
+
484
+ except Exception as e:
485
+ logger.warning(
486
+ "team_execution_environment_fetch_error",
487
+ error=str(e),
488
+ team_id=team_id[:8],
489
+ )
490
+ return {}
491
+
492
+
493
+ # Singleton instance
494
+ _control_plane_client: Optional[ControlPlaneClient] = None
495
+
496
+
497
+ def get_control_plane_client() -> ControlPlaneClient:
498
+ """
499
+ Get or create the Control Plane client singleton.
500
+
501
+ Reads configuration from environment variables:
502
+ - CONTROL_PLANE_URL: Control Plane URL
503
+ - KUBIYA_API_KEY: API key for authentication
504
+
505
+ Returns:
506
+ ControlPlaneClient instance
507
+
508
+ Raises:
509
+ ValueError: If required environment variables are not set
510
+ """
511
+ global _control_plane_client
512
+
513
+ if _control_plane_client is None:
514
+ base_url = os.environ.get("CONTROL_PLANE_URL")
515
+ api_key = os.environ.get("KUBIYA_API_KEY")
516
+
517
+ if not base_url:
518
+ raise ValueError("CONTROL_PLANE_URL environment variable not set")
519
+ if not api_key:
520
+ raise ValueError("KUBIYA_API_KEY environment variable not set")
521
+
522
+ _control_plane_client = ControlPlaneClient(base_url=base_url, api_key=api_key)
523
+
524
+ logger.info(
525
+ "control_plane_client_initialized",
526
+ base_url=base_url,
527
+ )
528
+
529
+ return _control_plane_client