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
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
+ ]