Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a09cbc-910e-77a0-b336-3130194350b3
123 lines
5.2 KiB
Python
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
|