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,348 @@
|
|
|
1
|
+
"""Backend API client for SDK."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import structlog
|
|
6
|
+
import httpx
|
|
7
|
+
from typing import Any
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from cortexhub.version import __version__
|
|
11
|
+
|
|
12
|
+
logger = structlog.get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CustomPattern:
|
|
17
|
+
"""Custom regex pattern for detection."""
|
|
18
|
+
name: str
|
|
19
|
+
pattern: str
|
|
20
|
+
description: str | None = None
|
|
21
|
+
enabled: bool = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class GuardrailConfig:
|
|
26
|
+
"""Configuration for guardrail policies (PII/Secrets).
|
|
27
|
+
|
|
28
|
+
Controls which types are detected and redacted.
|
|
29
|
+
"""
|
|
30
|
+
action: str = "redact" # redact, block, allow
|
|
31
|
+
pii_types: list[str] | None = None # None = all types
|
|
32
|
+
secret_types: list[str] | None = None
|
|
33
|
+
custom_patterns: list[CustomPattern] | None = None
|
|
34
|
+
# Where to apply redaction: "input_only", "output_only", or "both"
|
|
35
|
+
redaction_scope: str = "both"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class PolicyBundle:
|
|
40
|
+
"""Policy bundle from backend.
|
|
41
|
+
|
|
42
|
+
When policies is empty, SDK runs in observation mode.
|
|
43
|
+
When policies has content, SDK runs in enforcement mode.
|
|
44
|
+
"""
|
|
45
|
+
version: str
|
|
46
|
+
policies: str # Cedar policies (empty = observation mode)
|
|
47
|
+
schema: dict[str, Any] | None = None
|
|
48
|
+
policy_metadata: dict[str, dict[str, Any]] | None = None
|
|
49
|
+
# Guardrail configurations (keyed by policy type: "pii", "secrets")
|
|
50
|
+
guardrail_configs: dict[str, GuardrailConfig] | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SDKConfig:
|
|
55
|
+
"""SDK configuration from backend.
|
|
56
|
+
|
|
57
|
+
Returned during API key validation.
|
|
58
|
+
"""
|
|
59
|
+
project_id: str
|
|
60
|
+
organization_id: str
|
|
61
|
+
plan: str
|
|
62
|
+
policies: PolicyBundle
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def has_policies(self) -> bool:
|
|
66
|
+
"""Check if policies are present (enforcement mode)."""
|
|
67
|
+
return bool(self.policies.policies.strip())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BackendClient:
|
|
71
|
+
"""Client for communicating with CortexHub backend.
|
|
72
|
+
|
|
73
|
+
Handles:
|
|
74
|
+
- API key validation + policy retrieval
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, api_key: str | None, backend_url: str):
|
|
78
|
+
"""Initialize backend client.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
api_key: API key for authentication
|
|
82
|
+
backend_url: Backend API URL
|
|
83
|
+
"""
|
|
84
|
+
self.api_key = api_key
|
|
85
|
+
self.backend_url = backend_url.rstrip("/")
|
|
86
|
+
self._client: httpx.Client | None = None
|
|
87
|
+
|
|
88
|
+
if api_key:
|
|
89
|
+
self._client = httpx.Client(
|
|
90
|
+
base_url=self.backend_url,
|
|
91
|
+
headers={"X-API-Key": api_key},
|
|
92
|
+
timeout=10.0,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def validate_api_key(self) -> tuple[bool, SDKConfig | None]:
|
|
96
|
+
"""Validate API key with backend and get SDK configuration.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
(is_valid, SDKConfig or None)
|
|
100
|
+
|
|
101
|
+
SDKConfig includes:
|
|
102
|
+
- project_id, organization_id, environment
|
|
103
|
+
- policies (empty = observation mode, non-empty = enforcement mode)
|
|
104
|
+
"""
|
|
105
|
+
if not self.api_key or not self._client:
|
|
106
|
+
return False, None
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Note: base_url already includes /v1, so just /api-keys/validate
|
|
110
|
+
response = self._client.post(
|
|
111
|
+
"/api-keys/validate",
|
|
112
|
+
headers={"X-API-Key": self.api_key},
|
|
113
|
+
timeout=5.0,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if response.status_code == 200:
|
|
117
|
+
data = response.json()
|
|
118
|
+
|
|
119
|
+
# Check if valid
|
|
120
|
+
if not data.get("valid", False):
|
|
121
|
+
logger.warning("API key validation failed", message=data.get("message"))
|
|
122
|
+
return False, None
|
|
123
|
+
|
|
124
|
+
# Parse policy bundle from backend
|
|
125
|
+
# Backend returns: { policies: { version, policies: [...policy objects...] } }
|
|
126
|
+
policies_data = data.get("policies", {})
|
|
127
|
+
|
|
128
|
+
# Convert policy objects to Cedar string if needed
|
|
129
|
+
raw_policies = policies_data.get("policies", [])
|
|
130
|
+
guardrail_configs: dict[str, GuardrailConfig] = {}
|
|
131
|
+
|
|
132
|
+
if isinstance(raw_policies, list):
|
|
133
|
+
# Backend returns list of policy objects - extract Cedar code
|
|
134
|
+
cedar_parts = []
|
|
135
|
+
policy_map = {}
|
|
136
|
+
cedar_index = 0
|
|
137
|
+
for p in raw_policies:
|
|
138
|
+
if p.get("cedar_policy"):
|
|
139
|
+
cedar_parts.append(f"// Policy: {p.get('name')} (tool: {p.get('tool_name')})")
|
|
140
|
+
cedar_parts.append(p.get("cedar_policy"))
|
|
141
|
+
cedar_parts.append("")
|
|
142
|
+
policy_text = p.get("cedar_policy") or ""
|
|
143
|
+
statement_count = len(
|
|
144
|
+
re.findall(r"\b(permit|forbid)\s*\(", policy_text)
|
|
145
|
+
)
|
|
146
|
+
if statement_count == 0:
|
|
147
|
+
continue
|
|
148
|
+
for _ in range(statement_count):
|
|
149
|
+
policy_map[f"policy{cedar_index}"] = {
|
|
150
|
+
"name": p.get("name"),
|
|
151
|
+
"effect": p.get("effect"),
|
|
152
|
+
"tool_name": p.get("tool_name"),
|
|
153
|
+
"policy_document_id": p.get("id"),
|
|
154
|
+
"approval_destination": p.get("approval_destination"),
|
|
155
|
+
"approval_webhook_id": p.get("approval_webhook_id"),
|
|
156
|
+
}
|
|
157
|
+
cedar_index += 1
|
|
158
|
+
|
|
159
|
+
# Extract guardrail config if present
|
|
160
|
+
gc = p.get("guardrail_config")
|
|
161
|
+
if gc:
|
|
162
|
+
policy_key = p.get("policy_key", "")
|
|
163
|
+
if "pii" in policy_key:
|
|
164
|
+
custom_patterns = []
|
|
165
|
+
# Handle None explicitly (custom_patterns can be null in JSON)
|
|
166
|
+
raw_patterns = gc.get("custom_patterns") or []
|
|
167
|
+
for cp in raw_patterns:
|
|
168
|
+
custom_patterns.append(CustomPattern(
|
|
169
|
+
name=cp.get("name", ""),
|
|
170
|
+
pattern=cp.get("pattern", ""),
|
|
171
|
+
description=cp.get("description"),
|
|
172
|
+
enabled=cp.get("enabled", True),
|
|
173
|
+
))
|
|
174
|
+
guardrail_configs["pii"] = GuardrailConfig(
|
|
175
|
+
action=gc.get("action", "redact"),
|
|
176
|
+
pii_types=gc.get("pii_types"),
|
|
177
|
+
custom_patterns=custom_patterns if custom_patterns else None,
|
|
178
|
+
redaction_scope=gc.get("redaction_scope", "both"),
|
|
179
|
+
)
|
|
180
|
+
elif "secrets" in policy_key:
|
|
181
|
+
custom_patterns = []
|
|
182
|
+
raw_patterns = gc.get("custom_patterns") or []
|
|
183
|
+
for cp in raw_patterns:
|
|
184
|
+
custom_patterns.append(CustomPattern(
|
|
185
|
+
name=cp.get("name", ""),
|
|
186
|
+
pattern=cp.get("pattern", ""),
|
|
187
|
+
description=cp.get("description"),
|
|
188
|
+
enabled=cp.get("enabled", True),
|
|
189
|
+
))
|
|
190
|
+
guardrail_configs["secrets"] = GuardrailConfig(
|
|
191
|
+
action=gc.get("action", "redact"),
|
|
192
|
+
secret_types=gc.get("secret_types"),
|
|
193
|
+
custom_patterns=custom_patterns if custom_patterns else None,
|
|
194
|
+
redaction_scope=gc.get("redaction_scope", "both"),
|
|
195
|
+
)
|
|
196
|
+
policies_str = "\n".join(cedar_parts)
|
|
197
|
+
else:
|
|
198
|
+
policies_str = raw_policies or ""
|
|
199
|
+
policy_map = {}
|
|
200
|
+
|
|
201
|
+
policies = PolicyBundle(
|
|
202
|
+
version=policies_data.get("version", "0"),
|
|
203
|
+
policies=policies_str,
|
|
204
|
+
schema=policies_data.get("schema"),
|
|
205
|
+
policy_metadata=policy_map,
|
|
206
|
+
guardrail_configs=guardrail_configs if guardrail_configs else None,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
config = SDKConfig(
|
|
210
|
+
project_id=data.get("project_id", ""),
|
|
211
|
+
organization_id=data.get("org_id", ""),
|
|
212
|
+
plan=data.get("plan", "free"),
|
|
213
|
+
policies=policies,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
mode = "enforcement" if config.has_policies else "observation"
|
|
217
|
+
logger.info(
|
|
218
|
+
"API key validated",
|
|
219
|
+
project_id=config.project_id,
|
|
220
|
+
mode=mode,
|
|
221
|
+
policy_version=policies.version,
|
|
222
|
+
)
|
|
223
|
+
if os.getenv("CORTEXHUB_DEBUG_POLICY_BUNDLE", "").lower() in ("1", "true", "yes"):
|
|
224
|
+
logger.info(
|
|
225
|
+
"Policy bundle received",
|
|
226
|
+
policy_count=len(policy_map),
|
|
227
|
+
policy_ids=list(policy_map.keys()),
|
|
228
|
+
policy_effects={k: v.get("effect") for k, v in policy_map.items()},
|
|
229
|
+
)
|
|
230
|
+
if os.getenv("CORTEXHUB_DEBUG_POLICY_TEXT", "").lower() in (
|
|
231
|
+
"1",
|
|
232
|
+
"true",
|
|
233
|
+
"yes",
|
|
234
|
+
):
|
|
235
|
+
logger.info("Policy bundle cedar", policies=policies_str)
|
|
236
|
+
return True, config
|
|
237
|
+
else:
|
|
238
|
+
error_detail = response.json().get("detail", "Invalid API key")
|
|
239
|
+
logger.warning("API key validation failed", error=error_detail)
|
|
240
|
+
return False, None
|
|
241
|
+
|
|
242
|
+
except httpx.ConnectError:
|
|
243
|
+
logger.warning(
|
|
244
|
+
"Backend unreachable - running in offline observation mode",
|
|
245
|
+
backend_url=self.backend_url,
|
|
246
|
+
)
|
|
247
|
+
return False, None
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error("API key validation error", error=str(e))
|
|
250
|
+
return False, None
|
|
251
|
+
|
|
252
|
+
def close(self):
|
|
253
|
+
"""Close the HTTP client."""
|
|
254
|
+
if self._client:
|
|
255
|
+
self._client.close()
|
|
256
|
+
|
|
257
|
+
def register_tool_inventory(
|
|
258
|
+
self,
|
|
259
|
+
*,
|
|
260
|
+
agent_id: str,
|
|
261
|
+
framework: str,
|
|
262
|
+
tools: list[dict],
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
"""Register agent tool inventory on init. Overwrites previous."""
|
|
265
|
+
|
|
266
|
+
if not self._client:
|
|
267
|
+
return {"agent_id": agent_id, "tools_count": 0}
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
response = self._client.post(
|
|
271
|
+
"/tool-inventory",
|
|
272
|
+
json={
|
|
273
|
+
"agent_id": agent_id,
|
|
274
|
+
"framework": framework,
|
|
275
|
+
"sdk_version": __version__,
|
|
276
|
+
"tools": tools,
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
return response.json()
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.warning("Failed to register tool inventory", error=str(e))
|
|
282
|
+
return {"agent_id": agent_id, "tools_count": len(tools)}
|
|
283
|
+
|
|
284
|
+
def create_approval(
|
|
285
|
+
self,
|
|
286
|
+
*,
|
|
287
|
+
run_id: str,
|
|
288
|
+
trace_id: str | None,
|
|
289
|
+
tool_name: str,
|
|
290
|
+
tool_args_values: dict[str, Any] | None,
|
|
291
|
+
context_hash: str,
|
|
292
|
+
policy_id: str,
|
|
293
|
+
policy_name: str,
|
|
294
|
+
policy_explanation: str,
|
|
295
|
+
risk_category: str | None,
|
|
296
|
+
agent_id: str,
|
|
297
|
+
framework: str,
|
|
298
|
+
environment: str | None = None,
|
|
299
|
+
expires_in_seconds: int = 3600,
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Create approval request. Idempotent on natural key."""
|
|
302
|
+
|
|
303
|
+
if not self._client:
|
|
304
|
+
raise RuntimeError("Backend client not initialized")
|
|
305
|
+
|
|
306
|
+
response = self._client.post(
|
|
307
|
+
"/approvals",
|
|
308
|
+
json={
|
|
309
|
+
"run_id": run_id,
|
|
310
|
+
"trace_id": trace_id,
|
|
311
|
+
"tool_name": tool_name,
|
|
312
|
+
"tool_args_values": tool_args_values,
|
|
313
|
+
"context_hash": context_hash,
|
|
314
|
+
"policy_id": policy_id,
|
|
315
|
+
"policy_name": policy_name,
|
|
316
|
+
"policy_explanation": policy_explanation,
|
|
317
|
+
"risk_category": risk_category,
|
|
318
|
+
"agent_id": agent_id,
|
|
319
|
+
"framework": framework,
|
|
320
|
+
"environment": environment,
|
|
321
|
+
"expires_in_seconds": expires_in_seconds,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
try:
|
|
325
|
+
response.raise_for_status()
|
|
326
|
+
except httpx.HTTPStatusError:
|
|
327
|
+
logger.error(
|
|
328
|
+
"Approval creation failed",
|
|
329
|
+
status_code=response.status_code,
|
|
330
|
+
response_text=response.text,
|
|
331
|
+
)
|
|
332
|
+
raise
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
payload = response.json()
|
|
336
|
+
except ValueError as exc:
|
|
337
|
+
logger.error(
|
|
338
|
+
"Approval creation returned invalid JSON",
|
|
339
|
+
status_code=response.status_code,
|
|
340
|
+
response_text=response.text,
|
|
341
|
+
)
|
|
342
|
+
raise RuntimeError("Approval creation returned invalid JSON") from exc
|
|
343
|
+
|
|
344
|
+
if not payload.get("approval_id"):
|
|
345
|
+
logger.error("Approval creation missing approval_id", payload=payload)
|
|
346
|
+
raise RuntimeError("Approval creation response missing approval_id")
|
|
347
|
+
|
|
348
|
+
return payload
|