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/core/errors.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""CRP error taxonomy — codes 1001-1031 per §6.8."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ErrorCode(IntEnum):
|
|
12
|
+
"""All CRP error codes."""
|
|
13
|
+
|
|
14
|
+
# Budget / session (1001-1005)
|
|
15
|
+
BUDGET_EXHAUSTED = 1001
|
|
16
|
+
RATE_LIMIT_EXCEEDED = 1002
|
|
17
|
+
SESSION_EXPIRED = 1003
|
|
18
|
+
SESSION_LIMIT_EXCEEDED = 1004
|
|
19
|
+
SESSION_CLOSED = 1005
|
|
20
|
+
|
|
21
|
+
# Validation / security (1010-1013)
|
|
22
|
+
VALIDATION_ERROR = 1010
|
|
23
|
+
SECURITY_INVARIANT_ERROR = 1011
|
|
24
|
+
SIGNATURE_INVALID = 1012
|
|
25
|
+
RBAC_DENIED = 1013
|
|
26
|
+
|
|
27
|
+
# Provider (1020-1021)
|
|
28
|
+
PROVIDER_ERROR = 1020
|
|
29
|
+
PROVIDER_TIMEOUT = 1021
|
|
30
|
+
|
|
31
|
+
# State integrity (1030-1031)
|
|
32
|
+
STATE_CORRUPTED = 1030
|
|
33
|
+
CHAIN_VERIFICATION_FAILED = 1031
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CRPError(Exception):
|
|
37
|
+
"""Base exception for all CRP errors.
|
|
38
|
+
|
|
39
|
+
Each error carries a numeric code (1001-1031) per §6.8.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
code: int | ErrorCode,
|
|
45
|
+
message: str,
|
|
46
|
+
details: dict[str, Any] | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.code = int(code)
|
|
49
|
+
self.message = message
|
|
50
|
+
self.details: dict[str, Any] = details or {}
|
|
51
|
+
super().__init__(f"CRP-{self.code}: {message}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BudgetExhaustedError(CRPError):
|
|
55
|
+
"""Any cost cap (windows, tokens, rate) exceeded."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, message: str = "Budget exhausted", **details: Any) -> None:
|
|
58
|
+
super().__init__(ErrorCode.BUDGET_EXHAUSTED, message, details)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RateLimitExceededError(CRPError):
|
|
62
|
+
"""Per-session rate limit hit."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, message: str = "Rate limit exceeded", **details: Any) -> None:
|
|
65
|
+
super().__init__(ErrorCode.RATE_LIMIT_EXCEEDED, message, details)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SessionExpiredError(CRPError):
|
|
69
|
+
"""Session timeout reached."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, message: str = "Session expired", **details: Any) -> None:
|
|
72
|
+
super().__init__(ErrorCode.SESSION_EXPIRED, message, details)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SessionClosedError(CRPError):
|
|
76
|
+
"""Operation on closed session."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, message: str = "Session is closed", **details: Any) -> None:
|
|
79
|
+
super().__init__(ErrorCode.SESSION_CLOSED, message, details)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ValidationError(CRPError):
|
|
83
|
+
"""Structural validation failure (§7.4)."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, message: str = "Validation error", **details: Any) -> None:
|
|
86
|
+
super().__init__(ErrorCode.VALIDATION_ERROR, message, details)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ProviderError(CRPError):
|
|
90
|
+
"""LLM provider returned an error."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, message: str = "Provider error", **details: Any) -> None:
|
|
93
|
+
super().__init__(ErrorCode.PROVIDER_ERROR, message, details)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ProviderTimeoutError(CRPError):
|
|
97
|
+
"""LLM provider did not respond within timeout."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, message: str = "Provider timeout", **details: Any) -> None:
|
|
100
|
+
super().__init__(ErrorCode.PROVIDER_TIMEOUT, message, details)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class StateCorruptedError(CRPError):
|
|
104
|
+
"""Cold state integrity verification failed."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, message: str = "State corrupted", **details: Any) -> None:
|
|
107
|
+
super().__init__(ErrorCode.STATE_CORRUPTED, message, details)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ChainVerificationFailedError(CRPError):
|
|
111
|
+
"""Window DAG chain integrity check failed (§7.6)."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, message: str = "Chain verification failed", **details: Any) -> None:
|
|
114
|
+
super().__init__(ErrorCode.CHAIN_VERIFICATION_FAILED, message, details)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SecurityInvariantError(CRPError):
|
|
118
|
+
"""CRP security invariant violated (§7.4)."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, message: str = "Security invariant violated", **details: Any) -> None:
|
|
121
|
+
super().__init__(ErrorCode.SECURITY_INVARIANT_ERROR, message, details)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SignatureInvalidError(CRPError):
|
|
125
|
+
"""Cryptographic signature verification failed (§7.3)."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, message: str = "Signature invalid", **details: Any) -> None:
|
|
128
|
+
super().__init__(ErrorCode.SIGNATURE_INVALID, message, details)
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Extraction facade mixin — fact extraction and ingestion (§2.5).
|
|
4
|
+
|
|
5
|
+
Extracted from orchestrator.py for maintainability. This mixin provides
|
|
6
|
+
extraction and ingestion method implementations. The CRPOrchestrator
|
|
7
|
+
inherits from this class.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from crp.core.task_intent import TaskIntent
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from crp.extraction.types import ExtractionResult as PipelineExtractionResult
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("crp.orchestrator")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# ExtractionResult (for ingest)
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ExtractionResult:
|
|
33
|
+
"""Result of zero-LLM ingestion."""
|
|
34
|
+
facts_extracted: int = 0
|
|
35
|
+
source_label: str = ""
|
|
36
|
+
fact_ids: list[str] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ExtractionMixin:
|
|
40
|
+
"""Mixin providing CRP extraction and ingestion methods.
|
|
41
|
+
|
|
42
|
+
Methods access orchestrator state via ``self`` (multiple inheritance).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def _extract_and_store(
|
|
46
|
+
self,
|
|
47
|
+
text: str,
|
|
48
|
+
source_window_id: str,
|
|
49
|
+
task_intent: TaskIntent | None = None,
|
|
50
|
+
) -> PipelineExtractionResult:
|
|
51
|
+
"""Run graduated extraction on text, store facts in WarmStore + CKF."""
|
|
52
|
+
from crp.extraction.quality_gate import run_quality_gate
|
|
53
|
+
|
|
54
|
+
result = self._extraction.extract(
|
|
55
|
+
text,
|
|
56
|
+
task_intent=task_intent,
|
|
57
|
+
source_window_id=source_window_id,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Quality gate (3-tier validation)
|
|
61
|
+
result = run_quality_gate(
|
|
62
|
+
result,
|
|
63
|
+
history=list(self._extraction_history)[-10:] if self._extraction_history else None,
|
|
64
|
+
)
|
|
65
|
+
self._extraction_history.append(result)
|
|
66
|
+
|
|
67
|
+
# ── Output-side injection firewall (§7.5.2) ───────────
|
|
68
|
+
# Scan EACH extracted fact for injection patterns.
|
|
69
|
+
# If an LLM output contains injection payloads that survive
|
|
70
|
+
# extraction as verbatim text (e.g. key_sentence, list_item),
|
|
71
|
+
# penalize their confidence so they rank lower in envelope
|
|
72
|
+
# packing and are less likely to propagate to the next window.
|
|
73
|
+
if result.facts:
|
|
74
|
+
for fact in result.facts:
|
|
75
|
+
fact_report = self._injection_detector.scan(fact.text)
|
|
76
|
+
if fact_report.has_flags:
|
|
77
|
+
# Penalize confidence: 0.3× for high-confidence injection,
|
|
78
|
+
# 0.6× for lower-confidence matches
|
|
79
|
+
penalty = 0.3 if fact_report.highest_confidence >= 0.80 else 0.6
|
|
80
|
+
fact.confidence *= penalty
|
|
81
|
+
fact.flagged_confidence = True
|
|
82
|
+
fact.confidence_flag_reason = (
|
|
83
|
+
f"injection_in_fact:{fact_report.flags[0].pattern_name}"
|
|
84
|
+
f":{fact_report.highest_confidence:.2f}"
|
|
85
|
+
)
|
|
86
|
+
logger.warning(
|
|
87
|
+
"Output-side injection detected in fact %s: %s (conf=%.2f, "
|
|
88
|
+
"fact confidence penalized to %.2f)",
|
|
89
|
+
fact.id, fact_report.flags[0].pattern_name,
|
|
90
|
+
fact_report.highest_confidence, fact.confidence,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Store in WarmStateStore (fact ranking, aging, seen tracking)
|
|
94
|
+
if result.facts:
|
|
95
|
+
self._warm_store.add_facts(result.facts, result.edges or None)
|
|
96
|
+
|
|
97
|
+
# Add facts to integrity chain (§7.2, §7.7)
|
|
98
|
+
if result.facts:
|
|
99
|
+
for fact in result.facts:
|
|
100
|
+
self._integrity_chain.add_fact(fact.id, fact.text)
|
|
101
|
+
|
|
102
|
+
# Store in CKF (4-mode retrieval: graph walk, pattern, semantic, community)
|
|
103
|
+
if result.facts:
|
|
104
|
+
self._ckf.store(result.facts, window_id=source_window_id)
|
|
105
|
+
if result.edges:
|
|
106
|
+
self._ckf.store_edges(result.edges)
|
|
107
|
+
|
|
108
|
+
# Store source passages for high-confidence facts (§17)
|
|
109
|
+
if result.facts:
|
|
110
|
+
from crp.advanced.source_grounding import SourcePassage
|
|
111
|
+
for fact in result.facts:
|
|
112
|
+
passage = SourcePassage(
|
|
113
|
+
passage_id=f"{source_window_id}:{fact.id}",
|
|
114
|
+
text=fact.text,
|
|
115
|
+
source_window=self._windows_completed,
|
|
116
|
+
linked_fact_ids=[fact.id],
|
|
117
|
+
relevance_score=fact.confidence,
|
|
118
|
+
)
|
|
119
|
+
self._source_grounding.store_passage(
|
|
120
|
+
passage=passage,
|
|
121
|
+
fact_confidence=fact.confidence,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Auto-compact warm store if thresholds exceeded (§7.4)
|
|
125
|
+
from crp.state.compaction import should_compact, compact
|
|
126
|
+
if should_compact(self._warm_store):
|
|
127
|
+
try:
|
|
128
|
+
compact(
|
|
129
|
+
self._warm_store,
|
|
130
|
+
self._ckf._event_log,
|
|
131
|
+
source_window_id,
|
|
132
|
+
)
|
|
133
|
+
logger.debug("Auto-compaction complete")
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
logger.debug("Compaction skipped: %s", exc)
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Internal: envelope construction
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def ingest_batch(
|
|
144
|
+
self,
|
|
145
|
+
texts: list[str],
|
|
146
|
+
task_intent: str = "",
|
|
147
|
+
) -> list[int]:
|
|
148
|
+
"""Batch ingest multiple texts, returning facts extracted per text (§6.6)."""
|
|
149
|
+
self._check_session()
|
|
150
|
+
results: list[int] = []
|
|
151
|
+
for text in texts:
|
|
152
|
+
try:
|
|
153
|
+
facts_count = self.ingest(text)
|
|
154
|
+
results.append(facts_count)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
results.append(0)
|
|
157
|
+
logger.warning("Batch ingest item failed: %s", exc)
|
|
158
|
+
return results
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# Session operations
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def ingest(
|
|
165
|
+
self,
|
|
166
|
+
raw_text: str,
|
|
167
|
+
source_label: str | None = None,
|
|
168
|
+
) -> ExtractionResult:
|
|
169
|
+
"""Ingest raw text without LLM invocation (§2.5).
|
|
170
|
+
|
|
171
|
+
Runs graduated extraction pipeline on raw_text, adds facts to
|
|
172
|
+
WarmStateStore and CKF. No LLM call, no window creation, no envelope.
|
|
173
|
+
"""
|
|
174
|
+
with self._lock:
|
|
175
|
+
return self._ingest_locked(raw_text, source_label=source_label)
|
|
176
|
+
|
|
177
|
+
def _ingest_locked(
|
|
178
|
+
self,
|
|
179
|
+
raw_text: str,
|
|
180
|
+
source_label: str | None = None,
|
|
181
|
+
) -> ExtractionResult:
|
|
182
|
+
"""Internal ingest implementation — called under self._lock."""
|
|
183
|
+
# Set correlation ID for structured log tracing (§audit M10)
|
|
184
|
+
from crp.observability.structured_logging import new_correlation_id
|
|
185
|
+
cid = new_correlation_id()
|
|
186
|
+
logger.info("ingest started [correlation_id=%s, label=%s]", cid, source_label)
|
|
187
|
+
|
|
188
|
+
self._check_session()
|
|
189
|
+
|
|
190
|
+
# ---------- Input sanitization (§audit H10) ----------
|
|
191
|
+
max_ingest_bytes = self._config.get("max_ram_mb", 512) * 1024 * 1024
|
|
192
|
+
# Cap single ingest to 10 MB or 1% of memory budget, whichever is smaller
|
|
193
|
+
max_ingest_size = min(10 * 1024 * 1024, max_ingest_bytes // 100)
|
|
194
|
+
if len(raw_text) > max_ingest_size:
|
|
195
|
+
raise ValidationError(
|
|
196
|
+
f"Ingest text too large ({len(raw_text):,} chars, "
|
|
197
|
+
f"max {max_ingest_size:,})"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# ---------- Compliance imports (§7.14) ----------
|
|
201
|
+
from crp.security.audit_trail import ComplianceEventType
|
|
202
|
+
from crp.security.consent import ProcessingPurpose
|
|
203
|
+
from crp.security.privacy import DataClassification
|
|
204
|
+
|
|
205
|
+
# ---------- Compliance audit: ingest started (§7.14) ----------
|
|
206
|
+
label = source_label or "unnamed"
|
|
207
|
+
self._compliance_audit.record(
|
|
208
|
+
ComplianceEventType.DATA_INGESTED,
|
|
209
|
+
session_id=self._session.session_id,
|
|
210
|
+
data={"operation": "ingest", "phase": "started",
|
|
211
|
+
"source_label": label,
|
|
212
|
+
"input_length": len(raw_text)},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# ---------- Consent verification (§7.13) ----------
|
|
216
|
+
self._consent_manager.check_required(ProcessingPurpose.FACT_EXTRACTION)
|
|
217
|
+
|
|
218
|
+
# RBAC permission + rate limit check (§7.10)
|
|
219
|
+
from crp.security.rbac import Permission
|
|
220
|
+
perm_result = self._rbac.check_permission(Permission.INGEST)
|
|
221
|
+
if not perm_result.allowed:
|
|
222
|
+
self._compliance_audit.record(
|
|
223
|
+
ComplianceEventType.RBAC_DENIED,
|
|
224
|
+
session_id=self._session.session_id,
|
|
225
|
+
data={"operation": "ingest", "reason": perm_result.reason},
|
|
226
|
+
)
|
|
227
|
+
raise RateLimitExceededError(perm_result.reason)
|
|
228
|
+
|
|
229
|
+
if not raw_text:
|
|
230
|
+
return ExtractionResult(
|
|
231
|
+
facts_extracted=0,
|
|
232
|
+
source_label=source_label or "",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Input validation — Layer 1, cannot disable (§7.4)
|
|
236
|
+
val_result = self._input_validator.validate(raw_text)
|
|
237
|
+
if not val_result.valid:
|
|
238
|
+
raise ValidationError(val_result.warnings[0] if val_result.warnings else "Input validation failed")
|
|
239
|
+
raw_text = val_result.sanitized_text
|
|
240
|
+
|
|
241
|
+
# Rate limit check with payload size
|
|
242
|
+
payload_bytes = len(raw_text.encode("utf-8"))
|
|
243
|
+
rate_result = self._rbac.check_rate_limit(Permission.INGEST, payload_bytes=payload_bytes)
|
|
244
|
+
if not rate_result.allowed:
|
|
245
|
+
self._compliance_audit.record(
|
|
246
|
+
ComplianceEventType.RATE_LIMIT_HIT,
|
|
247
|
+
session_id=self._session.session_id,
|
|
248
|
+
data={"operation": "ingest", "reason": rate_result.reason},
|
|
249
|
+
)
|
|
250
|
+
raise RateLimitExceededError(rate_result.reason)
|
|
251
|
+
|
|
252
|
+
source_window_id = f"ingest:{label}"
|
|
253
|
+
|
|
254
|
+
# ---------- PII scanning on ingested text (§7.12) ----------
|
|
255
|
+
pii_result = self._pii_scanner.scan(raw_text)
|
|
256
|
+
if pii_result.has_pii:
|
|
257
|
+
self._compliance_audit.record(
|
|
258
|
+
ComplianceEventType.PII_DETECTED,
|
|
259
|
+
session_id=self._session.session_id,
|
|
260
|
+
data={
|
|
261
|
+
"operation": "ingest",
|
|
262
|
+
"source_label": label,
|
|
263
|
+
"pii_types": sorted(pii_result.pii_types_found),
|
|
264
|
+
"detection_count": len(pii_result.detections),
|
|
265
|
+
"classification": pii_result.highest_classification.name,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
if self._human_oversight.should_halt_on_pii():
|
|
269
|
+
self._human_oversight.record_halt(
|
|
270
|
+
"ingest", "PII detected in ingested text",
|
|
271
|
+
details={"pii_types": sorted(pii_result.pii_types_found)},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Advisory injection scan on ingested text (§7.5.2)
|
|
275
|
+
ingest_report = self._injection_detector.scan(raw_text)
|
|
276
|
+
if ingest_report.has_flags:
|
|
277
|
+
logger.warning(
|
|
278
|
+
"Injection patterns detected in ingested text '%s': %d flags, "
|
|
279
|
+
"highest confidence=%.2f — facts will be quarantined + penalized",
|
|
280
|
+
label, len(ingest_report.flags), ingest_report.highest_confidence,
|
|
281
|
+
)
|
|
282
|
+
self._compliance_audit.record(
|
|
283
|
+
ComplianceEventType.INJECTION_DETECTED,
|
|
284
|
+
session_id=self._session.session_id,
|
|
285
|
+
data={
|
|
286
|
+
"operation": "ingest",
|
|
287
|
+
"source_label": label,
|
|
288
|
+
"flags_count": len(ingest_report.flags),
|
|
289
|
+
"highest_confidence": ingest_report.highest_confidence,
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
if self._human_oversight.should_halt_on_injection():
|
|
293
|
+
self._human_oversight.record_halt(
|
|
294
|
+
"ingest", "Injection detected in ingested text",
|
|
295
|
+
details={"flags_count": len(ingest_report.flags)},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Run full graduated extraction pipeline
|
|
299
|
+
# (output-side injection firewall in _extract_and_store will
|
|
300
|
+
# penalize any facts that carry injection patterns)
|
|
301
|
+
pipeline_result = self._extract_and_store(raw_text, source_window_id)
|
|
302
|
+
|
|
303
|
+
# Quarantine ingested facts with 0.7× confidence penalty (§7.8)
|
|
304
|
+
if pipeline_result.facts:
|
|
305
|
+
window_id = f"ingest-q:{label}"
|
|
306
|
+
self._quarantine.quarantine_facts(
|
|
307
|
+
[(f.id, f.confidence, f.text) for f in pipeline_result.facts],
|
|
308
|
+
window_id=window_id,
|
|
309
|
+
source_label=label,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# ---------- Retention + lineage tracking for ingested facts (§7.12) ----------
|
|
313
|
+
_classification = (
|
|
314
|
+
pii_result.highest_classification if pii_result.has_pii
|
|
315
|
+
else DataClassification.INTERNAL
|
|
316
|
+
)
|
|
317
|
+
for fact in pipeline_result.facts:
|
|
318
|
+
self._retention_manager.register(
|
|
319
|
+
data_id=fact.id,
|
|
320
|
+
classification=_classification,
|
|
321
|
+
source_label=f"ingest:{label}",
|
|
322
|
+
)
|
|
323
|
+
self._lineage_tracker.record(
|
|
324
|
+
data_id=fact.id,
|
|
325
|
+
origin="ingest",
|
|
326
|
+
source_label=label,
|
|
327
|
+
classification=_classification,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# ---------- Processing record (§7.13 — GDPR Art. 30) ----------
|
|
331
|
+
self._processing_records.record(
|
|
332
|
+
purpose=ProcessingPurpose.FACT_EXTRACTION,
|
|
333
|
+
data_categories=["raw_text", "extracted_facts"],
|
|
334
|
+
legal_basis="legitimate_interest",
|
|
335
|
+
input_size_bytes=payload_bytes,
|
|
336
|
+
output_size_bytes=sum(len(f.text.encode("utf-8")) for f in pipeline_result.facts),
|
|
337
|
+
automated_decision=True,
|
|
338
|
+
human_oversight=False,
|
|
339
|
+
retention_period="session",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Record ingest for rate limiting
|
|
343
|
+
self._rbac.record_ingest(payload_bytes)
|
|
344
|
+
|
|
345
|
+
# ---------- Compliance audit: ingest completed (§7.14) ----------
|
|
346
|
+
self._compliance_audit.record(
|
|
347
|
+
ComplianceEventType.DATA_INGESTED,
|
|
348
|
+
session_id=self._session.session_id,
|
|
349
|
+
data={
|
|
350
|
+
"operation": "ingest",
|
|
351
|
+
"phase": "completed",
|
|
352
|
+
"source_label": label,
|
|
353
|
+
"facts_extracted": pipeline_result.total_facts,
|
|
354
|
+
"quarantined": self._quarantine.quarantine_count,
|
|
355
|
+
"pii_detected": pii_result.has_pii,
|
|
356
|
+
"injection_detected": ingest_report.has_flags,
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# ---------- Retention enforcement (§7.12) ----------
|
|
361
|
+
expired_ids = self._retention_manager.enforce()
|
|
362
|
+
if expired_ids:
|
|
363
|
+
self._compliance_audit.record(
|
|
364
|
+
ComplianceEventType.RETENTION_PURGED,
|
|
365
|
+
session_id=self._session.session_id,
|
|
366
|
+
data={"purged_count": len(expired_ids), "purged_ids": expired_ids[:10]},
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
logger.info(
|
|
370
|
+
"Ingested %d facts from source '%s' (stages=%s, quarantined=%d)",
|
|
371
|
+
pipeline_result.total_facts, label, pipeline_result.stages_run,
|
|
372
|
+
self._quarantine.quarantine_count,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return ExtractionResult(
|
|
376
|
+
facts_extracted=pipeline_result.total_facts,
|
|
377
|
+
source_label=label,
|
|
378
|
+
fact_ids=[f.id for f in pipeline_result.facts],
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# ------------------------------------------------------------------
|
|
382
|
+
# State export (§2.5)
|
|
383
|
+
# ------------------------------------------------------------------
|
|
384
|
+
|