intelligence-radar/intelligence_radar/bundles.py
tegwick 919aefbef5 Add portable evidence bundles and budgeted OpenRouter trials
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a09cbc-910e-77a0-b336-3130194350b3
2026-09-13 23:50:19 +02:00

123 lines
5.2 KiB
Python

"""Portable recommendation bundles with validated, content-addressed dependencies."""
import hashlib
import io
import json
from pathlib import Path
import re
import tarfile
import tempfile
from .advisor import replay
from .store import atomic, canonical, get
PATH = re.compile(r'data/(?:recommendations/[0-9a-f]{64}\.json|captures/[0-9a-f]{64}\.json|artifacts/[0-9a-f]{64})')
LIMIT = 128 * 1024 * 1024
def references(value, key):
if isinstance(value, dict):
if key in value:
yield value[key]
for child in value.values():
yield from references(child, key)
elif isinstance(value, list):
for child in value:
yield from references(child, key)
def export_bundle(root, recommendation_id, output):
root, output = Path(root), Path(output)
files = {}
visited = set()
def collect(ident):
if ident in visited:
return
visited.add(ident)
snapshot = get(root, 'recommendations', ident)
replay(root, ident)
name = f'data/recommendations/{ident}.json'
files[name] = (root/name).read_bytes()
for capture_id in set(references(snapshot, 'capture_id')):
receipt = get(root, 'captures', capture_id)
name = f'data/captures/{capture_id}.json'
files[name] = (root/name).read_bytes()
name = f"data/artifacts/{receipt['content_sha256']}"
blob = (root/name).read_bytes()
if hashlib.sha256(blob).hexdigest() != receipt['content_sha256']:
raise ValueError('artifact integrity mismatch')
files[name] = blob
for parent in set(references(snapshot, 'recommendation_id')):
collect(parent)
collect(recommendation_id)
manifest = {'schema_version': 1, 'recommendation_id': recommendation_id,
'files': {name: hashlib.sha256(data).hexdigest() for name, data in sorted(files.items())}}
files['manifest.json'] = canonical(manifest)
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode='w') as archive:
for name, data in sorted(files.items()):
member = tarfile.TarInfo(name)
member.size = len(data)
member.mode = 0o644
archive.addfile(member, io.BytesIO(data))
atomic(output, buffer.getvalue(), immutable=True)
return manifest
def import_bundle(root, bundle):
files = {}
total = 0
try:
archive = tarfile.open(bundle, 'r:*')
except tarfile.TarError as exc:
raise ValueError('invalid evidence bundle') from exc
with archive:
for member in archive:
if member.name in files or not member.isfile():
raise ValueError('duplicate or non-regular bundle member')
if member.name != 'manifest.json' and not PATH.fullmatch(member.name):
raise ValueError('invalid bundle member path')
total += member.size
if total > LIMIT or len(files) >= 10000:
raise ValueError('bundle exceeds import limit')
files[member.name] = archive.extractfile(member).read()
if 'manifest.json' not in files:
raise ValueError('bundle missing manifest')
manifest = json.loads(files.pop('manifest.json'))
if manifest.get('schema_version') != 1 or set(files) != set(manifest['files']):
raise ValueError('bundle manifest mismatch')
for name, blob in files.items():
if hashlib.sha256(blob).hexdigest() != manifest['files'][name]:
raise ValueError('bundle checksum mismatch')
with tempfile.TemporaryDirectory(prefix='radar-import-') as directory:
staging = Path(directory)
for name, blob in files.items():
atomic(staging/name, blob, immutable=True)
stem = Path(name).stem
if '/artifacts/' in name:
if hashlib.sha256(blob).hexdigest() != stem:
raise ValueError('artifact content ID mismatch')
else:
get(staging, name.split('/')[1], stem)
replay(staging, manifest['recommendation_id'])
# Validate every contained report, including parent recommendations.
for name in files:
if '/recommendations/' in name:
snapshot = get(staging, 'recommendations', Path(name).stem)
for capture_id in set(references(snapshot, 'capture_id')):
receipt = get(staging, 'captures', capture_id)
artifact = staging / 'data/artifacts' / receipt['content_sha256']
if hashlib.sha256(artifact.read_bytes()).hexdigest() != receipt['content_sha256']:
raise ValueError('capture artifact integrity mismatch')
for parent in set(references(snapshot, 'recommendation_id')):
get(staging, 'recommendations', parent)
replay(staging, Path(name).stem)
# Refuse collisions before publishing anything into the target ledger.
for name, blob in files.items():
destination = Path(root)/name
if destination.exists() and destination.read_bytes() != blob:
raise ValueError('destination contains conflicting evidence')
for name, blob in files.items():
atomic(Path(root)/name, blob, immutable=True)
return manifest