contextwall 0.1.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 (66) hide show
  1. context_firewall/__init__.py +3 -0
  2. context_firewall/analytics/__init__.py +5 -0
  3. context_firewall/analytics/engine.py +234 -0
  4. context_firewall/api/__init__.py +5 -0
  5. context_firewall/api/app.py +1020 -0
  6. context_firewall/api/models.py +54 -0
  7. context_firewall/classifier/__init__.py +5 -0
  8. context_firewall/classifier/classifier.py +165 -0
  9. context_firewall/cli/__init__.py +5 -0
  10. context_firewall/cli/main.py +513 -0
  11. context_firewall/compliance/__init__.py +1 -0
  12. context_firewall/compliance/baa.py +57 -0
  13. context_firewall/compliance/chain.py +84 -0
  14. context_firewall/compliance/control_mappings.py +92 -0
  15. context_firewall/compliance/export.py +280 -0
  16. context_firewall/compliance/keys.py +94 -0
  17. context_firewall/config.py +234 -0
  18. context_firewall/control_plane/__init__.py +0 -0
  19. context_firewall/control_plane/client.py +107 -0
  20. context_firewall/control_plane/models.py +72 -0
  21. context_firewall/control_plane/pusher.py +377 -0
  22. context_firewall/daemon/__init__.py +5 -0
  23. context_firewall/daemon/main.py +461 -0
  24. context_firewall/db/__init__.py +6 -0
  25. context_firewall/db/connection.py +30 -0
  26. context_firewall/db/migrations.py +408 -0
  27. context_firewall/entropy/__init__.py +5 -0
  28. context_firewall/entropy/engine.py +271 -0
  29. context_firewall/graph/__init__.py +5 -0
  30. context_firewall/graph/engine.py +162 -0
  31. context_firewall/lint/__init__.py +0 -0
  32. context_firewall/lint/engine.py +334 -0
  33. context_firewall/mcp/__init__.py +5 -0
  34. context_firewall/mcp/server.py +237 -0
  35. context_firewall/metrics.py +100 -0
  36. context_firewall/models.py +130 -0
  37. context_firewall/policy/__init__.py +10 -0
  38. context_firewall/policy/detectors/__init__.py +0 -0
  39. context_firewall/policy/detectors/injection.py +480 -0
  40. context_firewall/policy/dsl/__init__.py +0 -0
  41. context_firewall/policy/dsl/evaluator.py +208 -0
  42. context_firewall/policy/dsl/loader.py +150 -0
  43. context_firewall/policy/dsl/types.py +112 -0
  44. context_firewall/policy/engine.py +532 -0
  45. context_firewall/provenance/__init__.py +5 -0
  46. context_firewall/provenance/engine.py +658 -0
  47. context_firewall/provenance/models.py +91 -0
  48. context_firewall/proxy/__init__.py +1 -0
  49. context_firewall/proxy/router.py +519 -0
  50. context_firewall/proxy/scanner.py +214 -0
  51. context_firewall/proxy/tokens.py +156 -0
  52. context_firewall/runtime/__init__.py +5 -0
  53. context_firewall/runtime/engine.py +303 -0
  54. context_firewall/source/__init__.py +1 -0
  55. context_firewall/source/registry.py +299 -0
  56. context_firewall/source/types.py +12 -0
  57. context_firewall/synthesizer/__init__.py +5 -0
  58. context_firewall/synthesizer/synthesizer.py +255 -0
  59. context_firewall/trust/__init__.py +5 -0
  60. context_firewall/trust/engine.py +139 -0
  61. context_firewall/trust/signals.py +184 -0
  62. contextwall-0.1.0.dist-info/METADATA +431 -0
  63. contextwall-0.1.0.dist-info/RECORD +66 -0
  64. contextwall-0.1.0.dist-info/WHEEL +4 -0
  65. contextwall-0.1.0.dist-info/entry_points.txt +3 -0
  66. contextwall-0.1.0.dist-info/licenses/LICENSE +33 -0
