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.
- control_zero/__init__.py +31 -0
- control_zero/client.py +584 -0
- control_zero/integrations/crewai/__init__.py +53 -0
- control_zero/integrations/crewai/agent.py +267 -0
- control_zero/integrations/crewai/crew.py +381 -0
- control_zero/integrations/crewai/task.py +291 -0
- control_zero/integrations/crewai/tool.py +299 -0
- control_zero/integrations/langchain/__init__.py +58 -0
- control_zero/integrations/langchain/agent.py +311 -0
- control_zero/integrations/langchain/callbacks.py +441 -0
- control_zero/integrations/langchain/chain.py +319 -0
- control_zero/integrations/langchain/graph.py +441 -0
- control_zero/integrations/langchain/tool.py +271 -0
- control_zero/llm/__init__.py +77 -0
- control_zero/llm/anthropic/__init__.py +35 -0
- control_zero/llm/anthropic/client.py +136 -0
- control_zero/llm/anthropic/messages.py +375 -0
- control_zero/llm/base.py +551 -0
- control_zero/llm/cohere/__init__.py +32 -0
- control_zero/llm/cohere/client.py +402 -0
- control_zero/llm/gemini/__init__.py +34 -0
- control_zero/llm/gemini/client.py +486 -0
- control_zero/llm/groq/__init__.py +32 -0
- control_zero/llm/groq/client.py +330 -0
- control_zero/llm/mistral/__init__.py +32 -0
- control_zero/llm/mistral/client.py +319 -0
- control_zero/llm/ollama/__init__.py +31 -0
- control_zero/llm/ollama/client.py +439 -0
- control_zero/llm/openai/__init__.py +34 -0
- control_zero/llm/openai/chat.py +331 -0
- control_zero/llm/openai/client.py +182 -0
- control_zero/logging/__init__.py +5 -0
- control_zero/logging/async_logger.py +65 -0
- control_zero/mcp/__init__.py +5 -0
- control_zero/mcp/middleware.py +148 -0
- control_zero/policy/__init__.py +5 -0
- control_zero/policy/enforcer.py +99 -0
- control_zero/secrets/__init__.py +5 -0
- control_zero/secrets/manager.py +77 -0
- control_zero/types.py +51 -0
- control_zero-0.2.0.dist-info/METADATA +216 -0
- control_zero-0.2.0.dist-info/RECORD +44 -0
- control_zero-0.2.0.dist-info/WHEEL +4 -0
- control_zero-0.2.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Governed CrewAI Task wrapper.
|
|
3
|
+
|
|
4
|
+
Provides governance enforcement for CrewAI tasks including:
|
|
5
|
+
- Task-level policy checks
|
|
6
|
+
- Delegation governance
|
|
7
|
+
- Output validation
|
|
8
|
+
- Execution logging
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from functools import wraps
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from crewai import Task, Agent
|
|
19
|
+
CREWAI_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
CREWAI_AVAILABLE = False
|
|
22
|
+
class Task:
|
|
23
|
+
pass
|
|
24
|
+
class Agent:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
from control_zero.client import ControlZeroClient
|
|
28
|
+
from control_zero.policy import PolicyDecision, PolicyDeniedError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GovernedTask:
|
|
32
|
+
"""
|
|
33
|
+
Governance wrapper for CrewAI Task.
|
|
34
|
+
|
|
35
|
+
Wraps a CrewAI Task to provide:
|
|
36
|
+
- Task execution policy enforcement
|
|
37
|
+
- Delegation governance
|
|
38
|
+
- Output validation
|
|
39
|
+
- Execution audit logging
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
from crewai import Task
|
|
43
|
+
|
|
44
|
+
research_task = Task(
|
|
45
|
+
description="Research the topic...",
|
|
46
|
+
agent=researcher,
|
|
47
|
+
expected_output="A detailed report...",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
governed_task = GovernedTask(
|
|
51
|
+
task=research_task,
|
|
52
|
+
client=client,
|
|
53
|
+
task_name="research_task",
|
|
54
|
+
)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
task: Task,
|
|
60
|
+
client: ControlZeroClient,
|
|
61
|
+
task_name: Optional[str] = None,
|
|
62
|
+
allow_delegation: bool = True,
|
|
63
|
+
max_delegation_depth: int = 3,
|
|
64
|
+
log_outputs: bool = False,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Initialize governed task.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
task: The CrewAI Task to wrap
|
|
71
|
+
client: Control Zero client
|
|
72
|
+
task_name: Name for logging purposes
|
|
73
|
+
allow_delegation: Whether to allow task delegation
|
|
74
|
+
max_delegation_depth: Maximum delegation depth
|
|
75
|
+
log_outputs: Whether to log output content
|
|
76
|
+
"""
|
|
77
|
+
if not CREWAI_AVAILABLE:
|
|
78
|
+
raise ImportError(
|
|
79
|
+
"crewai is required. Install with: pip install crewai"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self._task = task
|
|
83
|
+
self._client = client
|
|
84
|
+
self._task_name = task_name or self._extract_task_name(task)
|
|
85
|
+
self._allow_delegation = allow_delegation
|
|
86
|
+
self._max_delegation_depth = max_delegation_depth
|
|
87
|
+
self._log_outputs = log_outputs
|
|
88
|
+
|
|
89
|
+
# Tracking
|
|
90
|
+
self._delegation_depth = 0
|
|
91
|
+
self._execution_count = 0
|
|
92
|
+
|
|
93
|
+
def _extract_task_name(self, task: Task) -> str:
|
|
94
|
+
"""Extract a name from task description."""
|
|
95
|
+
if hasattr(task, 'description'):
|
|
96
|
+
# Take first 30 chars of description
|
|
97
|
+
desc = str(task.description)[:30]
|
|
98
|
+
return desc.replace(" ", "_").lower()
|
|
99
|
+
return "unnamed_task"
|
|
100
|
+
|
|
101
|
+
def _check_policy(self, action: str) -> PolicyDecision:
|
|
102
|
+
"""Check policy for task action."""
|
|
103
|
+
if hasattr(self._client, '_policy_cache') and self._client._policy_cache:
|
|
104
|
+
return self._client._policy_cache.evaluate(
|
|
105
|
+
f"crewai:task:{self._task_name}",
|
|
106
|
+
action
|
|
107
|
+
)
|
|
108
|
+
return PolicyDecision(effect="allow")
|
|
109
|
+
|
|
110
|
+
def execute(
|
|
111
|
+
self,
|
|
112
|
+
agent: Optional[Agent] = None,
|
|
113
|
+
context: Optional[str] = None,
|
|
114
|
+
tools: Optional[List] = None,
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""
|
|
117
|
+
Execute the task with governance.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
agent: Agent to execute (overrides task.agent)
|
|
121
|
+
context: Additional context
|
|
122
|
+
tools: Override tools for execution
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Task output
|
|
126
|
+
"""
|
|
127
|
+
decision = self._check_policy("execute")
|
|
128
|
+
|
|
129
|
+
if decision.effect == "deny":
|
|
130
|
+
self._log_execution("denied", 0, decision)
|
|
131
|
+
raise PolicyDeniedError(decision)
|
|
132
|
+
|
|
133
|
+
start = time.perf_counter()
|
|
134
|
+
self._execution_count += 1
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Check delegation policy
|
|
138
|
+
if self._delegation_depth >= self._max_delegation_depth:
|
|
139
|
+
raise PolicyDeniedError(PolicyDecision(
|
|
140
|
+
effect="deny",
|
|
141
|
+
reason=f"Maximum delegation depth ({self._max_delegation_depth}) exceeded"
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
# Execute task
|
|
145
|
+
executing_agent = agent or self._task.agent
|
|
146
|
+
result = self._task.execute(
|
|
147
|
+
agent=executing_agent,
|
|
148
|
+
context=context,
|
|
149
|
+
tools=tools,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
153
|
+
|
|
154
|
+
metadata = {
|
|
155
|
+
"execution_count": self._execution_count,
|
|
156
|
+
"delegation_depth": self._delegation_depth,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if self._log_outputs and result:
|
|
160
|
+
metadata["output_preview"] = str(result)[:200]
|
|
161
|
+
|
|
162
|
+
self._log_execution("success", latency_ms, decision, metadata)
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
except PolicyDeniedError:
|
|
167
|
+
raise
|
|
168
|
+
except Exception as e:
|
|
169
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
170
|
+
self._log_execution("error", latency_ms, decision, error=e)
|
|
171
|
+
raise
|
|
172
|
+
|
|
173
|
+
def delegate(
|
|
174
|
+
self,
|
|
175
|
+
to_agent: Agent,
|
|
176
|
+
context: Optional[str] = None,
|
|
177
|
+
) -> Any:
|
|
178
|
+
"""
|
|
179
|
+
Delegate task to another agent with governance.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
to_agent: Agent to delegate to
|
|
183
|
+
context: Context for delegation
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Delegation result
|
|
187
|
+
"""
|
|
188
|
+
if not self._allow_delegation:
|
|
189
|
+
decision = PolicyDecision(
|
|
190
|
+
effect="deny",
|
|
191
|
+
reason="Task delegation is not allowed"
|
|
192
|
+
)
|
|
193
|
+
self._log_execution("denied", 0, decision, {"reason": "delegation_disabled"})
|
|
194
|
+
raise PolicyDeniedError(decision)
|
|
195
|
+
|
|
196
|
+
decision = self._check_policy("delegate")
|
|
197
|
+
|
|
198
|
+
if decision.effect == "deny":
|
|
199
|
+
self._log_execution("denied", 0, decision, {"action": "delegate"})
|
|
200
|
+
raise PolicyDeniedError(decision)
|
|
201
|
+
|
|
202
|
+
# Increment delegation depth
|
|
203
|
+
self._delegation_depth += 1
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
return self.execute(agent=to_agent, context=context)
|
|
207
|
+
finally:
|
|
208
|
+
self._delegation_depth -= 1
|
|
209
|
+
|
|
210
|
+
def _log_execution(
|
|
211
|
+
self,
|
|
212
|
+
status: str,
|
|
213
|
+
latency_ms: int,
|
|
214
|
+
decision: PolicyDecision,
|
|
215
|
+
metadata: Optional[Dict] = None,
|
|
216
|
+
error: Optional[Exception] = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Log task execution."""
|
|
219
|
+
log_data = metadata or {}
|
|
220
|
+
log_data["task_name"] = self._task_name
|
|
221
|
+
|
|
222
|
+
if hasattr(self._task, 'description'):
|
|
223
|
+
log_data["description_preview"] = str(self._task.description)[:100]
|
|
224
|
+
|
|
225
|
+
self._client._log(
|
|
226
|
+
tool=f"crewai:task:{self._task_name}",
|
|
227
|
+
method="execute",
|
|
228
|
+
status=status,
|
|
229
|
+
latency_ms=latency_ms,
|
|
230
|
+
policy_decision=decision,
|
|
231
|
+
error_type=type(error).__name__ if error else None,
|
|
232
|
+
error_message=str(error) if error else None,
|
|
233
|
+
**log_data
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def task(self) -> Task:
|
|
238
|
+
"""Get underlying CrewAI task."""
|
|
239
|
+
return self._task
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def description(self) -> str:
|
|
243
|
+
"""Get task description."""
|
|
244
|
+
return getattr(self._task, 'description', '')
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def agent(self) -> Optional[Agent]:
|
|
248
|
+
"""Get assigned agent."""
|
|
249
|
+
return getattr(self._task, 'agent', None)
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def execution_count(self) -> int:
|
|
253
|
+
"""Get total execution count."""
|
|
254
|
+
return self._execution_count
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def governed_task(
|
|
258
|
+
client: ControlZeroClient,
|
|
259
|
+
task_name: Optional[str] = None,
|
|
260
|
+
allow_delegation: bool = True,
|
|
261
|
+
max_delegation_depth: int = 3,
|
|
262
|
+
log_outputs: bool = False,
|
|
263
|
+
) -> Callable:
|
|
264
|
+
"""
|
|
265
|
+
Decorator to create a governed CrewAI task.
|
|
266
|
+
|
|
267
|
+
Usage:
|
|
268
|
+
@governed_task(client, task_name="research")
|
|
269
|
+
def create_research_task(agent):
|
|
270
|
+
return Task(
|
|
271
|
+
description="Research the topic...",
|
|
272
|
+
agent=agent,
|
|
273
|
+
expected_output="A detailed report",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
task = create_research_task(researcher) # Returns GovernedTask
|
|
277
|
+
"""
|
|
278
|
+
def decorator(func: Callable) -> Callable:
|
|
279
|
+
@wraps(func)
|
|
280
|
+
def wrapper(*args, **kwargs) -> GovernedTask:
|
|
281
|
+
task = func(*args, **kwargs)
|
|
282
|
+
return GovernedTask(
|
|
283
|
+
task=task,
|
|
284
|
+
client=client,
|
|
285
|
+
task_name=task_name,
|
|
286
|
+
allow_delegation=allow_delegation,
|
|
287
|
+
max_delegation_depth=max_delegation_depth,
|
|
288
|
+
log_outputs=log_outputs,
|
|
289
|
+
)
|
|
290
|
+
return wrapper
|
|
291
|
+
return decorator
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Governed CrewAI Tool wrappers.
|
|
3
|
+
|
|
4
|
+
Provides governance enforcement for CrewAI 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 functools import wraps
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from crewai.tools import BaseTool as CrewAIBaseTool
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
CREWAI_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
CREWAI_AVAILABLE = False
|
|
23
|
+
class CrewAIBaseTool:
|
|
24
|
+
pass
|
|
25
|
+
class BaseModel:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
from control_zero.client import ControlZeroClient
|
|
29
|
+
from control_zero.policy import PolicyDecision, PolicyDeniedError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GovernedCrewTool(CrewAIBaseTool):
|
|
33
|
+
"""
|
|
34
|
+
Wraps a CrewAI Tool to enforce Control Zero governance policies.
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
from crewai.tools import BaseTool
|
|
38
|
+
|
|
39
|
+
class MyTool(BaseTool):
|
|
40
|
+
name: str = "my_tool"
|
|
41
|
+
description: str = "Does something"
|
|
42
|
+
|
|
43
|
+
def _run(self, query: str) -> str:
|
|
44
|
+
return "result"
|
|
45
|
+
|
|
46
|
+
governed_tool = GovernedCrewTool(
|
|
47
|
+
base_tool=MyTool(),
|
|
48
|
+
client=client,
|
|
49
|
+
)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
name: str = ""
|
|
53
|
+
description: str = ""
|
|
54
|
+
base_tool: Any = None
|
|
55
|
+
client: Any = None
|
|
56
|
+
tool_name: str = ""
|
|
57
|
+
|
|
58
|
+
class Config:
|
|
59
|
+
arbitrary_types_allowed = True
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
base_tool: CrewAIBaseTool,
|
|
64
|
+
client: ControlZeroClient,
|
|
65
|
+
tool_name: Optional[str] = None,
|
|
66
|
+
**kwargs
|
|
67
|
+
):
|
|
68
|
+
if not CREWAI_AVAILABLE:
|
|
69
|
+
raise ImportError(
|
|
70
|
+
"crewai is required for CrewAI integration. "
|
|
71
|
+
"Install with: pip install crewai"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
name = base_tool.name
|
|
75
|
+
description = base_tool.description
|
|
76
|
+
tool_name = tool_name or name
|
|
77
|
+
|
|
78
|
+
super().__init__(
|
|
79
|
+
name=name,
|
|
80
|
+
description=description,
|
|
81
|
+
base_tool=base_tool,
|
|
82
|
+
client=client,
|
|
83
|
+
tool_name=tool_name,
|
|
84
|
+
**kwargs
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
|
88
|
+
"""Execute the tool with governance checks."""
|
|
89
|
+
method = "run"
|
|
90
|
+
|
|
91
|
+
# 1. Check Policy
|
|
92
|
+
decision = self._check_policy(method)
|
|
93
|
+
|
|
94
|
+
if decision.effect == "deny":
|
|
95
|
+
self._log_denied(method, decision)
|
|
96
|
+
raise PolicyDeniedError(decision)
|
|
97
|
+
|
|
98
|
+
start = time.perf_counter()
|
|
99
|
+
try:
|
|
100
|
+
# 2. Inject secrets if needed
|
|
101
|
+
kwargs = self._inject_secrets(kwargs)
|
|
102
|
+
|
|
103
|
+
# 3. Run Tool
|
|
104
|
+
result = self.base_tool._run(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
# 4. Log Success
|
|
107
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
108
|
+
self._log_success(method, latency_ms, decision)
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
except PolicyDeniedError:
|
|
113
|
+
raise
|
|
114
|
+
except Exception as e:
|
|
115
|
+
# 5. Log Error
|
|
116
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
117
|
+
self._log_error(method, latency_ms, decision, e)
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
async def _arun(self, *args: Any, **kwargs: Any) -> Any:
|
|
121
|
+
"""Async execution with governance."""
|
|
122
|
+
method = "run"
|
|
123
|
+
|
|
124
|
+
decision = self._check_policy(method)
|
|
125
|
+
|
|
126
|
+
if decision.effect == "deny":
|
|
127
|
+
self._log_denied(method, decision)
|
|
128
|
+
raise PolicyDeniedError(decision)
|
|
129
|
+
|
|
130
|
+
start = time.perf_counter()
|
|
131
|
+
try:
|
|
132
|
+
kwargs = self._inject_secrets(kwargs)
|
|
133
|
+
|
|
134
|
+
# Check if base tool has async support
|
|
135
|
+
if hasattr(self.base_tool, '_arun'):
|
|
136
|
+
result = await self.base_tool._arun(*args, **kwargs)
|
|
137
|
+
else:
|
|
138
|
+
result = self.base_tool._run(*args, **kwargs)
|
|
139
|
+
|
|
140
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
141
|
+
self._log_success(method, latency_ms, decision)
|
|
142
|
+
|
|
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_error(method, latency_ms, decision, e)
|
|
150
|
+
raise
|
|
151
|
+
|
|
152
|
+
def _check_policy(self, method: str) -> PolicyDecision:
|
|
153
|
+
"""Check policy for tool execution."""
|
|
154
|
+
if hasattr(self.client, '_policy_cache') and self.client._policy_cache:
|
|
155
|
+
return self.client._policy_cache.evaluate(self.tool_name, method)
|
|
156
|
+
return PolicyDecision(effect="allow")
|
|
157
|
+
|
|
158
|
+
def _inject_secrets(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
159
|
+
"""Inject secrets into kwargs if configured."""
|
|
160
|
+
if hasattr(self.client, '_secret_manager') and self.client._secret_manager:
|
|
161
|
+
return self.client._secret_manager.inject(self.tool_name, kwargs)
|
|
162
|
+
return kwargs
|
|
163
|
+
|
|
164
|
+
def _log_denied(self, method: str, decision: PolicyDecision) -> None:
|
|
165
|
+
"""Log a denied execution."""
|
|
166
|
+
self.client._log(
|
|
167
|
+
tool=self.tool_name,
|
|
168
|
+
method=method,
|
|
169
|
+
status="denied",
|
|
170
|
+
latency_ms=0,
|
|
171
|
+
policy_decision=decision
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _log_success(self, method: str, latency_ms: int, decision: PolicyDecision) -> None:
|
|
175
|
+
"""Log successful execution."""
|
|
176
|
+
self.client._log(
|
|
177
|
+
tool=self.tool_name,
|
|
178
|
+
method=method,
|
|
179
|
+
status="success",
|
|
180
|
+
latency_ms=latency_ms,
|
|
181
|
+
policy_decision=decision
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _log_error(
|
|
185
|
+
self,
|
|
186
|
+
method: str,
|
|
187
|
+
latency_ms: int,
|
|
188
|
+
decision: PolicyDecision,
|
|
189
|
+
error: Exception
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Log execution error."""
|
|
192
|
+
self.client._log(
|
|
193
|
+
tool=self.tool_name,
|
|
194
|
+
method=method,
|
|
195
|
+
status="error",
|
|
196
|
+
latency_ms=latency_ms,
|
|
197
|
+
policy_decision=decision,
|
|
198
|
+
error_type=type(error).__name__,
|
|
199
|
+
error_message=str(error)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def governed_crew_tool(
|
|
204
|
+
client: ControlZeroClient,
|
|
205
|
+
tool_name: Optional[str] = None,
|
|
206
|
+
) -> Callable:
|
|
207
|
+
"""
|
|
208
|
+
Decorator to create a governed CrewAI tool from a function.
|
|
209
|
+
|
|
210
|
+
Usage:
|
|
211
|
+
@governed_crew_tool(client, tool_name="search")
|
|
212
|
+
def search_web(query: str) -> str:
|
|
213
|
+
'''Search the web for information.'''
|
|
214
|
+
return search(query)
|
|
215
|
+
"""
|
|
216
|
+
def decorator(func: Callable) -> Callable:
|
|
217
|
+
if not CREWAI_AVAILABLE:
|
|
218
|
+
raise ImportError("crewai is required")
|
|
219
|
+
|
|
220
|
+
name = tool_name or func.__name__
|
|
221
|
+
description = func.__doc__ or ""
|
|
222
|
+
|
|
223
|
+
@wraps(func)
|
|
224
|
+
def governed_func(*args, **kwargs):
|
|
225
|
+
method = "run"
|
|
226
|
+
decision = PolicyDecision(effect="allow")
|
|
227
|
+
|
|
228
|
+
if hasattr(client, '_policy_cache') and client._policy_cache:
|
|
229
|
+
decision = client._policy_cache.evaluate(name, method)
|
|
230
|
+
|
|
231
|
+
if decision.effect == "deny":
|
|
232
|
+
client._log(
|
|
233
|
+
tool=name,
|
|
234
|
+
method=method,
|
|
235
|
+
status="denied",
|
|
236
|
+
latency_ms=0,
|
|
237
|
+
policy_decision=decision
|
|
238
|
+
)
|
|
239
|
+
raise PolicyDeniedError(decision)
|
|
240
|
+
|
|
241
|
+
start = time.perf_counter()
|
|
242
|
+
try:
|
|
243
|
+
# Inject secrets
|
|
244
|
+
if hasattr(client, '_secret_manager') and client._secret_manager:
|
|
245
|
+
kwargs = client._secret_manager.inject(name, kwargs)
|
|
246
|
+
|
|
247
|
+
result = func(*args, **kwargs)
|
|
248
|
+
|
|
249
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
250
|
+
client._log(
|
|
251
|
+
tool=name,
|
|
252
|
+
method=method,
|
|
253
|
+
status="success",
|
|
254
|
+
latency_ms=latency_ms,
|
|
255
|
+
policy_decision=decision
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
except PolicyDeniedError:
|
|
261
|
+
raise
|
|
262
|
+
except Exception as e:
|
|
263
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
264
|
+
client._log(
|
|
265
|
+
tool=name,
|
|
266
|
+
method=method,
|
|
267
|
+
status="error",
|
|
268
|
+
latency_ms=latency_ms,
|
|
269
|
+
policy_decision=decision,
|
|
270
|
+
error_type=type(e).__name__,
|
|
271
|
+
error_message=str(e)
|
|
272
|
+
)
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
# Return as a simple callable with metadata
|
|
276
|
+
governed_func.name = name
|
|
277
|
+
governed_func.description = description
|
|
278
|
+
governed_func.is_governed = True
|
|
279
|
+
|
|
280
|
+
return governed_func
|
|
281
|
+
|
|
282
|
+
return decorator
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def wrap_crew_tools(
|
|
286
|
+
tools: List[CrewAIBaseTool],
|
|
287
|
+
client: ControlZeroClient,
|
|
288
|
+
) -> List[GovernedCrewTool]:
|
|
289
|
+
"""
|
|
290
|
+
Wrap multiple CrewAI tools with governance.
|
|
291
|
+
|
|
292
|
+
Usage:
|
|
293
|
+
tools = [tool1, tool2, tool3]
|
|
294
|
+
governed_tools = wrap_crew_tools(tools, client)
|
|
295
|
+
"""
|
|
296
|
+
return [
|
|
297
|
+
GovernedCrewTool(base_tool=tool, client=client)
|
|
298
|
+
for tool in tools
|
|
299
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Control Zero LangChain Integration.
|
|
3
|
+
|
|
4
|
+
This module provides governance wrappers for LangChain components including:
|
|
5
|
+
- Tools (GovernedTool, GovernanceTool)
|
|
6
|
+
- Agents (GovernedAgent)
|
|
7
|
+
- Chains (GovernedChain)
|
|
8
|
+
- LangGraph (GovernedNode, governed_graph)
|
|
9
|
+
- Callbacks (ControlZeroCallbackHandler)
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from control_zero import ControlZeroClient
|
|
13
|
+
from control_zero.integrations.langchain import (
|
|
14
|
+
GovernedTool,
|
|
15
|
+
GovernedAgent,
|
|
16
|
+
ControlZeroCallbackHandler,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Initialize Control Zero
|
|
20
|
+
client = ControlZeroClient(api_key="...")
|
|
21
|
+
client.initialize()
|
|
22
|
+
|
|
23
|
+
# Wrap tools with governance
|
|
24
|
+
governed_tool = GovernedTool(
|
|
25
|
+
tool=my_tool,
|
|
26
|
+
client=client,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Use callback handler for comprehensive logging
|
|
30
|
+
callback = ControlZeroCallbackHandler(client)
|
|
31
|
+
agent.run("query", callbacks=[callback])
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from control_zero.integrations.langchain.tool import GovernanceTool, GovernedTool
|
|
35
|
+
from control_zero.integrations.langchain.agent import GovernedAgent
|
|
36
|
+
from control_zero.integrations.langchain.chain import GovernedChain
|
|
37
|
+
from control_zero.integrations.langchain.callbacks import ControlZeroCallbackHandler
|
|
38
|
+
from control_zero.integrations.langchain.graph import (
|
|
39
|
+
GovernedNode,
|
|
40
|
+
governed_graph,
|
|
41
|
+
GovernedStateGraph,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Tools
|
|
46
|
+
"GovernanceTool",
|
|
47
|
+
"GovernedTool",
|
|
48
|
+
# Agents
|
|
49
|
+
"GovernedAgent",
|
|
50
|
+
# Chains
|
|
51
|
+
"GovernedChain",
|
|
52
|
+
# Callbacks
|
|
53
|
+
"ControlZeroCallbackHandler",
|
|
54
|
+
# LangGraph
|
|
55
|
+
"GovernedNode",
|
|
56
|
+
"governed_graph",
|
|
57
|
+
"GovernedStateGraph",
|
|
58
|
+
]
|