feat: checkpoint standalone Custodian snapshot receiver
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Failing after 2m10s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a15374-f59f-7a31-86d2-d23b5d2ed181
This commit is contained in:
tegwick 2026-10-19 17:56:32 +02:00
parent f718108858
commit 9ef75e0e18
4 changed files with 257 additions and 0 deletions

View file

@ -0,0 +1,115 @@
"""Receive fixed-policy owner snapshots; no Forge credential or arbitrary destination."""
import base64
import fcntl
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import shutil
import tempfile
from urllib.parse import urlsplit
import httpx
from activity_core.custodian_inputs import _read, admit
from activity_core.custodian_operations import _path, _private_state
def receive(bundle, state, snapshots, policy_path):
state, snapshots = Path(state), Path(snapshots)
snapshot_id = bundle.get('snapshot_id', '')
if bundle.get('schema') != 'repo-manager.custodian-capture.v1' or not re.fullmatch('[0-9a-f]{64}', snapshot_id):
raise ValueError('invalid captured snapshot contract')
manifest_raw, receipts_raw = bundle['manifest'].encode(), bundle['receipts'].encode()
mh, rh = hashlib.sha256(manifest_raw).hexdigest(), hashlib.sha256(receipts_raw).hexdigest()
if mh != bundle['manifest_sha256'] or rh != bundle['receipts_sha256'] or snapshot_id != hashlib.sha256((mh + rh).encode()).hexdigest():
raise ValueError('capture digest mismatch')
policy = json.loads(_read(policy_path))
manifest = json.loads(manifest_raw)
expected = {row['slug'] + '/' + entry['path'] for row in manifest['repositories'] for entry in row['files']}
if set(bundle['files']) != expected:
raise ValueError('capture files differ from manifest')
inputs = _path(str(state / 'inputs'), directory=True)
receipts = inputs / (snapshot_id + '.receipts.json')
selected = snapshots / snapshot_id
temporary = Path(tempfile.mkdtemp(prefix='.capture-', dir=snapshots))
try:
total = 0
for name, encoded in bundle['files'].items():
relative = PurePosixPath(name)
if relative.is_absolute() or '..' in relative.parts or len(relative.parts) < 3:
raise ValueError('capture path escapes snapshot')
data = base64.b64decode(encoded, validate=True)
total += len(data)
if total > min(policy['max_snapshot_bytes'], 8 * 1024 * 1024):
raise ValueError('capture exceeds size policy')
target = temporary / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
target.chmod(0o400)
(temporary / 'manifest.json').write_bytes(manifest_raw)
(temporary / 'COMPLETE').write_text(mh + '\n')
# Validate receipts before advancing any descriptor. Retained name is immutable.
_path(str(receipts), existing=False)
if receipts.exists():
if receipts.read_bytes() != receipts_raw:
raise ValueError('retained receipt conflict')
else:
with receipts.open('xb') as stream:
stream.write(receipts_raw)
admit(temporary, policy_path, receipts, policy['repositories'])
_path(str(selected), directory=True, existing=False)
if selected.exists():
if _read(selected / 'manifest.json') != manifest_raw:
raise ValueError('retained snapshot conflict')
admit(selected, policy_path, receipts, policy['repositories'])
else:
temporary.rename(selected)
descriptor = {'schema': 'activity-core.custodian-snapshot-selection.v1', 'snapshot_id': snapshot_id,
'manifest_sha256': mh, 'receipts_sha256': rh}
current = _path(str(inputs / 'current.json'), existing=False)
fd, staged = tempfile.mkstemp(prefix='.descriptor-', dir=inputs)
try:
with os.fdopen(fd, 'w') as stream:
json.dump(descriptor, stream, sort_keys=True)
stream.flush()
os.fsync(stream.fileno())
os.replace(staged, current)
finally:
if os.path.exists(staged):
os.unlink(staged)
return {'kind': 'custodian_snapshot_capture', 'status': 'completed', 'snapshot_id': snapshot_id,
'manifest_sha256': mh, 'receipts_sha256': rh, 'repository_count': len(policy['repositories'])}
finally:
if temporary.exists():
shutil.rmtree(temporary)
def run_capture(params, *, transport=None):
if set(params) - {'evidence_sinks'}:
raise ValueError('capture accepts no definition command, path or endpoint')
state = _private_state()
snapshots = _path(os.environ.get('ACTIVITY_CORE_CUSTODIAN_SNAPSHOT_WRITE_ROOT'), directory=True)
policy = _path(os.environ.get('ACTIVITY_CORE_CUSTODIAN_INPUT_POLICY'))
base = os.environ.get('ACTIVITY_CORE_CUSTODIAN_PUBLISHER_URL', '')
url = urlsplit(base)
if url.scheme not in {'http', 'https'} or not url.hostname or url.username or url.password or url.query or url.fragment or url.path not in {'', '/'}:
raise ValueError('explicit non-credential publisher base URL required')
lock = _path(str(state / 'capture.lock'), existing=False)
with lock.open('w') as stream:
fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB)
try:
with httpx.Client(transport=transport, follow_redirects=False, timeout=250) as client:
with client.stream('POST', base.rstrip('/') + '/ports/repositories/custodian-snapshot') as response:
if response.status_code != 200:
raise ValueError('owner snapshot publisher unavailable')
data = bytearray()
for chunk in response.iter_bytes():
data.extend(chunk)
if len(data) > 16 * 1024 * 1024:
raise ValueError('capture response exceeds size bound')
return receive(json.loads(data), state, snapshots, policy)
except Exception as exc:
# Never persist raw HTTP payloads, paths or credential-bearing error text.
raise ValueError('bounded owner snapshot reception failed') from exc