@@ -0,0 +1,92 @@
1
+ """Control mapping definitions for HIPAA, SOC2 Type II, and FedRAMP Moderate.
2
+
3
+ Task 8.7: walk active policy rules, map to framework controls, set satisfied flag.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+
10
+
11
+ @dataclass
12
+ class ControlRef:
13
+ framework: str
14
+ control_id: str
15
+ description: str
16
+
17
+
18
+ # Built-in control mappings per rule name prefix / action type
19
+ RULE_CONTROL_MAP: dict[str, list[ControlRef]] = {
20
+ # HIPAA minimum-necessary rule
21
+ "hipaa-minimum-necessary": [
22
+ ControlRef("hipaa", "45 CFR 164.502(b)", "Minimum Necessary Standard"),
23
+ ControlRef("hipaa", "45 CFR 164.514(d)", "Minimum Necessary - workforce access"),
24
+ ],
25
+ # HIPAA audit control
26
+ "hipaa-audit-control": [
27
+ ControlRef("hipaa", "45 CFR 164.312(b)", "Audit Controls"),
28
+ ],
29
+ # Untrusted source sanitization
30
+ "untrusted-source-sanitization": [
31
+ ControlRef("soc2", "CC6.1", "Logical and Physical Access Controls"),
32
+ ControlRef("soc2", "CC6.6", "External Threats - logical access controls"),
33
+ ControlRef("fedramp", "SI-3", "Malicious Code Protection"),
34
+ ],
35
+ # Secret detection
36
+ "block_secrets": [
37
+ ControlRef("soc2", "CC6.1", "Logical and Physical Access Controls"),
38
+ ControlRef("fedramp", "AC-3", "Access Enforcement"),
39
+ ],
40
+ # PII redaction
41
+ "redact_pii": [
42
+ ControlRef("hipaa", "45 CFR 164.502(b)", "Minimum Necessary Standard"),
43
+ ControlRef("gdpr", "Art. 5(1)(c)", "Data Minimisation"),
44
+ ],
45
+ # SOC2 logging
46
+ "soc2-policy-action-logging": [
47
+ ControlRef("soc2", "CC7.2", "System Monitoring"),
48
+ ControlRef("soc2", "CC8.1", "Change Management"),
49
+ ControlRef("fedramp", "AU-2", "Audit Events"),
50
+ ControlRef("fedramp", "AU-9", "Protection of Audit Information"),
51
+ ],
52
+ }
53
+
54
+
55
+ @dataclass
56
+ class ControlMapping:
57
+ framework: str
58
+ control_id: str
59
+ description: str
60
+ satisfied: bool
61
+ rule_name: str
62
+
63
+
64
+ def resolve_control_mappings(
65
+ fired_rule_names: set[str],
66
+ requested_framework: str | None = None,
67
+ ) -> list[ControlMapping]:
68
+ """Map fired rules to compliance controls.
69
+
70
+ satisfied=True when the rule fired in the session (evidence exists).
71
+ satisfied=False when the control is required but the rule did not fire.
72
+ """
73
+ mappings: list[ControlMapping] = []
74
+ seen: set[tuple[str, str]] = set()
75
+
76
+ for rule_name, refs in RULE_CONTROL_MAP.items():
77
+ for ref in refs:
78
+ if requested_framework and ref.framework != requested_framework:
79
+ continue
80
+ key = (ref.framework, ref.control_id)
81
+ if key in seen:
82
+ continue
83
+ seen.add(key)
84
+ mappings.append(ControlMapping(
85
+ framework=ref.framework,
86
+ control_id=ref.control_id,
87
+ description=ref.description,
88
+ satisfied=rule_name in fired_rule_names,
89
+ rule_name=rule_name,
90
+ ))
91
+
92
+ return mappings
@@ -0,0 +1,280 @@
1
+ """Compliance export bundle assembler.
2
+
3
+ Generates signed, Merkle-chained ComplianceExportBundle sliced by session ID,
4
+ time range, or framework filter.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import logging
12
+ import uuid
13
+ from dataclasses import asdict, dataclass, field
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import aiosqlite
19
+
20
+ from context_firewall.compliance.chain import (
21
+ CHAIN_GENESIS_HASH,
22
+ ChainEntry,
23
+ compute_entry_hash,
24
+ verify_chain,
25
+ )
26
+ from context_firewall.compliance.control_mappings import (
27
+ ControlMapping,
28
+ resolve_control_mappings,
29
+ )
30
+ from context_firewall.compliance.keys import load_public_key_pem, sign_bundle
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ @dataclass
36
+ class ChainProof:
37
+ first_sequence: int
38
+ last_sequence: int
39
+ entry_count: int
40
+ first_hash: str
41
+ last_hash: str
42
+ hmac_algorithm: str = "HMAC-SHA256"
43
+ chain_valid: bool = True
44
+ chain_error: str = ""
45
+
46
+
47
+ @dataclass
48
+ class TenantMetadata:
49
+ tenant_id: str = ""
50
+ deployment_mode: str = "self-hosted"
51
+ baa_mode: bool = False
52
+ export_generated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
53
+ cre_version: str = "0.1.0"
54
+
55
+
56
+ @dataclass
57
+ class RetentionPolicy:
58
+ retention_days: int = 365
59
+ baa_override_applied: bool = False
60
+ framework: str = ""
61
+
62
+
63
+ @dataclass
64
+ class ComplianceExportBundle:
65
+ bundle_id: str
66
+ export_scope: dict[str, Any]
67
+ provenance_entries: list[dict[str, Any]]
68
+ chain_proof: ChainProof
69
+ control_mappings: list[dict[str, Any]]
70
+ tenant_metadata: TenantMetadata
71
+ retention_policy: RetentionPolicy
72
+ public_key_pem: str = ""
73
+ signature: str = ""
74
+
75
+ def canonical_json(self) -> bytes:
76
+ """Deterministic JSON for signing - excludes signature field."""
77
+ doc = {
78
+ "bundle_id": self.bundle_id,
79
+ "export_scope": self.export_scope,
80
+ "chain_proof": asdict(self.chain_proof),
81
+ "control_mappings": self.control_mappings,
82
+ "tenant_metadata": asdict(self.tenant_metadata),
83
+ "retention_policy": asdict(self.retention_policy),
84
+ "entry_count": len(self.provenance_entries),
85
+ }
86
+ return json.dumps(doc, sort_keys=True, separators=(",", ":")).encode()
87
+
88
+ def to_dict(self) -> dict[str, Any]:
89
+ return {
90
+ "bundle_id": self.bundle_id,
91
+ "export_scope": self.export_scope,
92
+ "provenance_entries": self.provenance_entries,
93
+ "chain_proof": asdict(self.chain_proof),
94
+ "control_mappings": self.control_mappings,
95
+ "tenant_metadata": asdict(self.tenant_metadata),
96
+ "retention_policy": asdict(self.retention_policy),
97
+ "public_key_pem": self.public_key_pem,
98
+ "signature": self.signature,
99
+ }
100
+
101
+
102
+ @dataclass
103
+ class ExportScope:
104
+ session_id: str | None = None
105
+ from_ts: str | None = None
106
+ to_ts: str | None = None
107
+ framework: str | None = None
108
+
109
+
110
+ class ComplianceExporter:
111
+ def __init__(
112
+ self,
113
+ db_path: str,
114
+ hmac_key: str,
115
+ baa_mode: bool = False,
116
+ key_dir: Path | None = None,
117
+ tenant_id: str = "",
118
+ ) -> None:
119
+ self._db_path = db_path
120
+ self._hmac_key = hmac_key
121
+ self._baa_mode = baa_mode
122
+ self._key_dir = key_dir or Path(".ctxfw/keys")
123
+ self._tenant_id = tenant_id
124
+
125
+ async def export(self, scope: ExportScope) -> ComplianceExportBundle:
126
+ """Build and sign a ComplianceExportBundle for the given scope."""
127
+ entries = await self._fetch_entries(scope)
128
+ chain_entries = self._to_chain_entries(entries)
129
+ chain_proof = self._build_chain_proof(chain_entries)
130
+
131
+ fired_rules = self._collect_fired_rules(entries)
132
+ control_mappings = resolve_control_mappings(fired_rules, scope.framework)
133
+
134
+ bundle_id = str(uuid.uuid4())
135
+ retention = self._build_retention_policy(scope.framework)
136
+ tenant_meta = TenantMetadata(
137
+ tenant_id=self._tenant_id,
138
+ baa_mode=self._baa_mode,
139
+ )
140
+
141
+ bundle = ComplianceExportBundle(
142
+ bundle_id=bundle_id,
143
+ export_scope={
144
+ "session_id": scope.session_id,
145
+ "from_ts": scope.from_ts,
146
+ "to_ts": scope.to_ts,
147
+ "framework": scope.framework,
148
+ },
149
+ provenance_entries=entries,
150
+ chain_proof=chain_proof,
151
+ control_mappings=[
152
+ {
153
+ "framework": cm.framework,
154
+ "control_id": cm.control_id,
155
+ "description": cm.description,
156
+ "satisfied": cm.satisfied,
157
+ "rule_name": cm.rule_name,
158
+ }
159
+ for cm in control_mappings
160
+ ],
161
+ tenant_metadata=tenant_meta,
162
+ retention_policy=retention,
163
+ public_key_pem=load_public_key_pem(self._key_dir) or "",
164
+ )
165
+
166
+ sig = sign_bundle(bundle.canonical_json(), self._key_dir)
167
+ bundle.signature = sig or ""
168
+
169
+ return bundle
170
+
171
+ async def verify(self, bundle: ComplianceExportBundle) -> tuple[bool, str]:
172
+ """Verify chain integrity and signature of a bundle."""
173
+ chain_entries = self._to_chain_entries(bundle.provenance_entries)
174
+ chain_ok, chain_err = verify_chain(chain_entries, self._hmac_key)
175
+ if not chain_ok:
176
+ return False, f"chain-integrity-violation: {chain_err}"
177
+
178
+ if bundle.signature and bundle.public_key_pem:
179
+ from context_firewall.compliance.keys import verify_signature
180
+ sig_ok = verify_signature(
181
+ bundle.canonical_json(), bundle.signature, bundle.public_key_pem
182
+ )
183
+ if not sig_ok:
184
+ return False, "signature-verification-failed"
185
+
186
+ return True, "ok"
187
+
188
+ async def _fetch_entries(self, scope: ExportScope) -> list[dict[str, Any]]:
189
+ query = """
190
+ SELECT pe.event_id, pe.event_type, pe.session_id, pe.request_id,
191
+ pe.occurred_at, pe.payload,
192
+ COALESCE(pc.sequence, 0) AS sequence,
193
+ COALESCE(pc.prev_hash, ?) AS prev_hash,
194
+ COALESCE(pc.entry_hash, '') AS entry_hash
195
+ FROM provenance_events pe
196
+ LEFT JOIN provenance_chain pc ON pc.event_id = pe.event_id
197
+ """
198
+ conditions: list[str] = []
199
+ params: list[Any] = [CHAIN_GENESIS_HASH]
200
+
201
+ if scope.session_id:
202
+ conditions.append("pe.session_id = ?")
203
+ params.append(scope.session_id)
204
+ if scope.from_ts:
205
+ conditions.append("pe.occurred_at >= ?")
206
+ params.append(scope.from_ts)
207
+ if scope.to_ts:
208
+ conditions.append("pe.occurred_at <= ?")
209
+ params.append(scope.to_ts)
210
+
211
+ if conditions:
212
+ query += " WHERE " + " AND ".join(conditions)
213
+ query += " ORDER BY COALESCE(pc.sequence, 0), pe.occurred_at ASC"
214
+
215
+ try:
216
+ async with aiosqlite.connect(self._db_path) as db:
217
+ db.row_factory = aiosqlite.Row
218
+ async with db.execute(query, params) as cur:
219
+ rows = await cur.fetchall()
220
+ return [dict(r) for r in rows]
221
+ except Exception as e:
222
+ logger.error("export fetch failed: %s", e)
223
+ return []
224
+
225
+ def _to_chain_entries(self, entries: list[dict[str, Any]]) -> list[ChainEntry]:
226
+ return [
227
+ ChainEntry(
228
+ sequence=e.get("sequence", i),
229
+ event_id=e.get("event_id", ""),
230
+ payload_json=e.get("payload", "{}"),
231
+ prev_hash=e.get("prev_hash", CHAIN_GENESIS_HASH),
232
+ entry_hash=e.get("entry_hash", ""),
233
+ )
234
+ for i, e in enumerate(entries)
235
+ ]
236
+
237
+ def _build_chain_proof(self, entries: list[ChainEntry]) -> ChainProof:
238
+ if not entries:
239
+ return ChainProof(
240
+ first_sequence=0, last_sequence=0, entry_count=0,
241
+ first_hash=CHAIN_GENESIS_HASH, last_hash=CHAIN_GENESIS_HASH,
242
+ )
243
+ ok, err = verify_chain(entries, self._hmac_key)
244
+ return ChainProof(
245
+ first_sequence=entries[0].sequence,
246
+ last_sequence=entries[-1].sequence,
247
+ entry_count=len(entries),
248
+ first_hash=entries[0].entry_hash or CHAIN_GENESIS_HASH,
249
+ last_hash=entries[-1].entry_hash or CHAIN_GENESIS_HASH,
250
+ chain_valid=ok,
251
+ chain_error=err,
252
+ )
253
+
254
+ def _collect_fired_rules(self, entries: list[dict[str, Any]]) -> set[str]:
255
+ fired: set[str] = set()
256
+ for entry in entries:
257
+ try:
258
+ payload = json.loads(entry.get("payload", "{}"))
259
+ rule_name = payload.get("rule_name", "")
260
+ if rule_name:
261
+ fired.add(rule_name)
262
+ except (json.JSONDecodeError, TypeError):
263
+ pass
264
+ return fired
265
+
266
+ def _build_retention_policy(self, framework: str | None) -> RetentionPolicy:
267
+ from context_firewall.compliance.baa import BAA_MIN_RETENTION_DAYS, enforce_baa_retention
268
+ base_days = 365
269
+ if framework in ("hipaa", "fedramp"):
270
+ base_days = 2190 # 6 years
271
+ elif framework == "soc2":
272
+ base_days = 365 # 1 year typical
273
+ if self._baa_mode:
274
+ effective, overridden = enforce_baa_retention(base_days)
275
+ return RetentionPolicy(
276
+ retention_days=effective,
277
+ baa_override_applied=overridden,
278
+ framework=framework or "",
279
+ )
280
+ return RetentionPolicy(retention_days=base_days, framework=framework or "")
@@ -0,0 +1,94 @@
1
+ """Ed25519 keypair management for compliance bundle signing.
2
+
3
+ Key store: encrypted private key written to <key_dir>/signing.key.enc
4
+ Public key exported as PEM to <key_dir>/signing.pub
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import hashlib
11
+ import json
12
+ import logging
13
+ import os
14
+ from pathlib import Path
15
+
16
+ from cryptography.hazmat.primitives import serialization
17
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ _DEFAULT_KEY_DIR = Path(".ctxfw/keys")
22
+
23
+
24
+ def generate_keypair(key_dir: Path = _DEFAULT_KEY_DIR) -> tuple[str, str]:
25
+ """Generate an Ed25519 signing keypair and persist to disk.
26
+
27
+ Returns (private_key_pem, public_key_pem).
28
+ """
29
+ key_dir.mkdir(parents=True, exist_ok=True)
30
+ private_key = Ed25519PrivateKey.generate()
31
+
32
+ private_pem = private_key.private_bytes(
33
+ encoding=serialization.Encoding.PEM,
34
+ format=serialization.PrivateFormat.PKCS8,
35
+ encryption_algorithm=serialization.NoEncryption(),
36
+ ).decode()
37
+
38
+ public_key = private_key.public_key()
39
+ public_pem = public_key.public_bytes(
40
+ encoding=serialization.Encoding.PEM,
41
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
42
+ ).decode()
43
+
44
+ (key_dir / "signing.key").write_text(private_pem)
45
+ (key_dir / "signing.key").chmod(0o600)
46
+ (key_dir / "signing.pub").write_text(public_pem)
47
+
48
+ logger.info("Ed25519 keypair generated in %s", key_dir)
49
+ return private_pem, public_pem
50
+
51
+
52
+ def load_private_key(key_dir: Path = _DEFAULT_KEY_DIR) -> Ed25519PrivateKey | None:
53
+ key_path = key_dir / "signing.key"
54
+ if not key_path.exists():
55
+ return None
56
+ pem = key_path.read_bytes()
57
+ return serialization.load_pem_private_key(pem, password=None)
58
+
59
+
60
+ def load_public_key_pem(key_dir: Path = _DEFAULT_KEY_DIR) -> str | None:
61
+ pub_path = key_dir / "signing.pub"
62
+ if not pub_path.exists():
63
+ return None
64
+ return pub_path.read_text()
65
+
66
+
67
+ def sign_bundle(canonical_json: bytes, key_dir: Path = _DEFAULT_KEY_DIR) -> str | None:
68
+ """Sign canonical JSON with the tenant private key.
69
+
70
+ Returns base64-encoded Ed25519 signature, or None if no key exists.
71
+ """
72
+ private_key = load_private_key(key_dir)
73
+ if private_key is None:
74
+ return None
75
+ signature = private_key.sign(canonical_json)
76
+ return base64.b64encode(signature).decode()
77
+
78
+
79
+ def verify_signature(
80
+ canonical_json: bytes,
81
+ signature_b64: str,
82
+ public_key_pem: str,
83
+ ) -> bool:
84
+ """Verify a bundle signature against the provided public key."""
85
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
86
+ from cryptography.exceptions import InvalidSignature
87
+
88
+ pub_key = serialization.load_pem_public_key(public_key_pem.encode())
89
+ signature = base64.b64decode(signature_b64)
90
+ try:
91
+ pub_key.verify(signature, canonical_json)
92
+ return True
93
+ except InvalidSignature:
94
+ return False
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+ from pydantic import BaseModel, Field
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+
13
+ class SourceDeclaration(BaseModel):
14
+ """Declarative source definition - declared in ctxfw.yaml, registered on startup."""
15
+ id: str
16
+ type: str = "unknown"
17
+ trust_tier: str = "untrusted"
18
+ owner: str = ""
19
+ region: str = ""
20
+ data_classification: str = ""
21
+ config: dict[str, Any] = Field(default_factory=dict)
22
+
23
+
24
+ class DetectionConfig(BaseModel):
25
+ """Injection detector sensitivity knobs."""
26
+ injection_block_threshold: float = 0.55
27
+ injection_warn_threshold: float = 0.35
28
+ default_source_trust_tier: str = "untrusted"
29
+
30
+
31
+ class EnforcementConfig(BaseModel):
32
+ """Trust penalty / reward tuning."""
33
+ penalty_increment: float = 0.15
34
+ decay_half_life_days: float = 1.0
35
+ reward_factor: float = 0.90
36
+
37
+
38
+ class ComplianceFrameworksConfig(BaseModel):
39
+ """Maps data_classification keywords to compliance framework identifiers.
40
+
41
+ Override to add custom classifications or additional frameworks.
42
+ Keys are substrings matched against data_classification (case-insensitive).
43
+ """
44
+ classification_frameworks: dict[str, list[str]] = Field(
45
+ default_factory=lambda: {
46
+ "phi": ["hipaa"],
47
+ "pii": ["hipaa", "gdpr"],
48
+ "pci": ["pci-dss"],
49
+ "financial": ["sox", "fedramp"],
50
+ "federal": ["fedramp"],
51
+ "classified": ["fedramp"],
52
+ "sensitive": ["soc2"],
53
+ "internal_code": [],
54
+ }
55
+ )
56
+
57
+
58
+ def _expand_env_vars(text: str) -> str:
59
+ """Expand ${VAR:-default} and ${VAR} patterns using os.environ."""
60
+ def _replace(m: re.Match) -> str:
61
+ var, _, default = m.group(1).partition(":-")
62
+ return os.environ.get(var, default)
63
+ return re.sub(r"\$\{([^}]+)\}", _replace, text)
64
+
65
+
66
+ class DaemonJobConfig(BaseModel):
67
+ schedule: str
68
+ timeout_min: int = 30
69
+
70
+
71
+ class DaemonIndexingConfig(BaseModel):
72
+ debounce_ms: int = 2000
73
+ max_batch_size: int = 100
74
+ max_concurrent_refreshes: int = 2
75
+
76
+
77
+ class DaemonConfig(BaseModel):
78
+ pid_file: str = ".ctxfw/daemon.pid"
79
+ shutdown_timeout_sec: int = 30
80
+ indexing: DaemonIndexingConfig = Field(default_factory=DaemonIndexingConfig)
81
+ jobs: dict[str, DaemonJobConfig] = Field(default_factory=lambda: {
82
+ "entropy_computation": DaemonJobConfig(schedule="0 */6 * * *", timeout_min=30),
83
+ "weight_calibration": DaemonJobConfig(schedule="0 2 * * *", timeout_min=15),
84
+ "provenance_compaction": DaemonJobConfig(schedule="0 3 * * *", timeout_min=60),
85
+ "annotation_invalidation": DaemonJobConfig(schedule="0 * * * *", timeout_min=10),
86
+ "symbol_table_refresh": DaemonJobConfig(schedule="*/15 * * * *", timeout_min=2),
87
+ "runtime_signal_aggregation": DaemonJobConfig(schedule="*/30 * * * *", timeout_min=5),
88
+ })
89
+
90
+
91
+ class RestApiAuthConfig(BaseModel):
92
+ enabled: bool = True
93
+ tokens: list[dict[str, Any]] = Field(default_factory=list)
94
+
95
+
96
+ class RestApiConfig(BaseModel):
97
+ port: int = 8080
98
+ read_timeout_sec: int = 30
99
+ write_timeout_sec: int = 60
100
+ auth: RestApiAuthConfig = Field(default_factory=RestApiAuthConfig)
101
+
102
+
103
+ class McpConfig(BaseModel):
104
+ transport: str = "stdio"
105
+
106
+
107
+ class GraphConfig(BaseModel):
108
+ max_depth: int = 4
109
+ max_nodes: int = 50
110
+ trust_cutoff: float = 0.30
111
+ traversal_timeout_ms: int = 100
112
+
113
+
114
+ class TrustConfig(BaseModel):
115
+ weights: dict[str, float] = Field(default_factory=lambda: {
116
+ "structural_relevance": 0.30,
117
+ "runtime_evidence": 0.25,
118
+ "freshness": 0.10,
119
+ "stability": 0.10,
120
+ "verification": 0.10,
121
+ "consistency": 0.05,
122
+ "injection_risk": -0.05,
123
+ "entropy_contribution": -0.15,
124
+ })
125
+ fallback_score: float = 0.50
126
+ phase2_learning_rate: float = 0.05
127
+
128
+
129
+ class EntropyConfig(BaseModel):
130
+ default_score: float = 0.30
131
+ high_entropy_threshold: float = 0.60
132
+ high_entropy_consecutive: int = 3
133
+ staleness_penalty: float = 0.15
134
+ analysis_cadence_hours: int = 6
135
+
136
+
137
+ class PolicyConfig(BaseModel):
138
+ policy_dir: str = ".ctxfw/policies"
139
+ max_file_size_kb: int = 50
140
+ exempt_private_ips: bool = True
141
+ cross_repo_auth_required: bool = True
142
+
143
+
144
+ class ProvenanceConfig(BaseModel):
145
+ queue_size: int = 10_000
146
+ session_timeout_min: int = 30
147
+ archival_after_days: int = 90
148
+
149
+
150
+ class SynthesizerConfig(BaseModel):
151
+ default_budget: int = 80_000
152
+ reserve_for_summary: int = 500
153
+ per_task_budgets: dict[str, int] = Field(default_factory=lambda: {
154
+ "BUG_FIX": 60_000,
155
+ "NEW_FEATURE": 80_000,
156
+ "REFACTOR": 100_000,
157
+ "SECURITY_REVIEW": 50_000,
158
+ "DEPENDENCY_AUDIT": 40_000,
159
+ })
160
+
161
+
162
+ class OtelConfig(BaseModel):
163
+ enabled: bool = True
164
+ grpc_port: int = 4317
165
+ http_port: int = 4318
166
+ queue_size: int = 10_000
167
+ consumer_workers: int = 4
168
+ trace_assembly_timeout_sec: int = 30
169
+ signal_batch_flush_ms: int = 500
170
+ signal_batch_size: int = 100
171
+ fuzzy_threshold: float = 0.75
172
+ latency_degraded_threshold_ms: int = 2000
173
+ exception_rate_high_threshold: float = 0.10
174
+
175
+
176
+ class AnalyticsConfig(BaseModel):
177
+ enabled: bool = True
178
+ degradation_threshold: float = 0.15
179
+ degradation_window_days: int = 30
180
+ entropy_trend_snapshots: int = 5
181
+ high_entropy_threshold: float = 0.60
182
+ high_entropy_consecutive: int = 3
183
+
184
+
185
+ class StorageConfig(BaseModel):
186
+ db_path: str = ".ctxfw/cre.db"
187
+ object_storage_bucket: str = ""
188
+ object_storage_prefix: str = "cre-provenance/"
189
+
190
+
191
+ class ControlPlaneConfig(BaseModel):
192
+ """Optional connection to the ContextWall control plane.
193
+
194
+ Leave url empty to run in fully local mode (no data leaves the host).
195
+ The registration_token is issued from the webapp Settings page.
196
+ """
197
+ url: str = ""
198
+ registration_token: str = ""
199
+ daemon_name: str = ""
200
+ push_interval_seconds: int = 60
201
+ heartbeat_interval_seconds: int = 30
202
+ policy_pull: bool = True
203
+
204
+
205
+ class Config(BaseModel):
206
+ repository_root: str = "."
207
+ compliance_hmac_key: str = ""
208
+ sources: list[SourceDeclaration] = Field(default_factory=list)
209
+ detection: DetectionConfig = Field(default_factory=DetectionConfig)
210
+ enforcement: EnforcementConfig = Field(default_factory=EnforcementConfig)
211
+ compliance: ComplianceFrameworksConfig = Field(default_factory=ComplianceFrameworksConfig)
212
+ daemon: DaemonConfig = Field(default_factory=DaemonConfig)
213
+ rest_api: RestApiConfig = Field(default_factory=RestApiConfig)
214
+ mcp: McpConfig = Field(default_factory=McpConfig)
215
+ graph: GraphConfig = Field(default_factory=GraphConfig)
216
+ trust: TrustConfig = Field(default_factory=TrustConfig)
217
+ entropy: EntropyConfig = Field(default_factory=EntropyConfig)
218
+ policy: PolicyConfig = Field(default_factory=PolicyConfig)
219
+ provenance: ProvenanceConfig = Field(default_factory=ProvenanceConfig)
220
+ synthesizer: SynthesizerConfig = Field(default_factory=SynthesizerConfig)
221
+ otel: OtelConfig = Field(default_factory=OtelConfig)
222
+ analytics: AnalyticsConfig = Field(default_factory=AnalyticsConfig)
223
+ storage: StorageConfig = Field(default_factory=StorageConfig)
224
+ control_plane: ControlPlaneConfig = Field(default_factory=ControlPlaneConfig)
225
+
226
+
227
+ def load_config(config_path: str | Path = "ctxfw.yaml") -> Config:
228
+ path = Path(config_path)
229
+ if not path.exists():
230
+ return Config()
231
+ with path.open() as f:
232
+ content = _expand_env_vars(f.read())
233
+ raw = yaml.safe_load(content) or {}
234
+ return Config.model_validate(raw)
File without changes