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.
Files changed (153) hide show
  1. crp/__init__.py +126 -0
  2. crp/__main__.py +8 -0
  3. crp/_typing.py +27 -0
  4. crp/_version.py +5 -0
  5. crp/adapters.py +31 -0
  6. crp/advanced/__init__.py +40 -0
  7. crp/advanced/auto_ingest.py +400 -0
  8. crp/advanced/cqs.py +235 -0
  9. crp/advanced/cross_window.py +477 -0
  10. crp/advanced/curator.py +265 -0
  11. crp/advanced/feedback.py +146 -0
  12. crp/advanced/hierarchical.py +211 -0
  13. crp/advanced/meta_learning.py +401 -0
  14. crp/advanced/parallel.py +98 -0
  15. crp/advanced/review_cycle.py +329 -0
  16. crp/advanced/scale_mode.py +129 -0
  17. crp/advanced/source_grounding.py +207 -0
  18. crp/ckf/__init__.py +35 -0
  19. crp/ckf/community.py +377 -0
  20. crp/ckf/fabric.py +445 -0
  21. crp/ckf/gc.py +175 -0
  22. crp/ckf/graph_walk.py +87 -0
  23. crp/ckf/merge.py +133 -0
  24. crp/ckf/pattern_query.py +122 -0
  25. crp/ckf/pubsub.py +128 -0
  26. crp/ckf/semantic.py +207 -0
  27. crp/cli/__init__.py +7 -0
  28. crp/cli/main.py +329 -0
  29. crp/cli/sidecar.py +929 -0
  30. crp/cli/startup.py +272 -0
  31. crp/continuation/__init__.py +103 -0
  32. crp/continuation/completion.py +348 -0
  33. crp/continuation/degradation.py +157 -0
  34. crp/continuation/document_map.py +160 -0
  35. crp/continuation/flow.py +109 -0
  36. crp/continuation/gap.py +419 -0
  37. crp/continuation/manager.py +484 -0
  38. crp/continuation/quality_monitor.py +179 -0
  39. crp/continuation/stitch.py +419 -0
  40. crp/continuation/trigger.py +142 -0
  41. crp/continuation/voice.py +157 -0
  42. crp/core/__init__.py +69 -0
  43. crp/core/batch.py +77 -0
  44. crp/core/circuit_breaker.py +116 -0
  45. crp/core/config.py +377 -0
  46. crp/core/context_tools.py +540 -0
  47. crp/core/dispatch_router.py +3977 -0
  48. crp/core/errors.py +128 -0
  49. crp/core/extraction_facade.py +384 -0
  50. crp/core/facilitator.py +713 -0
  51. crp/core/idempotency.py +215 -0
  52. crp/core/orchestrator.py +1435 -0
  53. crp/core/relay_strategies.py +613 -0
  54. crp/core/security_manager.py +140 -0
  55. crp/core/session.py +134 -0
  56. crp/core/task_intent.py +36 -0
  57. crp/core/window.py +363 -0
  58. crp/envelope/__init__.py +30 -0
  59. crp/envelope/builder.py +288 -0
  60. crp/envelope/decomposer.py +236 -0
  61. crp/envelope/formatter.py +168 -0
  62. crp/envelope/packer.py +211 -0
  63. crp/envelope/reranker.py +209 -0
  64. crp/envelope/scoring.py +310 -0
  65. crp/extraction/__init__.py +45 -0
  66. crp/extraction/complexity.py +96 -0
  67. crp/extraction/contradiction.py +132 -0
  68. crp/extraction/pipeline.py +360 -0
  69. crp/extraction/quality_gate.py +237 -0
  70. crp/extraction/stage1_regex.py +173 -0
  71. crp/extraction/stage2_statistical.py +244 -0
  72. crp/extraction/stage3_gliner.py +210 -0
  73. crp/extraction/stage4_uie.py +183 -0
  74. crp/extraction/stage5_discourse.py +175 -0
  75. crp/extraction/stage6_llm.py +178 -0
  76. crp/extraction/structured_output.py +219 -0
  77. crp/extraction/types.py +299 -0
  78. crp/license_guard.py +722 -0
  79. crp/observability/__init__.py +30 -0
  80. crp/observability/audit.py +118 -0
  81. crp/observability/events.py +233 -0
  82. crp/observability/metrics.py +264 -0
  83. crp/observability/quality.py +135 -0
  84. crp/observability/structured_logging.py +81 -0
  85. crp/observability/telemetry.py +117 -0
  86. crp/provenance/__init__.py +314 -0
  87. crp/provenance/_embeddings.py +97 -0
  88. crp/provenance/_types.py +378 -0
  89. crp/provenance/attribution_scorer.py +252 -0
  90. crp/provenance/claim_detector.py +229 -0
  91. crp/provenance/contradiction_detector.py +243 -0
  92. crp/provenance/distortion_detector.py +397 -0
  93. crp/provenance/entailment_verifier.py +358 -0
  94. crp/provenance/fabrication_detector.py +203 -0
  95. crp/provenance/hallucination_scorer.py +320 -0
  96. crp/provenance/omission_analyzer.py +106 -0
  97. crp/provenance/provenance_chain.py +205 -0
  98. crp/provenance/report_generator.py +440 -0
  99. crp/providers/__init__.py +43 -0
  100. crp/providers/anthropic.py +270 -0
  101. crp/providers/base.py +135 -0
  102. crp/providers/custom.py +63 -0
  103. crp/providers/diagnostic.py +251 -0
  104. crp/providers/llamacpp.py +224 -0
  105. crp/providers/manager.py +139 -0
  106. crp/providers/ollama.py +243 -0
  107. crp/providers/openai.py +628 -0
  108. crp/providers/tokenizers.py +48 -0
  109. crp/py.typed +0 -0
  110. crp/resources/__init__.py +53 -0
  111. crp/resources/adaptive_allocator.py +525 -0
  112. crp/resources/cost_model.py +388 -0
  113. crp/resources/overhead_manager.py +217 -0
  114. crp/resources/resource_manager.py +262 -0
  115. crp/schemas/__init__.py +20 -0
  116. crp/schemas/cost-estimate.json +33 -0
  117. crp/schemas/crp-error.json +43 -0
  118. crp/schemas/envelope-preview.json +40 -0
  119. crp/schemas/persisted-state-header.json +27 -0
  120. crp/schemas/quality-report.json +94 -0
  121. crp/schemas/session-handle.json +33 -0
  122. crp/schemas/session-status.json +57 -0
  123. crp/schemas/stream-event.json +18 -0
  124. crp/schemas/task-intent.json +42 -0
  125. crp/security/__init__.py +93 -0
  126. crp/security/audit_trail.py +392 -0
  127. crp/security/binding.py +192 -0
  128. crp/security/compliance.py +813 -0
  129. crp/security/consent.py +593 -0
  130. crp/security/embedding_defense.py +161 -0
  131. crp/security/encryption.py +202 -0
  132. crp/security/injection.py +335 -0
  133. crp/security/integrity.py +267 -0
  134. crp/security/privacy.py +662 -0
  135. crp/security/quarantine.py +249 -0
  136. crp/security/rbac.py +221 -0
  137. crp/security/validation.py +164 -0
  138. crp/state/__init__.py +31 -0
  139. crp/state/cold_storage.py +258 -0
  140. crp/state/compaction.py +263 -0
  141. crp/state/critical_state.py +104 -0
  142. crp/state/event_log.py +313 -0
  143. crp/state/fact.py +189 -0
  144. crp/state/serialization.py +189 -0
  145. crp/state/session_cleanup.py +77 -0
  146. crp/state/snapshot.py +290 -0
  147. crp/state/warm_store.py +346 -0
  148. crprotocol-2.0.0.dist-info/METADATA +1295 -0
  149. crprotocol-2.0.0.dist-info/RECORD +153 -0
  150. crprotocol-2.0.0.dist-info/WHEEL +4 -0
  151. crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
  152. crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
  153. crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
@@ -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
+ }