cairo-sdk 1.0.0__tar.gz
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.
- cairo_sdk-1.0.0/PKG-INFO +95 -0
- cairo_sdk-1.0.0/README.md +68 -0
- cairo_sdk-1.0.0/cairo_sdk/__init__.py +37 -0
- cairo_sdk-1.0.0/cairo_sdk/analytics.py +475 -0
- cairo_sdk-1.0.0/cairo_sdk/attestation/__init__.py +1 -0
- cairo_sdk-1.0.0/cairo_sdk/attestation/policy.py +248 -0
- cairo_sdk-1.0.0/cairo_sdk/containment/__init__.py +1 -0
- cairo_sdk-1.0.0/cairo_sdk/containment/scanner.py +414 -0
- cairo_sdk-1.0.0/cairo_sdk/governance_map.py +813 -0
- cairo_sdk-1.0.0/cairo_sdk/interception/__init__.py +10 -0
- cairo_sdk-1.0.0/cairo_sdk/interception/monitor.py +326 -0
- cairo_sdk-1.0.0/cairo_sdk/interception/sentinel.py +79 -0
- cairo_sdk-1.0.0/cairo_sdk/output_logic/__init__.py +17 -0
- cairo_sdk-1.0.0/cairo_sdk/output_logic/compliance.py +315 -0
- cairo_sdk-1.0.0/cairo_sdk/output_logic/validator.py +79 -0
- cairo_sdk-1.0.0/cairo_sdk/resilience/__init__.py +14 -0
- cairo_sdk-1.0.0/cairo_sdk/resilience/hardening.py +162 -0
- cairo_sdk-1.0.0/cairo_sdk/resilience/scrubber.py +331 -0
- cairo_sdk-1.0.0/cairo_sdk/telemetry.py +849 -0
- cairo_sdk-1.0.0/cairo_sdk/utils.py +273 -0
- cairo_sdk-1.0.0/cairo_sdk.egg-info/PKG-INFO +95 -0
- cairo_sdk-1.0.0/cairo_sdk.egg-info/SOURCES.txt +25 -0
- cairo_sdk-1.0.0/cairo_sdk.egg-info/dependency_links.txt +1 -0
- cairo_sdk-1.0.0/cairo_sdk.egg-info/requires.txt +1 -0
- cairo_sdk-1.0.0/cairo_sdk.egg-info/top_level.txt +1 -0
- cairo_sdk-1.0.0/pyproject.toml +41 -0
- cairo_sdk-1.0.0/setup.cfg +4 -0
cairo_sdk-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cairo-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CAIRO Protocol SDK — forensic AI governance telemetry, deterministic interception, and EU AI Act enforcement for agentic AI systems
|
|
5
|
+
Author-email: XSData Factory Private Limited <sdk@cairoprotocol.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://cairoprotocol.com
|
|
8
|
+
Project-URL: Documentation, https://cairoprotocol.com/docs
|
|
9
|
+
Project-URL: SDK Quickstart, https://cairoprotocol.com/docs/sdk-quickstart
|
|
10
|
+
Project-URL: Source Code, https://cairoprotocol.com/platform
|
|
11
|
+
Keywords: ai-governance,forensic-ai,eu-ai-act,cairo-protocol,agentic-ai,compliance,telemetry
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: Other/Proprietary License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: pydantic>=2.0
|
|
27
|
+
|
|
28
|
+
# CAIRO Protocol SDK
|
|
29
|
+
|
|
30
|
+
**Forensic AI governance for agentic systems** — deterministic interception, real-time telemetry, and EU AI Act 2026 enforcement.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
```bash
|
|
34
|
+
pip install cairo-sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
```python
|
|
39
|
+
from cairo_sdk import TelemetryStream, ValueTracker, RiskPulse
|
|
40
|
+
|
|
41
|
+
# Initialize telemetry
|
|
42
|
+
stream = TelemetryStream(
|
|
43
|
+
value_tracker=ValueTracker(
|
|
44
|
+
annual_turnover_eur=500_000_000,
|
|
45
|
+
usd_per_eur=1.08,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Emit a risk pulse
|
|
50
|
+
pulse = RiskPulse(
|
|
51
|
+
pillar="Containment",
|
|
52
|
+
component="prompt_injection",
|
|
53
|
+
sub_component="boundary_check",
|
|
54
|
+
risk_level=0.7,
|
|
55
|
+
eu_ai_act_status="high",
|
|
56
|
+
)
|
|
57
|
+
stream.emit(pulse)
|
|
58
|
+
|
|
59
|
+
# Get dashboard summary
|
|
60
|
+
summary = stream.value_tracker.dashboard_summary()
|
|
61
|
+
print(f"Total intercepts: {summary['total_intercepts']}")
|
|
62
|
+
print(f"Risk exposure avoided: ${summary['total_risk_usd']:,.2f}")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Five CAIRO Pillars
|
|
66
|
+
|
|
67
|
+
| Pillar | Purpose | Module |
|
|
68
|
+
|--------|---------|--------|
|
|
69
|
+
| **Containment** | Boundaries, scope lits, sandboxing | `cairo_sdk.containment` |
|
|
70
|
+
| **Attestation** | Compliance proof, audit trail | `cairo_sdk.attestation` |
|
|
71
|
+
| **Interception** | Pre/post request hooks, middleware | `cairo_sdk.interception` |
|
|
72
|
+
| **Resilience** | Fault handling, retries, fallback | `cairo_sdk.resilience` |
|
|
73
|
+
| **Output-Logic** | Output validation, guardrails | `cairo_sdk.output_logic` |
|
|
74
|
+
|
|
75
|
+
## Key Features
|
|
76
|
+
|
|
77
|
+
- **ValueTracker** — real-time EU AI Act fine-avoidance calculation
|
|
78
|
+
- **TelemetryStream** — high-speed forensic event streaming
|
|
79
|
+
- **LogicDriftDetector** — behavioral drift detection across agent sessions
|
|
80
|
+
- **Deterministic interception** — zero-trust validation at every CAIRO pillar
|
|
81
|
+
- **Compliance metadata** — NIST AI RMF, ISO 42001, EU AI Act mapping
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- Python 3.9+
|
|
86
|
+
- pydantic >= 2.0
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
- [SDK Quickstart](https://cairoprotocol.com/docs/sdk-quickstart)
|
|
91
|
+
- [Integration Guides](https://cairoprotocol.com/docs/integration-guides)
|
|
92
|
+
- [API Reference](https://cairoprotocol.com/docs/api-reference)
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
Proprietary — XSData Factory Private Limited. See [cairoprotocol.com/trust](https://cairoprotocol.com/trust) for terms.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# CAIRO Protocol SDK
|
|
2
|
+
|
|
3
|
+
**Forensic AI governance for agentic systems** — deterministic interception, real-time telemetry, and EU AI Act 2026 enforcement.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```bash
|
|
7
|
+
pip install cairo-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
```python
|
|
12
|
+
from cairo_sdk import TelemetryStream, ValueTracker, RiskPulse
|
|
13
|
+
|
|
14
|
+
# Initialize telemetry
|
|
15
|
+
stream = TelemetryStream(
|
|
16
|
+
value_tracker=ValueTracker(
|
|
17
|
+
annual_turnover_eur=500_000_000,
|
|
18
|
+
usd_per_eur=1.08,
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Emit a risk pulse
|
|
23
|
+
pulse = RiskPulse(
|
|
24
|
+
pillar="Containment",
|
|
25
|
+
component="prompt_injection",
|
|
26
|
+
sub_component="boundary_check",
|
|
27
|
+
risk_level=0.7,
|
|
28
|
+
eu_ai_act_status="high",
|
|
29
|
+
)
|
|
30
|
+
stream.emit(pulse)
|
|
31
|
+
|
|
32
|
+
# Get dashboard summary
|
|
33
|
+
summary = stream.value_tracker.dashboard_summary()
|
|
34
|
+
print(f"Total intercepts: {summary['total_intercepts']}")
|
|
35
|
+
print(f"Risk exposure avoided: ${summary['total_risk_usd']:,.2f}")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Five CAIRO Pillars
|
|
39
|
+
|
|
40
|
+
| Pillar | Purpose | Module |
|
|
41
|
+
|--------|---------|--------|
|
|
42
|
+
| **Containment** | Boundaries, scope lits, sandboxing | `cairo_sdk.containment` |
|
|
43
|
+
| **Attestation** | Compliance proof, audit trail | `cairo_sdk.attestation` |
|
|
44
|
+
| **Interception** | Pre/post request hooks, middleware | `cairo_sdk.interception` |
|
|
45
|
+
| **Resilience** | Fault handling, retries, fallback | `cairo_sdk.resilience` |
|
|
46
|
+
| **Output-Logic** | Output validation, guardrails | `cairo_sdk.output_logic` |
|
|
47
|
+
|
|
48
|
+
## Key Features
|
|
49
|
+
|
|
50
|
+
- **ValueTracker** — real-time EU AI Act fine-avoidance calculation
|
|
51
|
+
- **TelemetryStream** — high-speed forensic event streaming
|
|
52
|
+
- **LogicDriftDetector** — behavioral drift detection across agent sessions
|
|
53
|
+
- **Deterministic interception** — zero-trust validation at every CAIRO pillar
|
|
54
|
+
- **Compliance metadata** — NIST AI RMF, ISO 42001, EU AI Act mapping
|
|
55
|
+
|
|
56
|
+
## Requirements
|
|
57
|
+
|
|
58
|
+
- Python 3.9+
|
|
59
|
+
- pydantic >= 2.0
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
- [SDK Quickstart](https://cairoprotocol.com/docs/sdk-quickstart)
|
|
64
|
+
- [Integration Guides](https://cairoprotocol.com/docs/integration-guides)
|
|
65
|
+
- [API Reference](https://cairoprotocol.com/docs/api-reference)
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
Proprietary — XSData Factory Private Limited. See [cairoprotocol.com/trust](https://cairoprotocol.com/trust) for terms.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAIRO SDK — telemetry, deterministic interception, EU AI Act enforcement, and ValueTracker.
|
|
3
|
+
100% aligned with CAIRO Protocol Technical Architecture v1.0.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from cairo_sdk.telemetry import (
|
|
7
|
+
TelemetryStream,
|
|
8
|
+
RiskPulse,
|
|
9
|
+
ComplianceMetadata,
|
|
10
|
+
SafetyOverheadEvent,
|
|
11
|
+
LogicDriftDetector,
|
|
12
|
+
EUAIActStatus,
|
|
13
|
+
PulseSeverity,
|
|
14
|
+
get_global_stream,
|
|
15
|
+
# ValueTracker & EU AI Act 2026 cost reporting
|
|
16
|
+
ValueTracker,
|
|
17
|
+
InterceptRecord,
|
|
18
|
+
EnforcementTier,
|
|
19
|
+
EU_FINES_2026,
|
|
20
|
+
ACTION_CONTROL_MAP,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"TelemetryStream",
|
|
25
|
+
"RiskPulse",
|
|
26
|
+
"ComplianceMetadata",
|
|
27
|
+
"SafetyOverheadEvent",
|
|
28
|
+
"LogicDriftDetector",
|
|
29
|
+
"EUAIActStatus",
|
|
30
|
+
"PulseSeverity",
|
|
31
|
+
"get_global_stream",
|
|
32
|
+
"ValueTracker",
|
|
33
|
+
"InterceptRecord",
|
|
34
|
+
"EnforcementTier",
|
|
35
|
+
"EU_FINES_2026",
|
|
36
|
+
"ACTION_CONTROL_MAP",
|
|
37
|
+
]
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cairo_sdk/analytics.py — CAIRO SDK: CairoAnalyticsLogger
|
|
3
|
+
|
|
4
|
+
DynamoDB-backed audit log for the CairoAnalytics table. Every ArmorMiddleware
|
|
5
|
+
run writes one structured event containing:
|
|
6
|
+
|
|
7
|
+
event_type "agent_response" | "compliance_event" | "armor_run"
|
|
8
|
+
correlation_id from AgentRequest
|
|
9
|
+
compliance_label EU AI Act Art. 50 declaration
|
|
10
|
+
pillar_results per-pillar pass/fail summary
|
|
11
|
+
timestamp_utc ISO-8601
|
|
12
|
+
regulatory_tags AXON Compliance Engine — NIST / ISO 42001 / EU AI Act controls
|
|
13
|
+
risk_level "Low" | "Medium" | "High"
|
|
14
|
+
risk_mitigation_value representative per-incident "Saved Fine" in USD
|
|
15
|
+
|
|
16
|
+
Non-blocking write path
|
|
17
|
+
───────────────────────
|
|
18
|
+
log_request() is the production entry point for all middleware instrumentation.
|
|
19
|
+
After resolving regulatory tags (CPU-only, synchronous, < 1 ms), it submits the
|
|
20
|
+
DynamoDB put_item call to a module-level ThreadPoolExecutor so the operation is
|
|
21
|
+
fully fire-and-forget. The caller's async event loop is never blocked by network
|
|
22
|
+
I/O — CAIRO Shield adds zero latency to the user's AI experience.
|
|
23
|
+
|
|
24
|
+
Error handling
|
|
25
|
+
──────────────
|
|
26
|
+
All DynamoDB calls catch specific botocore exceptions (ClientError, BotoCoreError)
|
|
27
|
+
and log warnings via the standard Python logging system rather than silently
|
|
28
|
+
swallowing errors or surfacing them to the caller. Analytics failure is never
|
|
29
|
+
allowed to affect the primary agent response path.
|
|
30
|
+
|
|
31
|
+
Public API
|
|
32
|
+
──────────
|
|
33
|
+
log_event(event_type, data)
|
|
34
|
+
Low-level synchronous writer — stores exactly what you pass in.
|
|
35
|
+
Backward-compatible; mocked in tests via unittest.mock.MagicMock.
|
|
36
|
+
|
|
37
|
+
log_request(event_type, data, pillar, action_type)
|
|
38
|
+
High-level AXON entry point — non-blocking.
|
|
39
|
+
Resolves regulatory tags, enriches `data`, then submits the DynamoDB
|
|
40
|
+
write to the background thread pool.
|
|
41
|
+
|
|
42
|
+
scan_events(limit)
|
|
43
|
+
Read path — full table scan for the AXON dashboard.
|
|
44
|
+
|
|
45
|
+
In production, set CAIRO_DYNAMO_TABLE and CAIRO_DYNAMO_REGION env vars.
|
|
46
|
+
In tests, inject a unittest.mock.MagicMock in place of this class.
|
|
47
|
+
|
|
48
|
+
CAIRO Protocol Technical Architecture v1.0.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import atexit
|
|
54
|
+
import concurrent.futures
|
|
55
|
+
import datetime
|
|
56
|
+
import json
|
|
57
|
+
import logging
|
|
58
|
+
import uuid
|
|
59
|
+
from typing import Any
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Module-level infrastructure
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
_logger: logging.Logger = logging.getLogger(__name__)
|
|
66
|
+
|
|
67
|
+
# Thread pool for fire-and-forget DynamoDB writes.
|
|
68
|
+
# max_workers=4 handles burst writes without overwhelming DynamoDB capacity.
|
|
69
|
+
# The pool is shared across ALL CairoAnalyticsLogger instances (process-wide).
|
|
70
|
+
_EXECUTOR: concurrent.futures.ThreadPoolExecutor = (
|
|
71
|
+
concurrent.futures.ThreadPoolExecutor(
|
|
72
|
+
max_workers=4,
|
|
73
|
+
thread_name_prefix="cairo-analytics-writer",
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
# Graceful shutdown: cancel queued futures, do NOT wait for in-flight ones.
|
|
77
|
+
atexit.register(_EXECUTOR.shutdown, wait=False)
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# botocore exception guard
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Try to import the specific exception hierarchy so callers receive informative
|
|
83
|
+
# warnings. If botocore is not installed we fall back to catching Exception,
|
|
84
|
+
# which is equivalent behaviour but loses the structured log context.
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
from botocore.exceptions import BotoCoreError, ClientError as _BotoClientError # type: ignore[import-untyped]
|
|
88
|
+
_BOTOCORE_ERRORS: tuple[type[Exception], ...] = (BotoCoreError, _BotoClientError)
|
|
89
|
+
except ImportError:
|
|
90
|
+
_BOTOCORE_ERRORS = (Exception,)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ===========================================================================
|
|
94
|
+
# CairoAnalyticsLogger
|
|
95
|
+
# ===========================================================================
|
|
96
|
+
|
|
97
|
+
class CairoAnalyticsLogger:
|
|
98
|
+
"""
|
|
99
|
+
Writes compliance events to the CairoAnalytics DynamoDB table and reads
|
|
100
|
+
them back for the AXON Executive Dashboard.
|
|
101
|
+
|
|
102
|
+
Interface is intentionally minimal so the class can be swapped with a
|
|
103
|
+
``unittest.mock.MagicMock`` in test environments without any additional
|
|
104
|
+
dependencies or AWS access.
|
|
105
|
+
|
|
106
|
+
Write path — ``log_event`` (sync, low-level) and ``log_request``
|
|
107
|
+
(async/fire-and-forget, high-level with regulatory tags).
|
|
108
|
+
Read path — ``scan_events`` (full table scan, dashboard use only).
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
table_name : str
|
|
113
|
+
DynamoDB table name. Defaults to ``"CairoAnalytics"`` — must match
|
|
114
|
+
the table created by ``scripts/deploy_aws.sh``.
|
|
115
|
+
region : str
|
|
116
|
+
AWS region where the table lives. Defaults to ``"us-east-1"``.
|
|
117
|
+
endpoint_url : str or None
|
|
118
|
+
Override DynamoDB endpoint for local testing (e.g.
|
|
119
|
+
``"http://localhost:8000"``). Leave ``None`` in production.
|
|
120
|
+
|
|
121
|
+
Environment Variables
|
|
122
|
+
---------------------
|
|
123
|
+
CAIRO_DYNAMO_TABLE Table name override.
|
|
124
|
+
CAIRO_DYNAMO_REGION Region override.
|
|
125
|
+
CAIRO_DYNAMO_ENDPOINT_URL Local endpoint override.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
table_name: str = "CairoAnalytics",
|
|
131
|
+
region: str = "us-east-1",
|
|
132
|
+
endpoint_url: str | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
self.table_name = table_name
|
|
135
|
+
self.region = region
|
|
136
|
+
self.endpoint_url = endpoint_url or None
|
|
137
|
+
self._table: Any = None # lazy-initialised on first use
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Internal: DynamoDB resource initialisation
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _get_table(self) -> Any:
|
|
144
|
+
"""
|
|
145
|
+
Return a boto3 DynamoDB Table resource, initialising the connection
|
|
146
|
+
lazily on first call.
|
|
147
|
+
|
|
148
|
+
Raises
|
|
149
|
+
------
|
|
150
|
+
RuntimeError
|
|
151
|
+
If ``boto3`` is not installed.
|
|
152
|
+
BotoCoreError / ClientError
|
|
153
|
+
If the AWS connection or table lookup fails. Callers that need
|
|
154
|
+
resilience should catch ``_BOTOCORE_ERRORS``.
|
|
155
|
+
"""
|
|
156
|
+
if self._table is None:
|
|
157
|
+
try:
|
|
158
|
+
import boto3 # type: ignore[import-untyped]
|
|
159
|
+
|
|
160
|
+
kwargs: dict[str, Any] = {"region_name": self.region}
|
|
161
|
+
if self.endpoint_url:
|
|
162
|
+
kwargs["endpoint_url"] = self.endpoint_url
|
|
163
|
+
dynamodb = boto3.resource("dynamodb", **kwargs)
|
|
164
|
+
self._table = dynamodb.Table(self.table_name)
|
|
165
|
+
except ImportError:
|
|
166
|
+
raise RuntimeError(
|
|
167
|
+
"boto3 is required for DynamoDB logging. "
|
|
168
|
+
"Install it with: pip install boto3"
|
|
169
|
+
)
|
|
170
|
+
except _BOTOCORE_ERRORS as exc:
|
|
171
|
+
_logger.error(
|
|
172
|
+
"Failed to initialise DynamoDB resource "
|
|
173
|
+
"[table=%s region=%s]: %s: %s",
|
|
174
|
+
self.table_name, self.region,
|
|
175
|
+
type(exc).__name__, exc,
|
|
176
|
+
)
|
|
177
|
+
raise
|
|
178
|
+
return self._table
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
# Low-level writer (synchronous — kept for test-mock compatibility)
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def log_event(
|
|
185
|
+
self,
|
|
186
|
+
event_type: str,
|
|
187
|
+
data: dict[str, Any],
|
|
188
|
+
) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Write one compliance event to the CairoAnalytics DynamoDB table.
|
|
191
|
+
|
|
192
|
+
This is the low-level synchronous writer. It is kept synchronous so
|
|
193
|
+
that ``unittest.mock.MagicMock`` replacements in test suites can
|
|
194
|
+
directly assert ``log_event.call_args`` without threading concerns.
|
|
195
|
+
|
|
196
|
+
For production middleware instrumentation, prefer ``log_request()``
|
|
197
|
+
which is non-blocking and automatically attaches regulatory tags.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
event_type : str
|
|
202
|
+
``"agent_response"`` | ``"compliance_event"`` | ``"armor_run"``.
|
|
203
|
+
data : dict
|
|
204
|
+
Arbitrary payload — serialised to a JSON string for DynamoDB
|
|
205
|
+
storage. The dict is not mutated.
|
|
206
|
+
|
|
207
|
+
Error Handling
|
|
208
|
+
--------------
|
|
209
|
+
botocore ``ClientError`` / ``BotoCoreError`` are caught and logged as
|
|
210
|
+
WARNING messages so a DynamoDB outage never surfaces to the caller.
|
|
211
|
+
Any other unexpected exception is logged at WARNING level and swallowed.
|
|
212
|
+
"""
|
|
213
|
+
item: dict[str, Any] = {
|
|
214
|
+
"event_id": str(uuid.uuid4()),
|
|
215
|
+
"event_type": event_type,
|
|
216
|
+
"timestamp_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(
|
|
217
|
+
timespec="milliseconds"
|
|
218
|
+
),
|
|
219
|
+
"data": json.dumps(data, default=str),
|
|
220
|
+
}
|
|
221
|
+
try:
|
|
222
|
+
table = self._get_table()
|
|
223
|
+
table.put_item(Item=item)
|
|
224
|
+
except _BOTOCORE_ERRORS as exc:
|
|
225
|
+
_logger.warning(
|
|
226
|
+
"DynamoDB put_item failed [event_type=%s table=%s]: %s: %s",
|
|
227
|
+
event_type, self.table_name, type(exc).__name__, exc,
|
|
228
|
+
)
|
|
229
|
+
except Exception as exc: # noqa: BLE001
|
|
230
|
+
_logger.warning(
|
|
231
|
+
"Unexpected error in DynamoDB write [event_type=%s]: %s",
|
|
232
|
+
event_type, exc,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
# Internal: thread-pool target (runs in background thread)
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def _write_item(self, event_type: str, data: dict[str, Any]) -> None:
|
|
240
|
+
"""
|
|
241
|
+
Internal method executed in the background thread pool.
|
|
242
|
+
|
|
243
|
+
Calls ``log_event()`` and suppresses all exceptions so a thread crash
|
|
244
|
+
never propagates to the ``ThreadPoolExecutor`` error log.
|
|
245
|
+
|
|
246
|
+
This method is not part of the public API; it exists solely as the
|
|
247
|
+
callable submitted to ``_EXECUTOR``.
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
self.log_event(event_type, data)
|
|
251
|
+
except Exception as exc: # noqa: BLE001
|
|
252
|
+
_logger.warning("Background DynamoDB write raised: %s", exc)
|
|
253
|
+
|
|
254
|
+
# ------------------------------------------------------------------
|
|
255
|
+
# High-level AXON entry point (non-blocking, fire-and-forget)
|
|
256
|
+
# ------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
def log_request(
|
|
259
|
+
self,
|
|
260
|
+
event_type: str,
|
|
261
|
+
data: dict[str, Any],
|
|
262
|
+
pillar: str | None = None,
|
|
263
|
+
action_type: str | None = None,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""
|
|
266
|
+
AXON Compliance Engine entry point — **non-blocking**.
|
|
267
|
+
|
|
268
|
+
Resolves the full set of regulatory tags for the given ``pillar`` /
|
|
269
|
+
``action_type`` from ``governance_map.py``, merges them into a copy of
|
|
270
|
+
``data``, then submits the DynamoDB ``put_item`` call to the module-level
|
|
271
|
+
``ThreadPoolExecutor`` and returns immediately.
|
|
272
|
+
|
|
273
|
+
The caller's thread (and any async event loop) is never blocked by
|
|
274
|
+
network I/O. CAIRO Shield adds zero latency to the user's AI response.
|
|
275
|
+
|
|
276
|
+
Enrichment
|
|
277
|
+
──────────
|
|
278
|
+
Every item written by this method contains three additional top-level
|
|
279
|
+
DynamoDB attributes that make the CairoAnalytics table directly
|
|
280
|
+
queryable for compliance reporting:
|
|
281
|
+
|
|
282
|
+
``regulatory_tags``
|
|
283
|
+
Full NIST AI RMF / ISO 42001 / EU AI Act control cross-walk dict.
|
|
284
|
+
``risk_level``
|
|
285
|
+
``"Low"`` | ``"Medium"`` | ``"High"``
|
|
286
|
+
``risk_mitigation_value``
|
|
287
|
+
``{"amount_usd": float, "currency": "USD", "description": str}``
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
event_type : str
|
|
292
|
+
``"agent_response"`` | ``"compliance_event"`` | ``"armor_run"``.
|
|
293
|
+
data : dict
|
|
294
|
+
Payload dict. **Not mutated** — a copy is enriched before writing.
|
|
295
|
+
pillar : str or None
|
|
296
|
+
CAIRO pillar name: ``"containment"`` | ``"attestation"`` |
|
|
297
|
+
``"interception"`` | ``"resilience"`` | ``"output_logic"``.
|
|
298
|
+
Used when ``action_type`` is not known or a pillar-level override
|
|
299
|
+
is needed.
|
|
300
|
+
action_type : str or None
|
|
301
|
+
Action key from ``ACTION_RISK_MAP`` (e.g. ``"prompt_injection_blocked"``,
|
|
302
|
+
``"pii_blocked"``, ``"transparency_banner_added"``). When supplied,
|
|
303
|
+
the most granular action-level controls are used.
|
|
304
|
+
|
|
305
|
+
Resolution Precedence
|
|
306
|
+
─────────────────────
|
|
307
|
+
``action_type`` (most granular) → ``pillar`` (defaults) → containment fallback.
|
|
308
|
+
|
|
309
|
+
Thread Safety
|
|
310
|
+
─────────────
|
|
311
|
+
The method is thread-safe: each call builds its own independent ``enriched``
|
|
312
|
+
dict before submitting to the shared executor. The ``data`` argument is
|
|
313
|
+
never shared between threads.
|
|
314
|
+
|
|
315
|
+
Notes
|
|
316
|
+
─────
|
|
317
|
+
The regulatory-tag resolution (CPU-only, < 1 ms) runs synchronously on
|
|
318
|
+
the caller's thread so any ``ValueError`` from invalid pillar/action names
|
|
319
|
+
surfaces immediately. Only the DynamoDB I/O is deferred.
|
|
320
|
+
"""
|
|
321
|
+
# Deferred import prevents circular dependency at module load time.
|
|
322
|
+
from cairo_sdk.governance_map import resolve_regulatory_tags # noqa: PLC0415
|
|
323
|
+
|
|
324
|
+
# Regulatory tag resolution is CPU-only: stays on caller's thread.
|
|
325
|
+
tags = resolve_regulatory_tags(pillar=pillar, action_type=action_type)
|
|
326
|
+
|
|
327
|
+
enriched: dict[str, Any] = dict(data)
|
|
328
|
+
enriched["regulatory_tags"] = tags.model_dump()
|
|
329
|
+
enriched["risk_level"] = tags.risk_level
|
|
330
|
+
enriched["risk_mitigation_value"] = {
|
|
331
|
+
"amount_usd": tags.risk_mitigation_value_usd,
|
|
332
|
+
"currency": "USD",
|
|
333
|
+
"description": (
|
|
334
|
+
f"Theoretical per-incident 'Saved Fine' for "
|
|
335
|
+
f"{action_type or pillar or 'unknown'} — represents mitigated "
|
|
336
|
+
f"regulatory risk. Risk level: {tags.risk_level}."
|
|
337
|
+
),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Fire-and-forget: DynamoDB I/O runs in background thread.
|
|
341
|
+
_EXECUTOR.submit(self._write_item, event_type, enriched)
|
|
342
|
+
|
|
343
|
+
# ------------------------------------------------------------------
|
|
344
|
+
# Internal: item normaliser (static — shared by scan_events and tests)
|
|
345
|
+
# ------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def _normalize_item(item: dict[str, Any]) -> dict[str, Any] | None:
|
|
349
|
+
"""
|
|
350
|
+
Flatten one raw DynamoDB item into a dashboard-ready dict.
|
|
351
|
+
|
|
352
|
+
The DynamoDB item has the shape written by ``log_event`` / ``log_request``:
|
|
353
|
+
``{ event_id, event_type, timestamp_utc, data: "<JSON string>" }``
|
|
354
|
+
|
|
355
|
+
The returned dict exposes every nested field at the top level so the
|
|
356
|
+
dashboard and report generator can work with plain pandas DataFrames
|
|
357
|
+
without any further transformation.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
item : dict
|
|
362
|
+
Raw DynamoDB item — ``"data"`` may be a JSON string or an already-
|
|
363
|
+
decoded dict (both are handled transparently).
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
dict or None
|
|
368
|
+
Flat normalised record, or ``None`` if parsing fails. Callers
|
|
369
|
+
silently skip ``None`` entries.
|
|
370
|
+
|
|
371
|
+
Returned Keys
|
|
372
|
+
-------------
|
|
373
|
+
``event_id``, ``event_type``, ``timestamp_utc``, ``pillar``,
|
|
374
|
+
``action_type``, ``risk_level``, ``risk_mitigation_value_usd``,
|
|
375
|
+
``nist_categories``, ``iso_annex_controls``, ``eu_articles``,
|
|
376
|
+
``eu_enforcement_tier``, ``correlation_id``.
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
raw_data = item.get("data", "{}")
|
|
380
|
+
data: dict[str, Any] = (
|
|
381
|
+
json.loads(raw_data) if isinstance(raw_data, str) else raw_data
|
|
382
|
+
)
|
|
383
|
+
tags: dict[str, Any] = data.get("regulatory_tags", {})
|
|
384
|
+
rmv: dict[str, Any] = data.get("risk_mitigation_value", {})
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
"event_id": item.get("event_id", ""),
|
|
388
|
+
"event_type": item.get("event_type", "compliance_event"),
|
|
389
|
+
"timestamp_utc": item.get("timestamp_utc", ""),
|
|
390
|
+
"pillar": tags.get("pillar") or data.get("pillar", "unknown"),
|
|
391
|
+
"action_type": (
|
|
392
|
+
tags.get("action_type") or data.get("action_type", "unknown")
|
|
393
|
+
),
|
|
394
|
+
"risk_level": (
|
|
395
|
+
tags.get("risk_level") or data.get("risk_level", "Medium")
|
|
396
|
+
),
|
|
397
|
+
"risk_mitigation_value_usd": float(
|
|
398
|
+
rmv.get("amount_usd")
|
|
399
|
+
or tags.get("risk_mitigation_value_usd", 0.0)
|
|
400
|
+
or 0.0
|
|
401
|
+
),
|
|
402
|
+
"nist_categories": tags.get("nist_categories", []),
|
|
403
|
+
"iso_annex_controls": tags.get("iso_annex_controls", []),
|
|
404
|
+
"eu_articles": tags.get("eu_articles", []),
|
|
405
|
+
"eu_enforcement_tier": tags.get("eu_enforcement_tier", ""),
|
|
406
|
+
"correlation_id": data.get("correlation_id"),
|
|
407
|
+
}
|
|
408
|
+
except Exception: # noqa: BLE001
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
# ------------------------------------------------------------------
|
|
412
|
+
# Read path — dashboard data fetching
|
|
413
|
+
# ------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
def scan_events(self, limit: int = 500) -> list[dict[str, Any]]:
|
|
416
|
+
"""
|
|
417
|
+
Scan the CairoAnalytics DynamoDB table and return normalised records.
|
|
418
|
+
|
|
419
|
+
Intended for the AXON Executive Dashboard, which calls this method at
|
|
420
|
+
most every 60 seconds (``@st.cache_data(ttl=60)``).
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
limit : int
|
|
425
|
+
Maximum number of records to return after sorting. Pagination
|
|
426
|
+
exhausts all DynamoDB pages first, then trims to ``limit``.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
list[dict]
|
|
431
|
+
Normalised records (output of ``_normalize_item``), sorted by
|
|
432
|
+
``timestamp_utc`` descending (most-recent first).
|
|
433
|
+
Returns ``[]`` if DynamoDB is unavailable — the dashboard then
|
|
434
|
+
falls back to demo data automatically.
|
|
435
|
+
|
|
436
|
+
Notes
|
|
437
|
+
─────
|
|
438
|
+
Uses a full table scan (acceptable for compliance audit tables with
|
|
439
|
+
< 100 k items). For larger deployments, add a GSI on ``timestamp_utc``
|
|
440
|
+
and switch to a ``query`` call for O(log n) reads.
|
|
441
|
+
|
|
442
|
+
Error Handling
|
|
443
|
+
──────────────
|
|
444
|
+
``BotoCoreError`` / ``ClientError`` are caught and logged as WARNING.
|
|
445
|
+
Any other exception is also caught and logged. The method **never
|
|
446
|
+
raises** — a connectivity blip must not crash the dashboard.
|
|
447
|
+
"""
|
|
448
|
+
results: list[dict[str, Any]] = []
|
|
449
|
+
try:
|
|
450
|
+
table = self._get_table()
|
|
451
|
+
scan_kwargs: dict[str, Any] = {}
|
|
452
|
+
|
|
453
|
+
while True:
|
|
454
|
+
response = table.scan(**scan_kwargs)
|
|
455
|
+
for raw_item in response.get("Items", []):
|
|
456
|
+
norm = self._normalize_item(raw_item)
|
|
457
|
+
if norm:
|
|
458
|
+
results.append(norm)
|
|
459
|
+
|
|
460
|
+
if "LastEvaluatedKey" not in response:
|
|
461
|
+
break
|
|
462
|
+
scan_kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
|
|
463
|
+
|
|
464
|
+
results.sort(key=lambda x: x.get("timestamp_utc", ""), reverse=True)
|
|
465
|
+
return results[:limit]
|
|
466
|
+
|
|
467
|
+
except _BOTOCORE_ERRORS as exc:
|
|
468
|
+
_logger.warning(
|
|
469
|
+
"DynamoDB scan failed [table=%s]: %s: %s",
|
|
470
|
+
self.table_name, type(exc).__name__, exc,
|
|
471
|
+
)
|
|
472
|
+
return []
|
|
473
|
+
except Exception as exc: # noqa: BLE001
|
|
474
|
+
_logger.warning("Unexpected error in scan_events: %s", exc)
|
|
475
|
+
return []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# CAIRO SDK — Attestation pillar (compliance proof, audit)
|