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/license_guard.py
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""
|
|
4
|
+
License enforcement guard for CRP™ — Context Relay Protocol.
|
|
5
|
+
|
|
6
|
+
This module implements CODE-LEVEL IP protections that complement the
|
|
7
|
+
Elastic License 2.0 (ELv2) legal protections. It MUST NOT be removed,
|
|
8
|
+
modified, or bypassed. Doing so constitutes a violation of the license.
|
|
9
|
+
|
|
10
|
+
Enforcement levels (graduated):
|
|
11
|
+
BLOCK — Hard stop (raises CRPError). Managed-service usage without
|
|
12
|
+
a commercial license is prohibited under ELv2 §2.
|
|
13
|
+
DEGRADE — Feature degradation. If license headers are tampered with,
|
|
14
|
+
advanced features (Stage 3+, CKF graph retrieval, continuation)
|
|
15
|
+
are disabled for the session.
|
|
16
|
+
WARN — Advisory warning logged and emitted. Informational only.
|
|
17
|
+
|
|
18
|
+
Protections:
|
|
19
|
+
1. Runtime license header verification on core modules
|
|
20
|
+
2. Managed-service usage detection & blocking (§ELv2 Limitation)
|
|
21
|
+
3. Attribution verification (copyright notices must be preserved)
|
|
22
|
+
4. Module integrity fingerprint validation
|
|
23
|
+
5. Output watermarking with license metadata
|
|
24
|
+
6. Violation telemetry (local logging only — no phone-home)
|
|
25
|
+
7. Fork/clone provenance binding — origin verification
|
|
26
|
+
8. Code-signature chain — tamper-evident module registry
|
|
27
|
+
9. Redistribution detection — package metadata binding
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import hashlib
|
|
33
|
+
import importlib
|
|
34
|
+
import json
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import time
|
|
38
|
+
import warnings
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Constants
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_LICENSE_MARKER = "Licensed under Elastic License 2.0"
|
|
50
|
+
_COPYRIGHT_MARKER = "Constantinos Vidiniotis"
|
|
51
|
+
_LICENSOR = "Constantinos Vidiniotis / AutoCyber AI Pty Ltd"
|
|
52
|
+
_LICENSE_URL = "https://crprotocol.io"
|
|
53
|
+
_ABN = "22 697 087 166"
|
|
54
|
+
_PRODUCT = "Context Relay Protocol (CRP)™"
|
|
55
|
+
|
|
56
|
+
# Core modules that MUST retain license headers
|
|
57
|
+
_CORE_MODULES = [
|
|
58
|
+
"crp.core.orchestrator",
|
|
59
|
+
"crp.core.dispatch_router",
|
|
60
|
+
"crp.core.session",
|
|
61
|
+
"crp.core.config",
|
|
62
|
+
"crp.envelope.builder",
|
|
63
|
+
"crp.extraction.pipeline",
|
|
64
|
+
"crp.continuation.manager",
|
|
65
|
+
"crp.ckf.fabric",
|
|
66
|
+
"crp.security.integrity",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Environment variables that indicate managed-service deployment
|
|
70
|
+
_MANAGED_SERVICE_INDICATORS = [
|
|
71
|
+
"CRP_MANAGED_SERVICE", # Explicit flag
|
|
72
|
+
"CRP_MULTI_TENANT", # Multi-tenant mode
|
|
73
|
+
"CRP_SAAS_MODE", # SaaS deployment
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Infrastructure indicators that strongly suggest managed-service
|
|
77
|
+
_INFRA_INDICATORS = [
|
|
78
|
+
"KUBERNETES_SERVICE_HOST", # Running in k8s
|
|
79
|
+
"ECS_CONTAINER_METADATA_URI", # AWS ECS
|
|
80
|
+
"GAE_APPLICATION", # Google App Engine
|
|
81
|
+
"WEBSITE_INSTANCE_ID", # Azure App Service
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Enforcement state
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class _EnforcementState:
|
|
91
|
+
"""Tracks the enforcement state for the current session."""
|
|
92
|
+
managed_service_blocked: bool = False
|
|
93
|
+
tamper_detected: bool = False
|
|
94
|
+
tampered_modules: list[str] = field(default_factory=list)
|
|
95
|
+
features_degraded: bool = False
|
|
96
|
+
violation_count: int = 0
|
|
97
|
+
checked_at: float = 0.0
|
|
98
|
+
commercial_license: bool = False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_state = _EnforcementState()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Commercial license bypass
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def _check_commercial_license() -> bool:
|
|
109
|
+
"""Check if a valid commercial license key is present.
|
|
110
|
+
|
|
111
|
+
Commercial license holders set CRP_LICENSE_KEY to their key.
|
|
112
|
+
This bypasses managed-service restrictions (but NOT header tampering).
|
|
113
|
+
"""
|
|
114
|
+
key = os.environ.get("CRP_LICENSE_KEY", "").strip()
|
|
115
|
+
if not key:
|
|
116
|
+
return False
|
|
117
|
+
# Commercial keys are 64-char hex strings with a known prefix
|
|
118
|
+
if len(key) >= 64 and key[:4] == "CRP-":
|
|
119
|
+
# Validate key format: CRP-<60 hex chars>
|
|
120
|
+
try:
|
|
121
|
+
int(key[4:], 16)
|
|
122
|
+
logger.info("Commercial license key detected — managed-service restriction lifted")
|
|
123
|
+
return True
|
|
124
|
+
except ValueError:
|
|
125
|
+
pass
|
|
126
|
+
logger.warning("Invalid CRP_LICENSE_KEY format — commercial license not recognized")
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# License verification (DEGRADE on failure)
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def verify_license_headers() -> list[str]:
|
|
135
|
+
"""Verify that core modules retain their license headers.
|
|
136
|
+
|
|
137
|
+
Returns a list of modules with missing/modified headers.
|
|
138
|
+
If any are found, sets _state.tamper_detected = True and
|
|
139
|
+
triggers feature degradation.
|
|
140
|
+
"""
|
|
141
|
+
violations: list[str] = []
|
|
142
|
+
for mod_name in _CORE_MODULES:
|
|
143
|
+
try:
|
|
144
|
+
mod = importlib.import_module(mod_name)
|
|
145
|
+
source_file = getattr(mod, "__file__", None)
|
|
146
|
+
if source_file and os.path.isfile(source_file):
|
|
147
|
+
with open(source_file, encoding="utf-8", errors="ignore") as f:
|
|
148
|
+
header = f.read(500)
|
|
149
|
+
if _LICENSE_MARKER not in header or _COPYRIGHT_MARKER not in header:
|
|
150
|
+
violations.append(mod_name)
|
|
151
|
+
except (ImportError, OSError):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
if violations:
|
|
155
|
+
_state.tamper_detected = True
|
|
156
|
+
_state.tampered_modules = violations
|
|
157
|
+
_state.features_degraded = True
|
|
158
|
+
_state.violation_count += len(violations)
|
|
159
|
+
|
|
160
|
+
for mod_name in violations:
|
|
161
|
+
logger.error(
|
|
162
|
+
"LICENSE VIOLATION: License header removed or modified in %s. "
|
|
163
|
+
"This software is protected by copyright and Elastic License 2.0. "
|
|
164
|
+
"Removing license headers is a violation of the license and "
|
|
165
|
+
"applicable copyright law. "
|
|
166
|
+
"Advanced features have been DISABLED for this session.",
|
|
167
|
+
mod_name,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
warnings.warn(
|
|
171
|
+
f"CRP™ license headers modified in {len(violations)} module(s): "
|
|
172
|
+
f"{', '.join(violations)}. "
|
|
173
|
+
f"Advanced features (Stage 3+ extraction, CKF graph retrieval, "
|
|
174
|
+
f"continuation beyond 3 windows) have been DISABLED. "
|
|
175
|
+
f"Restore original license headers to re-enable full functionality. "
|
|
176
|
+
f"License: Elastic License 2.0 | Licensor: {_LICENSOR}",
|
|
177
|
+
UserWarning,
|
|
178
|
+
stacklevel=2,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
_log_violation("license_header_tamper", {
|
|
182
|
+
"modules": violations,
|
|
183
|
+
"degraded": True,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return violations
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Managed-service detection (BLOCK on detection)
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def check_managed_service_restriction() -> bool:
|
|
194
|
+
"""Check if CRP is being deployed as a managed service.
|
|
195
|
+
|
|
196
|
+
Under the Elastic License 2.0 §2, providing the functionality
|
|
197
|
+
of CRP to third parties as a managed service is PROHIBITED
|
|
198
|
+
without a commercial license from the Licensor.
|
|
199
|
+
|
|
200
|
+
This function BLOCKS (raises CRPError) if managed-service indicators
|
|
201
|
+
are detected AND no commercial license key is present.
|
|
202
|
+
|
|
203
|
+
Returns True if managed-service indicators are detected (blocked or licensed).
|
|
204
|
+
"""
|
|
205
|
+
if _state.commercial_license:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
detected_indicators: list[str] = []
|
|
209
|
+
for env_var in _MANAGED_SERVICE_INDICATORS:
|
|
210
|
+
val = os.environ.get(env_var)
|
|
211
|
+
if val and val.lower() not in ("0", "false", "no"):
|
|
212
|
+
detected_indicators.append(env_var)
|
|
213
|
+
|
|
214
|
+
infra_signals: list[str] = []
|
|
215
|
+
for env_var in _INFRA_INDICATORS:
|
|
216
|
+
if os.environ.get(env_var):
|
|
217
|
+
infra_signals.append(env_var)
|
|
218
|
+
|
|
219
|
+
if not detected_indicators:
|
|
220
|
+
if infra_signals:
|
|
221
|
+
logger.info(
|
|
222
|
+
"CRP running in cloud infrastructure (%s). If this is a "
|
|
223
|
+
"managed service offering, set CRP_LICENSE_KEY or contact "
|
|
224
|
+
"contact@crprotocol.io for a commercial license.",
|
|
225
|
+
", ".join(infra_signals),
|
|
226
|
+
)
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
# Managed-service explicitly flagged — BLOCK
|
|
230
|
+
_state.managed_service_blocked = True
|
|
231
|
+
_state.violation_count += 1
|
|
232
|
+
|
|
233
|
+
_log_violation("managed_service_blocked", {
|
|
234
|
+
"indicators": detected_indicators,
|
|
235
|
+
"infra_signals": infra_signals,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
from crp.core.errors import CRPError, ErrorCode
|
|
239
|
+
|
|
240
|
+
raise CRPError(
|
|
241
|
+
code=ErrorCode.SECURITY_INVARIANT_ERROR,
|
|
242
|
+
message=(
|
|
243
|
+
f"CRP™ MANAGED SERVICE VIOLATION — Elastic License 2.0 §2\n"
|
|
244
|
+
f"\n"
|
|
245
|
+
f"Providing the functionality of CRP to third parties as a managed "
|
|
246
|
+
f"service is PROHIBITED without a commercial license.\n"
|
|
247
|
+
f"\n"
|
|
248
|
+
f"Detected indicators: {', '.join(detected_indicators)}\n"
|
|
249
|
+
f"\n"
|
|
250
|
+
f"To resolve:\n"
|
|
251
|
+
f" 1. Obtain a commercial license: contact@crprotocol.io\n"
|
|
252
|
+
f" 2. Set CRP_LICENSE_KEY=<your-key> in environment\n"
|
|
253
|
+
f" 3. Or remove the managed-service flags if this is self-hosted use\n"
|
|
254
|
+
f"\n"
|
|
255
|
+
f"Licensor: {_LICENSOR} | ABN: {_ABN}\n"
|
|
256
|
+
f"Website: {_LICENSE_URL}"
|
|
257
|
+
),
|
|
258
|
+
details={
|
|
259
|
+
"violation_type": "managed_service",
|
|
260
|
+
"indicators": detected_indicators,
|
|
261
|
+
"license": "Elastic License 2.0",
|
|
262
|
+
"contact": "contact@crprotocol.io",
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
# Feature gate — enforcement point for degraded mode
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
def is_feature_allowed(feature: str) -> bool:
|
|
272
|
+
"""Check if a feature is allowed under current enforcement state.
|
|
273
|
+
|
|
274
|
+
When license headers have been tampered with, advanced features are
|
|
275
|
+
disabled. This function is called at feature entry points.
|
|
276
|
+
|
|
277
|
+
Features gated:
|
|
278
|
+
- "stage_3": GLiNER extraction
|
|
279
|
+
- "stage_4": UIE relation extraction
|
|
280
|
+
- "stage_5": Discourse extraction
|
|
281
|
+
- "stage_6": LLM-assisted extraction
|
|
282
|
+
- "ckf_graph": CKF graph-aware retrieval
|
|
283
|
+
- "continuation_extended": Continuation beyond 3 windows
|
|
284
|
+
- "cross_encoder": Cross-encoder reranking
|
|
285
|
+
"""
|
|
286
|
+
if not _state.features_degraded:
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
_ALLOWED_IN_DEGRADED = {"stage_1", "stage_2", "basic_dispatch"}
|
|
290
|
+
if feature in _ALLOWED_IN_DEGRADED:
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
logger.warning(
|
|
294
|
+
"Feature '%s' BLOCKED — license headers have been tampered with. "
|
|
295
|
+
"Restore original license headers to re-enable. "
|
|
296
|
+
"License: Elastic License 2.0 | Licensor: %s",
|
|
297
|
+
feature, _LICENSOR,
|
|
298
|
+
)
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
# Output watermarking
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def watermark_output(output: str, session_id: str = "") -> str:
|
|
307
|
+
"""Embed a license watermark in CRP output.
|
|
308
|
+
|
|
309
|
+
The watermark is a minimal metadata comment appended to the output.
|
|
310
|
+
It identifies this output as produced by CRP under the Elastic
|
|
311
|
+
License 2.0.
|
|
312
|
+
"""
|
|
313
|
+
if not output or not output.strip():
|
|
314
|
+
return output
|
|
315
|
+
|
|
316
|
+
content_hash = hashlib.sha256(output.encode("utf-8")).hexdigest()[:12]
|
|
317
|
+
ts = int(time.time())
|
|
318
|
+
|
|
319
|
+
watermark = (
|
|
320
|
+
f"\n<!-- CRP™ | ELv2 | {_LICENSOR} | "
|
|
321
|
+
f"h:{content_hash} | t:{ts} -->"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return output + watermark
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Module integrity fingerprinting
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def compute_module_fingerprint(module_name: str) -> str | None:
|
|
332
|
+
"""Compute SHA-256 fingerprint of a module's source file."""
|
|
333
|
+
try:
|
|
334
|
+
mod = importlib.import_module(module_name)
|
|
335
|
+
source_file = getattr(mod, "__file__", None)
|
|
336
|
+
if source_file and os.path.isfile(source_file):
|
|
337
|
+
with open(source_file, "rb") as f:
|
|
338
|
+
return hashlib.sha256(f.read()).hexdigest()
|
|
339
|
+
except (ImportError, OSError):
|
|
340
|
+
pass
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_module_fingerprints() -> dict[str, str | None]:
|
|
345
|
+
"""Compute fingerprints for all core modules."""
|
|
346
|
+
return {mod: compute_module_fingerprint(mod) for mod in _CORE_MODULES}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# Provenance binding — detects unauthorized redistribution
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
# Canonical package metadata that MUST match. If someone forks the repo,
|
|
354
|
+
# renames the package, and publishes it — this check fires.
|
|
355
|
+
_CANONICAL_PACKAGE = "crp"
|
|
356
|
+
_CANONICAL_AUTHOR = "Constantinos Vidiniotis"
|
|
357
|
+
_CANONICAL_REPO = "Constantinos-uni/context-relay-protocol"
|
|
358
|
+
|
|
359
|
+
def verify_package_provenance() -> list[str]:
|
|
360
|
+
"""Verify that the installed package matches canonical provenance.
|
|
361
|
+
|
|
362
|
+
Catches:
|
|
363
|
+
- Renamed forks published to PyPI under a different name
|
|
364
|
+
- Vendored copies with altered metadata
|
|
365
|
+
- pip-installed copies with stripped attribution
|
|
366
|
+
|
|
367
|
+
Returns list of provenance violations (empty = clean).
|
|
368
|
+
"""
|
|
369
|
+
violations: list[str] = []
|
|
370
|
+
|
|
371
|
+
# 1. Check importlib.metadata (pip-installed copy)
|
|
372
|
+
try:
|
|
373
|
+
from importlib.metadata import metadata as _pkg_metadata
|
|
374
|
+
meta = _pkg_metadata(_CANONICAL_PACKAGE)
|
|
375
|
+
author = meta.get("Author", "") or ""
|
|
376
|
+
author_email = meta.get("Author-email", "") or ""
|
|
377
|
+
pkg_license = meta.get("License", "") or ""
|
|
378
|
+
home_page = meta.get("Home-page", "") or meta.get("Project-URL", "") or ""
|
|
379
|
+
|
|
380
|
+
if _CANONICAL_AUTHOR.lower() not in author.lower() and \
|
|
381
|
+
_CANONICAL_AUTHOR.lower() not in author_email.lower():
|
|
382
|
+
violations.append(
|
|
383
|
+
f"package_author_mismatch: expected '{_CANONICAL_AUTHOR}', "
|
|
384
|
+
f"got '{author}' / '{author_email}'"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if "elastic" not in pkg_license.lower() and "elv2" not in pkg_license.lower():
|
|
388
|
+
if pkg_license and "unknown" not in pkg_license.lower():
|
|
389
|
+
violations.append(
|
|
390
|
+
f"package_license_mismatch: expected ELv2, got '{pkg_license}'"
|
|
391
|
+
)
|
|
392
|
+
except Exception:
|
|
393
|
+
# Not installed via pip (dev mode / editable) — skip
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
# 2. Check that the package wasn't cloned and re-branded
|
|
397
|
+
try:
|
|
398
|
+
from crp._version import __version__ as _ver
|
|
399
|
+
# The version module MUST exist and contain our version string
|
|
400
|
+
if not _ver or not isinstance(_ver, str):
|
|
401
|
+
violations.append("version_missing: crp._version.__version__ is empty")
|
|
402
|
+
except ImportError:
|
|
403
|
+
violations.append("version_module_missing: crp._version not found")
|
|
404
|
+
|
|
405
|
+
# 3. Verify this module itself hasn't been hollowed out
|
|
406
|
+
# (attacker keeps the file but guts the enforcement logic)
|
|
407
|
+
_self_markers = [
|
|
408
|
+
"_startup_check",
|
|
409
|
+
"verify_license_headers",
|
|
410
|
+
"check_managed_service_restriction",
|
|
411
|
+
"_CANONICAL_PACKAGE",
|
|
412
|
+
"_log_violation",
|
|
413
|
+
]
|
|
414
|
+
try:
|
|
415
|
+
with open(__file__, encoding="utf-8", errors="ignore") as f:
|
|
416
|
+
self_source = f.read()
|
|
417
|
+
for marker in _self_markers:
|
|
418
|
+
if marker not in self_source:
|
|
419
|
+
violations.append(f"self_tamper: '{marker}' removed from license_guard.py")
|
|
420
|
+
except OSError:
|
|
421
|
+
violations.append("self_tamper: cannot read license_guard.py source")
|
|
422
|
+
|
|
423
|
+
if violations:
|
|
424
|
+
_state.tamper_detected = True
|
|
425
|
+
_state.violation_count += len(violations)
|
|
426
|
+
_log_violation("provenance_violation", {"violations": violations})
|
|
427
|
+
|
|
428
|
+
for v in violations:
|
|
429
|
+
logger.error(
|
|
430
|
+
"PROVENANCE VIOLATION: %s — This software is the intellectual "
|
|
431
|
+
"property of %s, protected by copyright and Elastic License 2.0. "
|
|
432
|
+
"Unauthorized redistribution is prohibited.",
|
|
433
|
+
v, _LICENSOR,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return violations
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
# Fork/clone origin binding
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
def verify_origin_binding() -> list[str]:
|
|
444
|
+
"""Check if the code is running from an authorized origin.
|
|
445
|
+
|
|
446
|
+
Detects:
|
|
447
|
+
- Git remote pointing to a non-canonical repository (fork/clone theft)
|
|
448
|
+
- Missing .git directory (source archive redistribution)
|
|
449
|
+
- Modified origin URL (repo transferred/stolen)
|
|
450
|
+
|
|
451
|
+
This is ADVISORY — does not block, only logs. Forks for personal
|
|
452
|
+
study/modification are permitted under ELv2, but redistribution as
|
|
453
|
+
a managed service is not.
|
|
454
|
+
"""
|
|
455
|
+
warnings_list: list[str] = []
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
import subprocess
|
|
459
|
+
# Find the repo root relative to this file
|
|
460
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
461
|
+
git_dir = repo_root / ".git"
|
|
462
|
+
|
|
463
|
+
if not git_dir.is_dir():
|
|
464
|
+
# Not a git repo — could be a pip install or archive extraction
|
|
465
|
+
# This is fine for end-users, but we log it
|
|
466
|
+
return warnings_list
|
|
467
|
+
|
|
468
|
+
result = subprocess.run(
|
|
469
|
+
["git", "-C", str(repo_root), "remote", "get-url", "origin"],
|
|
470
|
+
capture_output=True, text=True, timeout=5,
|
|
471
|
+
)
|
|
472
|
+
if result.returncode == 0:
|
|
473
|
+
origin = result.stdout.strip().lower()
|
|
474
|
+
canonical_lower = _CANONICAL_REPO.lower()
|
|
475
|
+
if canonical_lower not in origin and "context-relay-protocol" not in origin:
|
|
476
|
+
warnings_list.append(
|
|
477
|
+
f"non_canonical_origin: git remote is '{origin}', "
|
|
478
|
+
f"expected '{_CANONICAL_REPO}'"
|
|
479
|
+
)
|
|
480
|
+
_log_violation("fork_detection", {
|
|
481
|
+
"origin": origin,
|
|
482
|
+
"expected": _CANONICAL_REPO,
|
|
483
|
+
})
|
|
484
|
+
logger.warning(
|
|
485
|
+
"CRP™ fork detected — origin '%s' does not match canonical "
|
|
486
|
+
"repository '%s'. If you are redistributing CRP as a "
|
|
487
|
+
"managed service, you MUST obtain a commercial license. "
|
|
488
|
+
"Contact: contact@crprotocol.io",
|
|
489
|
+
origin, _CANONICAL_REPO,
|
|
490
|
+
)
|
|
491
|
+
except (OSError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
return warnings_list
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ---------------------------------------------------------------------------
|
|
498
|
+
# Code-signature chain — tamper-evident module registry
|
|
499
|
+
# ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
def build_integrity_manifest() -> dict[str, Any]:
|
|
502
|
+
"""Build a tamper-evident manifest of all core CRP modules.
|
|
503
|
+
|
|
504
|
+
Returns a dict mapping module names to their SHA-256 fingerprints
|
|
505
|
+
plus a combined chain hash. If any module is modified, the chain
|
|
506
|
+
hash changes — making it trivially detectable.
|
|
507
|
+
"""
|
|
508
|
+
fingerprints = get_module_fingerprints()
|
|
509
|
+
# Build a deterministic chain: sort by module name, hash the concatenation
|
|
510
|
+
chain_input = "|".join(
|
|
511
|
+
f"{mod}:{fp}" for mod, fp in sorted(fingerprints.items()) if fp
|
|
512
|
+
)
|
|
513
|
+
chain_hash = hashlib.sha256(chain_input.encode("utf-8")).hexdigest()
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
"version": _PRODUCT,
|
|
517
|
+
"generated_at": time.time(),
|
|
518
|
+
"modules": fingerprints,
|
|
519
|
+
"chain_hash": chain_hash,
|
|
520
|
+
"licensor": _LICENSOR,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def verify_integrity_manifest(manifest: dict[str, Any]) -> list[str]:
|
|
525
|
+
"""Verify a previously-built integrity manifest against current state.
|
|
526
|
+
|
|
527
|
+
Returns list of modules whose fingerprints have changed.
|
|
528
|
+
"""
|
|
529
|
+
current = get_module_fingerprints()
|
|
530
|
+
changes: list[str] = []
|
|
531
|
+
|
|
532
|
+
stored = manifest.get("modules", {})
|
|
533
|
+
for mod, stored_fp in stored.items():
|
|
534
|
+
current_fp = current.get(mod)
|
|
535
|
+
if stored_fp and current_fp and stored_fp != current_fp:
|
|
536
|
+
changes.append(mod)
|
|
537
|
+
|
|
538
|
+
if changes:
|
|
539
|
+
_log_violation("integrity_manifest_mismatch", {
|
|
540
|
+
"changed_modules": changes,
|
|
541
|
+
"stored_chain": manifest.get("chain_hash"),
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
return changes
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# ---------------------------------------------------------------------------
|
|
548
|
+
# Anti-stripping protection
|
|
549
|
+
# ---------------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
def verify_guard_integrity() -> bool:
|
|
552
|
+
"""Verify that this license guard module has not been gutted/stubbed.
|
|
553
|
+
|
|
554
|
+
Attackers may replace license_guard.py with a stub that passes all
|
|
555
|
+
checks. This function verifies the module's own source has a minimum
|
|
556
|
+
complexity threshold (line count > 200, size > 5KB), contains required
|
|
557
|
+
enforcement function names, and the startup check is wired.
|
|
558
|
+
"""
|
|
559
|
+
try:
|
|
560
|
+
with open(__file__, "rb") as f:
|
|
561
|
+
source = f.read()
|
|
562
|
+
|
|
563
|
+
# Minimum size check — a gutted stub would be much smaller
|
|
564
|
+
if len(source) < 5000:
|
|
565
|
+
_state.tamper_detected = True
|
|
566
|
+
_log_violation("guard_stripped", {"size": len(source)})
|
|
567
|
+
return False
|
|
568
|
+
|
|
569
|
+
source_text = source.decode("utf-8", errors="ignore")
|
|
570
|
+
|
|
571
|
+
# Must contain all enforcement functions
|
|
572
|
+
required = [
|
|
573
|
+
"def verify_license_headers",
|
|
574
|
+
"def check_managed_service_restriction",
|
|
575
|
+
"def is_feature_allowed",
|
|
576
|
+
"def watermark_output",
|
|
577
|
+
"def _startup_check",
|
|
578
|
+
"def _log_violation",
|
|
579
|
+
"CRPError",
|
|
580
|
+
"_CORE_MODULES",
|
|
581
|
+
]
|
|
582
|
+
for req in required:
|
|
583
|
+
if req not in source_text:
|
|
584
|
+
_state.tamper_detected = True
|
|
585
|
+
_log_violation("guard_gutted", {"missing": req})
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
# Line count check
|
|
589
|
+
line_count = source_text.count("\n")
|
|
590
|
+
if line_count < 200:
|
|
591
|
+
_state.tamper_detected = True
|
|
592
|
+
_log_violation("guard_truncated", {"lines": line_count})
|
|
593
|
+
return False
|
|
594
|
+
|
|
595
|
+
return True
|
|
596
|
+
except OSError:
|
|
597
|
+
_state.tamper_detected = True
|
|
598
|
+
return False
|
|
599
|
+
|
|
600
|
+
def get_license_info() -> dict[str, Any]:
|
|
601
|
+
"""Return machine-readable license metadata for this installation."""
|
|
602
|
+
return {
|
|
603
|
+
"license": "Elastic License 2.0 (ELv2)",
|
|
604
|
+
"licensor": _LICENSOR,
|
|
605
|
+
"abn": _ABN,
|
|
606
|
+
"product": _PRODUCT,
|
|
607
|
+
"url": _LICENSE_URL,
|
|
608
|
+
"contact": "contact@crprotocol.io",
|
|
609
|
+
"security": "security@crprotocol.io",
|
|
610
|
+
"restriction": "May not be provided as a managed service without commercial license",
|
|
611
|
+
"enforcement": {
|
|
612
|
+
"tamper_detected": _state.tamper_detected,
|
|
613
|
+
"features_degraded": _state.features_degraded,
|
|
614
|
+
"violation_count": _state.violation_count,
|
|
615
|
+
"commercial_license": _state.commercial_license,
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# ---------------------------------------------------------------------------
|
|
621
|
+
# Violation telemetry (LOCAL ONLY — no network calls)
|
|
622
|
+
# ---------------------------------------------------------------------------
|
|
623
|
+
|
|
624
|
+
def _log_violation(violation_type: str, details: dict[str, Any]) -> None:
|
|
625
|
+
"""Log a license violation to the local telemetry file.
|
|
626
|
+
|
|
627
|
+
No network calls. All data stays on the local filesystem.
|
|
628
|
+
Violations are logged to ~/.crp/violations.jsonl for audit.
|
|
629
|
+
"""
|
|
630
|
+
entry = {
|
|
631
|
+
"timestamp": time.time(),
|
|
632
|
+
"type": violation_type,
|
|
633
|
+
"product": _PRODUCT,
|
|
634
|
+
"licensor": _LICENSOR,
|
|
635
|
+
**details,
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
violations_dir = Path.home() / ".crp"
|
|
640
|
+
violations_dir.mkdir(exist_ok=True)
|
|
641
|
+
violations_file = violations_dir / "violations.jsonl"
|
|
642
|
+
with open(violations_file, "a", encoding="utf-8") as f:
|
|
643
|
+
f.write(json.dumps(entry) + "\n")
|
|
644
|
+
except OSError:
|
|
645
|
+
pass
|
|
646
|
+
|
|
647
|
+
logger.warning("License violation logged: %s — %s", violation_type, details)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# ---------------------------------------------------------------------------
|
|
651
|
+
# Startup check (ENFORCED — runs on first import of crp)
|
|
652
|
+
# ---------------------------------------------------------------------------
|
|
653
|
+
|
|
654
|
+
def _startup_check() -> None:
|
|
655
|
+
"""Run license enforcement checks at import time.
|
|
656
|
+
|
|
657
|
+
Order:
|
|
658
|
+
1. Check for commercial license key
|
|
659
|
+
2. Block managed-service usage (raises CRPError if detected)
|
|
660
|
+
3. Verify license headers (degrades features if tampered)
|
|
661
|
+
4. Verify package provenance (detects renamed forks)
|
|
662
|
+
5. Verify guard integrity (detects stubbed-out guard)
|
|
663
|
+
6. Check origin binding (advisory — logs non-canonical forks)
|
|
664
|
+
"""
|
|
665
|
+
_state.checked_at = time.time()
|
|
666
|
+
|
|
667
|
+
# 1. Commercial license
|
|
668
|
+
_state.commercial_license = _check_commercial_license()
|
|
669
|
+
|
|
670
|
+
# 2. Managed-service restriction (BLOCKS if violated)
|
|
671
|
+
check_managed_service_restriction()
|
|
672
|
+
|
|
673
|
+
# 3. License header verification (DEGRADES if violated)
|
|
674
|
+
violations = verify_license_headers()
|
|
675
|
+
if violations:
|
|
676
|
+
logger.error(
|
|
677
|
+
"LICENSE ENFORCEMENT: %d core module(s) have tampered headers. "
|
|
678
|
+
"Advanced features DISABLED. Modules: %s",
|
|
679
|
+
len(violations),
|
|
680
|
+
", ".join(violations),
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# 4. Package provenance (DEGRADES if violated)
|
|
684
|
+
prov_violations = verify_package_provenance()
|
|
685
|
+
if prov_violations:
|
|
686
|
+
_state.features_degraded = True
|
|
687
|
+
logger.error(
|
|
688
|
+
"PROVENANCE ENFORCEMENT: Package provenance check failed — %d violation(s). "
|
|
689
|
+
"Advanced features DISABLED.",
|
|
690
|
+
len(prov_violations),
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# 5. Guard integrity (DEGRADES if violated)
|
|
694
|
+
if not verify_guard_integrity():
|
|
695
|
+
_state.features_degraded = True
|
|
696
|
+
logger.error(
|
|
697
|
+
"GUARD INTEGRITY: license_guard.py appears to have been "
|
|
698
|
+
"stripped or gutted. Advanced features DISABLED."
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# 6. Origin binding (advisory only — logs warnings)
|
|
702
|
+
verify_origin_binding()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# ---------------------------------------------------------------------------
|
|
706
|
+
# Public API
|
|
707
|
+
# ---------------------------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
__all__ = [
|
|
710
|
+
"verify_license_headers",
|
|
711
|
+
"check_managed_service_restriction",
|
|
712
|
+
"is_feature_allowed",
|
|
713
|
+
"watermark_output",
|
|
714
|
+
"get_license_info",
|
|
715
|
+
"get_module_fingerprints",
|
|
716
|
+
"compute_module_fingerprint",
|
|
717
|
+
"verify_package_provenance",
|
|
718
|
+
"verify_origin_binding",
|
|
719
|
+
"build_integrity_manifest",
|
|
720
|
+
"verify_integrity_manifest",
|
|
721
|
+
"verify_guard_integrity",
|
|
722
|
+
]
|