feat: checkpoint standalone Custodian snapshot receiver
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a15374-f59f-7a31-86d2-d23b5d2ed181
This commit is contained in:
parent
f718108858
commit
9ef75e0e18
4 changed files with 257 additions and 0 deletions
115
src/activity_core/custodian_capture.py
Normal file
115
src/activity_core/custodian_capture.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue