#!/usr/bin/env python3 """Bounded code-review publication using the caller's authenticated adapter.""" from __future__ import annotations import hashlib import importlib.util import json import re from pathlib import Path # Reuse public JSON/page/cache and canonical spec codecs, not the spec-only publisher. _SPEC_PATH = (Path(__file__).resolve().parents[4] / 'spec/skills/gitea-implementation-spec-review/scripts/review_support.py') _spec = importlib.util.spec_from_file_location('gitea_native_spec_support', _SPEC_PATH) _support = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_support) _LOCK_PATH = Path(__file__).resolve().parents[5] / 'lib/managed_lock.py' _lock_spec = importlib.util.spec_from_file_location('gitea_managed_lock', _LOCK_PATH) _locks = importlib.util.module_from_spec(_lock_spec) _lock_spec.loader.exec_module(_locks) _BROKER_PATH = Path(__file__).resolve().parents[5] / 'lib/managed_lock_broker.py' _broker_spec = importlib.util.spec_from_file_location('gitea_managed_lock_broker', _BROKER_PATH) _broker = importlib.util.module_from_spec(_broker_spec) _broker_spec.loader.exec_module(_broker) _JOURNAL_PATH = Path(__file__).resolve().with_name('publication_journal.py') _journal_spec = importlib.util.spec_from_file_location('gitea_publication_journal', _JOURNAL_PATH) _journal = importlib.util.module_from_spec(_journal_spec) _journal_spec.loader.exec_module(_journal) ValidationError = _support.ValidationError loads_json = _support.loads_json collect_pages = _support.collect_pages ORIGIN = 'https://gitea.jsfamily.quest' SCHEMA = 'gitea-agent.code-review-publication/v1' def canonical_journal_path(plan): """Return the fixed, versioned path for this publication binding.""" return _journal.canonical_path(plan, anchor_file=__file__, origin=ORIGIN, validate=validate_plan) def _expected_intent(plan, before): payload = build_payload(plan) return {'schema': 'gitea-agent.native-review-intent/v1', 'fingerprint': hashlib.sha256(canonical(plan)).hexdigest(), 'before_sha256': hashlib.sha256(canonical(before)).hexdigest(), 'head': plan['target']['head'], 'event': payload['event'], 'body_sha256': hashlib.sha256(payload['body'].encode()).hexdigest()} def _legacy_path(directory): import os, stat value = os.fspath(directory) if not isinstance(value, str) or not value.startswith('/') or any( part in ('', '.', '..') for part in value.split('/')[1:]): fail('unsafe legacy journal path') source = Path(value) try: _journal._check_ancestor_chain(source) st = source.lstat() except (OSError, ValueError): fail('unsafe legacy journal') if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o700: fail('unsafe legacy journal') return source, (st.st_dev, st.st_ino) from contextlib import contextmanager @contextmanager def _locked_legacy(directory): import fcntl, os, stat source, identity = _legacy_path(directory) fd = None try: # Never create a source mutex: registration preserves the entire source. fd = os.open(source / 'publication.lock', os.O_RDWR | os.O_NOFOLLOW | os.O_NONBLOCK) st = os.fstat(fd) if not stat.S_ISREG(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o600 or st.st_nlink != 1: fail('unsafe legacy journal lock') fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) def check(): _, current = _legacy_path(source) live = (source / 'publication.lock').lstat() if current != identity or (live.st_dev, live.st_ino) != (st.st_dev, st.st_ino): fail('legacy journal identity changed') check() yield source, check check() except ValidationError: raise except (OSError, ValueError): fail('legacy journal unavailable or unsafe') finally: if fd is not None: os.close(fd) def _capture_legacy(source): import stat records = {}; raw_records = {}; inventory = [] allowed = {'before.json', 'intent.json', 'verified.json', 'publication.lock'} try: entries = list(source.iterdir()) except OSError: fail('legacy journal artifact unavailable or unsafe') for entry in entries: if entry.name not in allowed: fail('unknown legacy journal artifact') try: st = entry.lstat() except OSError: fail('legacy journal artifact unavailable or unsafe') if entry.name == 'publication.lock': continue if not stat.S_ISREG(st.st_mode) or st.st_nlink != 1: fail('unsafe legacy journal artifact') for name in ('before.json', 'intent.json', 'verified.json'): try: raw = _journal._secure_file(source / name, cap=16 * 1024 * 1024 if name == 'before.json' else 1024 * 1024) except FileNotFoundError: continue except (OSError, ValueError): fail('legacy journal artifact unavailable or unsafe') try: records[name] = loads_json(raw.decode('utf-8')) raw_records[name] = raw except (UnicodeError, ValueError): fail('malformed legacy journal artifact') inventory.append({'name': name, 'size': len(raw), 'sha256': hashlib.sha256(raw).hexdigest()}) framed = {'schema': 'gitea-agent.legacy-native-journal-digest/v1', 'files': inventory} return records, raw_records, framed, hashlib.sha256(canonical(framed)).hexdigest() def legacy_journal_digest(legacy_directory): """Read-only digest of exact, named source bytes under its existing mutex.""" with _locked_legacy(legacy_directory) as (source, check): _, _, _, digest_value = _capture_legacy(source) check() return digest_value def _assert_registry(entry, plan): try: _journal.assert_bound(entry, plan, anchor_file=__file__, origin=ORIGIN, validate=validate_plan) except (OSError, ValueError): fail('canonical journal binding unavailable or changed') def _check_registration(journal, plan): if journal.read('import.pending.json') is not None: fail('interrupted legacy registration') receipt = journal.read('registration.json') if receipt is None: return keys = {'schema', 'state', 'legacy_sha256', 'source_files', 'imported_files', 'before_sha256', 'intent_sha256'} if type(receipt) is not dict or set(receipt) != keys or receipt['schema'] != 'gitea-agent.native-review-import/v1' or receipt['state'] != 'complete': fail('interrupted or malformed legacy registration') files = receipt['source_files'] if type(files) is not list or len(files) not in (2, 3): fail('malformed legacy registration file inventory') if [f.get('name') if type(f) is dict else None for f in files] not in ( ['before.json', 'intent.json'], ['before.json', 'intent.json', 'verified.json']): fail('malformed legacy registration file inventory') for f in files: if set(f) != {'name', 'size', 'sha256'} or type(f['size']) is not int or not 0 <= f['size'] <= (16 * 1024 * 1024 if f['name'] == 'before.json' else 1024 * 1024): fail('malformed legacy registration file inventory') digest(f['sha256'], 64) imported = receipt['imported_files'] if type(imported) is not list or [f.get('source') if type(f) is dict else None for f in imported] not in ( ['before.json', 'intent.json'], ['before.json', 'intent.json', 'verified.json']): fail('malformed imported legacy file inventory') if [f['source'] for f in imported] != [f['name'] for f in files]: fail('incomplete imported legacy file inventory') for f in imported: if (set(f) != {'source', 'target', 'size', 'sha256'} or f['target'] != ('legacy-verified.json' if f['source'] == 'verified.json' else f['source']) or type(f['size']) is not int or f['size'] < 0): fail('malformed imported legacy file inventory') digest(f['sha256'], 64) source = next((row for row in files if row['name'] == f['source']), None) if source is None or source['size'] != f['size'] or source['sha256'] != f['sha256']: fail('legacy source/import receipt mismatch') for field in ('legacy_sha256', 'before_sha256', 'intent_sha256'): digest(receipt[field], 64) framed = {'schema': 'gitea-agent.legacy-native-journal-digest/v1', 'files': files} if hashlib.sha256(canonical(framed)).hexdigest() != receipt['legacy_sha256']: fail('legacy registration receipt mismatch') before = journal.read('before.json'); intent = journal.read('intent.json') if before is None or intent is None or hashlib.sha256(canonical(before)).hexdigest() != receipt['before_sha256'] or hashlib.sha256(canonical(intent)).hexdigest() != receipt['intent_sha256']: fail('legacy registration artifact mismatch') if canonical(intent) != canonical(_expected_intent(plan, before)): fail('legacy registration intent mismatch') for f in imported: try: raw = _journal._secure_file(Path(journal.path) / f['target'], cap=16 * 1024 * 1024 if f['source'] == 'before.json' else 1024 * 1024) except (OSError, ValueError): fail('legacy imported artifact unavailable or unsafe') if len(raw) != f['size'] or hashlib.sha256(raw).hexdigest() != f['sha256']: fail('legacy imported artifact integrity mismatch') try: loads_json(raw.decode('utf-8')) except (UnicodeError, ValueError): fail('malformed legacy imported artifact') def register_legacy_journal(plan, legacy_directory, *, expected_legacy_sha256, api, lease, declaration=None): """Explicit, local-only import; preserve source and never retry native POST.""" validate_plan(plan) if type(expected_legacy_sha256) is not str or not re.fullmatch(r'[0-9a-f]{64}', expected_legacy_sha256): fail('invalid legacy journal digest') require_adoption(declaration, plan['target']['head']) check_lease(lease, plan) role_identity(read_api(api, 'identity'), plan['reviewer']) validate_snapshot(plan, read_api(api, 'snapshot', plan['target'])) with _locked_legacy(legacy_directory) as (source, check): records, raw_records, framed, digest_value = _capture_legacy(source) if digest_value != expected_legacy_sha256: fail('legacy journal digest mismatch') before = records.get('before.json'); intent = records.get('intent.json') if type(before) is not dict or type(intent) is not dict: fail('legacy journal lacks durable intent') validate_snapshot(plan, before) if canonical(intent) != canonical(_expected_intent(plan, before)): fail('legacy journal intent mismatch') if 'verified.json' in records and type(records['verified.json']) is not dict: fail('invalid legacy verification artifact') check(); check_lease(lease, plan) target = canonical_journal_path(plan) try: entry = _journal.open_binding(target, plan, anchor_file=__file__, origin=ORIGIN, validate=validate_plan, require_new=True) except (OSError, ValueError): fail('canonical registration target unavailable or already exists') with Journal(entry) as journal: _assert_registry(entry, plan) if journal.read('import.pending.json') != {'schema': 'gitea-agent.native-review-import/v1', 'state': 'pending'}: fail('legacy registration bootstrap incomplete') if any(journal.read(name) is not None for name in ('before.json', 'intent.json', 'verified.json', 'registration.json')): fail('canonical journal already populated') receipt = {'schema': 'gitea-agent.native-review-import/v1', 'state': 'pending', 'legacy_sha256': digest_value, 'source_files': framed['files'], 'before_sha256': hashlib.sha256(canonical(before)).hexdigest(), 'intent_sha256': hashlib.sha256(canonical(intent)).hexdigest()} # Durable pending receipt precedes copying any former attempt. journal.write('registration.json', receipt) imported_files = [] for name in ('before.json', 'intent.json', 'verified.json'): if name in raw_records: target_name = 'legacy-verified.json' if name == 'verified.json' else name try: _journal._atomic_bytes(entry, target_name, raw_records[name]) persisted = _journal._secure_file(entry / target_name, cap=16 * 1024 * 1024 if name == 'before.json' else 1024 * 1024) except (OSError, ValueError): fail('legacy artifact import failed') if persisted != raw_records[name]: fail('legacy artifact integrity mismatch') imported_files.append({'source': name, 'target': target_name, 'size': len(raw_records[name]), 'sha256': hashlib.sha256(raw_records[name]).hexdigest()}) receipt['imported_files'] = imported_files journal.write('registration.json', receipt) check() _assert_registry(entry, plan) receipt['state'] = 'complete' journal.write('registration.json', receipt) # The final receipt is durable before removing bootstrap's sentinel. import os journal.check_identity() os.unlink(entry / 'import.pending.json') _journal._fsync_dir(entry) return {'registered': True, 'journal_directory': str(entry), 'read_only': True} def fail(message): raise ValidationError(message) def require_adoption(declaration, revision): if declaration is None: fail('delegation adoption declaration required') try: repo_root = Path(__file__).resolve().parents[6] import sys if str(repo_root) not in sys.path: sys.path.insert(0, str(repo_root)) from harness.lib.delegation_adoption import AdoptionError, require_adoption as adopt except Exception: fail('delegation adoption unavailable') try: return adopt(declaration, expected_source={'kind': 'git_tree', 'revision': revision}, expected_stage='code-review') except AdoptionError as exc: fail(str(exc)) except Exception: fail('delegation adoption unavailable') def positive_id(value): if type(value) is not int or value < 1: fail('invalid positive identity') return value def digest(value, length): if type(value) is not str or not re.fullmatch('[0-9a-f]{' + str(length) + '}', value): fail('invalid digest') return value def marker(plan): t = plan['target'] return (f'') def validate_plan(plan): keys = {'schema', 'target', 'contract_sha256', 'reviewer', 'spec_reviewer', 'spec_review_id', 'spec_paths', 'verdict', 'body', 'provider_contract'} if type(plan) is not dict or set(plan) != keys or plan['schema'] != SCHEMA: fail('invalid publication plan') t = plan['target'] if type(t) is not dict or set(t) != {'owner', 'repository', 'issue', 'pull', 'head', 'base'}: fail('invalid target schema') for k in ['owner', 'repository']: value = t[k] if type(value) is not str or not re.fullmatch(r'[A-Za-z0-9_][A-Za-z0-9_.-]{0,254}', value): fail('invalid target identifier') for k in ['issue', 'pull']: positive_id(t[k]) for k in ['head', 'base']: digest(t[k], 40) digest(plan['contract_sha256'], 64) for k, login in [('reviewer', 'gitea-review-agent'), ('spec_reviewer', 'gitea-spec-agent')]: who = plan[k] if type(who) is not dict or set(who) != {'id', 'login'} or who['login'] != login: fail('invalid role identity') positive_id(who['id']) if plan['reviewer']['id'] == plan['spec_reviewer']['id']: fail('reviewer independence violation') positive_id(plan['spec_review_id']) paths = plan['spec_paths'] if type(paths) is not list or any(type(x) is not str or not x or x.startswith('/') or any(p in ['', '.', '..'] for p in x.split('/')) or any(ord(c) < 32 for c in x) for x in paths) or len(set(paths)) != len(paths): fail('invalid declared spec path scope') if type(plan['verdict']) is not str or plan['verdict'] not in ['approve', 'request_changes']: fail('unsupported publication verdict') body = plan['body'] if type(body) is not str or not 1 <= len(body) <= 250000: fail('invalid body') try: body.encode('utf-8') except UnicodeError: fail('invalid body encoding') if (not body.startswith(marker(plan) + '\n') or body.count('