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 +143 -0
- cortexhub/adapters/__init__.py +5 -0
- cortexhub/adapters/base.py +131 -0
- cortexhub/adapters/claude_agents.py +322 -0
- cortexhub/adapters/crewai.py +297 -0
- cortexhub/adapters/langgraph.py +386 -0
- cortexhub/adapters/openai_agents.py +192 -0
- cortexhub/audit/__init__.py +25 -0
- cortexhub/audit/events.py +165 -0
- cortexhub/auto_protect.py +128 -0
- cortexhub/backend/__init__.py +5 -0
- cortexhub/backend/client.py +348 -0
- cortexhub/client.py +2149 -0
- cortexhub/config.py +37 -0
- cortexhub/context/__init__.py +5 -0
- cortexhub/context/enricher.py +172 -0
- cortexhub/errors.py +123 -0
- cortexhub/frameworks.py +83 -0
- cortexhub/guardrails/__init__.py +3 -0
- cortexhub/guardrails/injection.py +180 -0
- cortexhub/guardrails/pii.py +378 -0
- cortexhub/guardrails/secrets.py +206 -0
- cortexhub/interceptors/__init__.py +3 -0
- cortexhub/interceptors/llm.py +62 -0
- cortexhub/interceptors/mcp.py +96 -0
- cortexhub/pipeline.py +92 -0
- cortexhub/policy/__init__.py +6 -0
- cortexhub/policy/effects.py +87 -0
- cortexhub/policy/evaluator.py +267 -0
- cortexhub/policy/loader.py +158 -0
- cortexhub/policy/models.py +123 -0
- cortexhub/policy/sync.py +183 -0
- cortexhub/telemetry/__init__.py +40 -0
- cortexhub/telemetry/otel.py +481 -0
- cortexhub/version.py +3 -0
- cortexhub-0.1.0.dist-info/METADATA +275 -0
- cortexhub-0.1.0.dist-info/RECORD +38 -0
- cortexhub-0.1.0.dist-info/WHEEL +4 -0
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,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 {}
|