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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) server interceptor.
|
|
2
|
+
|
|
3
|
+
Intercepts calls to MCP servers and enforces policies before execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
logger = structlog.get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MCPInterceptor:
|
|
14
|
+
"""Intercepts and governs MCP server calls.
|
|
15
|
+
|
|
16
|
+
MCP provides:
|
|
17
|
+
- Tool/resource discovery
|
|
18
|
+
- Prompts and sampling
|
|
19
|
+
- Bidirectional communication with context providers
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, cortex_hub: Any): # Type: CortexHub
|
|
23
|
+
"""Initialize MCP interceptor.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
cortex_hub: CortexHub instance for policy enforcement
|
|
27
|
+
"""
|
|
28
|
+
self.cortex_hub = cortex_hub
|
|
29
|
+
logger.info("MCP interceptor initialized")
|
|
30
|
+
|
|
31
|
+
def intercept_mcp_client(self) -> None:
|
|
32
|
+
"""Intercept MCP client calls."""
|
|
33
|
+
try:
|
|
34
|
+
# MCP client library (if available)
|
|
35
|
+
from mcp import Client
|
|
36
|
+
|
|
37
|
+
if hasattr(Client, "_original_call_tool"):
|
|
38
|
+
logger.info("MCP client already intercepted")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
original_call_tool = Client.call_tool
|
|
42
|
+
|
|
43
|
+
async def governed_call_tool(self, server_name: str, tool_name: str, arguments: dict):
|
|
44
|
+
from cortexhub.policy.models import AuthorizationRequest
|
|
45
|
+
|
|
46
|
+
request = AuthorizationRequest.create(
|
|
47
|
+
principal_id=self.cortex_hub.session_id,
|
|
48
|
+
action_name=f"mcp.{tool_name}",
|
|
49
|
+
resource_id=f"{server_name}.{tool_name}",
|
|
50
|
+
args=arguments,
|
|
51
|
+
framework="mcp",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self.cortex_hub.enforce(request)
|
|
55
|
+
return await original_call_tool(self, server_name, tool_name, arguments)
|
|
56
|
+
|
|
57
|
+
Client.call_tool = governed_call_tool
|
|
58
|
+
Client._original_call_tool = original_call_tool
|
|
59
|
+
|
|
60
|
+
logger.info("MCP client interceptor applied")
|
|
61
|
+
|
|
62
|
+
except ImportError:
|
|
63
|
+
logger.debug("MCP client not available, skipping interception")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error("Failed to intercept MCP client", error=str(e))
|
|
66
|
+
|
|
67
|
+
def apply_all(self) -> None:
|
|
68
|
+
"""Apply all MCP interceptors."""
|
|
69
|
+
self.intercept_mcp_client()
|
|
70
|
+
logger.info("All MCP interceptors applied")
|
|
71
|
+
|
|
72
|
+
async def discover_mcp_tools(self, mcp_client) -> list[dict]:
|
|
73
|
+
"""Discover tools from MCP server via list_tools()."""
|
|
74
|
+
|
|
75
|
+
tools: list[dict] = []
|
|
76
|
+
try:
|
|
77
|
+
result = await mcp_client.list_tools()
|
|
78
|
+
for tool in result.tools:
|
|
79
|
+
tools.append(
|
|
80
|
+
{
|
|
81
|
+
"name": tool.name,
|
|
82
|
+
"description": tool.description,
|
|
83
|
+
"parameters_schema": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": tool.inputSchema.get("properties", {}),
|
|
86
|
+
"required": tool.inputSchema.get("required", []),
|
|
87
|
+
}
|
|
88
|
+
if tool.inputSchema
|
|
89
|
+
else None,
|
|
90
|
+
"source": "mcp",
|
|
91
|
+
"mcp_server_name": getattr(mcp_client, "server_name", None),
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning("Failed to discover MCP tools", error=str(e))
|
|
96
|
+
return tools
|
cortexhub/pipeline.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Unified governance pipeline for adapters that can't use SDK entrypoint directly.
|
|
2
|
+
|
|
3
|
+
This is a thin wrapper around cortex_hub.execute_governed_tool() for adapters
|
|
4
|
+
that need to create governed wrappers (e.g., CrewAI, LlamaIndex).
|
|
5
|
+
|
|
6
|
+
Architectural rules:
|
|
7
|
+
- This is a convenience wrapper, not a separate pipeline
|
|
8
|
+
- All governance logic lives in CortexHub client
|
|
9
|
+
- Adapters should prefer calling SDK entrypoint directly when possible
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_async_callable(fn: Callable) -> bool:
|
|
21
|
+
"""Check if a callable is async."""
|
|
22
|
+
return asyncio.iscoroutinefunction(fn)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def govern_execution(
|
|
26
|
+
tool_fn: Callable,
|
|
27
|
+
tool_metadata: dict[str, Any],
|
|
28
|
+
cortex_hub: Any, # Type: CortexHub
|
|
29
|
+
) -> Callable:
|
|
30
|
+
"""Create governed wrapper for sync or async execution.
|
|
31
|
+
|
|
32
|
+
This wraps the SDK's execute_governed_tool() for adapters that need
|
|
33
|
+
to create function wrappers.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
tool_fn: The actual function to execute
|
|
37
|
+
tool_metadata: Tool information (name, framework, description)
|
|
38
|
+
cortex_hub: CortexHub instance
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Wrapped function (sync or async based on tool_fn)
|
|
42
|
+
"""
|
|
43
|
+
execution_kind = tool_metadata.get("kind", "tool")
|
|
44
|
+
tool_name = tool_metadata.get("name", "unknown")
|
|
45
|
+
tool_description = tool_metadata.get("description")
|
|
46
|
+
framework = tool_metadata.get("framework", "unknown")
|
|
47
|
+
model = tool_metadata.get("model", "unknown")
|
|
48
|
+
prompt = tool_metadata.get("prompt")
|
|
49
|
+
call_original = tool_metadata.get("call_original")
|
|
50
|
+
if call_original is None:
|
|
51
|
+
call_original = tool_fn
|
|
52
|
+
|
|
53
|
+
if execution_kind == "llm":
|
|
54
|
+
if is_async_callable(call_original):
|
|
55
|
+
async def async_wrapper(*args, **kwargs):
|
|
56
|
+
return await cortex_hub.execute_governed_llm_call_async(
|
|
57
|
+
model=model,
|
|
58
|
+
prompt=prompt,
|
|
59
|
+
framework=framework,
|
|
60
|
+
call_original=call_original,
|
|
61
|
+
)
|
|
62
|
+
return async_wrapper
|
|
63
|
+
else:
|
|
64
|
+
def sync_wrapper(*args, **kwargs):
|
|
65
|
+
return cortex_hub.execute_governed_llm_call(
|
|
66
|
+
model=model,
|
|
67
|
+
prompt=prompt,
|
|
68
|
+
framework=framework,
|
|
69
|
+
call_original=call_original,
|
|
70
|
+
)
|
|
71
|
+
return sync_wrapper
|
|
72
|
+
|
|
73
|
+
if is_async_callable(tool_fn):
|
|
74
|
+
async def async_wrapper(*args, **kwargs):
|
|
75
|
+
return await cortex_hub.execute_governed_tool_async(
|
|
76
|
+
tool_name=tool_name,
|
|
77
|
+
tool_description=tool_description,
|
|
78
|
+
args=kwargs, # Use kwargs for structured arguments
|
|
79
|
+
framework=framework,
|
|
80
|
+
call_original=lambda: tool_fn(*args, **kwargs),
|
|
81
|
+
)
|
|
82
|
+
return async_wrapper
|
|
83
|
+
else:
|
|
84
|
+
def sync_wrapper(*args, **kwargs):
|
|
85
|
+
return cortex_hub.execute_governed_tool(
|
|
86
|
+
tool_name=tool_name,
|
|
87
|
+
tool_description=tool_description,
|
|
88
|
+
args=kwargs, # Use kwargs for structured arguments
|
|
89
|
+
framework=framework,
|
|
90
|
+
call_original=lambda: tool_fn(*args, **kwargs),
|
|
91
|
+
)
|
|
92
|
+
return sync_wrapper
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Decision effects from policy evaluation.
|
|
2
|
+
|
|
3
|
+
Critical: Determinism guarantee - Same AuthorizationRequest MUST always produce
|
|
4
|
+
same Decision. This is essential for testing and auditability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Effect(str, Enum):
|
|
13
|
+
"""Policy decision effect."""
|
|
14
|
+
|
|
15
|
+
ALLOW = "allow"
|
|
16
|
+
DENY = "deny"
|
|
17
|
+
ESCALATE = "escalate"
|
|
18
|
+
REDACT = "redact" # PII/secrets were redacted before LLM call
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Decision(BaseModel):
|
|
22
|
+
"""Result of policy evaluation.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
effect: The decision (ALLOW/DENY/ESCALATE)
|
|
26
|
+
reasoning: Human-readable explanation
|
|
27
|
+
policy_id: Which policy triggered this decision (if any)
|
|
28
|
+
policy_name: Friendly policy name (if available)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
effect: Effect
|
|
32
|
+
reasoning: str
|
|
33
|
+
policy_id: str | None = None
|
|
34
|
+
policy_name: str | None = None
|
|
35
|
+
|
|
36
|
+
def is_allowed(self) -> bool:
|
|
37
|
+
"""Check if the decision allows execution."""
|
|
38
|
+
return self.effect == Effect.ALLOW
|
|
39
|
+
|
|
40
|
+
def is_denied(self) -> bool:
|
|
41
|
+
"""Check if the decision denies execution."""
|
|
42
|
+
return self.effect == Effect.DENY
|
|
43
|
+
|
|
44
|
+
def requires_approval(self) -> bool:
|
|
45
|
+
"""Check if the decision requires approval (escalation)."""
|
|
46
|
+
return self.effect == Effect.ESCALATE
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def allow(
|
|
50
|
+
cls,
|
|
51
|
+
reasoning: str = "Allowed by policy",
|
|
52
|
+
policy_id: str | None = None,
|
|
53
|
+
policy_name: str | None = None,
|
|
54
|
+
):
|
|
55
|
+
"""Create an ALLOW decision."""
|
|
56
|
+
return cls(
|
|
57
|
+
effect=Effect.ALLOW, reasoning=reasoning, policy_id=policy_id, policy_name=policy_name
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def deny(cls, reasoning: str, policy_id: str | None = None, policy_name: str | None = None):
|
|
62
|
+
"""Create a DENY decision."""
|
|
63
|
+
return cls(
|
|
64
|
+
effect=Effect.DENY, reasoning=reasoning, policy_id=policy_id, policy_name=policy_name
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def escalate(
|
|
69
|
+
cls, reasoning: str, policy_id: str | None = None, policy_name: str | None = None
|
|
70
|
+
):
|
|
71
|
+
"""Create an ESCALATE decision."""
|
|
72
|
+
return cls(
|
|
73
|
+
effect=Effect.ESCALATE, reasoning=reasoning, policy_id=policy_id, policy_name=policy_name
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def redact(
|
|
78
|
+
cls, reasoning: str, policy_id: str | None = None, policy_name: str | None = None
|
|
79
|
+
):
|
|
80
|
+
"""Create a REDACT decision (PII/secrets were redacted before execution)."""
|
|
81
|
+
return cls(
|
|
82
|
+
effect=Effect.REDACT, reasoning=reasoning, policy_id=policy_id, policy_name=policy_name
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def is_redacted(self) -> bool:
|
|
86
|
+
"""Check if the decision resulted in redaction."""
|
|
87
|
+
return self.effect == Effect.REDACT
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Policy evaluator using Cedar.
|
|
2
|
+
|
|
3
|
+
Architectural invariants (from AGENTS.md):
|
|
4
|
+
- MUST NOT read files directly (use loader.py)
|
|
5
|
+
- MUST NOT make decisions (only evaluate)
|
|
6
|
+
|
|
7
|
+
Uses cedarpy for production-grade policy evaluation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
from cortexhub.errors import PolicyLoadError
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import cedarpy as cedar_module
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
raise PolicyLoadError(
|
|
23
|
+
"Cedar policy engine is required for enforcement. Install with: uv add cedarpy",
|
|
24
|
+
policies_dir="backend",
|
|
25
|
+
) from exc
|
|
26
|
+
|
|
27
|
+
from cortexhub.policy.effects import Decision, Effect
|
|
28
|
+
from cortexhub.policy.loader import PolicyBundle
|
|
29
|
+
from cortexhub.policy.models import AuthorizationRequest
|
|
30
|
+
|
|
31
|
+
logger = structlog.get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PolicyEvaluator:
|
|
35
|
+
"""Evaluates authorization requests against Cedar policies.
|
|
36
|
+
|
|
37
|
+
Deterministic guarantee: Same input → same output, always.
|
|
38
|
+
Performance target: <0.5ms
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, policy_bundle: PolicyBundle):
|
|
42
|
+
"""Initialize policy evaluator.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
policy_bundle: Loaded policy bundle (from loader.py)
|
|
46
|
+
"""
|
|
47
|
+
self.policy_bundle = policy_bundle
|
|
48
|
+
self.default_behavior = policy_bundle.default_behavior
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
logger.info(
|
|
52
|
+
"Cedar policy evaluator initialized",
|
|
53
|
+
version=policy_bundle.version,
|
|
54
|
+
default_behavior=self.default_behavior,
|
|
55
|
+
cedar_version="cedarpy",
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise PolicyLoadError(
|
|
59
|
+
f"Failed to initialize Cedar policy evaluator: {e}",
|
|
60
|
+
policies_dir="backend",
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
def evaluate(self, request: AuthorizationRequest) -> Decision:
|
|
64
|
+
"""Evaluate authorization request against policies.
|
|
65
|
+
|
|
66
|
+
Deterministic: Same request always produces same decision.
|
|
67
|
+
Performance target: <0.5ms
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
request: Authorization request to evaluate
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Decision (ALLOW/DENY/ESCALATE)
|
|
74
|
+
"""
|
|
75
|
+
start_time = time.perf_counter()
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
decision = self._evaluate_cedar(request)
|
|
79
|
+
|
|
80
|
+
latency_ms = (time.perf_counter() - start_time) * 1000
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
"Policy evaluated",
|
|
84
|
+
effect=decision.effect,
|
|
85
|
+
latency_ms=f"{latency_ms:.3f}",
|
|
86
|
+
trace_id=request.trace_id,
|
|
87
|
+
tool=request.action.name,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return decision
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error("Policy evaluation failed", error=str(e), trace_id=request.trace_id)
|
|
94
|
+
# Fail closed - deny on error
|
|
95
|
+
return Decision.deny(f"Policy evaluation error: {e}")
|
|
96
|
+
|
|
97
|
+
def _evaluate_cedar(self, request: AuthorizationRequest) -> Decision:
|
|
98
|
+
"""Evaluate using real Cedar engine.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
request: Authorization request
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Decision
|
|
105
|
+
"""
|
|
106
|
+
principal_type = request.principal.type
|
|
107
|
+
principal_id = request.principal.id
|
|
108
|
+
action = request.action.type
|
|
109
|
+
resource_type = request.resource.type
|
|
110
|
+
resource_id = request.resource.id
|
|
111
|
+
|
|
112
|
+
cedar_request = {
|
|
113
|
+
"principal": f'{principal_type}::"{principal_id}"',
|
|
114
|
+
"action": f'Action::"{action}"',
|
|
115
|
+
"resource": f'{resource_type}::"{resource_id}"',
|
|
116
|
+
"context": self._json_safe(request.context),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
result = cedar_module.is_authorized(
|
|
120
|
+
request=cedar_request,
|
|
121
|
+
policies=self.policy_bundle.policies,
|
|
122
|
+
entities=[],
|
|
123
|
+
schema=self.policy_bundle.schema or None,
|
|
124
|
+
)
|
|
125
|
+
policy_map = self.policy_bundle.metadata.get("policy_map", {}) if self.policy_bundle else {}
|
|
126
|
+
if os.getenv("CORTEXHUB_CEDAR_DEBUG", "").lower() in ("1", "true", "yes"):
|
|
127
|
+
logger.debug(
|
|
128
|
+
"Cedar evaluation",
|
|
129
|
+
decision=str(result.decision),
|
|
130
|
+
reasons=result.diagnostics.reasons,
|
|
131
|
+
policy_count=len(policy_map),
|
|
132
|
+
action=request.action.type,
|
|
133
|
+
resource=request.resource.type,
|
|
134
|
+
guardrails=self._json_safe(request.context.get("guardrails", {})),
|
|
135
|
+
redaction=self._json_safe(request.context.get("redaction", {})),
|
|
136
|
+
context_summary=self._summarize_context(request.context),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _resolve_policy_metadata(policy_id: str | None) -> tuple[str | None, str | None, str | None]:
|
|
140
|
+
if policy_id:
|
|
141
|
+
meta = policy_map.get(policy_id, {})
|
|
142
|
+
return (
|
|
143
|
+
meta.get("policy_document_id") or policy_id,
|
|
144
|
+
meta.get("name"),
|
|
145
|
+
meta.get("effect"),
|
|
146
|
+
)
|
|
147
|
+
if len(policy_map) == 1:
|
|
148
|
+
only_id = next(iter(policy_map.keys()))
|
|
149
|
+
meta = policy_map.get(only_id, {})
|
|
150
|
+
return (
|
|
151
|
+
meta.get("policy_document_id") or only_id,
|
|
152
|
+
meta.get("name"),
|
|
153
|
+
meta.get("effect"),
|
|
154
|
+
)
|
|
155
|
+
return None, None, None
|
|
156
|
+
|
|
157
|
+
if result.decision == cedar_module.Decision.Allow:
|
|
158
|
+
reason = result.diagnostics.reasons[0] if result.diagnostics.reasons else None
|
|
159
|
+
policy_id, policy_name, policy_effect = _resolve_policy_metadata(reason)
|
|
160
|
+
return Decision.allow(
|
|
161
|
+
reasoning="Allowed by Cedar policy",
|
|
162
|
+
policy_id=policy_id,
|
|
163
|
+
policy_name=policy_name,
|
|
164
|
+
)
|
|
165
|
+
if result.decision == cedar_module.Decision.Deny:
|
|
166
|
+
if not result.diagnostics.reasons:
|
|
167
|
+
if self.default_behavior == "allow_and_log":
|
|
168
|
+
return Decision.allow(
|
|
169
|
+
f"Tool '{request.action.name}' allowed by default behavior",
|
|
170
|
+
policy_id="default",
|
|
171
|
+
)
|
|
172
|
+
if self.default_behavior == "deny_and_log":
|
|
173
|
+
return Decision.deny(
|
|
174
|
+
f"Tool '{request.action.name}' denied by default behavior (no matching policy)",
|
|
175
|
+
policy_id="default",
|
|
176
|
+
)
|
|
177
|
+
if self.default_behavior == "escalate":
|
|
178
|
+
return Decision.escalate(
|
|
179
|
+
f"Tool '{request.action.name}' requires approval (unknown tool)",
|
|
180
|
+
policy_id="default",
|
|
181
|
+
)
|
|
182
|
+
return Decision.deny("Invalid default behavior configuration", policy_id="error")
|
|
183
|
+
reason = result.diagnostics.reasons[0]
|
|
184
|
+
policy_id, policy_name, policy_effect = _resolve_policy_metadata(reason)
|
|
185
|
+
if policy_effect == "require_approval":
|
|
186
|
+
return Decision.escalate(
|
|
187
|
+
reasoning="Approval required by policy",
|
|
188
|
+
policy_id=policy_id,
|
|
189
|
+
policy_name=policy_name,
|
|
190
|
+
)
|
|
191
|
+
if self._should_escalate(request):
|
|
192
|
+
return Decision.escalate(
|
|
193
|
+
reasoning="High-risk operation requires approval",
|
|
194
|
+
policy_id=policy_id,
|
|
195
|
+
policy_name=policy_name,
|
|
196
|
+
)
|
|
197
|
+
return Decision.deny(
|
|
198
|
+
reasoning="Denied by CortexHub Policy",
|
|
199
|
+
policy_id=policy_id,
|
|
200
|
+
policy_name=policy_name,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# NoDecision: apply default behavior
|
|
204
|
+
if result.decision == cedar_module.Decision.NoDecision and self.default_behavior == "allow_and_log":
|
|
205
|
+
return Decision.allow(
|
|
206
|
+
f"Tool '{request.action.name}' allowed by default behavior",
|
|
207
|
+
policy_id="default",
|
|
208
|
+
)
|
|
209
|
+
if result.decision == cedar_module.Decision.NoDecision and self.default_behavior == "deny_and_log":
|
|
210
|
+
return Decision.deny(
|
|
211
|
+
f"Tool '{request.action.name}' denied by default behavior (no matching policy)",
|
|
212
|
+
policy_id="default",
|
|
213
|
+
)
|
|
214
|
+
if result.decision == cedar_module.Decision.NoDecision and self.default_behavior == "escalate":
|
|
215
|
+
return Decision.escalate(
|
|
216
|
+
f"Tool '{request.action.name}' requires approval (unknown tool)",
|
|
217
|
+
policy_id="default",
|
|
218
|
+
)
|
|
219
|
+
return Decision.deny("Invalid default behavior configuration", policy_id="error")
|
|
220
|
+
|
|
221
|
+
def _json_safe(self, value: Any) -> Any:
|
|
222
|
+
if value is None:
|
|
223
|
+
return None
|
|
224
|
+
if isinstance(value, datetime):
|
|
225
|
+
return value.isoformat()
|
|
226
|
+
if isinstance(value, dict):
|
|
227
|
+
cleaned: dict[str, Any] = {}
|
|
228
|
+
for key, val in value.items():
|
|
229
|
+
safe_val = self._json_safe(val)
|
|
230
|
+
if safe_val is not None:
|
|
231
|
+
cleaned[key] = safe_val
|
|
232
|
+
return cleaned
|
|
233
|
+
if isinstance(value, list):
|
|
234
|
+
return [item for item in (self._json_safe(item) for item in value) if item is not None]
|
|
235
|
+
return value
|
|
236
|
+
|
|
237
|
+
def _summarize_context(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
238
|
+
def summarize(value: Any) -> Any:
|
|
239
|
+
if isinstance(value, dict):
|
|
240
|
+
return {key: summarize(val) for key, val in value.items()}
|
|
241
|
+
if isinstance(value, list):
|
|
242
|
+
return [summarize(item) for item in value]
|
|
243
|
+
return type(value).__name__
|
|
244
|
+
|
|
245
|
+
return summarize(context)
|
|
246
|
+
|
|
247
|
+
def _should_escalate(self, request: AuthorizationRequest) -> bool:
|
|
248
|
+
"""Determine if a DENY should be escalated to approval (Cedar version).
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
request: Authorization request
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if should escalate
|
|
255
|
+
"""
|
|
256
|
+
tool_name = request.action.name
|
|
257
|
+
args = request.context.get("args", {})
|
|
258
|
+
|
|
259
|
+
# High-value refunds
|
|
260
|
+
if tool_name == "refund_payment":
|
|
261
|
+
amount = args.get("amount", 0)
|
|
262
|
+
return amount > 100
|
|
263
|
+
|
|
264
|
+
# Other escalation rules can be added here
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
|