control-zero 0.2.0__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.
Files changed (44) hide show
  1. control_zero/__init__.py +31 -0
  2. control_zero/client.py +584 -0
  3. control_zero/integrations/crewai/__init__.py +53 -0
  4. control_zero/integrations/crewai/agent.py +267 -0
  5. control_zero/integrations/crewai/crew.py +381 -0
  6. control_zero/integrations/crewai/task.py +291 -0
  7. control_zero/integrations/crewai/tool.py +299 -0
  8. control_zero/integrations/langchain/__init__.py +58 -0
  9. control_zero/integrations/langchain/agent.py +311 -0
  10. control_zero/integrations/langchain/callbacks.py +441 -0
  11. control_zero/integrations/langchain/chain.py +319 -0
  12. control_zero/integrations/langchain/graph.py +441 -0
  13. control_zero/integrations/langchain/tool.py +271 -0
  14. control_zero/llm/__init__.py +77 -0
  15. control_zero/llm/anthropic/__init__.py +35 -0
  16. control_zero/llm/anthropic/client.py +136 -0
  17. control_zero/llm/anthropic/messages.py +375 -0
  18. control_zero/llm/base.py +551 -0
  19. control_zero/llm/cohere/__init__.py +32 -0
  20. control_zero/llm/cohere/client.py +402 -0
  21. control_zero/llm/gemini/__init__.py +34 -0
  22. control_zero/llm/gemini/client.py +486 -0
  23. control_zero/llm/groq/__init__.py +32 -0
  24. control_zero/llm/groq/client.py +330 -0
  25. control_zero/llm/mistral/__init__.py +32 -0
  26. control_zero/llm/mistral/client.py +319 -0
  27. control_zero/llm/ollama/__init__.py +31 -0
  28. control_zero/llm/ollama/client.py +439 -0
  29. control_zero/llm/openai/__init__.py +34 -0
  30. control_zero/llm/openai/chat.py +331 -0
  31. control_zero/llm/openai/client.py +182 -0
  32. control_zero/logging/__init__.py +5 -0
  33. control_zero/logging/async_logger.py +65 -0
  34. control_zero/mcp/__init__.py +5 -0
  35. control_zero/mcp/middleware.py +148 -0
  36. control_zero/policy/__init__.py +5 -0
  37. control_zero/policy/enforcer.py +99 -0
  38. control_zero/secrets/__init__.py +5 -0
  39. control_zero/secrets/manager.py +77 -0
  40. control_zero/types.py +51 -0
  41. control_zero-0.2.0.dist-info/METADATA +216 -0
  42. control_zero-0.2.0.dist-info/RECORD +44 -0
  43. control_zero-0.2.0.dist-info/WHEEL +4 -0
  44. control_zero-0.2.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,441 @@
