crprotocol 2.0.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.
- crp/__init__.py +126 -0
- crp/__main__.py +8 -0
- crp/_typing.py +27 -0
- crp/_version.py +5 -0
- crp/adapters.py +31 -0
- crp/advanced/__init__.py +40 -0
- crp/advanced/auto_ingest.py +400 -0
- crp/advanced/cqs.py +235 -0
- crp/advanced/cross_window.py +477 -0
- crp/advanced/curator.py +265 -0
- crp/advanced/feedback.py +146 -0
- crp/advanced/hierarchical.py +211 -0
- crp/advanced/meta_learning.py +401 -0
- crp/advanced/parallel.py +98 -0
- crp/advanced/review_cycle.py +329 -0
- crp/advanced/scale_mode.py +129 -0
- crp/advanced/source_grounding.py +207 -0
- crp/ckf/__init__.py +35 -0
- crp/ckf/community.py +377 -0
- crp/ckf/fabric.py +445 -0
- crp/ckf/gc.py +175 -0
- crp/ckf/graph_walk.py +87 -0
- crp/ckf/merge.py +133 -0
- crp/ckf/pattern_query.py +122 -0
- crp/ckf/pubsub.py +128 -0
- crp/ckf/semantic.py +207 -0
- crp/cli/__init__.py +7 -0
- crp/cli/main.py +329 -0
- crp/cli/sidecar.py +929 -0
- crp/cli/startup.py +272 -0
- crp/continuation/__init__.py +103 -0
- crp/continuation/completion.py +348 -0
- crp/continuation/degradation.py +157 -0
- crp/continuation/document_map.py +160 -0
- crp/continuation/flow.py +109 -0
- crp/continuation/gap.py +419 -0
- crp/continuation/manager.py +484 -0
- crp/continuation/quality_monitor.py +179 -0
- crp/continuation/stitch.py +419 -0
- crp/continuation/trigger.py +142 -0
- crp/continuation/voice.py +157 -0
- crp/core/__init__.py +69 -0
- crp/core/batch.py +77 -0
- crp/core/circuit_breaker.py +116 -0
- crp/core/config.py +377 -0
- crp/core/context_tools.py +540 -0
- crp/core/dispatch_router.py +3977 -0
- crp/core/errors.py +128 -0
- crp/core/extraction_facade.py +384 -0
- crp/core/facilitator.py +713 -0
- crp/core/idempotency.py +215 -0
- crp/core/orchestrator.py +1435 -0
- crp/core/relay_strategies.py +613 -0
- crp/core/security_manager.py +140 -0
- crp/core/session.py +134 -0
- crp/core/task_intent.py +36 -0
- crp/core/window.py +363 -0
- crp/envelope/__init__.py +30 -0
- crp/envelope/builder.py +288 -0
- crp/envelope/decomposer.py +236 -0
- crp/envelope/formatter.py +168 -0
- crp/envelope/packer.py +211 -0
- crp/envelope/reranker.py +209 -0
- crp/envelope/scoring.py +310 -0
- crp/extraction/__init__.py +45 -0
- crp/extraction/complexity.py +96 -0
- crp/extraction/contradiction.py +132 -0
- crp/extraction/pipeline.py +360 -0
- crp/extraction/quality_gate.py +237 -0
- crp/extraction/stage1_regex.py +173 -0
- crp/extraction/stage2_statistical.py +244 -0
- crp/extraction/stage3_gliner.py +210 -0
- crp/extraction/stage4_uie.py +183 -0
- crp/extraction/stage5_discourse.py +175 -0
- crp/extraction/stage6_llm.py +178 -0
- crp/extraction/structured_output.py +219 -0
- crp/extraction/types.py +299 -0
- crp/license_guard.py +722 -0
- crp/observability/__init__.py +30 -0
- crp/observability/audit.py +118 -0
- crp/observability/events.py +233 -0
- crp/observability/metrics.py +264 -0
- crp/observability/quality.py +135 -0
- crp/observability/structured_logging.py +81 -0
- crp/observability/telemetry.py +117 -0
- crp/provenance/__init__.py +314 -0
- crp/provenance/_embeddings.py +97 -0
- crp/provenance/_types.py +378 -0
- crp/provenance/attribution_scorer.py +252 -0
- crp/provenance/claim_detector.py +229 -0
- crp/provenance/contradiction_detector.py +243 -0
- crp/provenance/distortion_detector.py +397 -0
- crp/provenance/entailment_verifier.py +358 -0
- crp/provenance/fabrication_detector.py +203 -0
- crp/provenance/hallucination_scorer.py +320 -0
- crp/provenance/omission_analyzer.py +106 -0
- crp/provenance/provenance_chain.py +205 -0
- crp/provenance/report_generator.py +440 -0
- crp/providers/__init__.py +43 -0
- crp/providers/anthropic.py +270 -0
- crp/providers/base.py +135 -0
- crp/providers/custom.py +63 -0
- crp/providers/diagnostic.py +251 -0
- crp/providers/llamacpp.py +224 -0
- crp/providers/manager.py +139 -0
- crp/providers/ollama.py +243 -0
- crp/providers/openai.py +628 -0
- crp/providers/tokenizers.py +48 -0
- crp/py.typed +0 -0
- crp/resources/__init__.py +53 -0
- crp/resources/adaptive_allocator.py +525 -0
- crp/resources/cost_model.py +388 -0
- crp/resources/overhead_manager.py +217 -0
- crp/resources/resource_manager.py +262 -0
- crp/schemas/__init__.py +20 -0
- crp/schemas/cost-estimate.json +33 -0
- crp/schemas/crp-error.json +43 -0
- crp/schemas/envelope-preview.json +40 -0
- crp/schemas/persisted-state-header.json +27 -0
- crp/schemas/quality-report.json +94 -0
- crp/schemas/session-handle.json +33 -0
- crp/schemas/session-status.json +57 -0
- crp/schemas/stream-event.json +18 -0
- crp/schemas/task-intent.json +42 -0
- crp/security/__init__.py +93 -0
- crp/security/audit_trail.py +392 -0
- crp/security/binding.py +192 -0
- crp/security/compliance.py +813 -0
- crp/security/consent.py +593 -0
- crp/security/embedding_defense.py +161 -0
- crp/security/encryption.py +202 -0
- crp/security/injection.py +335 -0
- crp/security/integrity.py +267 -0
- crp/security/privacy.py +662 -0
- crp/security/quarantine.py +249 -0
- crp/security/rbac.py +221 -0
- crp/security/validation.py +164 -0
- crp/state/__init__.py +31 -0
- crp/state/cold_storage.py +258 -0
- crp/state/compaction.py +263 -0
- crp/state/critical_state.py +104 -0
- crp/state/event_log.py +313 -0
- crp/state/fact.py +189 -0
- crp/state/serialization.py +189 -0
- crp/state/session_cleanup.py +77 -0
- crp/state/snapshot.py +290 -0
- crp/state/warm_store.py +346 -0
- crprotocol-2.0.0.dist-info/METADATA +1295 -0
- crprotocol-2.0.0.dist-info/RECORD +153 -0
- crprotocol-2.0.0.dist-info/WHEEL +4 -0
- crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
- crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
- crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
crp/security/consent.py
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Consent & data rights management — GDPR + EU AI Act transparency (§7.13).
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
- Consent state management (opt-in, opt-out, withdrawal)
|
|
7
|
+
- Purpose limitation (data processing tied to declared purposes)
|
|
8
|
+
- Processing records (GDPR Article 30 / EU AI Act Art. 12)
|
|
9
|
+
- Data portability support (export in standard format)
|
|
10
|
+
- Human oversight controls (EU AI Act Art. 14)
|
|
11
|
+
|
|
12
|
+
EU AI Act: Art. 13 (transparency), Art. 14 (human oversight), Art. 12 (record-keeping)
|
|
13
|
+
ISO 42001: A.6.2.3 (human oversight), A.6.2.5 (data collection), A.6.2.7 (data subject rights)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("crp.security.consent")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Consent management
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConsentStatus(str, Enum):
|
|
34
|
+
"""Consent states (§7.13.1)."""
|
|
35
|
+
|
|
36
|
+
NOT_GIVEN = "not_given" # Default — no consent recorded
|
|
37
|
+
GRANTED = "granted" # Consent explicitly given
|
|
38
|
+
WITHDRAWN = "withdrawn" # Consent was given then withdrawn
|
|
39
|
+
DENIED = "denied" # Consent explicitly denied
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ProcessingPurpose(str, Enum):
|
|
43
|
+
"""Data processing purposes — must be declared before processing (§7.13.2).
|
|
44
|
+
|
|
45
|
+
EU AI Act Art. 10: Data governance requires clear purpose limitation.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
CONTEXT_MANAGEMENT = "context_management" # Core CRP functionality
|
|
49
|
+
FACT_EXTRACTION = "fact_extraction" # Extracting knowledge from text
|
|
50
|
+
KNOWLEDGE_GRAPH = "knowledge_graph" # Building knowledge graphs
|
|
51
|
+
QUALITY_ASSESSMENT = "quality_assessment" # Assessing output quality
|
|
52
|
+
SECURITY_SCANNING = "security_scanning" # Injection detection, PII scanning
|
|
53
|
+
ANALYTICS = "analytics" # Usage analytics (opt-in only)
|
|
54
|
+
IMPROVEMENT = "improvement" # Service improvement (opt-in only)
|
|
55
|
+
EXPORT = "export" # Data export / portability
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Default purposes that CRP requires for core functionality
|
|
59
|
+
_REQUIRED_PURPOSES: frozenset[ProcessingPurpose] = frozenset(
|
|
60
|
+
{
|
|
61
|
+
ProcessingPurpose.CONTEXT_MANAGEMENT,
|
|
62
|
+
ProcessingPurpose.FACT_EXTRACTION,
|
|
63
|
+
ProcessingPurpose.KNOWLEDGE_GRAPH,
|
|
64
|
+
ProcessingPurpose.QUALITY_ASSESSMENT,
|
|
65
|
+
ProcessingPurpose.SECURITY_SCANNING,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Purposes that require explicit opt-in
|
|
70
|
+
_OPT_IN_PURPOSES: frozenset[ProcessingPurpose] = frozenset(
|
|
71
|
+
{
|
|
72
|
+
ProcessingPurpose.ANALYTICS,
|
|
73
|
+
ProcessingPurpose.IMPROVEMENT,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class ConsentRecord:
|
|
80
|
+
"""Records a consent decision (§7.13.1)."""
|
|
81
|
+
|
|
82
|
+
consent_id: str
|
|
83
|
+
purpose: ProcessingPurpose
|
|
84
|
+
status: ConsentStatus
|
|
85
|
+
recorded_at: float = field(default_factory=time.time)
|
|
86
|
+
expires_at: float = 0.0 # 0 = no expiry
|
|
87
|
+
reason: str = "" # Why consent was granted/denied/withdrawn
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class ConsentState:
|
|
92
|
+
"""Aggregate consent state for a session."""
|
|
93
|
+
|
|
94
|
+
session_id: str
|
|
95
|
+
records: dict[ProcessingPurpose, ConsentRecord] = field(default_factory=dict)
|
|
96
|
+
created_at: float = field(default_factory=time.time)
|
|
97
|
+
|
|
98
|
+
def is_granted(self, purpose: ProcessingPurpose) -> bool:
|
|
99
|
+
"""Check if consent is currently granted for a purpose."""
|
|
100
|
+
rec = self.records.get(purpose)
|
|
101
|
+
if rec is None:
|
|
102
|
+
# Required purposes are implicitly consented (core functionality)
|
|
103
|
+
return purpose in _REQUIRED_PURPOSES
|
|
104
|
+
if rec.status != ConsentStatus.GRANTED:
|
|
105
|
+
return False
|
|
106
|
+
# Check expiry
|
|
107
|
+
if rec.expires_at > 0 and time.time() > rec.expires_at:
|
|
108
|
+
return False
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def denied_purposes(self) -> list[ProcessingPurpose]:
|
|
112
|
+
"""Return purposes that have been explicitly denied or withdrawn."""
|
|
113
|
+
denied = []
|
|
114
|
+
for purpose, rec in self.records.items():
|
|
115
|
+
if rec.status in (ConsentStatus.DENIED, ConsentStatus.WITHDRAWN):
|
|
116
|
+
denied.append(purpose)
|
|
117
|
+
return denied
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict[str, Any]:
|
|
120
|
+
return {
|
|
121
|
+
"session_id": self.session_id,
|
|
122
|
+
"created_at": self.created_at,
|
|
123
|
+
"purposes": {
|
|
124
|
+
p.value: {
|
|
125
|
+
"status": r.status.value,
|
|
126
|
+
"recorded_at": r.recorded_at,
|
|
127
|
+
"reason": r.reason,
|
|
128
|
+
}
|
|
129
|
+
for p, r in self.records.items()
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ConsentManager:
|
|
135
|
+
"""Manage consent state for data processing purposes (§7.13.1).
|
|
136
|
+
|
|
137
|
+
EU AI Act Art. 13: Transparency requires clear communication about
|
|
138
|
+
how data is processed and for what purposes.
|
|
139
|
+
|
|
140
|
+
Usage::
|
|
141
|
+
|
|
142
|
+
cm = ConsentManager("session-123")
|
|
143
|
+
cm.grant(ProcessingPurpose.ANALYTICS, reason="User opted in")
|
|
144
|
+
if cm.check(ProcessingPurpose.ANALYTICS):
|
|
145
|
+
# Process analytics data
|
|
146
|
+
...
|
|
147
|
+
cm.withdraw(ProcessingPurpose.ANALYTICS, reason="User opted out")
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, session_id: str) -> None:
|
|
151
|
+
self._state = ConsentState(session_id=session_id)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def state(self) -> ConsentState:
|
|
155
|
+
return self._state
|
|
156
|
+
|
|
157
|
+
def grant(
|
|
158
|
+
self,
|
|
159
|
+
purpose: ProcessingPurpose,
|
|
160
|
+
reason: str = "",
|
|
161
|
+
expires_hours: float = 0.0,
|
|
162
|
+
) -> ConsentRecord:
|
|
163
|
+
"""Grant consent for a processing purpose."""
|
|
164
|
+
now = time.time()
|
|
165
|
+
record = ConsentRecord(
|
|
166
|
+
consent_id=f"consent-{uuid.uuid4().hex[:8]}",
|
|
167
|
+
purpose=purpose,
|
|
168
|
+
status=ConsentStatus.GRANTED,
|
|
169
|
+
recorded_at=now,
|
|
170
|
+
expires_at=now + (expires_hours * 3600) if expires_hours > 0 else 0.0,
|
|
171
|
+
reason=reason,
|
|
172
|
+
)
|
|
173
|
+
self._state.records[purpose] = record
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
"Consent granted: purpose=%s, session=%s, reason=%s",
|
|
177
|
+
purpose.value,
|
|
178
|
+
self._state.session_id,
|
|
179
|
+
reason or "(none)",
|
|
180
|
+
)
|
|
181
|
+
return record
|
|
182
|
+
|
|
183
|
+
def deny(
|
|
184
|
+
self,
|
|
185
|
+
purpose: ProcessingPurpose,
|
|
186
|
+
reason: str = "",
|
|
187
|
+
) -> ConsentRecord:
|
|
188
|
+
"""Deny consent for a processing purpose."""
|
|
189
|
+
if purpose in _REQUIRED_PURPOSES:
|
|
190
|
+
logger.warning(
|
|
191
|
+
"Consent denied for required purpose %s — core functionality "
|
|
192
|
+
"may be degraded. Session=%s",
|
|
193
|
+
purpose.value,
|
|
194
|
+
self._state.session_id,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
record = ConsentRecord(
|
|
198
|
+
consent_id=f"consent-{uuid.uuid4().hex[:8]}",
|
|
199
|
+
purpose=purpose,
|
|
200
|
+
status=ConsentStatus.DENIED,
|
|
201
|
+
recorded_at=time.time(),
|
|
202
|
+
reason=reason,
|
|
203
|
+
)
|
|
204
|
+
self._state.records[purpose] = record
|
|
205
|
+
|
|
206
|
+
logger.info(
|
|
207
|
+
"Consent denied: purpose=%s, session=%s, reason=%s",
|
|
208
|
+
purpose.value,
|
|
209
|
+
self._state.session_id,
|
|
210
|
+
reason or "(none)",
|
|
211
|
+
)
|
|
212
|
+
return record
|
|
213
|
+
|
|
214
|
+
def withdraw(
|
|
215
|
+
self,
|
|
216
|
+
purpose: ProcessingPurpose,
|
|
217
|
+
reason: str = "",
|
|
218
|
+
) -> ConsentRecord:
|
|
219
|
+
"""Withdraw previously granted consent."""
|
|
220
|
+
record = ConsentRecord(
|
|
221
|
+
consent_id=f"consent-{uuid.uuid4().hex[:8]}",
|
|
222
|
+
purpose=purpose,
|
|
223
|
+
status=ConsentStatus.WITHDRAWN,
|
|
224
|
+
recorded_at=time.time(),
|
|
225
|
+
reason=reason,
|
|
226
|
+
)
|
|
227
|
+
self._state.records[purpose] = record
|
|
228
|
+
|
|
229
|
+
logger.info(
|
|
230
|
+
"Consent withdrawn: purpose=%s, session=%s, reason=%s",
|
|
231
|
+
purpose.value,
|
|
232
|
+
self._state.session_id,
|
|
233
|
+
reason or "(none)",
|
|
234
|
+
)
|
|
235
|
+
return record
|
|
236
|
+
|
|
237
|
+
def check(self, purpose: ProcessingPurpose) -> bool:
|
|
238
|
+
"""Check if consent is granted for a purpose.
|
|
239
|
+
|
|
240
|
+
Required purposes return True by default (core CRP functionality).
|
|
241
|
+
Opt-in purposes return False until explicitly granted.
|
|
242
|
+
"""
|
|
243
|
+
return self._state.is_granted(purpose)
|
|
244
|
+
|
|
245
|
+
def check_required(self, purpose: ProcessingPurpose) -> bool:
|
|
246
|
+
"""Check consent and raise if denied for a required purpose."""
|
|
247
|
+
if self.check(purpose):
|
|
248
|
+
return True
|
|
249
|
+
if purpose in _REQUIRED_PURPOSES:
|
|
250
|
+
raise ConsentRequiredError(
|
|
251
|
+
f"Consent required for '{purpose.value}' — this is a core "
|
|
252
|
+
f"CRP function. Grant consent or accept degraded functionality."
|
|
253
|
+
)
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def to_dict(self) -> dict[str, Any]:
|
|
257
|
+
"""Export consent state for compliance reporting."""
|
|
258
|
+
return self._state.to_dict()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ConsentRequiredError(Exception):
|
|
262
|
+
"""Raised when a required consent is missing."""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Processing records (GDPR Article 30)
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class ProcessingActivity:
|
|
272
|
+
"""Records a single data processing activity (GDPR Art. 30) (§7.13.3).
|
|
273
|
+
|
|
274
|
+
EU AI Act Art. 12: Systems must automatically record events relevant
|
|
275
|
+
for identifying risks and substantial modifications.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
activity_id: str
|
|
279
|
+
purpose: ProcessingPurpose
|
|
280
|
+
data_categories: list[str] # e.g. ["text_input", "extracted_facts"]
|
|
281
|
+
legal_basis: str # e.g. "legitimate_interest", "consent", "contract"
|
|
282
|
+
timestamp: float = field(default_factory=time.time)
|
|
283
|
+
session_id: str = ""
|
|
284
|
+
input_size_bytes: int = 0
|
|
285
|
+
output_size_bytes: int = 0
|
|
286
|
+
data_subjects: str = "user" # Who the data relates to
|
|
287
|
+
retention_period: str = "" # e.g. "24h", "30d", "session"
|
|
288
|
+
automated_decision: bool = False # Was there automated decision-making?
|
|
289
|
+
human_oversight: bool = False # Was human oversight available?
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class ProcessingRecordKeeper:
|
|
293
|
+
"""Maintain GDPR Article 30 processing records (§7.13.3).
|
|
294
|
+
|
|
295
|
+
Every data processing activity within a CRP session is recorded
|
|
296
|
+
with its purpose, legal basis, data categories, and retention.
|
|
297
|
+
|
|
298
|
+
Usage::
|
|
299
|
+
|
|
300
|
+
keeper = ProcessingRecordKeeper("session-123")
|
|
301
|
+
keeper.record(
|
|
302
|
+
purpose=ProcessingPurpose.FACT_EXTRACTION,
|
|
303
|
+
data_categories=["text_input"],
|
|
304
|
+
legal_basis="legitimate_interest",
|
|
305
|
+
input_size_bytes=4096,
|
|
306
|
+
)
|
|
307
|
+
records = keeper.export()
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def __init__(self, session_id: str) -> None:
|
|
311
|
+
self._session_id = session_id
|
|
312
|
+
self._activities: list[ProcessingActivity] = []
|
|
313
|
+
|
|
314
|
+
def record(
|
|
315
|
+
self,
|
|
316
|
+
purpose: ProcessingPurpose,
|
|
317
|
+
data_categories: list[str],
|
|
318
|
+
legal_basis: str = "legitimate_interest",
|
|
319
|
+
input_size_bytes: int = 0,
|
|
320
|
+
output_size_bytes: int = 0,
|
|
321
|
+
automated_decision: bool = False,
|
|
322
|
+
human_oversight: bool = False,
|
|
323
|
+
retention_period: str = "session",
|
|
324
|
+
) -> ProcessingActivity:
|
|
325
|
+
"""Record a data processing activity."""
|
|
326
|
+
activity = ProcessingActivity(
|
|
327
|
+
activity_id=f"proc-{uuid.uuid4().hex[:8]}",
|
|
328
|
+
purpose=purpose,
|
|
329
|
+
data_categories=data_categories,
|
|
330
|
+
legal_basis=legal_basis,
|
|
331
|
+
timestamp=time.time(),
|
|
332
|
+
session_id=self._session_id,
|
|
333
|
+
input_size_bytes=input_size_bytes,
|
|
334
|
+
output_size_bytes=output_size_bytes,
|
|
335
|
+
automated_decision=automated_decision,
|
|
336
|
+
human_oversight=human_oversight,
|
|
337
|
+
retention_period=retention_period,
|
|
338
|
+
)
|
|
339
|
+
self._activities.append(activity)
|
|
340
|
+
return activity
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def activity_count(self) -> int:
|
|
344
|
+
return len(self._activities)
|
|
345
|
+
|
|
346
|
+
def export(self) -> list[dict[str, Any]]:
|
|
347
|
+
"""Export all processing records for regulatory review."""
|
|
348
|
+
return [
|
|
349
|
+
{
|
|
350
|
+
"activity_id": a.activity_id,
|
|
351
|
+
"purpose": a.purpose.value,
|
|
352
|
+
"data_categories": a.data_categories,
|
|
353
|
+
"legal_basis": a.legal_basis,
|
|
354
|
+
"timestamp": a.timestamp,
|
|
355
|
+
"session_id": a.session_id,
|
|
356
|
+
"input_size_bytes": a.input_size_bytes,
|
|
357
|
+
"output_size_bytes": a.output_size_bytes,
|
|
358
|
+
"data_subjects": a.data_subjects,
|
|
359
|
+
"retention_period": a.retention_period,
|
|
360
|
+
"automated_decision": a.automated_decision,
|
|
361
|
+
"human_oversight": a.human_oversight,
|
|
362
|
+
}
|
|
363
|
+
for a in self._activities
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
def summary(self) -> dict[str, Any]:
|
|
367
|
+
"""Summarize processing activities for compliance dashboard."""
|
|
368
|
+
by_purpose: dict[str, int] = {}
|
|
369
|
+
total_input = 0
|
|
370
|
+
total_output = 0
|
|
371
|
+
for a in self._activities:
|
|
372
|
+
by_purpose[a.purpose.value] = by_purpose.get(a.purpose.value, 0) + 1
|
|
373
|
+
total_input += a.input_size_bytes
|
|
374
|
+
total_output += a.output_size_bytes
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
"session_id": self._session_id,
|
|
378
|
+
"total_activities": len(self._activities),
|
|
379
|
+
"by_purpose": by_purpose,
|
|
380
|
+
"total_input_bytes": total_input,
|
|
381
|
+
"total_output_bytes": total_output,
|
|
382
|
+
"automated_decisions": sum(
|
|
383
|
+
1 for a in self._activities if a.automated_decision
|
|
384
|
+
),
|
|
385
|
+
"human_oversight_available": sum(
|
|
386
|
+
1 for a in self._activities if a.human_oversight
|
|
387
|
+
),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
# Human oversight controls (EU AI Act Art. 14)
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class HumanOversightLevel(str, Enum):
|
|
397
|
+
"""Levels of human oversight (EU AI Act Art. 14) (§7.13.4)."""
|
|
398
|
+
|
|
399
|
+
NONE = "none" # Fully autonomous (CRP default for context management)
|
|
400
|
+
INFORMED = "informed" # Human is informed of AI actions (logging/alerts)
|
|
401
|
+
APPROVAL = "approval" # Human must approve before action
|
|
402
|
+
CONTROL = "control" # Human can intervene and override at any point
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@dataclass
|
|
406
|
+
class OversightConfig:
|
|
407
|
+
"""Human oversight configuration (§7.13.4).
|
|
408
|
+
|
|
409
|
+
EU AI Act Art. 14: High-risk AI systems must be designed to allow
|
|
410
|
+
effective human oversight during their period of use.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
level: HumanOversightLevel = HumanOversightLevel.INFORMED
|
|
414
|
+
require_approval_for_dispatch: bool = False
|
|
415
|
+
require_approval_for_ingest: bool = False
|
|
416
|
+
require_approval_for_export: bool = False
|
|
417
|
+
require_approval_for_deletion: bool = False
|
|
418
|
+
halt_on_injection_detection: bool = False # Halt if injection detected
|
|
419
|
+
halt_on_pii_detection: bool = False # Halt if PII detected
|
|
420
|
+
max_autonomous_dispatches: int = 0 # 0 = unlimited
|
|
421
|
+
alert_on_quality_below: str = "" # Quality tier threshold (e.g. "C")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@dataclass
|
|
425
|
+
class OversightEvent:
|
|
426
|
+
"""Records a human oversight event."""
|
|
427
|
+
|
|
428
|
+
event_id: str
|
|
429
|
+
event_type: str # "approval_requested", "approved", "denied", "halted"
|
|
430
|
+
operation: str # "dispatch", "ingest", "export", "deletion"
|
|
431
|
+
timestamp: float = field(default_factory=time.time)
|
|
432
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
433
|
+
approved_by: str = "" # Hash of approver identity
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class HumanOversightController:
|
|
437
|
+
"""Implements human oversight controls (EU AI Act Art. 14) (§7.13.4).
|
|
438
|
+
|
|
439
|
+
ISO 42001 A.6.2.3: Organizations must establish processes for human
|
|
440
|
+
oversight of AI systems appropriate to the risk level.
|
|
441
|
+
|
|
442
|
+
Usage::
|
|
443
|
+
|
|
444
|
+
hoc = HumanOversightController(OversightConfig(
|
|
445
|
+
level=HumanOversightLevel.APPROVAL,
|
|
446
|
+
require_approval_for_dispatch=True,
|
|
447
|
+
))
|
|
448
|
+
|
|
449
|
+
# Before dispatch:
|
|
450
|
+
if hoc.requires_approval("dispatch"):
|
|
451
|
+
approval = hoc.request_approval("dispatch", {"task": "..."})
|
|
452
|
+
# ... wait for human approval ...
|
|
453
|
+
hoc.record_decision(approval.event_id, approved=True)
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
def __init__(self, config: OversightConfig | None = None) -> None:
|
|
457
|
+
self._config = config or OversightConfig()
|
|
458
|
+
self._events: list[OversightEvent] = []
|
|
459
|
+
self._autonomous_count = 0
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def config(self) -> OversightConfig:
|
|
463
|
+
return self._config
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def level(self) -> HumanOversightLevel:
|
|
467
|
+
return self._config.level
|
|
468
|
+
|
|
469
|
+
def requires_approval(self, operation: str) -> bool:
|
|
470
|
+
"""Check if an operation requires human approval."""
|
|
471
|
+
if self._config.level in (
|
|
472
|
+
HumanOversightLevel.NONE,
|
|
473
|
+
HumanOversightLevel.INFORMED,
|
|
474
|
+
):
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
if operation == "dispatch":
|
|
478
|
+
return self._config.require_approval_for_dispatch
|
|
479
|
+
if operation == "ingest":
|
|
480
|
+
return self._config.require_approval_for_ingest
|
|
481
|
+
if operation == "export":
|
|
482
|
+
return self._config.require_approval_for_export
|
|
483
|
+
if operation == "deletion":
|
|
484
|
+
return self._config.require_approval_for_deletion
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
def check_autonomous_limit(self) -> bool:
|
|
488
|
+
"""Check if the autonomous dispatch limit has been reached.
|
|
489
|
+
|
|
490
|
+
Returns True if more autonomous dispatches are allowed.
|
|
491
|
+
"""
|
|
492
|
+
if self._config.max_autonomous_dispatches <= 0:
|
|
493
|
+
return True
|
|
494
|
+
return self._autonomous_count < self._config.max_autonomous_dispatches
|
|
495
|
+
|
|
496
|
+
def record_autonomous_dispatch(self) -> None:
|
|
497
|
+
"""Record an autonomous dispatch for limit tracking."""
|
|
498
|
+
self._autonomous_count += 1
|
|
499
|
+
|
|
500
|
+
def request_approval(
|
|
501
|
+
self,
|
|
502
|
+
operation: str,
|
|
503
|
+
details: dict[str, Any] | None = None,
|
|
504
|
+
) -> OversightEvent:
|
|
505
|
+
"""Create an approval request event."""
|
|
506
|
+
event = OversightEvent(
|
|
507
|
+
event_id=f"oversight-{uuid.uuid4().hex[:8]}",
|
|
508
|
+
event_type="approval_requested",
|
|
509
|
+
operation=operation,
|
|
510
|
+
details=details or {},
|
|
511
|
+
)
|
|
512
|
+
self._events.append(event)
|
|
513
|
+
|
|
514
|
+
logger.info(
|
|
515
|
+
"Human oversight: approval requested for %s (event=%s)",
|
|
516
|
+
operation,
|
|
517
|
+
event.event_id,
|
|
518
|
+
)
|
|
519
|
+
return event
|
|
520
|
+
|
|
521
|
+
def record_decision(
|
|
522
|
+
self,
|
|
523
|
+
event_id: str,
|
|
524
|
+
approved: bool,
|
|
525
|
+
approved_by: str = "",
|
|
526
|
+
reason: str = "",
|
|
527
|
+
) -> OversightEvent:
|
|
528
|
+
"""Record a human oversight decision."""
|
|
529
|
+
event = OversightEvent(
|
|
530
|
+
event_id=event_id,
|
|
531
|
+
event_type="approved" if approved else "denied",
|
|
532
|
+
operation="decision",
|
|
533
|
+
details={"reason": reason},
|
|
534
|
+
approved_by=approved_by,
|
|
535
|
+
)
|
|
536
|
+
self._events.append(event)
|
|
537
|
+
|
|
538
|
+
logger.info(
|
|
539
|
+
"Human oversight: %s (event=%s, by=%s)",
|
|
540
|
+
"approved" if approved else "DENIED",
|
|
541
|
+
event_id,
|
|
542
|
+
approved_by or "anonymous",
|
|
543
|
+
)
|
|
544
|
+
return event
|
|
545
|
+
|
|
546
|
+
def record_halt(
|
|
547
|
+
self,
|
|
548
|
+
operation: str,
|
|
549
|
+
reason: str,
|
|
550
|
+
details: dict[str, Any] | None = None,
|
|
551
|
+
) -> OversightEvent:
|
|
552
|
+
"""Record a halt event (system stopped due to policy)."""
|
|
553
|
+
event = OversightEvent(
|
|
554
|
+
event_id=f"halt-{uuid.uuid4().hex[:8]}",
|
|
555
|
+
event_type="halted",
|
|
556
|
+
operation=operation,
|
|
557
|
+
details={"reason": reason, **(details or {})},
|
|
558
|
+
)
|
|
559
|
+
self._events.append(event)
|
|
560
|
+
|
|
561
|
+
logger.warning(
|
|
562
|
+
"Human oversight: HALTED %s — %s", operation, reason
|
|
563
|
+
)
|
|
564
|
+
return event
|
|
565
|
+
|
|
566
|
+
def should_halt_on_injection(self) -> bool:
|
|
567
|
+
"""Check if processing should halt when injection is detected."""
|
|
568
|
+
return self._config.halt_on_injection_detection
|
|
569
|
+
|
|
570
|
+
def should_halt_on_pii(self) -> bool:
|
|
571
|
+
"""Check if processing should halt when PII is detected."""
|
|
572
|
+
return self._config.halt_on_pii_detection
|
|
573
|
+
|
|
574
|
+
def to_dict(self) -> dict[str, Any]:
|
|
575
|
+
"""Export oversight state for compliance reporting."""
|
|
576
|
+
return {
|
|
577
|
+
"level": self._config.level.value,
|
|
578
|
+
"autonomous_dispatches": self._autonomous_count,
|
|
579
|
+
"max_autonomous_dispatches": self._config.max_autonomous_dispatches,
|
|
580
|
+
"total_oversight_events": len(self._events),
|
|
581
|
+
"approvals_requested": sum(
|
|
582
|
+
1 for e in self._events if e.event_type == "approval_requested"
|
|
583
|
+
),
|
|
584
|
+
"approved": sum(
|
|
585
|
+
1 for e in self._events if e.event_type == "approved"
|
|
586
|
+
),
|
|
587
|
+
"denied": sum(
|
|
588
|
+
1 for e in self._events if e.event_type == "denied"
|
|
589
|
+
),
|
|
590
|
+
"halts": sum(
|
|
591
|
+
1 for e in self._events if e.event_type == "halted"
|
|
592
|
+
),
|
|
593
|
+
}
|