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,148 @@
|
|
|
1
|
+
"""MCP middleware for intercepting and wrapping MCP calls."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from control_zero.types import AuditLogEntry
|
|
7
|
+
from control_zero.policy import PolicyDecision, PolicyDeniedError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MCPMiddleware:
|
|
11
|
+
"""
|
|
12
|
+
Middleware for wrapping MCP client calls.
|
|
13
|
+
|
|
14
|
+
This intercepts calls to MCP servers and:
|
|
15
|
+
1. Checks policies before execution
|
|
16
|
+
2. Injects secrets into the request
|
|
17
|
+
3. Logs the call for audit
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
from mcp import Client
|
|
21
|
+
from control_zero import ControlZeroClient, MCPMiddleware
|
|
22
|
+
|
|
23
|
+
cz = ControlZeroClient(api_key="cz_live_xxx")
|
|
24
|
+
await cz.initialize()
|
|
25
|
+
|
|
26
|
+
mcp_client = Client()
|
|
27
|
+
wrapped_client = MCPMiddleware(cz).wrap(mcp_client)
|
|
28
|
+
|
|
29
|
+
# Use wrapped client normally
|
|
30
|
+
result = await wrapped_client.call_tool("github", "list_issues")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, control_zero_client: Any):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the middleware.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
control_zero_client: The ControlZeroClient instance
|
|
39
|
+
"""
|
|
40
|
+
self._cz = control_zero_client
|
|
41
|
+
|
|
42
|
+
def wrap(self, mcp_client: Any) -> Any:
|
|
43
|
+
"""
|
|
44
|
+
Wrap an MCP client with Control Zero middleware.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
mcp_client: The MCP client to wrap
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The wrapped client
|
|
51
|
+
"""
|
|
52
|
+
# Store reference to original call_tool
|
|
53
|
+
original_call_tool = getattr(mcp_client, "call_tool", None)
|
|
54
|
+
|
|
55
|
+
if original_call_tool is None:
|
|
56
|
+
raise ValueError("MCP client must have a call_tool method")
|
|
57
|
+
|
|
58
|
+
# Create wrapped version
|
|
59
|
+
async def wrapped_call_tool(
|
|
60
|
+
tool: str,
|
|
61
|
+
method: str,
|
|
62
|
+
arguments: Optional[dict[str, Any]] = None,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
) -> Any:
|
|
65
|
+
return await self._intercept_call(
|
|
66
|
+
original_call_tool,
|
|
67
|
+
tool,
|
|
68
|
+
method,
|
|
69
|
+
arguments,
|
|
70
|
+
**kwargs,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Replace the method
|
|
74
|
+
mcp_client.call_tool = wrapped_call_tool
|
|
75
|
+
|
|
76
|
+
return mcp_client
|
|
77
|
+
|
|
78
|
+
async def _intercept_call(
|
|
79
|
+
self,
|
|
80
|
+
original_fn: Callable[..., Any],
|
|
81
|
+
tool: str,
|
|
82
|
+
method: str,
|
|
83
|
+
arguments: Optional[dict[str, Any]],
|
|
84
|
+
**kwargs: Any,
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""Intercept and wrap an MCP call."""
|
|
87
|
+
start = time.perf_counter()
|
|
88
|
+
|
|
89
|
+
# Check policy
|
|
90
|
+
decision = self._cz._policy_cache.evaluate(tool, method)
|
|
91
|
+
if decision.effect == "deny":
|
|
92
|
+
await self._log(tool, method, "denied", 0, decision)
|
|
93
|
+
raise PolicyDeniedError(decision)
|
|
94
|
+
|
|
95
|
+
# Get secrets for this tool
|
|
96
|
+
secrets = self._cz._secrets.get_for_tool(tool)
|
|
97
|
+
|
|
98
|
+
# Inject secrets into arguments or kwargs
|
|
99
|
+
if secrets:
|
|
100
|
+
if arguments is None:
|
|
101
|
+
arguments = {}
|
|
102
|
+
arguments["_secrets"] = secrets
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Call original function
|
|
106
|
+
result = await original_fn(tool, method, arguments, **kwargs)
|
|
107
|
+
|
|
108
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
109
|
+
await self._log(tool, method, "success", latency_ms, decision)
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
except PolicyDeniedError:
|
|
114
|
+
raise
|
|
115
|
+
except Exception as e:
|
|
116
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
117
|
+
await self._log(
|
|
118
|
+
tool, method, "error", latency_ms, decision,
|
|
119
|
+
error_type=type(e).__name__,
|
|
120
|
+
error_message=str(e),
|
|
121
|
+
)
|
|
122
|
+
raise
|
|
123
|
+
|
|
124
|
+
async def _log(
|
|
125
|
+
self,
|
|
126
|
+
tool: str,
|
|
127
|
+
method: str,
|
|
128
|
+
status: str,
|
|
129
|
+
latency_ms: int,
|
|
130
|
+
decision: PolicyDecision,
|
|
131
|
+
error_type: Optional[str] = None,
|
|
132
|
+
error_message: Optional[str] = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Log the call."""
|
|
135
|
+
entry = AuditLogEntry(
|
|
136
|
+
tool_name=tool,
|
|
137
|
+
method_name=method,
|
|
138
|
+
latency_ms=latency_ms,
|
|
139
|
+
status=status,
|
|
140
|
+
policy_decision=decision.effect,
|
|
141
|
+
policy_id=decision.policy_id,
|
|
142
|
+
error_type=error_type,
|
|
143
|
+
error_message=error_message,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Queue for async sending
|
|
147
|
+
if hasattr(self._cz, "_queue"):
|
|
148
|
+
await self._cz._queue.put(entry)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Local policy cache and enforcement."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from control_zero.types import PolicyRule
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class PolicyDecision:
|
|
11
|
+
"""Result of a policy evaluation."""
|
|
12
|
+
effect: str # "allow" or "deny"
|
|
13
|
+
policy_id: Optional[str] = None
|
|
14
|
+
reason: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PolicyDeniedError(Exception):
|
|
18
|
+
"""Raised when a policy denies an action."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, decision: PolicyDecision):
|
|
21
|
+
self.decision = decision
|
|
22
|
+
super().__init__(
|
|
23
|
+
f"Policy denied: {decision.reason or 'no reason provided'}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PolicyCache:
|
|
28
|
+
"""
|
|
29
|
+
Local cache of policies for fast evaluation.
|
|
30
|
+
|
|
31
|
+
Policies are fetched from Control Zero on init and cached locally.
|
|
32
|
+
The SDK evaluates policies before every tool call to enforce access control.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._policies: list[PolicyRule] = []
|
|
37
|
+
|
|
38
|
+
def load(self, policies: list[PolicyRule]) -> None:
|
|
39
|
+
"""Load policies into the cache."""
|
|
40
|
+
self._policies = policies
|
|
41
|
+
|
|
42
|
+
def evaluate(self, tool: str, method: str) -> PolicyDecision:
|
|
43
|
+
"""
|
|
44
|
+
Evaluate policies for a tool call.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
tool: The tool name
|
|
48
|
+
method: The method name
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
PolicyDecision with effect "allow" or "deny"
|
|
52
|
+
"""
|
|
53
|
+
action = f"{tool}:{method}"
|
|
54
|
+
|
|
55
|
+
# Check each policy in order
|
|
56
|
+
for policy in self._policies:
|
|
57
|
+
if self._matches_action(policy.actions, action):
|
|
58
|
+
return PolicyDecision(
|
|
59
|
+
effect=policy.effect,
|
|
60
|
+
policy_id=policy.id,
|
|
61
|
+
reason=f"Matched policy {policy.id}",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Default: allow if no policy matches
|
|
65
|
+
return PolicyDecision(
|
|
66
|
+
effect="allow",
|
|
67
|
+
reason="No matching policy",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _matches_action(self, policy_actions: list[str], action: str) -> bool:
|
|
71
|
+
"""Check if an action matches any of the policy actions."""
|
|
72
|
+
tool, method = action.split(":", 1)
|
|
73
|
+
|
|
74
|
+
for pa in policy_actions:
|
|
75
|
+
# Exact match
|
|
76
|
+
if pa == action:
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
# Wildcard match
|
|
80
|
+
if pa == "*":
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
# Tool wildcard: "github:*"
|
|
84
|
+
if pa.endswith(":*"):
|
|
85
|
+
policy_tool = pa[:-2]
|
|
86
|
+
if policy_tool == tool:
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
# Method wildcard: "*:read"
|
|
90
|
+
if pa.startswith("*:"):
|
|
91
|
+
policy_method = pa[2:]
|
|
92
|
+
if policy_method == method:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def clear(self) -> None:
|
|
98
|
+
"""Clear the policy cache."""
|
|
99
|
+
self._policies.clear()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""In-memory secrets manager with secure handling."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from control_zero.types import ToolConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SecretManager:
|
|
11
|
+
"""
|
|
12
|
+
Manages decrypted secrets in memory.
|
|
13
|
+
|
|
14
|
+
Secrets are:
|
|
15
|
+
1. Received encrypted from Control Zero
|
|
16
|
+
2. Decrypted using the session key
|
|
17
|
+
3. Held in memory only (never written to disk)
|
|
18
|
+
4. Wiped from memory when the session ends
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
# Store secrets by tool name
|
|
23
|
+
self._secrets: dict[str, dict[str, str]] = {}
|
|
24
|
+
self._session_key: Optional[bytes] = None
|
|
25
|
+
|
|
26
|
+
def set_session_key(self, key: bytes) -> None:
|
|
27
|
+
"""Set the session key for decryption."""
|
|
28
|
+
self._session_key = key
|
|
29
|
+
|
|
30
|
+
def load_tool_secrets(self, tool: ToolConfig) -> None:
|
|
31
|
+
"""Load and decrypt secrets for a tool."""
|
|
32
|
+
tool_secrets: dict[str, str] = {}
|
|
33
|
+
|
|
34
|
+
for secret in tool.secrets:
|
|
35
|
+
# In production, decrypt with session key
|
|
36
|
+
# For now, treat encrypted_value as base64-encoded plaintext
|
|
37
|
+
try:
|
|
38
|
+
decrypted = base64.b64decode(secret.encrypted_value).decode("utf-8")
|
|
39
|
+
except Exception:
|
|
40
|
+
# If not base64, use as-is (for development)
|
|
41
|
+
decrypted = secret.encrypted_value
|
|
42
|
+
|
|
43
|
+
tool_secrets[secret.name] = decrypted
|
|
44
|
+
|
|
45
|
+
self._secrets[tool.name] = tool_secrets
|
|
46
|
+
|
|
47
|
+
def get_for_tool(self, tool_name: str) -> dict[str, str]:
|
|
48
|
+
"""Get all secrets for a tool."""
|
|
49
|
+
return self._secrets.get(tool_name, {})
|
|
50
|
+
|
|
51
|
+
def get_secret(self, tool_name: str, secret_name: str) -> Optional[str]:
|
|
52
|
+
"""Get a specific secret."""
|
|
53
|
+
tool_secrets = self._secrets.get(tool_name, {})
|
|
54
|
+
return tool_secrets.get(secret_name)
|
|
55
|
+
|
|
56
|
+
def wipe(self) -> None:
|
|
57
|
+
"""Securely wipe all secrets from memory."""
|
|
58
|
+
# Overwrite with random data before clearing
|
|
59
|
+
for tool_name in list(self._secrets.keys()):
|
|
60
|
+
for secret_name in list(self._secrets[tool_name].keys()):
|
|
61
|
+
# Overwrite the string content
|
|
62
|
+
secret_len = len(self._secrets[tool_name][secret_name])
|
|
63
|
+
self._secrets[tool_name][secret_name] = os.urandom(secret_len).hex()
|
|
64
|
+
|
|
65
|
+
self._secrets[tool_name].clear()
|
|
66
|
+
|
|
67
|
+
self._secrets.clear()
|
|
68
|
+
|
|
69
|
+
# Clear session key
|
|
70
|
+
if self._session_key:
|
|
71
|
+
key_len = len(self._session_key)
|
|
72
|
+
self._session_key = os.urandom(key_len)
|
|
73
|
+
self._session_key = None
|
|
74
|
+
|
|
75
|
+
def __del__(self):
|
|
76
|
+
"""Ensure secrets are wiped on garbage collection."""
|
|
77
|
+
self.wipe()
|
control_zero/types.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Type definitions for Control Zero SDK."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecretValue(BaseModel):
|
|
9
|
+
"""An encrypted secret value."""
|
|
10
|
+
name: str
|
|
11
|
+
encrypted_value: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToolConfig(BaseModel):
|
|
15
|
+
"""Configuration for an MCP tool."""
|
|
16
|
+
name: str
|
|
17
|
+
methods: list[str] = Field(default_factory=list)
|
|
18
|
+
auth_type: str = "managed" # "managed" or "passthrough"
|
|
19
|
+
auth_header: Optional[str] = None
|
|
20
|
+
secrets: list[SecretValue] = Field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PolicyRule(BaseModel):
|
|
24
|
+
"""A policy rule for access control."""
|
|
25
|
+
id: str
|
|
26
|
+
effect: str # "allow" or "deny"
|
|
27
|
+
actions: list[str] = Field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SessionConfig(BaseModel):
|
|
31
|
+
"""Configuration returned from SDK init."""
|
|
32
|
+
project_id: str
|
|
33
|
+
agent_id: str
|
|
34
|
+
session_token: str
|
|
35
|
+
tools: list[ToolConfig] = Field(default_factory=list)
|
|
36
|
+
policies: list[PolicyRule] = Field(default_factory=list)
|
|
37
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AuditLogEntry(BaseModel):
|
|
41
|
+
"""An audit log entry for tool calls."""
|
|
42
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
43
|
+
tool_name: str
|
|
44
|
+
method_name: str
|
|
45
|
+
latency_ms: int
|
|
46
|
+
status: str # "success", "denied", "error"
|
|
47
|
+
policy_decision: str = "allow"
|
|
48
|
+
policy_id: Optional[str] = None
|
|
49
|
+
error_type: Optional[str] = None
|
|
50
|
+
error_message: Optional[str] = None
|
|
51
|
+
tags: dict[str, str] = Field(default_factory=dict)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: control-zero
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Enterprise MCP governance SDK - secrets, policies, and observability
|
|
5
|
+
Project-URL: Homepage, https://controlzero.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.controlzero.dev
|
|
7
|
+
Author-email: Control Zero <hello@controlzero.dev>
|
|
8
|
+
License: Proprietary
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: governance,mcp,observability,policy,secrets
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: cryptography>=41.0.0
|
|
20
|
+
Requires-Dist: httpx>=0.25.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0.0
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: anthropic>=0.20.0; extra == 'all'
|
|
24
|
+
Requires-Dist: crewai>=0.30.0; extra == 'all'
|
|
25
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'all'
|
|
26
|
+
Requires-Dist: langchain>=0.1.0; extra == 'all'
|
|
27
|
+
Requires-Dist: langgraph>=0.0.20; extra == 'all'
|
|
28
|
+
Requires-Dist: mcp>=0.1.0; extra == 'all'
|
|
29
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
30
|
+
Provides-Extra: crewai
|
|
31
|
+
Requires-Dist: crewai>=0.30.0; extra == 'crewai'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: anthropic>=0.20.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: openai>=1.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: respx>=0.20.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
42
|
+
Provides-Extra: langchain
|
|
43
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
44
|
+
Requires-Dist: langchain>=0.1.0; extra == 'langchain'
|
|
45
|
+
Requires-Dist: langgraph>=0.0.20; extra == 'langchain'
|
|
46
|
+
Provides-Extra: mcp
|
|
47
|
+
Requires-Dist: mcp>=0.1.0; extra == 'mcp'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# Control Zero Python SDK
|
|
51
|
+
|
|
52
|
+
Enterprise MCP governance - secrets, policies, and observability for AI agents.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install control-zero
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from control_zero import ControlZeroClient
|
|
64
|
+
|
|
65
|
+
# Initialize with your API key
|
|
66
|
+
client = ControlZeroClient(api_key="cz_live_xxx")
|
|
67
|
+
|
|
68
|
+
# This fetches config, secrets, and policies from Control Zero
|
|
69
|
+
client.initialize()
|
|
70
|
+
|
|
71
|
+
# Call MCP tools - secrets are injected automatically!
|
|
72
|
+
result = client.call_tool("github", "list_issues", {"repo": "acme/app"})
|
|
73
|
+
|
|
74
|
+
# Close when done (flushes logs)
|
|
75
|
+
client.close()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
### Automatic Secret Injection
|
|
81
|
+
|
|
82
|
+
Secrets configured in the Control Zero dashboard are automatically:
|
|
83
|
+
1. Fetched encrypted from the server
|
|
84
|
+
2. Decrypted in memory
|
|
85
|
+
3. Injected into your MCP calls
|
|
86
|
+
4. Wiped from memory on session end
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# No need to manage secrets in your code!
|
|
90
|
+
# GitHub PAT is automatically injected
|
|
91
|
+
result = client.call_tool("github", "create_issue", {
|
|
92
|
+
"repo": "acme/app",
|
|
93
|
+
"title": "Bug report",
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Policy Enforcement
|
|
98
|
+
|
|
99
|
+
Policies defined in the dashboard are enforced locally:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from control_zero import PolicyDeniedError
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
result = client.call_tool("stripe", "create_charge", {...})
|
|
106
|
+
except PolicyDeniedError as e:
|
|
107
|
+
print(f"Denied: {e.decision.reason}")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Audit Logging
|
|
111
|
+
|
|
112
|
+
All tool calls are automatically logged for compliance:
|
|
113
|
+
|
|
114
|
+
- Timestamp, tool, method
|
|
115
|
+
- Latency and status
|
|
116
|
+
- Policy decisions
|
|
117
|
+
- Errors (without sensitive data)
|
|
118
|
+
|
|
119
|
+
Logs are batched and sent asynchronously to avoid impacting performance.
|
|
120
|
+
|
|
121
|
+
## Async Usage
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from control_zero import AsyncControlZeroClient
|
|
125
|
+
|
|
126
|
+
async with AsyncControlZeroClient(api_key="cz_live_xxx") as client:
|
|
127
|
+
result = await client.call_tool("github", "list_issues", {"repo": "acme/app"})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## MCP Client Wrapping
|
|
131
|
+
|
|
132
|
+
Wrap an existing MCP client:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from mcp import Client
|
|
136
|
+
from control_zero import ControlZeroClient, MCPMiddleware
|
|
137
|
+
|
|
138
|
+
# Initialize Control Zero
|
|
139
|
+
cz = ControlZeroClient(api_key="cz_live_xxx")
|
|
140
|
+
cz.initialize()
|
|
141
|
+
|
|
142
|
+
# Wrap your MCP client
|
|
143
|
+
mcp = Client()
|
|
144
|
+
wrapped = MCPMiddleware(cz).wrap(mcp)
|
|
145
|
+
|
|
146
|
+
# Use wrapped client - all calls go through Control Zero
|
|
147
|
+
result = await wrapped.call_tool("github", "list_issues")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
client = ControlZeroClient(
|
|
154
|
+
api_key="cz_live_xxx", # Required
|
|
155
|
+
agent_name="marketing-bot", # Optional, for logging
|
|
156
|
+
base_url="https://api.controlzero.dev", # Or self-hosted
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## API Key Types
|
|
161
|
+
|
|
162
|
+
- `cz_live_*` - Production keys (real secrets, real logging)
|
|
163
|
+
- `cz_test_*` - Test keys (mock responses, test logging)
|
|
164
|
+
|
|
165
|
+
## Security
|
|
166
|
+
|
|
167
|
+
- Secrets are held in memory only, never written to disk
|
|
168
|
+
- Memory is securely wiped on session end
|
|
169
|
+
- All communication is encrypted (TLS)
|
|
170
|
+
- Policies are enforced locally for low latency
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
### Running Tests
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Install dev dependencies
|
|
178
|
+
pip install -e ".[dev]"
|
|
179
|
+
|
|
180
|
+
# Run all tests
|
|
181
|
+
python -m pytest tests/ -v
|
|
182
|
+
|
|
183
|
+
# Run with coverage
|
|
184
|
+
python -m pytest tests/ --cov=control_zero --cov-report=term-missing
|
|
185
|
+
|
|
186
|
+
# Run specific test files
|
|
187
|
+
python -m pytest tests/test_types.py -v
|
|
188
|
+
python -m pytest tests/test_policy.py -v
|
|
189
|
+
python -m pytest tests/test_secrets.py -v
|
|
190
|
+
python -m pytest tests/test_client.py -v
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Code Quality
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Format and lint
|
|
197
|
+
ruff check --fix .
|
|
198
|
+
ruff format .
|
|
199
|
+
|
|
200
|
+
# Type checking
|
|
201
|
+
mypy control_zero/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Examples
|
|
205
|
+
|
|
206
|
+
See the [examples/](./examples/) directory for complete usage examples:
|
|
207
|
+
|
|
208
|
+
- **slack_bot.py** - Secure Slack messaging with channel restrictions
|
|
209
|
+
- **database_agent.py** - PostgreSQL with read-only policies
|
|
210
|
+
- **analytics_agent.py** - ClickHouse queries with governance
|
|
211
|
+
- **github_agent.py** - GitHub operations with branch protection
|
|
212
|
+
- **multi_tool_agent.py** - Complete multi-tool workflow
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
Proprietary - Control Zero, Inc.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
control_zero/__init__.py,sha256=DMNACbWO97maJHYzHxTtsv5mzVNGCyOfU5_Of4t_Tnw,902
|
|
2
|
+
control_zero/client.py,sha256=xC7lyEqx5WsaL7lOUwO2cHSQOsiV_HIXj8EbMgzpo7Q,20503
|
|
3
|
+
control_zero/types.py,sha256=osJfsVNoNSTM9h1gnYBCSDAmdC_vTrD2yTFcZGS6zVU,1505
|
|
4
|
+
control_zero/integrations/crewai/__init__.py,sha256=olaRH8myZpZ8-qruWIiDNs6HrLDO1C6Z2LYjJViJzsQ,1244
|
|
5
|
+
control_zero/integrations/crewai/agent.py,sha256=EWY3sq4aG9HHbFJREkWDLn1f-GJZuv5gaeNJHYUVCYg,7772
|
|
6
|
+
control_zero/integrations/crewai/crew.py,sha256=NgMksCotezh-S6mcXVJhj3JD8zIc4tVqHbEiasjv-II,11241
|
|
7
|
+
control_zero/integrations/crewai/task.py,sha256=T61O67VwNyl88kTomn5tcs9W1QCAq8K0WS666tRFUT0,8542
|
|
8
|
+
control_zero/integrations/crewai/tool.py,sha256=VgVVIrFykoDhkZ1pU7xQU-qugAE89yIxZQ57nj9BJpg,8803
|
|
9
|
+
control_zero/integrations/langchain/__init__.py,sha256=Hz8wOmjPdU4sTCQYM7QBQz1i-0KM5m24S-kDUi0DjFc,1555
|
|
10
|
+
control_zero/integrations/langchain/agent.py,sha256=hI7x8K65qyUhTbE-lxJtOh0WqI8FYWlXwydp2yU9_UM,9460
|
|
11
|
+
control_zero/integrations/langchain/callbacks.py,sha256=3cQvX_7KQL_34eMWpIpqBbFff71FP0ihH_Ygf4chLTQ,12645
|
|
12
|
+
control_zero/integrations/langchain/chain.py,sha256=5JWy08JtMTUuqIGiNlSrCt4igBFBNshnjqjVdlTWS14,9747
|
|
13
|
+
control_zero/integrations/langchain/graph.py,sha256=iU5dh4VCTLbF0Oqg1hjZ7cAJtC9IwygEa7z6JAjegJ4,13618
|
|
14
|
+
control_zero/integrations/langchain/tool.py,sha256=Y20MpZNIVWNTNMa7asYwicbSfVyPOyMXp54YSJwSiRw,7633
|
|
15
|
+
control_zero/llm/__init__.py,sha256=_NgSQ6K54umBjL1_i--E6989ZVOAiDc-4xs9qW2JPQc,1965
|
|
16
|
+
control_zero/llm/base.py,sha256=CHHdrVcE32yeybrcVoPzlcs9YEUz6H7nzG5vTblruec,19675
|
|
17
|
+
control_zero/llm/anthropic/__init__.py,sha256=yzqsfqmnuXMxGpXbzOQiyIV5-WR2PXD7JqhdPyxXIeo,1038
|
|
18
|
+
control_zero/llm/anthropic/client.py,sha256=d4yoUI2OMRSSpekxfgNnDFy8Xk4Cq_m5N04UKKlPVFI,4156
|
|
19
|
+
control_zero/llm/anthropic/messages.py,sha256=V_3F0MdVo1uXFkNDhmQx8FuLzND421HMyqpxozCxfEE,13956
|
|
20
|
+
control_zero/llm/cohere/__init__.py,sha256=rcQUnPc3tFPLsnNm3PVbvKlY4IsaEU4yOsL_t2UJMso,881
|
|
21
|
+
control_zero/llm/cohere/client.py,sha256=ECHMc754BOsIr_dUVf7FDk6lZZi4bJjINATNEjqMvG4,12822
|
|
22
|
+
control_zero/llm/gemini/__init__.py,sha256=y8FUmrFmrdwwROcQjXuXQlXJBww2Rk6VAmEx_9U7x78,887
|
|
23
|
+
control_zero/llm/gemini/client.py,sha256=-9ymNeZz_GzmtXEqhW9r_7NPHgvUBUrQ0z-EzT2NkhY,17337
|
|
24
|
+
control_zero/llm/groq/__init__.py,sha256=pHj2jUEvS4M4HYo0h6I5imw4ctVDbT44Rb8RU0Etk8Q,865
|
|
25
|
+
control_zero/llm/groq/client.py,sha256=G4qgU2AwfnmC6IJiCOQEH9VrVGMmMy1QsYMlMn3nNYM,11195
|
|
26
|
+
control_zero/llm/mistral/__init__.py,sha256=BhIqAkIQ_LU3F-rDZEXd177dyjIotkiid531ETc8bSQ,919
|
|
27
|
+
control_zero/llm/mistral/client.py,sha256=LwHLA6pC1QaAq8QhVsLGZPP3gKg-f9MDQtVzQwZAamA,10768
|
|
28
|
+
control_zero/llm/ollama/__init__.py,sha256=GLPD8tgqsSku4XtvmRlCce2aKcPLdgtuaN0AaTON5P0,797
|
|
29
|
+
control_zero/llm/ollama/client.py,sha256=M2cwmLvjD_MupAArjhmBFuNv799ea8UJSk2sQp51W6s,13671
|
|
30
|
+
control_zero/llm/openai/__init__.py,sha256=3W4htjS--GBi-fdQDOruuanbvzmVBMWmuReKCw_WZiQ,967
|
|
31
|
+
control_zero/llm/openai/chat.py,sha256=uBQVimVFd8Dd5uYz--uwjZ_2Hhw4hRpHInyHvAvSmrM,12576
|
|
32
|
+
control_zero/llm/openai/client.py,sha256=yHYirEivmAqF1NIgEYF_9fcNJUCzs_pcGVTnr4sOPdE,5274
|
|
33
|
+
control_zero/logging/__init__.py,sha256=GtbB9WBhHB-av7OcSrvqaXl-8KYZLKEXpVjKEJzbyDM,128
|
|
34
|
+
control_zero/logging/async_logger.py,sha256=A2BvsD3bEnJJ6_8UxiIKMlBY8eTzTz9H_lRKR-RSFWU,1977
|
|
35
|
+
control_zero/mcp/__init__.py,sha256=mUFuysBHmVe4rtRwrPGcfQJLnZggmRXOn3a2alwFck4,127
|
|
36
|
+
control_zero/mcp/middleware.py,sha256=6S5paoWjODOkYO-YpLMQ9ThoB5S_3Nf6ASoSN4KjWJw,4268
|
|
37
|
+
control_zero/policy/__init__.py,sha256=Ex0TGXRBRcO-Tv4r5IRQgpfw1vri8-SwigE2gwHXUUQ,202
|
|
38
|
+
control_zero/policy/enforcer.py,sha256=iedLkTtHEHIb8SY5b-Ii71R6plHLv8OYnl1wMEy6a1M,2766
|
|
39
|
+
control_zero/secrets/__init__.py,sha256=esYdDAuk7j7wMIbPsFq36JpzMtYNq5aYXnu3-FQUBoo,132
|
|
40
|
+
control_zero/secrets/manager.py,sha256=Kby3x1SBqG_5JLY4eyFpE2tiVQ-9UyuF58MfKkTTIcU,2597
|
|
41
|
+
control_zero-0.2.0.dist-info/METADATA,sha256=Q_o4_LwMq4-nqOWelS5SjLM-kWDDchq5Xa11g7e5E60,5793
|
|
42
|
+
control_zero-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
43
|
+
control_zero-0.2.0.dist-info/licenses/LICENSE,sha256=I_jItrMUVjobSKqIUgcU8NFUA2ttg8-ylLkqwuVZXt8,741
|
|
44
|
+
control_zero-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2024 Control Zero
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|