1
+ """
2
+ Control Zero LangGraph Integration.
3
+
4
+ Provides governance for LangGraph state machines including:
5
+ - Node-level policy enforcement
6
+ - State transition logging
7
+ - Graph execution governance
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from functools import wraps
14
+ from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
15
+
16
+ try:
17
+ from langgraph.graph import StateGraph, END
18
+ from langgraph.graph.state import CompiledStateGraph
19
+ LANGGRAPH_AVAILABLE = True
20
+ except ImportError:
21
+ LANGGRAPH_AVAILABLE = False
22
+ class StateGraph:
23
+ pass
24
+ class CompiledStateGraph:
25
+ pass
26
+ END = "__end__"
27
+
28
+ from control_zero.client import ControlZeroClient
29
+ from control_zero.policy import PolicyDecision, PolicyDeniedError
30
+
31
+ StateType = TypeVar("StateType", bound=Dict[str, Any])
32
+
33
+
34
+ class GovernedNode:
35
+ """
36
+ Decorator for governing LangGraph node functions.
37
+
38
+ Wraps a node function to add:
39
+ - Policy checks before execution
40
+ - State transition logging
41
+ - Error handling
42
+
43
+ Usage:
44
+ @GovernedNode(client, node_name="process_query")
45
+ def process_query(state: State) -> State:
46
+ # Node logic
47
+ return {"result": "processed"}
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ client: ControlZeroClient,
53
+ node_name: Optional[str] = None,
54
+ log_state: bool = False,
55
+ ):
56
+ """
57
+ Initialize governed node decorator.
58
+
59
+ Args:
60
+ client: Control Zero client
61
+ node_name: Name for the node (defaults to function name)
62
+ log_state: Whether to log state content
63
+ """
64
+ self._client = client
65
+ self._node_name = node_name
66
+ self._log_state = log_state
67
+
68
+ def __call__(self, func: Callable) -> Callable:
69
+ """Wrap the node function."""
70
+ node_name = self._node_name or func.__name__
71
+
72
+ @wraps(func)
73
+ def governed_node(state: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]:
74
+ return self._execute_governed(func, node_name, state, *args, **kwargs)
75
+
76
+ @wraps(func)
77
+ async def governed_node_async(state: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]:
78
+ return await self._execute_governed_async(func, node_name, state, *args, **kwargs)
79
+
80
+ # Return async or sync based on function type
81
+ import asyncio
82
+ if asyncio.iscoroutinefunction(func):
83
+ return governed_node_async
84
+ return governed_node
85
+
86
+ def _check_policy(self, node_name: str) -> PolicyDecision:
87
+ """Check policy for node execution."""
88
+ if hasattr(self._client, '_policy_cache') and self._client._policy_cache:
89
+ return self._client._policy_cache.evaluate(f"langgraph:{node_name}", "execute")
90
+ return PolicyDecision(effect="allow")
91
+
92
+ def _execute_governed(
93
+ self,
94
+ func: Callable,
95
+ node_name: str,
96
+ state: Dict[str, Any],
97
+ *args,
98
+ **kwargs,
99
+ ) -> Dict[str, Any]:
100
+ """Execute node with governance (sync)."""
101
+ decision = self._check_policy(node_name)
102
+
103
+ if decision.effect == "deny":
104
+ self._log(node_name, 0, "denied", decision, state)
105
+ raise PolicyDeniedError(decision)
106
+
107
+ start = time.perf_counter()
108
+
109
+ try:
110
+ result = func(state, *args, **kwargs)
111
+ latency_ms = int((time.perf_counter() - start) * 1000)
112
+ self._log(node_name, latency_ms, "success", decision, state, result)
113
+ return result
114
+
115
+ except PolicyDeniedError:
116
+ raise
117
+ except Exception as e:
118
+ latency_ms = int((time.perf_counter() - start) * 1000)
119
+ self._log(node_name, latency_ms, "error", decision, state, error=e)
120
+ raise
121
+
122
+ async def _execute_governed_async(
123
+ self,
124
+ func: Callable,
125
+ node_name: str,
126
+ state: Dict[str, Any],
127
+ *args,
128
+ **kwargs,
129
+ ) -> Dict[str, Any]:
130
+ """Execute node with governance (async)."""
131
+ decision = self._check_policy(node_name)
132
+
133
+ if decision.effect == "deny":
134
+ self._log(node_name, 0, "denied", decision, state)
135
+ raise PolicyDeniedError(decision)
136
+
137
+ start = time.perf_counter()
138
+
139
+ try:
140
+ result = await func(state, *args, **kwargs)
141
+ latency_ms = int((time.perf_counter() - start) * 1000)
142
+ self._log(node_name, latency_ms, "success", decision, state, result)
143
+ return result
144
+
145
+ except PolicyDeniedError:
146
+ raise
147
+ except Exception as e:
148
+ latency_ms = int((time.perf_counter() - start) * 1000)
149
+ self._log(node_name, latency_ms, "error", decision, state, error=e)
150
+ raise
151
+
152
+ def _log(
153
+ self,
154
+ node_name: str,
155
+ latency_ms: int,
156
+ status: str,
157
+ decision: PolicyDecision,
158
+ input_state: Optional[Dict] = None,
159
+ output_state: Optional[Dict] = None,
160
+ error: Optional[Exception] = None,
161
+ ) -> None:
162
+ """Log node execution."""
163
+ log_data = {}
164
+
165
+ if self._log_state:
166
+ if input_state:
167
+ log_data["input_state"] = {k: str(v)[:100] for k, v in input_state.items()}
168
+ if output_state:
169
+ log_data["output_state"] = {k: str(v)[:100] for k, v in output_state.items()}
170
+
171
+ self._client._log(
172
+ tool=f"langgraph:{node_name}",
173
+ method="execute",
174
+ status=status,
175
+ latency_ms=latency_ms,
176
+ policy_decision=decision,
177
+ error_type=type(error).__name__ if error else None,
178
+ error_message=str(error) if error else None,
179
+ **log_data
180
+ )
181
+
182
+
183
+ def governed_graph(
184
+ client: ControlZeroClient,
185
+ graph_name: str = "langgraph",
186
+ ) -> Callable:
187
+ """
188
+ Decorator to wrap an entire LangGraph with governance.
189
+
190
+ Usage:
191
+ @governed_graph(client, graph_name="my_workflow")
192
+ def create_graph():
193
+ graph = StateGraph(State)
194
+ graph.add_node("process", process_node)
195
+ graph.add_edge("process", END)
196
+ return graph.compile()
197
+ """
198
+ def decorator(func: Callable) -> Callable:
199
+ @wraps(func)
200
+ def wrapper(*args, **kwargs):
201
+ # Create the graph
202
+ compiled_graph = func(*args, **kwargs)
203
+
204
+ # Wrap with governance
205
+ return GovernedStateGraph(
206
+ graph=compiled_graph,
207
+ client=client,
208
+ graph_name=graph_name,
209
+ )
210
+ return wrapper
211
+ return decorator
212
+
213
+
214
+ class GovernedStateGraph:
215
+ """
216
+ Governance wrapper for compiled LangGraph.
217
+
218
+ Provides:
219
+ - Graph-level policy enforcement
220
+ - Execution logging
221
+ - State tracking
222
+
223
+ Usage:
224
+ graph = StateGraph(State)
225
+ # ... add nodes and edges
226
+ compiled = graph.compile()
227
+
228
+ governed = GovernedStateGraph(
229
+ graph=compiled,
230
+ client=client,
231
+ graph_name="my_workflow"
232
+ )
233
+
234
+ result = governed.invoke({"input": "data"})
235
+ """
236
+
237
+ def __init__(
238
+ self,
239
+ graph: Any, # CompiledStateGraph
240
+ client: ControlZeroClient,
241
+ graph_name: str = "langgraph",
242
+ log_states: bool = False,
243
+ ):
244
+ """
245
+ Initialize governed graph.
246
+
247
+ Args:
248
+ graph: Compiled LangGraph
249
+ client: Control Zero client
250
+ graph_name: Name for logging
251
+ log_states: Whether to log state content
252
+ """
253
+ if not LANGGRAPH_AVAILABLE:
254
+ raise ImportError(
255
+ "langgraph is required. Install with: pip install langgraph"
256
+ )
257
+
258
+ self._graph = graph
259
+ self._client = client
260
+ self._graph_name = graph_name
261
+ self._log_states = log_states
262
+
263
+ def _check_policy(self, method: str) -> PolicyDecision:
264
+ """Check policy for graph execution."""
265
+ if hasattr(self._client, '_policy_cache') and self._client._policy_cache:
266
+ return self._client._policy_cache.evaluate(f"langgraph:{self._graph_name}", method)
267
+ return PolicyDecision(effect="allow")
268
+
269
+ def invoke(
270
+ self,
271
+ input: Dict[str, Any],
272
+ config: Optional[Dict] = None,
273
+ **kwargs,
274
+ ) -> Dict[str, Any]:
275
+ """
276
+ Invoke the graph with governance.
277
+
278
+ Args:
279
+ input: Initial state
280
+ config: Run configuration
281
+ **kwargs: Additional arguments
282
+
283
+ Returns:
284
+ Final state
285
+ """
286
+ decision = self._check_policy("invoke")
287
+ if decision.effect == "deny":
288
+ self._log("invoke", 0, "denied", decision, input)
289
+ raise PolicyDeniedError(decision)
290
+
291
+ start = time.perf_counter()
292
+
293
+ try:
294
+ result = self._graph.invoke(input, config=config, **kwargs)
295
+ latency_ms = int((time.perf_counter() - start) * 1000)
296
+ self._log("invoke", latency_ms, "success", decision, input, result)
297
+ return result
298
+
299
+ except PolicyDeniedError:
300
+ raise
301
+ except Exception as e:
302
+ latency_ms = int((time.perf_counter() - start) * 1000)
303
+ self._log("invoke", latency_ms, "error", decision, input, error=e)
304
+ raise
305
+
306
+ async def ainvoke(
307
+ self,
308
+ input: Dict[str, Any],
309
+ config: Optional[Dict] = None,
310
+ **kwargs,
311
+ ) -> Dict[str, Any]:
312
+ """Async invoke with governance."""
313
+ decision = self._check_policy("invoke")
314
+ if decision.effect == "deny":
315
+ self._log("invoke", 0, "denied", decision, input)
316
+ raise PolicyDeniedError(decision)
317
+
318
+ start = time.perf_counter()
319
+
320
+ try:
321
+ result = await self._graph.ainvoke(input, config=config, **kwargs)
322
+ latency_ms = int((time.perf_counter() - start) * 1000)
323
+ self._log("invoke", latency_ms, "success", decision, input, result)
324
+ return result
325
+
326
+ except PolicyDeniedError:
327
+ raise
328
+ except Exception as e:
329
+ latency_ms = int((time.perf_counter() - start) * 1000)
330
+ self._log("invoke", latency_ms, "error", decision, input, error=e)
331
+ raise
332
+
333
+ def stream(
334
+ self,
335
+ input: Dict[str, Any],
336
+ config: Optional[Dict] = None,
337
+ **kwargs,
338
+ ):
339
+ """
340
+ Stream graph execution with governance.
341
+
342
+ Args:
343
+ input: Initial state
344
+ config: Run configuration
345
+ **kwargs: Additional arguments
346
+
347
+ Yields:
348
+ State updates
349
+ """
350
+ decision = self._check_policy("stream")
351
+ if decision.effect == "deny":
352
+ self._log("stream", 0, "denied", decision, input)
353
+ raise PolicyDeniedError(decision)
354
+
355
+ start = time.perf_counter()
356
+ states = []
357
+
358
+ try:
359
+ for state in self._graph.stream(input, config=config, **kwargs):
360
+ states.append(state)
361
+ yield state
362
+
363
+ latency_ms = int((time.perf_counter() - start) * 1000)
364
+ self._log("stream", latency_ms, "success", decision, input, metadata={"num_states": len(states)})
365
+
366
+ except PolicyDeniedError:
367
+ raise
368
+ except Exception as e:
369
+ latency_ms = int((time.perf_counter() - start) * 1000)
370
+ self._log("stream", latency_ms, "error", decision, input, error=e)
371
+ raise
372
+
373
+ async def astream(
374
+ self,
375
+ input: Dict[str, Any],
376
+ config: Optional[Dict] = None,
377
+ **kwargs,
378
+ ):
379
+ """Async stream with governance."""
380
+ decision = self._check_policy("stream")
381
+ if decision.effect == "deny":
382
+ self._log("stream", 0, "denied", decision, input)
383
+ raise PolicyDeniedError(decision)
384
+
385
+ start = time.perf_counter()
386
+ states = []
387
+
388
+ try:
389
+ async for state in self._graph.astream(input, config=config, **kwargs):
390
+ states.append(state)
391
+ yield state
392
+
393
+ latency_ms = int((time.perf_counter() - start) * 1000)
394
+ self._log("stream", latency_ms, "success", decision, input, metadata={"num_states": len(states)})
395
+
396
+ except PolicyDeniedError:
397
+ raise
398
+ except Exception as e:
399
+ latency_ms = int((time.perf_counter() - start) * 1000)
400
+ self._log("stream", latency_ms, "error", decision, input, error=e)
401
+ raise
402
+
403
+ def _log(
404
+ self,
405
+ method: str,
406
+ latency_ms: int,
407
+ status: str,
408
+ decision: PolicyDecision,
409
+ input_state: Optional[Dict] = None,
410
+ output_state: Optional[Dict] = None,
411
+ error: Optional[Exception] = None,
412
+ metadata: Optional[Dict] = None,
413
+ ) -> None:
414
+ """Log graph execution."""
415
+ log_data = metadata or {}
416
+
417
+ if self._log_states:
418
+ if input_state:
419
+ log_data["input_state"] = {k: str(v)[:100] for k, v in input_state.items()}
420
+ if output_state:
421
+ log_data["output_state"] = {k: str(v)[:100] for k, v in output_state.items()}
422
+
423
+ self._client._log(
424
+ tool=f"langgraph:{self._graph_name}",
425
+ method=method,
426
+ status=status,
427
+ latency_ms=latency_ms,
428
+ policy_decision=decision,
429
+ error_type=type(error).__name__ if error else None,
430
+ error_message=str(error) if error else None,
431
+ **log_data
432
+ )
433
+
434
+ @property
435
+ def graph(self) -> Any:
436
+ """Get underlying graph."""
437
+ return self._graph
438
+
439
+ def get_graph(self, **kwargs):
440
+ """Get graph visualization."""
441
+ return self._graph.get_graph(**kwargs)
@@ -0,0 +1,271 @@
1
+ """
2
+ Governed LangChain Tool wrappers.
3
+
4
+ Provides governance enforcement for LangChain tools including:
5
+ - Policy checks before execution
6
+ - Audit logging
7
+ - Error tracking
8
+ - Secret injection
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
15
+ from functools import wraps
16
+
17
+ try:
18
+ from langchain_core.tools import BaseTool, Tool, StructuredTool
19
+ from langchain_core.pydantic_v1 import BaseModel, Field
20
+ from langchain_core.callbacks import CallbackManagerForToolRun
21
+ LANGCHAIN_AVAILABLE = True
22
+ except ImportError:
23
+ LANGCHAIN_AVAILABLE = False
24
+ # Dummy classes for type hints
25
+ class BaseTool:
26
+ pass
27
+ class Tool:
28
+ pass
29
+ class StructuredTool:
30
+ pass
31
+ class BaseModel:
32
+ pass
33
+ class CallbackManagerForToolRun:
34
+ pass
35
+
36
+ from control_zero.client import ControlZeroClient
37
+ from control_zero.policy import PolicyDecision, PolicyDeniedError
38
+
39
+
40
+ class GovernanceTool(BaseTool):
41
+ """
42
+ Wraps a LangChain Tool to enforce Control Zero governance policies.
43
+
44
+ This is the original wrapper that directly wraps BaseTool instances.
45
+
46
+ Usage:
47
+ client = ControlZeroClient(api_key="...")
48
+ client.initialize()
49
+
50
+ base_tool = MyCustomTool()
51
+ gov_tool = GovernanceTool(
52
+ base_tool=base_tool,
53
+ client=client,
54
+ tool_name="my_custom_tool"
55
+ )
56
+
57
+ agent = initialize_agent([gov_tool], ...)
58
+ """
59
+
60
+ base_tool: BaseTool
61
+ client: ControlZeroClient
62
+ tool_name: str
63
+
64
+ # Forward metadata from base tool
65
+ name: str = ""
66
+ description: str = ""
67
+ args_schema: Optional[Type[BaseModel]] = None
68
+
69
+ class Config:
70
+ arbitrary_types_allowed = True
71
+
72
+ def __init__(
73
+ self,
74
+ base_tool: BaseTool,
75
+ client: ControlZeroClient,
76
+ tool_name: Optional[str] = None,
77
+ **kwargs
78
+ ):
79
+ if not LANGCHAIN_AVAILABLE:
80
+ raise ImportError(
81
+ "langchain is required for LangChain integration. "
82
+ "Install with: pip install langchain-core"
83
+ )
84
+
85
+ name = base_tool.name
86
+ description = base_tool.description
87
+ args_schema = getattr(base_tool, 'args_schema', None)
88
+ tool_name = tool_name or name
89
+
90
+ super().__init__(
91
+ base_tool=base_tool,
92
+ client=client,
93
+ tool_name=tool_name,
94
+ name=name,
95
+ description=description,
96
+ args_schema=args_schema,
97
+ **kwargs
98
+ )
99
+
100
+ def _run(
101
+ self,
102
+ *args: Any,
103
+ run_manager: Optional[CallbackManagerForToolRun] = None,
104
+ **kwargs: Any,
105
+ ) -> Any:
106
+ """Execute the tool with governance checks."""
107
+ method = "run"
108
+
109
+ # 1. Check Policy
110
+ decision = self._check_policy(method)
111
+
112
+ if decision.effect == "deny":
113
+ self._log_denied(method, decision)
114
+ raise PolicyDeniedError(decision)
115
+
116
+ start = time.perf_counter()
117
+ try:
118
+ # 2. Inject secrets if needed
119
+ kwargs = self._inject_secrets(kwargs)
120
+
121
+ # 3. Run Tool
122
+ result = self.base_tool._run(*args, run_manager=run_manager, **kwargs)
123
+
124
+ # 4. Log Success
125
+ latency_ms = int((time.perf_counter() - start) * 1000)
126
+ self._log_success(method, latency_ms, decision)
127
+
128
+ return result
129
+
130
+ except PolicyDeniedError:
131
+ raise
132
+ except Exception as e:
133
+ # 5. Log Error
134
+ latency_ms = int((time.perf_counter() - start) * 1000)
135
+ self._log_error(method, latency_ms, decision, e)
136
+ raise
137
+
138
+ async def _arun(
139
+ self,
140
+ *args: Any,
141
+ run_manager: Optional[Any] = None,
142
+ **kwargs: Any,
143
+ ) -> Any:
144
+ """Async execution with governance."""
145
+ method = "run"
146
+
147
+ # 1. Check Policy
148
+ decision = self._check_policy(method)
149
+
150
+ if decision.effect == "deny":
151
+ self._log_denied(method, decision)
152
+ raise PolicyDeniedError(decision)
153
+
154
+ start = time.perf_counter()
155
+ try:
156
+ # 2. Inject secrets
157
+ kwargs = self._inject_secrets(kwargs)
158
+
159
+ # 3. Run Tool
160
+ result = await self.base_tool._arun(*args, run_manager=run_manager, **kwargs)
161
+
162
+ # 4. Log Success
163
+ latency_ms = int((time.perf_counter() - start) * 1000)
164
+ self._log_success(method, latency_ms, decision)
165
+
166
+ return result
167
+
168
+ except PolicyDeniedError:
169
+ raise
170
+ except Exception as e:
171
+ latency_ms = int((time.perf_counter() - start) * 1000)
172
+ self._log_error(method, latency_ms, decision, e)
173
+ raise
174
+
175
+ def _check_policy(self, method: str) -> PolicyDecision:
176
+ """Check policy for tool execution."""
177
+ if hasattr(self.client, '_policy_cache') and self.client._policy_cache:
178
+ return self.client._policy_cache.evaluate(self.tool_name, method)
179
+ return PolicyDecision(effect="allow")
180
+
181
+ def _inject_secrets(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
182
+ """Inject secrets into kwargs if configured."""
183
+ if hasattr(self.client, '_secret_manager') and self.client._secret_manager:
184
+ return self.client._secret_manager.inject(self.tool_name, kwargs)
185
+ return kwargs
186
+
187
+ def _log_denied(self, method: str, decision: PolicyDecision) -> None:
188
+ """Log a denied execution."""
189
+ self.client._log(
190
+ self.tool_name, method, "denied", 0,
191
+ policy_decision=decision
192
+ )
193
+
194
+ def _log_success(self, method: str, latency_ms: int, decision: PolicyDecision) -> None:
195
+ """Log successful execution."""
196
+ self.client._log(
197
+ self.tool_name, method, "success", latency_ms,
198
+ policy_decision=decision
199
+ )
200
+
201
+ def _log_error(
202
+ self,
203
+ method: str,
204
+ latency_ms: int,
205
+ decision: PolicyDecision,
206
+ error: Exception
207
+ ) -> None:
208
+ """Log execution error."""
209
+ self.client._log(
210
+ self.tool_name, method, "error", latency_ms,
211
+ policy_decision=decision,
212
+ error_type=type(error).__name__,
213
+ error_message=str(error)
214
+ )
215
+
216
+
217
+ # Alias for consistency
218
+ GovernedTool = GovernanceTool
219
+
220
+
221
+ def governed_tool(
222
+ client: ControlZeroClient,
223
+ tool_name: Optional[str] = None,
224
+ ) -> Callable:
225
+ """
226
+ Decorator to create a governed tool from a function.
227
+
228
+ Usage:
229
+ @governed_tool(client, tool_name="my_tool")
230
+ def my_function(query: str) -> str:
231
+ '''Search for information.'''
232
+ return search(query)
233
+
234
+ # Creates a governed StructuredTool
235
+ """
236
+ def decorator(func: Callable) -> StructuredTool:
237
+ if not LANGCHAIN_AVAILABLE:
238
+ raise ImportError("langchain is required")
239
+
240
+ # Create base tool
241
+ base_tool = StructuredTool.from_function(
242
+ func=func,
243
+ name=tool_name or func.__name__,
244
+ description=func.__doc__ or "",
245
+ )
246
+
247
+ # Wrap with governance
248
+ return GovernanceTool(
249
+ base_tool=base_tool,
250
+ client=client,
251
+ tool_name=tool_name or func.__name__,
252
+ )
253
+
254
+ return decorator
255
+
256
+
257
+ def wrap_tools(
258
+ tools: List[BaseTool],
259
+ client: ControlZeroClient,
260
+ ) -> List[GovernanceTool]:
261
+ """
262
+ Wrap multiple tools with governance.
263
+
264
+ Usage:
265
+ tools = [tool1, tool2, tool3]
266
+ governed_tools = wrap_tools(tools, client)
267
+ """
268
+ return [
269
+ GovernanceTool(base_tool=tool, client=client)
270
+ for tool in tools
271
+ ]