cortexhub 0.1.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.
cortexhub/__init__.py ADDED
@@ -0,0 +1,143 @@
1
+ """CortexHub Python SDK - Runtime Governance for AI Agents.
2
+
3
+ Add 2 lines to your existing agent code to get full governance:
4
+
5
+ import cortexhub
6
+ cortex = cortexhub.init("my_agent", cortexhub.Framework.LANGGRAPH)
7
+
8
+ That's it! CortexHub will:
9
+ - Monitor all tool calls and LLM interactions
10
+ - Detect PII, secrets, and sensitive data in real-time
11
+ - Enforce policies (block, redact, require approval) based on your configuration
12
+
13
+ IMPORTANT: A valid API key is REQUIRED. Get yours at https://app.cortexhub.ai
14
+
15
+ Supported Agent Frameworks:
16
+ - LangGraph: Stateful agents with checkpointing and interrupt()
17
+ - CrewAI: Multi-agent crews with human_input support
18
+ - OpenAI Agents SDK: Native agents with needsApproval
19
+ - Claude Agent SDK: Computer-use agents with subagents
20
+
21
+ Installation:
22
+ # Core SDK (includes Cedar, OpenTelemetry, Presidio, detect-secrets)
23
+ pip install cortexhub
24
+
25
+ # With framework adapter
26
+ pip install cortexhub[langgraph] # + LangGraph
27
+ pip install cortexhub[crewai] # + CrewAI
28
+ pip install cortexhub[openai-agents] # + OpenAI Agents SDK
29
+ pip install cortexhub[claude-agents] # + Claude Agent SDK
30
+
31
+ Usage:
32
+ import cortexhub
33
+
34
+ # Set your API key (or use CORTEXHUB_API_KEY environment variable)
35
+ # For LangGraph
36
+ cortex = cortexhub.init("my_agent", cortexhub.Framework.LANGGRAPH)
37
+
38
+ # For CrewAI
39
+ cortex = cortexhub.init("my_crew", cortexhub.Framework.CREWAI)
40
+
41
+ # For OpenAI Agents
42
+ cortex = cortexhub.init("my_agent", cortexhub.Framework.OPENAI_AGENTS)
43
+
44
+ # For Claude Agents
45
+ cortex = cortexhub.init("my_agent", cortexhub.Framework.CLAUDE_AGENTS)
46
+ """
47
+
48
+ from cortexhub.client import CortexHub
49
+ from cortexhub.frameworks import Framework
50
+ from cortexhub.version import __version__
51
+ from cortexhub.errors import (
52
+ CortexHubError,
53
+ ConfigurationError,
54
+ PolicyViolationError,
55
+ GuardrailViolationError,
56
+ ApprovalRequiredError,
57
+ ApprovalDeniedError,
58
+ PolicyLoadError,
59
+ )
60
+
61
+ # Global instance for simple init() usage
62
+ _global_instance: CortexHub | None = None
63
+
64
+
65
+ def init(
66
+ agent_id: str,
67
+ framework: Framework,
68
+ *,
69
+ api_key: str | None = None,
70
+ privacy: bool = True,
71
+ ) -> CortexHub:
72
+ """Initialize CortexHub governance for your AI agent.
73
+
74
+ Call this ONCE at the start of your application, BEFORE importing
75
+ your AI framework. CortexHub will automatically govern all tool
76
+ calls and LLM interactions.
77
+
78
+ Args:
79
+ agent_id: A stable identifier for your agent. Use something
80
+ descriptive like "customer_support_agent" or
81
+ "financial_analysis_crew".
82
+ framework: The AI agent framework you're using:
83
+ - Framework.LANGGRAPH (LangGraph)
84
+ - Framework.CREWAI (CrewAI)
85
+ - Framework.OPENAI_AGENTS (OpenAI Agents SDK)
86
+ - Framework.CLAUDE_AGENTS (Claude Agent SDK)
87
+ api_key: Your CortexHub API key. Can also be set via the
88
+ CORTEXHUB_API_KEY environment variable.
89
+ privacy: If True (default), only metadata is sent to cloud.
90
+ If False, raw data is included for testing redaction.
91
+
92
+ Returns:
93
+ CortexHub instance
94
+
95
+ Example:
96
+ import cortexhub
97
+
98
+ # Initialize before importing your framework
99
+ cortex = cortexhub.init("customer_support", cortexhub.Framework.LANGGRAPH)
100
+
101
+ # Now import and use LangGraph as normal
102
+ from langgraph.prebuilt import create_react_agent
103
+
104
+ # All tool calls are now governed by CortexHub
105
+ """
106
+ global _global_instance
107
+
108
+ _global_instance = CortexHub(
109
+ api_key=api_key,
110
+ agent_id=agent_id,
111
+ privacy=privacy,
112
+ )
113
+
114
+ # Apply the framework-specific adapter
115
+ _global_instance.protect(framework)
116
+
117
+ return _global_instance
118
+
119
+
120
+ def get_instance() -> CortexHub | None:
121
+ """Get the global CortexHub instance.
122
+
123
+ Returns None if init() hasn't been called.
124
+ """
125
+ return _global_instance
126
+
127
+
128
+ __all__ = [
129
+ # Main API
130
+ "init",
131
+ "get_instance",
132
+ "CortexHub",
133
+ "Framework",
134
+ "__version__",
135
+ # Errors (for exception handling)
136
+ "CortexHubError",
137
+ "ConfigurationError",
138
+ "PolicyViolationError",
139
+ "GuardrailViolationError",
140
+ "ApprovalRequiredError",
141
+ "ApprovalDeniedError",
142
+ "PolicyLoadError",
143
+ ]
@@ -0,0 +1,5 @@
1
+ """Framework adapters for intercepting tool calls."""
2
+
3
+ from cortexhub.adapters.base import ToolAdapter
4
+
5
+ __all__ = ["ToolAdapter"]
@@ -0,0 +1,131 @@
1
+ """Base adapter interface for framework interception.
2
+
3
+ Critical: Adapters MUST NOT infer intent, classify tools, rewrite arguments, or suppress failures.
4
+ They MUST ONLY construct AuthorizationRequest and run the enforcement pipeline.
5
+
6
+ Architectural rules:
7
+ - Adapter is DUMB plumbing
8
+ - SDK orchestrates everything
9
+ - Adapter explicitly branches on Decision
10
+ - Async-safe with output guardrails
11
+ - No hidden behavior
12
+ """
13
+
14
+ from abc import ABC, abstractmethod
15
+ from typing import Any, Callable
16
+
17
+ import structlog
18
+
19
+ logger = structlog.get_logger(__name__)
20
+
21
+
22
+ class ToolAdapter(ABC):
23
+ """Base class for framework-specific tool adapters.
24
+
25
+ Adapters intercept tool calls at the framework level and enforce policies
26
+ before execution.
27
+
28
+ Architectural Rules:
29
+ - MUST NOT infer intent
30
+ - MUST NOT classify tools
31
+ - MUST NOT rewrite arguments
32
+ - MUST NOT suppress failures
33
+ - MUST construct AuthorizationRequest and run pipeline only
34
+ - MUST explicitly branch on Decision (ALLOW/DENY/ESCALATE)
35
+ - MUST support unpatch() for test isolation
36
+ """
37
+
38
+ def __init__(self, cortex_hub: Any): # Type: CortexHub, but avoiding circular import
39
+ """Initialize adapter.
40
+
41
+ Args:
42
+ cortex_hub: CortexHub instance for policy enforcement
43
+ """
44
+ self.cortex_hub = cortex_hub
45
+ logger.info(
46
+ "Adapter initialized", adapter=self.__class__.__name__, framework=self.framework_name
47
+ )
48
+
49
+ @property
50
+ @abstractmethod
51
+ def framework_name(self) -> str:
52
+ """Name of the framework this adapter supports."""
53
+ pass
54
+
55
+ @abstractmethod
56
+ def patch(self) -> None:
57
+ """Patch the framework to intercept tool calls.
58
+
59
+ This method should monkey-patch or wrap the framework's tool execution
60
+ mechanism to enforce governance before actual execution.
61
+
62
+ Pipeline that MUST be followed:
63
+ 1. Build AuthorizationRequest (don't flatten!)
64
+ 2. Log tool invocation (pending)
65
+ 3. Run input guardrails
66
+ 4. Evaluate policy -> get Decision
67
+ 5. EXPLICITLY branch on Decision
68
+ 6. Execute tool
69
+ 7. Run output guardrails (async-safe)
70
+ 8. Log execution result
71
+ """
72
+ pass
73
+
74
+ def unpatch(self) -> None:
75
+ """Restore original framework methods.
76
+
77
+ Useful for:
78
+ - Test isolation
79
+ - REPL usage
80
+ - Multiple SDK instances
81
+
82
+ Default implementation does nothing.
83
+ Subclasses should override if they store original methods.
84
+ """
85
+ logger.debug("unpatch() not implemented for this adapter")
86
+
87
+ @abstractmethod
88
+ def intercept(
89
+ self, tool_fn: Callable, tool_name: str, args: dict[str, Any], **kwargs: Any
90
+ ) -> Any:
91
+ """Intercept a tool call and enforce policies.
92
+
93
+ NOTE: Most adapters should use govern_execution() from pipeline.py
94
+ instead of implementing this directly.
95
+
96
+ Args:
97
+ tool_fn: The actual tool function to execute (if allowed)
98
+ tool_name: Name of the tool being invoked
99
+ args: Arguments passed to the tool
100
+ **kwargs: Additional metadata from the framework
101
+
102
+ Returns:
103
+ Result of tool execution (if allowed)
104
+
105
+ Raises:
106
+ PolicyViolationError: If policy denies execution
107
+ GuardrailViolationError: If guardrails block execution
108
+ ApprovalDeniedError: If approval is denied
109
+ """
110
+ pass
111
+
112
+ def is_framework_available(self) -> bool:
113
+ """Check if the framework is available (imported).
114
+
115
+ Returns:
116
+ True if the framework is available
117
+ """
118
+ import sys
119
+
120
+ # Check if framework module is in sys.modules
121
+ framework_modules = self._get_framework_modules()
122
+ return any(mod in sys.modules for mod in framework_modules)
123
+
124
+ @abstractmethod
125
+ def _get_framework_modules(self) -> list[str]:
126
+ """Get list of module names that indicate framework presence.
127
+
128
+ Returns:
129
+ List of module names to check in sys.modules
130
+ """
131
+ pass
@@ -0,0 +1,322 @@
1
+ """Claude Agent SDK adapter for tool interception.
2
+
3
+ The Claude Agent SDK provides agentic capabilities including:
4
+ - Computer use (bash, files, code)
5
+ - Custom MCP tools via @tool decorator
6
+ - Subagents for parallelization
7
+ - Hooks for pre/post tool execution
8
+
9
+ We integrate by:
10
+ 1. Wrapping the @tool decorator to govern custom tools
11
+ 2. Using hooks (PreToolUse, PostToolUse) to govern built-in tools
12
+
13
+ Reference: https://docs.anthropic.com/en/docs/agent-sdk/python
14
+
15
+ Architectural rules:
16
+ - Adapter is DUMB plumbing
17
+ - Adapter calls ONE SDK entrypoint: govern_execution()
18
+ - SDK orchestrates everything
19
+ - No governance logic in adapter
20
+ """
21
+
22
+ import os
23
+ from functools import wraps
24
+ from typing import TYPE_CHECKING, Any, Callable, Awaitable
25
+
26
+ import structlog
27
+
28
+ from cortexhub.adapters.base import ToolAdapter
29
+ from cortexhub.pipeline import govern_execution
30
+
31
+ if TYPE_CHECKING:
32
+ from cortexhub.client import CortexHub
33
+
34
+ logger = structlog.get_logger(__name__)
35
+
36
+ # Attribute names for storing originals
37
+ _ORIGINAL_TOOL_ATTR = "__cortexhub_original_tool__"
38
+ _PATCHED_ATTR = "__cortexhub_patched__"
39
+
40
+
41
+ class ClaudeAgentsAdapter(ToolAdapter):
42
+ """Adapter for Claude Agent SDK.
43
+
44
+ Integrates CortexHub governance via two mechanisms:
45
+
46
+ 1. @tool decorator wrapping:
47
+ Custom MCP tools created with @tool are wrapped to run
48
+ governance pipeline before/after execution.
49
+
50
+ 2. Hooks integration:
51
+ PreToolUse and PostToolUse hooks intercept built-in tools
52
+ like Bash, Read, Write, Edit, etc.
53
+
54
+ For approval workflows:
55
+ - PreToolUse hook can block tool execution
56
+ - Custom tools can raise ApprovalRequiredError
57
+
58
+ Key properties:
59
+ - Adapter is dumb plumbing
60
+ - Calls SDK entrypoint, doesn't implement governance
61
+ """
62
+
63
+ @property
64
+ def framework_name(self) -> str:
65
+ return "claude_agents"
66
+
67
+ def _get_framework_modules(self) -> list[str]:
68
+ return ["claude_agent_sdk"]
69
+
70
+ def patch(self) -> None:
71
+ """Patch Claude Agent SDK tool creation.
72
+
73
+ Wraps the @tool decorator to intercept custom tool creation
74
+ and add CortexHub governance.
75
+ """
76
+ try:
77
+ import claude_agent_sdk
78
+ from claude_agent_sdk import tool as original_tool_decorator
79
+
80
+ # Check if already patched
81
+ if getattr(claude_agent_sdk, _PATCHED_ATTR, False):
82
+ logger.info("Claude Agent SDK already patched")
83
+ return
84
+
85
+ cortex_hub = self.cortex_hub
86
+ tools = self._discover_tools()
87
+ if tools:
88
+ cortex_hub.backend.register_tool_inventory(
89
+ agent_id=cortex_hub.agent_id,
90
+ framework=self.framework_name,
91
+ tools=tools,
92
+ )
93
+
94
+ # Store original decorator
95
+ if not hasattr(claude_agent_sdk, _ORIGINAL_TOOL_ATTR):
96
+ setattr(claude_agent_sdk, _ORIGINAL_TOOL_ATTR, original_tool_decorator)
97
+
98
+ original_tool = getattr(claude_agent_sdk, _ORIGINAL_TOOL_ATTR)
99
+
100
+ def patched_tool(
101
+ name: str,
102
+ description: str,
103
+ input_schema: type | dict[str, Any],
104
+ ) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], Any]:
105
+ """Wrapped @tool decorator that adds CortexHub governance."""
106
+
107
+ def decorator(handler: Callable[[Any], Awaitable[dict[str, Any]]]) -> Any:
108
+ # Create the original tool
109
+ original_decorated = original_tool(name, description, input_schema)(handler)
110
+
111
+ # Wrap the handler with governance
112
+ original_handler = original_decorated.handler
113
+ tool_name = original_decorated.name
114
+ tool_description = original_decorated.description
115
+
116
+ @wraps(original_handler)
117
+ async def governed_handler(args: dict[str, Any]) -> dict[str, Any]:
118
+ """Governed tool execution."""
119
+ tool_metadata = {
120
+ "name": tool_name,
121
+ "description": tool_description,
122
+ "framework": "claude_agents",
123
+ }
124
+
125
+ governed_fn = govern_execution(
126
+ tool_fn=lambda **kw: original_handler(args),
127
+ tool_metadata=tool_metadata,
128
+ cortex_hub=cortex_hub,
129
+ )
130
+
131
+ # Execute with governance (async)
132
+ result = governed_fn(**args)
133
+ if hasattr(result, '__await__'):
134
+ result = await result
135
+ return result
136
+
137
+ # Replace the handler
138
+ original_decorated.handler = governed_handler
139
+ return original_decorated
140
+
141
+ return decorator
142
+
143
+ # Apply patch
144
+ claude_agent_sdk.tool = patched_tool
145
+ setattr(claude_agent_sdk, _PATCHED_ATTR, True)
146
+
147
+ logger.info("Claude Agent SDK @tool decorator patched successfully")
148
+
149
+ except ImportError:
150
+ logger.debug("Claude Agent SDK not installed, skipping adapter")
151
+ except Exception as e:
152
+ logger.error("Failed to patch Claude Agent SDK", error=str(e))
153
+
154
+ def unpatch(self) -> None:
155
+ """Restore original @tool decorator."""
156
+ try:
157
+ import claude_agent_sdk
158
+
159
+ if hasattr(claude_agent_sdk, _ORIGINAL_TOOL_ATTR):
160
+ original = getattr(claude_agent_sdk, _ORIGINAL_TOOL_ATTR)
161
+ claude_agent_sdk.tool = original
162
+ setattr(claude_agent_sdk, _PATCHED_ATTR, False)
163
+ logger.info("Claude Agent SDK adapter unpatched")
164
+ except ImportError:
165
+ pass
166
+
167
+ def intercept(self, tool_fn, tool_name, args, **kwargs):
168
+ """Not used - governance happens via wrapped decorator."""
169
+ raise NotImplementedError("Use govern_execution via wrapped decorator")
170
+
171
+ def _discover_tools(self) -> list[dict[str, Any]]:
172
+ """Discover tools from Claude Agent SDK (best-effort)."""
173
+ return []
174
+
175
+ def create_governance_hooks(self) -> dict[str, list]:
176
+ """Create CortexHub governance hooks for Claude Agent SDK.
177
+
178
+ These hooks can be passed to ClaudeAgentOptions to govern
179
+ built-in tools like Bash, Read, Write, Edit, etc.
180
+
181
+ Returns:
182
+ Dict of hook configurations for PreToolUse and PostToolUse
183
+
184
+ Example:
185
+ adapter = ClaudeAgentsAdapter(cortex_hub)
186
+ hooks = adapter.create_governance_hooks()
187
+
188
+ options = ClaudeAgentOptions(
189
+ hooks=hooks,
190
+ allowed_tools=["Bash", "Read", "Write"],
191
+ )
192
+ """
193
+ cortex_hub = self.cortex_hub
194
+
195
+ async def pre_tool_governance(
196
+ input_data: dict[str, Any],
197
+ tool_use_id: str | None,
198
+ context: Any,
199
+ ) -> dict[str, Any]:
200
+ """Pre-tool governance hook.
201
+
202
+ Evaluates policy BEFORE tool execution.
203
+ Can block the tool by returning decision="block".
204
+ """
205
+ tool_name = input_data.get("tool_name", "unknown")
206
+ tool_input = input_data.get("tool_input", {})
207
+
208
+ tool_metadata = {
209
+ "name": tool_name,
210
+ "description": f"Claude Agent SDK built-in tool: {tool_name}",
211
+ "framework": "claude_agents",
212
+ }
213
+
214
+ # Build authorization request and evaluate
215
+ from cortexhub.policy.models import (
216
+ Action,
217
+ Principal,
218
+ Resource as PolicyResource,
219
+ )
220
+
221
+ request = cortex_hub.build_request(
222
+ principal=Principal(type="Agent", id=cortex_hub.agent_id),
223
+ action=Action(type="tool.invoke", name=tool_name),
224
+ resource=PolicyResource(type="Tool", id=tool_name),
225
+ args=tool_input,
226
+ framework="claude_agents",
227
+ )
228
+
229
+ # Evaluate policy if in enforcement mode
230
+ if cortex_hub.enforce and cortex_hub.evaluator:
231
+ from cortexhub.policy.effects import Effect
232
+ decision = cortex_hub.evaluator.evaluate(request)
233
+
234
+ if decision.effect == Effect.DENY:
235
+ return {
236
+ "hookSpecificOutput": {
237
+ "hookEventName": "PreToolUse",
238
+ "permissionDecision": "deny",
239
+ "permissionDecisionReason": decision.reasoning,
240
+ }
241
+ }
242
+
243
+ if decision.effect == Effect.ESCALATE:
244
+ try:
245
+ context_hash = cortex_hub._compute_context_hash(tool_name, tool_input)
246
+ approval_response = cortex_hub.backend.create_approval(
247
+ run_id=cortex_hub.session_id,
248
+ trace_id=cortex_hub._get_current_trace_id(),
249
+ tool_name=tool_name,
250
+ tool_args_values=tool_input if not cortex_hub.privacy else None,
251
+ context_hash=context_hash,
252
+ policy_id=decision.policy_id or "",
253
+ policy_name=decision.policy_name or "Unknown Policy",
254
+ policy_explanation=decision.reasoning,
255
+ risk_category=None,
256
+ agent_id=cortex_hub.agent_id,
257
+ framework="claude_agents",
258
+ environment=os.getenv("CORTEXHUB_ENVIRONMENT"),
259
+ )
260
+ approval_id = approval_response.get("approval_id", "unknown")
261
+ except Exception as e:
262
+ logger.error("Failed to create approval", error=str(e))
263
+ return {
264
+ "hookSpecificOutput": {
265
+ "hookEventName": "PreToolUse",
266
+ "permissionDecision": "deny",
267
+ "permissionDecisionReason": (
268
+ f"Tool '{tool_name}' requires approval but failed to create approval record: {e}"
269
+ ),
270
+ }
271
+ }
272
+
273
+ return {
274
+ "hookSpecificOutput": {
275
+ "hookEventName": "PreToolUse",
276
+ "permissionDecision": "ask",
277
+ "permissionDecisionReason": (
278
+ f"Approval required: {decision.reasoning}\n\nApproval ID: {approval_id}"
279
+ ),
280
+ }
281
+ }
282
+
283
+ # Allow execution
284
+ return {}
285
+
286
+ async def post_tool_governance(
287
+ input_data: dict[str, Any],
288
+ tool_use_id: str | None,
289
+ context: Any,
290
+ ) -> dict[str, Any]:
291
+ """Post-tool governance hook.
292
+
293
+ Logs tool execution results for audit.
294
+ """
295
+ tool_name = input_data.get("tool_name", "unknown")
296
+ tool_response = input_data.get("tool_response", {})
297
+
298
+ # Log the tool execution
299
+ logger.debug(
300
+ "Tool executed",
301
+ tool=tool_name,
302
+ framework="claude_agents",
303
+ )
304
+
305
+ return {}
306
+
307
+ # Return hook configuration
308
+ # Note: HookMatcher is from claude_agent_sdk
309
+ try:
310
+ from claude_agent_sdk import HookMatcher
311
+
312
+ return {
313
+ "PreToolUse": [
314
+ HookMatcher(hooks=[pre_tool_governance])
315
+ ],
316
+ "PostToolUse": [
317
+ HookMatcher(hooks=[post_tool_governance])
318
+ ],
319
+ }
320
+ except ImportError:
321
+ logger.warning("Could not create hooks - claude_agent_sdk not installed")
322
+ return {}