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.
- context_firewall/__init__.py +3 -0
- context_firewall/analytics/__init__.py +5 -0
- context_firewall/analytics/engine.py +234 -0
- context_firewall/api/__init__.py +5 -0
- context_firewall/api/app.py +1020 -0
- context_firewall/api/models.py +54 -0
- context_firewall/classifier/__init__.py +5 -0
- context_firewall/classifier/classifier.py +165 -0
- context_firewall/cli/__init__.py +5 -0
- context_firewall/cli/main.py +513 -0
- context_firewall/compliance/__init__.py +1 -0
- context_firewall/compliance/baa.py +57 -0
- context_firewall/compliance/chain.py +84 -0
- context_firewall/compliance/control_mappings.py +92 -0
- context_firewall/compliance/export.py +280 -0
- context_firewall/compliance/keys.py +94 -0
- context_firewall/config.py +234 -0
- context_firewall/control_plane/__init__.py +0 -0
- context_firewall/control_plane/client.py +107 -0
- context_firewall/control_plane/models.py +72 -0
- context_firewall/control_plane/pusher.py +377 -0
- context_firewall/daemon/__init__.py +5 -0
- context_firewall/daemon/main.py +461 -0
- context_firewall/db/__init__.py +6 -0
- context_firewall/db/connection.py +30 -0
- context_firewall/db/migrations.py +408 -0
- context_firewall/entropy/__init__.py +5 -0
- context_firewall/entropy/engine.py +271 -0
- context_firewall/graph/__init__.py +5 -0
- context_firewall/graph/engine.py +162 -0
- context_firewall/lint/__init__.py +0 -0
- context_firewall/lint/engine.py +334 -0
- context_firewall/mcp/__init__.py +5 -0
- context_firewall/mcp/server.py +237 -0
- context_firewall/metrics.py +100 -0
- context_firewall/models.py +130 -0
- context_firewall/policy/__init__.py +10 -0
- context_firewall/policy/detectors/__init__.py +0 -0
- context_firewall/policy/detectors/injection.py +480 -0
- context_firewall/policy/dsl/__init__.py +0 -0
- context_firewall/policy/dsl/evaluator.py +208 -0
- context_firewall/policy/dsl/loader.py +150 -0
- context_firewall/policy/dsl/types.py +112 -0
- context_firewall/policy/engine.py +532 -0
- context_firewall/provenance/__init__.py +5 -0
- context_firewall/provenance/engine.py +658 -0
- context_firewall/provenance/models.py +91 -0
- context_firewall/proxy/__init__.py +1 -0
- context_firewall/proxy/router.py +519 -0
- context_firewall/proxy/scanner.py +214 -0
- context_firewall/proxy/tokens.py +156 -0
- context_firewall/runtime/__init__.py +5 -0
- context_firewall/runtime/engine.py +303 -0
- context_firewall/source/__init__.py +1 -0
- context_firewall/source/registry.py +299 -0
- context_firewall/source/types.py +12 -0
- context_firewall/synthesizer/__init__.py +5 -0
- context_firewall/synthesizer/synthesizer.py +255 -0
- context_firewall/trust/__init__.py +5 -0
- context_firewall/trust/engine.py +139 -0
- context_firewall/trust/signals.py +184 -0
- contextwall-0.1.0.dist-info/METADATA +431 -0
- contextwall-0.1.0.dist-info/RECORD +66 -0
- contextwall-0.1.0.dist-info/WHEEL +4 -0
- contextwall-0.1.0.dist-info/entry_points.txt +3 -0
- contextwall-0.1.0.dist-info/licenses/LICENSE +33 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Analytics Engine - DuckDB in-process queries with snapshot pre-computation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
import aiosqlite
|
|
12
|
+
import duckdb
|
|
13
|
+
|
|
14
|
+
from context_firewall.config import Config
|
|
15
|
+
from context_firewall.models import SubsystemHealth
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="cre-analytics")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_duckdb_conn(sqlite_path: str) -> duckdb.DuckDBPyConnection:
|
|
23
|
+
conn = duckdb.connect(":memory:", read_only=False)
|
|
24
|
+
conn.execute(f"ATTACH '{sqlite_path}' AS cre (TYPE SQLITE, READ_ONLY TRUE)")
|
|
25
|
+
return conn
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _compute_retrieval_metrics_sync(sqlite_path: str) -> list[dict]:
|
|
29
|
+
with _get_duckdb_conn(sqlite_path) as conn:
|
|
30
|
+
rows = conn.execute("""
|
|
31
|
+
SELECT
|
|
32
|
+
json_extract(payload, '$.file_path') AS file_path,
|
|
33
|
+
COUNT(*) FILTER (WHERE event_type = 'slice_included') AS inclusion_count,
|
|
34
|
+
COUNT(*) FILTER (WHERE event_type = 'slice_excluded') AS exclusion_count,
|
|
35
|
+
AVG(json_extract(payload, '$.trust_score')::FLOAT) AS avg_trust_score
|
|
36
|
+
FROM cre.provenance_events
|
|
37
|
+
WHERE event_type IN ('slice_included', 'slice_excluded')
|
|
38
|
+
GROUP BY file_path
|
|
39
|
+
ORDER BY inclusion_count DESC
|
|
40
|
+
LIMIT 100
|
|
41
|
+
""").fetchall()
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
"file_path": r[0],
|
|
45
|
+
"inclusion_count": r[1],
|
|
46
|
+
"exclusion_count": r[2],
|
|
47
|
+
"avg_trust_score": float(r[3]) if r[3] is not None else None,
|
|
48
|
+
}
|
|
49
|
+
for r in rows
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _compute_trust_degradation_sync(sqlite_path: str, threshold: float, window_days: int) -> list[dict]:
|
|
54
|
+
with _get_duckdb_conn(sqlite_path) as conn:
|
|
55
|
+
rows = conn.execute(f"""
|
|
56
|
+
WITH ranked AS (
|
|
57
|
+
SELECT
|
|
58
|
+
node_id,
|
|
59
|
+
file_path,
|
|
60
|
+
trust_score,
|
|
61
|
+
snapshot_at,
|
|
62
|
+
ROW_NUMBER() OVER (PARTITION BY node_id ORDER BY snapshot_at DESC) AS rn,
|
|
63
|
+
ROW_NUMBER() OVER (PARTITION BY node_id ORDER BY snapshot_at ASC) AS rn_asc
|
|
64
|
+
FROM cre.trust_score_snapshots
|
|
65
|
+
WHERE snapshot_at > (CURRENT_TIMESTAMP - INTERVAL '{window_days} days')
|
|
66
|
+
),
|
|
67
|
+
first_last AS (
|
|
68
|
+
SELECT
|
|
69
|
+
node_id,
|
|
70
|
+
file_path,
|
|
71
|
+
MAX(CASE WHEN rn_asc = 1 THEN trust_score END) AS initial_score,
|
|
72
|
+
MAX(CASE WHEN rn = 1 THEN trust_score END) AS latest_score
|
|
73
|
+
FROM ranked
|
|
74
|
+
GROUP BY node_id, file_path
|
|
75
|
+
)
|
|
76
|
+
SELECT node_id, file_path, initial_score, latest_score,
|
|
77
|
+
initial_score - latest_score AS degradation
|
|
78
|
+
FROM first_last
|
|
79
|
+
WHERE initial_score - latest_score > {threshold}
|
|
80
|
+
ORDER BY degradation DESC
|
|
81
|
+
LIMIT 50
|
|
82
|
+
""").fetchall()
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
"node_id": r[0],
|
|
86
|
+
"file_path": r[1],
|
|
87
|
+
"initial_score": float(r[2]) if r[2] is not None else None,
|
|
88
|
+
"latest_score": float(r[3]) if r[3] is not None else None,
|
|
89
|
+
"degradation": float(r[4]) if r[4] is not None else None,
|
|
90
|
+
}
|
|
91
|
+
for r in rows
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AnalyticsEngine:
|
|
96
|
+
name = "analytics_engine"
|
|
97
|
+
critical = False
|
|
98
|
+
|
|
99
|
+
def __init__(self) -> None:
|
|
100
|
+
self._config: Config | None = None
|
|
101
|
+
self._db: aiosqlite.Connection | None = None
|
|
102
|
+
self._sqlite_path: str = ".ctxfw/cre.db"
|
|
103
|
+
|
|
104
|
+
async def init(self, config: Config) -> None:
|
|
105
|
+
self._config = config
|
|
106
|
+
self._sqlite_path = config.storage.db_path
|
|
107
|
+
from context_firewall.db.connection import get_db
|
|
108
|
+
self._db = await get_db()
|
|
109
|
+
logger.info("AnalyticsEngine initialized")
|
|
110
|
+
|
|
111
|
+
def health_check(self) -> SubsystemHealth:
|
|
112
|
+
return SubsystemHealth(name=self.name, healthy=True)
|
|
113
|
+
|
|
114
|
+
async def shutdown(self) -> None:
|
|
115
|
+
self._db = None
|
|
116
|
+
|
|
117
|
+
async def get_retrieval_metrics(self) -> dict:
|
|
118
|
+
snapshot = await self._latest_snapshot("retrieval_metrics")
|
|
119
|
+
if snapshot:
|
|
120
|
+
return {"data": snapshot, "source": "snapshot"}
|
|
121
|
+
try:
|
|
122
|
+
loop = asyncio.get_event_loop()
|
|
123
|
+
data = await loop.run_in_executor(
|
|
124
|
+
_executor,
|
|
125
|
+
_compute_retrieval_metrics_sync,
|
|
126
|
+
self._sqlite_path,
|
|
127
|
+
)
|
|
128
|
+
return {"data": data, "source": "live"}
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error("retrieval metrics failed", extra={"error": str(e)})
|
|
131
|
+
return {"data": [], "error": str(e)}
|
|
132
|
+
|
|
133
|
+
async def get_trust_degradation(self) -> dict:
|
|
134
|
+
cfg = self._config.analytics if self._config else None
|
|
135
|
+
threshold = cfg.degradation_threshold if cfg else 0.15
|
|
136
|
+
window_days = cfg.degradation_window_days if cfg else 30
|
|
137
|
+
try:
|
|
138
|
+
loop = asyncio.get_event_loop()
|
|
139
|
+
data = await loop.run_in_executor(
|
|
140
|
+
_executor,
|
|
141
|
+
_compute_trust_degradation_sync,
|
|
142
|
+
self._sqlite_path,
|
|
143
|
+
threshold,
|
|
144
|
+
window_days,
|
|
145
|
+
)
|
|
146
|
+
return {"data": data, "threshold": threshold, "window_days": window_days}
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error("trust degradation failed", extra={"error": str(e)})
|
|
149
|
+
return {"data": [], "error": str(e)}
|
|
150
|
+
|
|
151
|
+
async def get_entropy_trends(self) -> dict:
|
|
152
|
+
if self._db is None:
|
|
153
|
+
return {"data": []}
|
|
154
|
+
async with self._db.execute(
|
|
155
|
+
"""
|
|
156
|
+
SELECT node_id, file_path, entropy_score, snapshot_at
|
|
157
|
+
FROM entropy_snapshots
|
|
158
|
+
ORDER BY snapshot_at DESC
|
|
159
|
+
LIMIT 500
|
|
160
|
+
"""
|
|
161
|
+
) as cursor:
|
|
162
|
+
rows = await cursor.fetchall()
|
|
163
|
+
return {
|
|
164
|
+
"data": [
|
|
165
|
+
{"node_id": r["node_id"], "file_path": r["file_path"],
|
|
166
|
+
"entropy_score": r["entropy_score"], "snapshot_at": r["snapshot_at"]}
|
|
167
|
+
for r in rows
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async def get_budget_utilization(self) -> dict:
|
|
172
|
+
snapshot = await self._latest_snapshot("budget_utilization")
|
|
173
|
+
if snapshot:
|
|
174
|
+
return {"data": snapshot, "source": "snapshot"}
|
|
175
|
+
if self._db is None:
|
|
176
|
+
return {"data": []}
|
|
177
|
+
async with self._db.execute(
|
|
178
|
+
"""
|
|
179
|
+
SELECT
|
|
180
|
+
json_extract(payload, '$.task_type') AS task_type,
|
|
181
|
+
AVG(CAST(json_extract(payload, '$.total_tokens') AS REAL)
|
|
182
|
+
/ CAST(json_extract(payload, '$.token_budget') AS REAL)) AS avg_utilization,
|
|
183
|
+
COUNT(*) AS bundle_count
|
|
184
|
+
FROM provenance_events
|
|
185
|
+
WHERE event_type = 'context_request'
|
|
186
|
+
GROUP BY task_type
|
|
187
|
+
"""
|
|
188
|
+
) as cursor:
|
|
189
|
+
rows = await cursor.fetchall()
|
|
190
|
+
return {
|
|
191
|
+
"data": [
|
|
192
|
+
{"task_type": r["task_type"], "avg_utilization": r["avg_utilization"], "bundle_count": r["bundle_count"]}
|
|
193
|
+
for r in rows
|
|
194
|
+
],
|
|
195
|
+
"source": "live",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async def _latest_snapshot(self, metric_type: str) -> list | None:
|
|
199
|
+
if self._db is None:
|
|
200
|
+
return None
|
|
201
|
+
async with self._db.execute(
|
|
202
|
+
"""
|
|
203
|
+
SELECT payload FROM analytics_snapshots
|
|
204
|
+
WHERE metric_type = ?
|
|
205
|
+
ORDER BY computed_at DESC
|
|
206
|
+
LIMIT 1
|
|
207
|
+
""",
|
|
208
|
+
(metric_type,),
|
|
209
|
+
) as cursor:
|
|
210
|
+
row = await cursor.fetchone()
|
|
211
|
+
if row:
|
|
212
|
+
return json.loads(row["payload"])
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
async def compute_and_store_snapshots(self) -> None:
|
|
216
|
+
"""Called by the offline job scheduler."""
|
|
217
|
+
now = datetime.now(timezone.utc)
|
|
218
|
+
try:
|
|
219
|
+
loop = asyncio.get_event_loop()
|
|
220
|
+
retrieval_data = await loop.run_in_executor(
|
|
221
|
+
_executor, _compute_retrieval_metrics_sync, self._sqlite_path
|
|
222
|
+
)
|
|
223
|
+
if self._db:
|
|
224
|
+
await self._db.execute(
|
|
225
|
+
"""
|
|
226
|
+
INSERT INTO analytics_snapshots
|
|
227
|
+
(metric_type, granularity, key, payload, computed_at)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?)
|
|
229
|
+
""",
|
|
230
|
+
("retrieval_metrics", "hourly", "all", json.dumps(retrieval_data), now.isoformat()),
|
|
231
|
+
)
|
|
232
|
+
await self._db.commit()
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error("snapshot computation failed", extra={"error": str(e)})
|