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.
Files changed (81) hide show
  1. {controlzero-1.0.0 → controlzero-1.1.0}/PKG-INFO +1 -1
  2. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/__init__.py +1 -1
  3. controlzero-1.1.0/controlzero/integrations/__init__.py +20 -0
  4. controlzero-1.1.0/controlzero/integrations/anthropic.py +188 -0
  5. controlzero-1.1.0/controlzero/integrations/braintrust.py +142 -0
  6. controlzero-1.1.0/controlzero/integrations/crewai/__init__.py +52 -0
  7. controlzero-1.1.0/controlzero/integrations/crewai/agent.py +257 -0
  8. controlzero-1.1.0/controlzero/integrations/crewai/crew.py +421 -0
  9. controlzero-1.1.0/controlzero/integrations/crewai/task.py +281 -0
  10. controlzero-1.1.0/controlzero/integrations/crewai/tool.py +252 -0
  11. controlzero-1.1.0/controlzero/integrations/google.py +184 -0
  12. controlzero-1.1.0/controlzero/integrations/google_adk/__init__.py +59 -0
  13. controlzero-1.1.0/controlzero/integrations/google_adk/agent.py +253 -0
  14. controlzero-1.1.0/controlzero/integrations/google_adk/tool.py +259 -0
  15. controlzero-1.1.0/controlzero/integrations/langchain/__init__.py +57 -0
  16. controlzero-1.1.0/controlzero/integrations/langchain/agent.py +298 -0
  17. controlzero-1.1.0/controlzero/integrations/langchain/callbacks.py +396 -0
  18. controlzero-1.1.0/controlzero/integrations/langchain/chain.py +327 -0
  19. controlzero-1.1.0/controlzero/integrations/langchain/graph.py +450 -0
  20. controlzero-1.1.0/controlzero/integrations/langchain/tool.py +226 -0
  21. controlzero-1.1.0/controlzero/integrations/langfuse.py +176 -0
  22. controlzero-1.1.0/controlzero/integrations/litellm.py +137 -0
  23. controlzero-1.1.0/controlzero/integrations/openai.py +225 -0
  24. controlzero-1.1.0/controlzero/integrations/vercel_ai.py +140 -0
  25. {controlzero-1.0.0 → controlzero-1.1.0}/pyproject.toml +1 -1
  26. {controlzero-1.0.0 → controlzero-1.1.0}/.gitignore +0 -0
  27. {controlzero-1.0.0 → controlzero-1.1.0}/Dockerfile.test +0 -0
  28. {controlzero-1.0.0 → controlzero-1.1.0}/LICENSE +0 -0
  29. {controlzero-1.0.0 → controlzero-1.1.0}/README.md +0 -0
  30. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/__init__.py +0 -0
  31. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/dlp_scanner.py +0 -0
  32. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/enforcer.py +0 -0
  33. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/_internal/types.py +0 -0
  34. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/audit_local.py +0 -0
  35. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/audit_remote.py +0 -0
  36. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/__init__.py +0 -0
  37. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/main.py +0 -0
  38. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/autogen.yaml +0 -0
  39. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/claude-code.yaml +0 -0
  40. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/codex-cli.yaml +0 -0
  41. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/cost-cap.yaml +0 -0
  42. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/crewai.yaml +0 -0
  43. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/cursor.yaml +0 -0
  44. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  45. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/generic.yaml +0 -0
  46. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/langchain.yaml +0 -0
  47. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/mcp.yaml +0 -0
  48. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/cli/templates/rag.yaml +0 -0
  49. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/client.py +0 -0
  50. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/enrollment.py +0 -0
  51. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/errors.py +0 -0
  52. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/policy_loader.py +0 -0
  53. {controlzero-1.0.0 → controlzero-1.1.0}/controlzero/tamper.py +0 -0
  54. {controlzero-1.0.0 → controlzero-1.1.0}/examples/hello_world.py +0 -0
  55. {controlzero-1.0.0 → controlzero-1.1.0}/tests/conftest.py +0 -0
  56. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_audit_remote.py +0 -0
  57. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_audit_sink_isolation.py +0 -0
  58. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_hook.py +0 -0
  59. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_init.py +0 -0
  60. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_init_templates.py +0 -0
  61. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_tail.py +0 -0
  62. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_test.py +0 -0
  63. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_cli_validate.py +0 -0
  64. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_dlp_scanner.py +0 -0
  65. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_enrollment.py +0 -0
  66. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_fail_closed_eval.py +0 -0
  67. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_glob_matching.py +0 -0
  68. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_hybrid_mode_strict.py +0 -0
  69. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_hybrid_mode_warn.py +0 -0
  70. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_install_hooks.py +0 -0
  71. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_local_mode_dict.py +0 -0
  72. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_local_mode_file_json.py +0 -0
  73. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_local_mode_file_yaml.py +0 -0
  74. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_log_fallback_stderr.py +0 -0
  75. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_log_options_ignored_hosted.py +0 -0
  76. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_log_rotation.py +0 -0
  77. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_no_policy_no_key.py +0 -0
  78. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_package_rename_shim.py +0 -0
  79. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_policy_freshness.py +0 -0
  80. {controlzero-1.0.0 → controlzero-1.1.0}/tests/test_tamper.py +0 -0
  81. {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.0.0
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
@@ -28,7 +28,7 @@ from controlzero.errors import (
28
28
  )
29
29
  from controlzero.policy_loader import load_policy
30
30
 
31
- __version__ = "1.0.0"
31
+ __version__ = "1.1.0"
32
32
 
33
33
  __all__ = [
34
34
  "Client",
@@ -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