controlzero 1.0.0__tar.gz → 1.1.0__tar.gz
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.
- {controlzero-1.0.0 → controlzero-1.1.0}/PKG-INFO +1 -1
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/__init__.py +1 -1
- controlzero-1.1.0/controlzero/integrations/__init__.py +20 -0
- controlzero-1.1.0/controlzero/integrations/anthropic.py +188 -0
- controlzero-1.1.0/controlzero/integrations/braintrust.py +142 -0
- controlzero-1.1.0/controlzero/integrations/crewai/__init__.py +52 -0
- controlzero-1.1.0/controlzero/integrations/crewai/agent.py +257 -0
- controlzero-1.1.0/controlzero/integrations/crewai/crew.py +421 -0
- controlzero-1.1.0/controlzero/integrations/crewai/task.py +281 -0
- controlzero-1.1.0/controlzero/integrations/crewai/tool.py +252 -0
- controlzero-1.1.0/controlzero/integrations/google.py +184 -0
- controlzero-1.1.0/controlzero/integrations/google_adk/__init__.py +59 -0
- controlzero-1.1.0/controlzero/integrations/google_adk/agent.py +253 -0
- controlzero-1.1.0/controlzero/integrations/google_adk/tool.py +259 -0
- controlzero-1.1.0/controlzero/integrations/langchain/__init__.py +57 -0
- controlzero-1.1.0/controlzero/integrations/langchain/agent.py +298 -0
- controlzero-1.1.0/controlzero/integrations/langchain/callbacks.py +396 -0
- controlzero-1.1.0/controlzero/integrations/langchain/chain.py +327 -0
- controlzero-1.1.0/controlzero/integrations/langchain/graph.py +450 -0
- controlzero-1.1.0/controlzero/integrations/langchain/tool.py +226 -0
- controlzero-1.1.0/controlzero/integrations/langfuse.py +176 -0
- controlzero-1.1.0/controlzero/integrations/litellm.py +137 -0
- controlzero-1.1.0/controlzero/integrations/openai.py +225 -0
- controlzero-1.1.0/controlzero/integrations/vercel_ai.py +140 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/pyproject.toml +1 -1
- {controlzero-1.0.0 → controlzero-1.1.0}/.gitignore +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/Dockerfile.test +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/LICENSE +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/README.md +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/types.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/audit_local.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/audit_remote.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/main.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/client.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/enrollment.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/errors.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/policy_loader.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/tamper.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/examples/hello_world.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/conftest.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_audit_remote.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_hook.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_init.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_tail.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_test.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_validate.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_enrollment.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_glob_matching.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_install_hooks.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_log_rotation.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_tamper.py +0 -0
- {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_tamper_hook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup.
|
|
5
5
|
Project-URL: Homepage, https://controlzero.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.controlzero.ai
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""ControlZero framework integrations.
|
|
2
|
+
|
|
3
|
+
Each submodule provides a thin wrapper or callback handler for a
|
|
4
|
+
specific AI framework. Import directly from the submodule you need:
|
|
5
|
+
|
|
6
|
+
from controlzero.integrations.openai import wrap_openai
|
|
7
|
+
from controlzero.integrations.anthropic import wrap_anthropic
|
|
8
|
+
from controlzero.integrations.google import wrap_google
|
|
9
|
+
from controlzero.integrations.langchain import ControlZeroCallbackHandler
|
|
10
|
+
from controlzero.integrations.crewai import create_step_callback
|
|
11
|
+
from controlzero.integrations.litellm import ControlZeroLiteLLMCallback
|
|
12
|
+
from controlzero.integrations.google_adk import GovernedAgent, GovernedTool
|
|
13
|
+
from controlzero.integrations.vercel_ai import ControlZeroVercelAIMiddleware
|
|
14
|
+
from controlzero.integrations.braintrust import BraintrustGovernanceLogger
|
|
15
|
+
from controlzero.integrations.langfuse import LangfuseGovernanceHandler
|
|
16
|
+
|
|
17
|
+
No framework dependencies are installed by default. Install the
|
|
18
|
+
framework you need separately (e.g. ``pip install openai``,
|
|
19
|
+
``pip install google-adk``, ``pip install langfuse``).
|
|
20
|
+
"""
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Anthropic SDK integration for ControlZero.
|
|
2
|
+
|
|
3
|
+
Provides ``wrap_anthropic()`` which transparently intercepts
|
|
4
|
+
``client.messages.create()`` and ``client.messages.stream()``
|
|
5
|
+
to enforce governance policies before the request reaches Anthropic.
|
|
6
|
+
|
|
7
|
+
All other client methods pass through unchanged.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from controlzero import Client
|
|
12
|
+
from controlzero.integrations.anthropic import wrap_anthropic
|
|
13
|
+
import anthropic
|
|
14
|
+
|
|
15
|
+
cz = Client(policy={"rules": [{"deny": "delete_*"}]})
|
|
16
|
+
client = wrap_anthropic(anthropic.Anthropic(), cz)
|
|
17
|
+
|
|
18
|
+
# Policy enforcement happens automatically
|
|
19
|
+
response = client.messages.create(
|
|
20
|
+
model="claude-sonnet-4-20250514",
|
|
21
|
+
max_tokens=1024,
|
|
22
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import time
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import anthropic as _anthropic # noqa: F401
|
|
33
|
+
except ImportError:
|
|
34
|
+
raise ImportError(
|
|
35
|
+
"The 'anthropic' package is required for this integration. "
|
|
36
|
+
"Install it with: pip install anthropic"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from controlzero.client import Client
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
|
|
43
|
+
"""Rough cost estimate fallback (USD per token)."""
|
|
44
|
+
return (input_tokens + output_tokens) / 1_000_000 * 1.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _WrappedMessages:
|
|
48
|
+
"""Proxy for ``client.messages`` that enforces policy on create() and stream()."""
|
|
49
|
+
|
|
50
|
+
__slots__ = ("_inner", "_cz", "_agent_id")
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
inner: Any,
|
|
55
|
+
cz: Client,
|
|
56
|
+
agent_id: str,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._inner = inner
|
|
59
|
+
self._cz = cz
|
|
60
|
+
self._agent_id = agent_id
|
|
61
|
+
|
|
62
|
+
def _enforce_model(self, kwargs: dict, method: str) -> None:
|
|
63
|
+
"""Enforce policy for a messages API call."""
|
|
64
|
+
model = kwargs.get("model", "unknown")
|
|
65
|
+
context: dict[str, Any] = {
|
|
66
|
+
"agent_id": self._agent_id,
|
|
67
|
+
"provider": "anthropic",
|
|
68
|
+
"method": method,
|
|
69
|
+
"resource": f"model/{model}",
|
|
70
|
+
}
|
|
71
|
+
tools = kwargs.get("tools")
|
|
72
|
+
if tools:
|
|
73
|
+
context["tool_count"] = len(tools)
|
|
74
|
+
context["tool_names"] = [
|
|
75
|
+
t.get("name", "") for t in tools if isinstance(t, dict)
|
|
76
|
+
]
|
|
77
|
+
self._cz.guard(
|
|
78
|
+
tool="llm:anthropic",
|
|
79
|
+
method=method,
|
|
80
|
+
raise_on_deny=True,
|
|
81
|
+
context=context,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def create(self, **kwargs: Any) -> Any:
|
|
85
|
+
"""Enforce policy then delegate to the original create()."""
|
|
86
|
+
model = kwargs.get("model", "unknown")
|
|
87
|
+
self._enforce_model(kwargs, "messages.create")
|
|
88
|
+
start = time.perf_counter()
|
|
89
|
+
response = self._inner.create(**kwargs)
|
|
90
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
91
|
+
|
|
92
|
+
# Capture token usage from the response
|
|
93
|
+
input_tokens = 0
|
|
94
|
+
output_tokens = 0
|
|
95
|
+
if hasattr(response, "usage") and response.usage is not None:
|
|
96
|
+
input_tokens = getattr(response.usage, "input_tokens", 0) or 0
|
|
97
|
+
output_tokens = getattr(response.usage, "output_tokens", 0) or 0
|
|
98
|
+
|
|
99
|
+
# Audit the successful call
|
|
100
|
+
try:
|
|
101
|
+
from controlzero._internal.enforcer import PolicyDecision
|
|
102
|
+
decision = PolicyDecision(
|
|
103
|
+
effect="allow",
|
|
104
|
+
reason="guard passed",
|
|
105
|
+
)
|
|
106
|
+
self._cz._audit_decision(
|
|
107
|
+
tool="llm:anthropic",
|
|
108
|
+
method="messages.create",
|
|
109
|
+
args={
|
|
110
|
+
"model": model,
|
|
111
|
+
"input_tokens": input_tokens,
|
|
112
|
+
"output_tokens": output_tokens,
|
|
113
|
+
"latency_ms": latency_ms,
|
|
114
|
+
},
|
|
115
|
+
decision=decision,
|
|
116
|
+
)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
return response
|
|
121
|
+
|
|
122
|
+
def stream(self, **kwargs: Any) -> Any:
|
|
123
|
+
"""Enforce policy then delegate to the original stream()."""
|
|
124
|
+
self._enforce_model(kwargs, "messages.stream")
|
|
125
|
+
return self._inner.stream(**kwargs)
|
|
126
|
+
|
|
127
|
+
def __getattr__(self, name: str) -> Any:
|
|
128
|
+
return getattr(self._inner, name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _WrappedAnthropic:
|
|
132
|
+
"""Proxy for ``anthropic.Anthropic`` that wraps the messages resource."""
|
|
133
|
+
|
|
134
|
+
__slots__ = ("_inner", "_messages")
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
inner: Any,
|
|
139
|
+
cz: Client,
|
|
140
|
+
agent_id: str,
|
|
141
|
+
) -> None:
|
|
142
|
+
self._inner = inner
|
|
143
|
+
self._messages = _WrappedMessages(inner.messages, cz, agent_id)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def messages(self) -> _WrappedMessages:
|
|
147
|
+
return self._messages
|
|
148
|
+
|
|
149
|
+
def __getattr__(self, name: str) -> Any:
|
|
150
|
+
return getattr(self._inner, name)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def wrap_anthropic(
|
|
154
|
+
client: Any,
|
|
155
|
+
cz: Client,
|
|
156
|
+
agent_id: str = "",
|
|
157
|
+
) -> Any:
|
|
158
|
+
"""Wrap an Anthropic client with ControlZero policy enforcement.
|
|
159
|
+
|
|
160
|
+
Returns a proxy object that intercepts ``messages.create()`` and
|
|
161
|
+
``messages.stream()`` to enforce governance policies. All other
|
|
162
|
+
client methods and properties pass through unchanged.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
client: An ``anthropic.Anthropic()`` instance.
|
|
166
|
+
cz: A ``controlzero.Client`` instance with a policy loaded.
|
|
167
|
+
agent_id: Optional identifier for the agent making requests.
|
|
168
|
+
Included in the enforcement context for audit logging.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
A wrapped client with transparent policy enforcement.
|
|
172
|
+
|
|
173
|
+
Example::
|
|
174
|
+
|
|
175
|
+
from controlzero import Client
|
|
176
|
+
from controlzero.integrations.anthropic import wrap_anthropic
|
|
177
|
+
import anthropic
|
|
178
|
+
|
|
179
|
+
cz = Client(policy={"rules": [{"allow": "*"}]})
|
|
180
|
+
client = wrap_anthropic(anthropic.Anthropic(), cz, agent_id="my-agent")
|
|
181
|
+
|
|
182
|
+
response = client.messages.create(
|
|
183
|
+
model="claude-sonnet-4-20250514",
|
|
184
|
+
max_tokens=1024,
|
|
185
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
186
|
+
)
|
|
187
|
+
"""
|
|
188
|
+
return _WrappedAnthropic(client, cz, agent_id)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Braintrust integration for ControlZero.
|
|
2
|
+
|
|
3
|
+
Provides a callback handler that sends governance decisions and audit
|
|
4
|
+
events to Braintrust for evaluation and monitoring.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from controlzero import Client
|
|
9
|
+
from controlzero.integrations.braintrust import BraintrustGovernanceLogger
|
|
10
|
+
|
|
11
|
+
cz = Client(policy={"rules": [{"allow": "*"}]})
|
|
12
|
+
|
|
13
|
+
logger = BraintrustGovernanceLogger(project_name="my-agent")
|
|
14
|
+
|
|
15
|
+
decision = cz.guard("database", {"sql": "SELECT 1"})
|
|
16
|
+
logger.log_decision(
|
|
17
|
+
tool="database",
|
|
18
|
+
method="query",
|
|
19
|
+
decision=decision.effect,
|
|
20
|
+
latency_ms=0.12,
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import time
|
|
27
|
+
from typing import Any, Optional
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import braintrust # noqa: F401
|
|
31
|
+
|
|
32
|
+
_HAS_BRAINTRUST = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
_HAS_BRAINTRUST = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BraintrustGovernanceLogger:
|
|
38
|
+
"""Logs ControlZero governance decisions to Braintrust for evaluation.
|
|
39
|
+
|
|
40
|
+
If the braintrust package is not installed, operations are no-ops.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
project_name: str = "controlzero",
|
|
46
|
+
experiment_name: Optional[str] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._project_name = project_name
|
|
49
|
+
self._experiment_name = experiment_name
|
|
50
|
+
self._logger = None
|
|
51
|
+
|
|
52
|
+
if _HAS_BRAINTRUST:
|
|
53
|
+
self._logger = braintrust.init_logger(project=project_name)
|
|
54
|
+
|
|
55
|
+
def log_decision(
|
|
56
|
+
self,
|
|
57
|
+
tool: str,
|
|
58
|
+
method: str,
|
|
59
|
+
decision: str,
|
|
60
|
+
latency_ms: float = 0,
|
|
61
|
+
policy_id: Optional[str] = None,
|
|
62
|
+
context: Optional[dict] = None,
|
|
63
|
+
metadata: Optional[dict] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Log a governance decision to Braintrust.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
tool: Tool name
|
|
69
|
+
method: Method name
|
|
70
|
+
decision: Policy decision (allow/deny)
|
|
71
|
+
latency_ms: Policy evaluation latency
|
|
72
|
+
policy_id: ID of the matched policy
|
|
73
|
+
context: Request context
|
|
74
|
+
metadata: Additional metadata
|
|
75
|
+
"""
|
|
76
|
+
if not self._logger:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
self._logger.log(
|
|
80
|
+
input={
|
|
81
|
+
"tool": tool,
|
|
82
|
+
"method": method,
|
|
83
|
+
"action": f"{tool}:{method}",
|
|
84
|
+
"context": context or {},
|
|
85
|
+
},
|
|
86
|
+
output={
|
|
87
|
+
"decision": decision,
|
|
88
|
+
"policy_id": policy_id,
|
|
89
|
+
},
|
|
90
|
+
metadata={
|
|
91
|
+
"latency_ms": latency_ms,
|
|
92
|
+
"source": "controlzero",
|
|
93
|
+
**(metadata or {}),
|
|
94
|
+
},
|
|
95
|
+
scores={
|
|
96
|
+
"governance_pass": 1.0 if decision == "allow" else 0.0,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def log_tool_call(
|
|
101
|
+
self,
|
|
102
|
+
tool: str,
|
|
103
|
+
method: str,
|
|
104
|
+
arguments: dict,
|
|
105
|
+
result: Any = None,
|
|
106
|
+
error: Optional[str] = None,
|
|
107
|
+
duration_ms: float = 0,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Log a complete tool call (decision + execution) to Braintrust.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
tool: Tool name
|
|
113
|
+
method: Method name
|
|
114
|
+
arguments: Tool call arguments
|
|
115
|
+
result: Tool call result (if successful)
|
|
116
|
+
error: Error message (if failed)
|
|
117
|
+
duration_ms: Total call duration
|
|
118
|
+
"""
|
|
119
|
+
if not self._logger:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
self._logger.log(
|
|
123
|
+
input={
|
|
124
|
+
"tool": tool,
|
|
125
|
+
"method": method,
|
|
126
|
+
"arguments": arguments,
|
|
127
|
+
},
|
|
128
|
+
output={
|
|
129
|
+
"result": str(result)[:1000] if result else None,
|
|
130
|
+
"error": error,
|
|
131
|
+
},
|
|
132
|
+
metadata={
|
|
133
|
+
"duration_ms": duration_ms,
|
|
134
|
+
"source": "controlzero",
|
|
135
|
+
"status": "error" if error else "success",
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def flush(self) -> None:
|
|
140
|
+
"""Flush any buffered logs to Braintrust."""
|
|
141
|
+
if self._logger and hasattr(self._logger, "flush"):
|
|
142
|
+
self._logger.flush()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ControlZero CrewAI Integration.
|
|
3
|
+
|
|
4
|
+
Provides governance enforcement for CrewAI including:
|
|
5
|
+
- Crew-level policy enforcement
|
|
6
|
+
- Agent role-based access control
|
|
7
|
+
- Task execution governance
|
|
8
|
+
- Tool-level policy checks
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from controlzero import Client
|
|
12
|
+
from controlzero.integrations.crewai import (
|
|
13
|
+
GovernedCrew,
|
|
14
|
+
GovernedCrewAgent,
|
|
15
|
+
GovernedTask,
|
|
16
|
+
GovernedCrewTool,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
client = Client(policy={"rules": [{"allow": "*"}]})
|
|
20
|
+
|
|
21
|
+
# Wrap a crew with governance
|
|
22
|
+
governed_crew = GovernedCrew(
|
|
23
|
+
crew=my_crew,
|
|
24
|
+
client=client,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
result = governed_crew.kickoff()
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from controlzero.integrations.crewai.crew import GovernedCrew
|
|
31
|
+
from controlzero.integrations.crewai.agent import GovernedCrewAgent, governed_agent
|
|
32
|
+
from controlzero.integrations.crewai.task import GovernedTask, governed_task
|
|
33
|
+
from controlzero.integrations.crewai.tool import (
|
|
34
|
+
GovernedCrewTool,
|
|
35
|
+
governed_crew_tool,
|
|
36
|
+
wrap_crew_tools,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Crew
|
|
41
|
+
"GovernedCrew",
|
|
42
|
+
# Agent
|
|
43
|
+
"GovernedCrewAgent",
|
|
44
|
+
"governed_agent",
|
|
45
|
+
# Task
|
|
46
|
+
"GovernedTask",
|
|
47
|
+
"governed_task",
|
|
48
|
+
# Tool
|
|
49
|
+
"GovernedCrewTool",
|
|
50
|
+
"governed_crew_tool",
|
|
51
|
+
"wrap_crew_tools",
|
|
52
|
+
]
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Governed CrewAI Agent wrapper.
|
|
3
|
+
|
|
4
|
+
Provides governance enforcement for CrewAI agents including:
|
|
5
|
+
- Role-based access control
|
|
6
|
+
- Tool filtering based on agent role
|
|
7
|
+
- Action logging
|
|
8
|
+
- Execution tracking
|
|
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 Agent
|
|
19
|
+
from crewai.tools import BaseTool as CrewAIBaseTool
|
|
20
|
+
CREWAI_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
CREWAI_AVAILABLE = False
|
|
23
|
+
class Agent:
|
|
24
|
+
pass
|
|
25
|
+
class CrewAIBaseTool:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
from controlzero.client import Client
|
|
29
|
+
from controlzero.errors import PolicyDeniedError, PolicyDecision
|
|
30
|
+
from controlzero.integrations.crewai.tool import GovernedCrewTool, wrap_crew_tools
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GovernedCrewAgent:
|
|
34
|
+
"""
|
|
35
|
+
Governance wrapper for CrewAI Agent.
|
|
36
|
+
|
|
37
|
+
Wraps a CrewAI Agent to provide:
|
|
38
|
+
- Role-based policy enforcement
|
|
39
|
+
- Tool-level governance
|
|
40
|
+
- Action audit logging
|
|
41
|
+
- Execution tracking
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
from crewai import Agent
|
|
45
|
+
|
|
46
|
+
researcher = Agent(
|
|
47
|
+
role="Senior Researcher",
|
|
48
|
+
goal="Research topics thoroughly",
|
|
49
|
+
backstory="You are an expert researcher...",
|
|
50
|
+
tools=[search_tool, scrape_tool],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
governed_researcher = GovernedCrewAgent(
|
|
54
|
+
agent=researcher,
|
|
55
|
+
client=client,
|
|
56
|
+
wrap_tools=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Use in a crew
|
|
60
|
+
crew = Crew(
|
|
61
|
+
agents=[governed_researcher.agent],
|
|
62
|
+
tasks=[...],
|
|
63
|
+
)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
agent: Agent,
|
|
69
|
+
client: Client,
|
|
70
|
+
wrap_tools: bool = True,
|
|
71
|
+
agent_name: Optional[str] = None,
|
|
72
|
+
allowed_tools: Optional[List[str]] = None,
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Initialize governed agent.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
agent: The CrewAI Agent to wrap
|
|
79
|
+
client: ControlZero client
|
|
80
|
+
wrap_tools: Whether to wrap agent tools with governance
|
|
81
|
+
agent_name: Name for logging (defaults to agent.role)
|
|
82
|
+
allowed_tools: List of allowed tool names (filters tools)
|
|
83
|
+
"""
|
|
84
|
+
if not CREWAI_AVAILABLE:
|
|
85
|
+
raise ImportError(
|
|
86
|
+
"crewai is required. Install with: pip install crewai"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self._agent = agent
|
|
90
|
+
self._client = client
|
|
91
|
+
self._agent_name = agent_name or agent.role
|
|
92
|
+
self._allowed_tools = allowed_tools
|
|
93
|
+
|
|
94
|
+
# Filter tools if allowlist specified
|
|
95
|
+
if allowed_tools and agent.tools:
|
|
96
|
+
agent.tools = [
|
|
97
|
+
t for t in agent.tools
|
|
98
|
+
if getattr(t, 'name', str(t)) in allowed_tools
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# Wrap tools with governance
|
|
102
|
+
if wrap_tools and agent.tools:
|
|
103
|
+
self._wrap_agent_tools()
|
|
104
|
+
|
|
105
|
+
# Tracking
|
|
106
|
+
self._total_actions = 0
|
|
107
|
+
self._session_start = time.time()
|
|
108
|
+
|
|
109
|
+
def _wrap_agent_tools(self) -> None:
|
|
110
|
+
"""Wrap all agent tools with governance."""
|
|
111
|
+
governed_tools = []
|
|
112
|
+
for tool in self._agent.tools:
|
|
113
|
+
if isinstance(tool, GovernedCrewTool):
|
|
114
|
+
governed_tools.append(tool)
|
|
115
|
+
elif isinstance(tool, CrewAIBaseTool):
|
|
116
|
+
governed_tools.append(
|
|
117
|
+
GovernedCrewTool(
|
|
118
|
+
base_tool=tool,
|
|
119
|
+
client=self._client,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
# Callable tools - wrap inline
|
|
124
|
+
governed_tools.append(tool)
|
|
125
|
+
self._agent.tools = governed_tools
|
|
126
|
+
|
|
127
|
+
def execute_task(
|
|
128
|
+
self,
|
|
129
|
+
task: Any,
|
|
130
|
+
context: Optional[str] = None,
|
|
131
|
+
tools: Optional[List] = None,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Execute a task with governance.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
task: The task to execute
|
|
138
|
+
context: Additional context
|
|
139
|
+
tools: Override tools for this execution
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Task result string
|
|
143
|
+
"""
|
|
144
|
+
tool_name = f"crewai:agent:{self._agent_name}"
|
|
145
|
+
decision = self._client.guard(
|
|
146
|
+
tool=tool_name,
|
|
147
|
+
method="execute_task",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if decision.denied:
|
|
151
|
+
raise PolicyDeniedError(decision)
|
|
152
|
+
|
|
153
|
+
start = time.perf_counter()
|
|
154
|
+
self._total_actions += 1
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = self._agent.execute_task(
|
|
158
|
+
task=task,
|
|
159
|
+
context=context,
|
|
160
|
+
tools=tools or self._agent.tools,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
164
|
+
try:
|
|
165
|
+
self._client._audit_decision(
|
|
166
|
+
tool=tool_name,
|
|
167
|
+
method="execute_task",
|
|
168
|
+
args={
|
|
169
|
+
"status": "success",
|
|
170
|
+
"latency_ms": latency_ms,
|
|
171
|
+
"agent_role": self._agent.role,
|
|
172
|
+
"total_actions": self._total_actions,
|
|
173
|
+
},
|
|
174
|
+
decision=decision,
|
|
175
|
+
)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
except PolicyDeniedError:
|
|
182
|
+
raise
|
|
183
|
+
except Exception as e:
|
|
184
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
185
|
+
try:
|
|
186
|
+
err_decision = PolicyDecision(
|
|
187
|
+
effect="allow",
|
|
188
|
+
reason=f"error: {type(e).__name__}: {e}",
|
|
189
|
+
)
|
|
190
|
+
self._client._audit_decision(
|
|
191
|
+
tool=tool_name,
|
|
192
|
+
method="execute_task",
|
|
193
|
+
args={"status": "error", "latency_ms": latency_ms},
|
|
194
|
+
decision=err_decision,
|
|
195
|
+
)
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
raise
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def agent(self) -> Agent:
|
|
202
|
+
"""Get underlying CrewAI agent."""
|
|
203
|
+
return self._agent
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def role(self) -> str:
|
|
207
|
+
"""Get agent role."""
|
|
208
|
+
return self._agent.role
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def goal(self) -> str:
|
|
212
|
+
"""Get agent goal."""
|
|
213
|
+
return self._agent.goal
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def tools(self) -> List:
|
|
217
|
+
"""Get agent tools."""
|
|
218
|
+
return self._agent.tools
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def total_actions(self) -> int:
|
|
222
|
+
"""Get total actions executed."""
|
|
223
|
+
return self._total_actions
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def governed_agent(
|
|
227
|
+
client: Client,
|
|
228
|
+
wrap_tools: bool = True,
|
|
229
|
+
allowed_tools: Optional[List[str]] = None,
|
|
230
|
+
) -> Callable:
|
|
231
|
+
"""
|
|
232
|
+
Decorator to create a governed CrewAI agent.
|
|
233
|
+
|
|
234
|
+
Usage:
|
|
235
|
+
@governed_agent(client, wrap_tools=True)
|
|
236
|
+
def create_researcher():
|
|
237
|
+
return Agent(
|
|
238
|
+
role="Researcher",
|
|
239
|
+
goal="Research topics",
|
|
240
|
+
backstory="You are an expert researcher",
|
|
241
|
+
tools=[search_tool],
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
researcher = create_researcher() # Returns GovernedCrewAgent
|
|
245
|
+
"""
|
|
246
|
+
def decorator(func: Callable) -> Callable:
|
|
247
|
+
@wraps(func)
|
|
248
|
+
def wrapper(*args, **kwargs) -> GovernedCrewAgent:
|
|
249
|
+
agent = func(*args, **kwargs)
|
|
250
|
+
return GovernedCrewAgent(
|
|
251
|
+
agent=agent,
|
|
252
|
+
client=client,
|
|
253
|
+
wrap_tools=wrap_tools,
|
|
254
|
+
allowed_tools=allowed_tools,
|
|
255
|
+
)
|
|
256
|
+
return wrapper
|
|
257
|
+
return decorator
|