Files
gitea-agent/harness/agents/review/skills/gitea-code-review/scripts/native_review.py
T

864 lines
44 KiB
Python

#!/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)
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 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'<!-- gitea-agent:code-review/v1 owner={t["owner"]} repository={t["repository"]} '
f'issue={t["issue"]} pr={t["pull"]} head={t["head"]} '
f'contract-sha256={plan["contract_sha256"]} -->')
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('<!-- gitea-agent:code-review/v1 ') != 1
or [x for x in body.splitlines() if x.startswith('- Verdict:')] != [f'- Verdict: `{plan["verdict"]}`']):
fail('body binding mismatch')
provider = plan['provider_contract']
fields = {'supersede_same_author', 'retouch_dismissed', 'unordered_commit_files', 'evidence'}
if type(provider) is not dict or set(provider) != fields:
fail('invalid provider contract')
flags = [provider[k] for k in fields - {'evidence'}]
if any(type(x) is not bool for x in flags) or type(provider['evidence']) is not list:
fail('invalid provider projection flags')
if any(type(x) is not str or not x.strip() for x in provider['evidence']):
fail('invalid provider evidence')
if any(flags) and not provider['evidence']:
fail('provider projection requires evidence')
return plan
def build_payload(plan):
validate_plan(plan)
return {'commit_id': plan['target']['head'], 'body': plan['body'],
'event': {'approve': 'APPROVED', 'request_changes': 'REQUEST_CHANGES'}[plan['verdict']],
'comments': []}
def canonical(value):
# Reuse duplicate/nonfinite validation before fingerprinting or comparison.
try:
text = json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False, separators=(',', ':'))
loads_json(text)
return text.encode('utf-8')
except (TypeError, ValueError, UnicodeError):
fail('invalid JSON artifact')
def indexed(items, key='id'):
if type(items) is not list:
fail('invalid collection')
out = {}
for item in items:
if type(item) is not dict or key not in item:
fail('invalid collection item')
identity = item[key]
if key == 'id':
positive_id(identity)
elif key == 'sha':
digest(identity, 40)
elif (type(identity) is not str or not identity or identity.startswith('/')
or '\\' in identity or any(p in ['', '.', '..'] for p in identity.split('/'))):
fail('invalid filename')
if identity in out:
fail('duplicate collection identity')
out[identity] = item
return out
def check_pages(items, pages):
if type(pages) is not list or not pages:
fail('missing complete pagination evidence')
total = None; counted = 0
for number, page in enumerate(pages, 1):
if type(page) is not dict or set(page) != {'page', 'count', 'declared_total', 'link'}:
fail('invalid pagination record')
if type(page['page']) is not int or page['page'] != number:
fail('invalid page sequence')
count = page['count']; declared = page['declared_total']; link = page['link']
if type(count) is not int or not 0 <= count <= 50:
fail('invalid page size')
if declared is not None:
if type(declared) is not int or declared < 0 or (total is not None and declared != total):
fail('conflicting declared total')
total = declared
if link is not None and type(link) is not str:
fail('invalid Link header')
if number < len(pages) and count != 50:
fail('invalid pagination continuation')
counted += count
last = pages[-1]
if counted != len(items) or (total is not None and total != counted):
fail('collection cardinality mismatch')
if (last['link'] and re.search(r'rel=["\']?next', last['link'])) or (total is None and last['count'] == 50):
fail('truncated pagination')
def role_identity(value, expected):
if type(value) is not dict or type(value.get('id')) is not int or value.get('id') != expected['id'] or value.get('login') != expected['login']:
fail('role identity mismatch')
if value.get('origin', ORIGIN + '/api/v1') != ORIGIN + '/api/v1':
fail('origin mismatch')
def validate_snapshot(plan, snapshot):
validate_plan(plan)
required = {'identity', 'spec_user', 'repository', 'issue', 'pr', 'files', 'commits',
'reviews', 'review_comments', 'issue_comments', 'pr_comments', 'statuses', 'pagination'}
if type(snapshot) is not dict or not required <= set(snapshot):
fail('incomplete snapshot')
canonical(snapshot)
t = plan['target']; full = t['owner'] + '/' + t['repository']
role_identity(snapshot['identity'], plan['reviewer'])
role_identity(snapshot['spec_user'], plan['spec_reviewer'])
repo = snapshot['repository']; issue = snapshot['issue']; pr = snapshot['pr']
if any(type(x) is not dict for x in [repo, issue, pr, snapshot['pagination']]):
fail('invalid target snapshot shape')
positive_id(issue.get('number')); positive_id(pr.get('number'))
if 'repository' in issue and (type(issue['repository']) is not dict or issue['repository'].get('full_name') != full):
fail('source Issue repository mismatch')
for side in ['base', 'head']:
if (type(pr.get(side)) is not dict or type(pr[side].get('repo')) is not dict
or type(pr[side]['repo'].get('full_name')) is not str):
fail('invalid PR repository binding')
if (repo.get('full_name') != full or issue.get('number') != t['issue']
or pr.get('number') != t['pull'] or pr.get('html_url') != f'{ORIGIN}/{full}/pulls/{t["pull"]}'
or issue.get('state') != 'open' or pr.get('state') != 'open' or pr.get('merged') is not False):
fail('target or lifecycle mismatch')
if type(issue.get('body')) is not str or hashlib.sha256(issue['body'].encode('utf-8')).hexdigest() != plan['contract_sha256']:
fail('contract drift')
if snapshot.get('contract_sha256', plan['contract_sha256']) != plan['contract_sha256']:
fail('contract digest mismatch')
if (pr['base']['sha'] != t['base'] or pr['head']['sha'] != t['head']
or pr['base']['repo']['full_name'] != full or pr['base']['ref'] != repo['default_branch']):
fail('head/base/default branch drift')
author = positive_id(pr['user']['id'])
if author in [plan['reviewer']['id'], plan['spec_reviewer']['id']]:
fail('reviewer independence violation')
links = re.findall(r'(?im)^(?:closes|fixes|resolves)[ \t]+[^\r\n]+$', pr.get('body', ''))
if links != ['Closes #' + str(t['issue'])]:
fail('source issue linkage mismatch')
for name, key in [('files', 'filename'), ('commits', 'sha'), ('reviews', 'id'),
('issue_comments', 'id'), ('pr_comments', 'id'), ('statuses', 'id')]:
indexed(snapshot[name], key)
check_pages(snapshot[name], snapshot['pagination'].get(name))
review_map = indexed(snapshot['reviews'])
comments = snapshot['review_comments']
if type(comments) is not dict or set(comments) != {str(rid) for rid in review_map}:
fail('incomplete review comment collection')
for rid, review in review_map.items():
if type(review.get('user')) is not dict:
fail('invalid historical reviewer identity')
positive_id(review['user'].get('id'))
digest(review.get('commit_id'), 40)
if (type(review.get('body')) is not str or review.get('state') not in ['COMMENT', 'PENDING', 'APPROVED', 'REQUEST_CHANGES']
or any(type(review.get(k)) is not bool for k in ['official', 'dismissed', 'stale'])):
fail('invalid native review shape')
indexed(comments[str(rid)])
check_pages(comments[str(rid)], snapshot['pagination'].get('review_comments_' + str(rid)))
if type(review.get('comments_count')) is not int or review['comments_count'] != len(comments[str(rid)]):
fail('review comment cardinality mismatch')
approved = [r for r in snapshot['reviews'] if r.get('user', {}).get('id') == plan['spec_reviewer']['id']
and r.get('commit_id') == t['head'] and r.get('state') == 'APPROVED'
and r.get('stale') is False and r.get('dismissed') is False]
if len(approved) != 1 or approved[0]['id'] != plan['spec_review_id']:
fail('missing or ambiguous current-head spec approval')
role_identity(approved[0]['user'], plan['spec_reviewer'])
parsed = _support.parse_review(approved[0]['body'], set(plan['spec_paths']))
if (parsed['verdict'] != 'approve' or parsed['contract_sha256'] != plan['contract_sha256']
or parsed['target'] != {k: t[k] for k in ['owner', 'repository', 'issue', 'pull', 'head']}):
fail('spec approval binding mismatch')
return snapshot
def normalized_commits(commits, enabled):
import copy
out = copy.deepcopy(commits)
for row in out:
files = row.get('files', [])
indexed(files, 'filename')
if enabled and 'files' in row:
row['files'] = sorted(files, key=lambda f: f['filename'])
return out
def same_source(plan, before, after, new_review=None):
import copy
validate_snapshot(plan, before); validate_snapshot(plan, after)
a = copy.deepcopy(before); b = copy.deepcopy(after)
mutable = {'reviews', 'review_comments', 'pagination'}
# Provider-derived changes are tied to this exact review, never globally ignored.
if new_review is not None:
times = {new_review['submitted_at'], new_review['updated_at']}
for section in ['issue', 'pr']:
if a[section].get('updated_at') != b[section].get('updated_at'):
if b[section].get('updated_at') not in times:
fail('unrelated timestamp mutation')
b[section]['updated_at'] = a[section].get('updated_at')
if 'review_comments' in a['pr']:
old = a['pr']['review_comments']; new = b['pr'].get('review_comments')
if type(old) is not int or type(new) is not int or new != old + 1:
fail('PR review count projection mismatch')
b['pr']['review_comments'] = old
requested_before = indexed(a['pr'].get('requested_reviewers', []))
requested_after = indexed(b['pr'].get('requested_reviewers', []))
extra = set(requested_after) - set(requested_before)
if set(requested_before) - set(requested_after) or extra - {plan['reviewer']['id']}:
fail('unrelated requested reviewer mutation')
if any(canonical(row) != canonical(requested_after[rid]) for rid, row in requested_before.items()):
fail('requested reviewer content changed')
if extra:
role_identity(requested_after[plan['reviewer']['id']], plan['reviewer'])
b['pr']['requested_reviewers'] = a['pr'].get('requested_reviewers', [])
for data in [a, b]:
data['files'] = sorted(data['files'], key=lambda f: f['filename'])
data['commits'] = normalized_commits(data['commits'], plan['provider_contract']['unordered_commit_files'])
if canonical({k: v for k, v in a.items() if k not in mutable}) != canonical({k: v for k, v in b.items() if k not in mutable}):
fail('source snapshot drift')
def exact_binding(plan, snapshot, exact):
payload = build_payload(plan)
if type(exact) is not dict:
fail('invalid exact review')
rid = positive_id(exact.get('id'))
role_identity(exact.get('user'), plan['reviewer'])
if (exact.get('body') != payload['body'] or exact.get('commit_id') != payload['commit_id']
or exact.get('state') != payload['event'] or exact.get('stale') is not False
or exact.get('dismissed') is not False or type(exact.get('official')) is not bool
or type(exact.get('comments_count')) is not int or exact['comments_count'] != 0
or exact.get('pull_request_url') != snapshot['pr']['html_url']):
fail('exact review binding mismatch')
if snapshot['review_comments'].get(str(rid)) != []:
fail('inline comments are unsupported')
if canonical(indexed(snapshot['reviews']).get(rid)) != canonical(exact):
fail('exact review differs from complete collection')
for key in ['submitted_at', 'updated_at']:
from datetime import datetime
try:
stamp = datetime.fromisoformat(exact[key].replace('Z', '+00:00'))
if stamp.utcoffset() is None:
fail('unbound review timestamp')
except (KeyError, ValueError, TypeError, AttributeError):
fail('invalid review timestamp')
return rid
def verify_publication(plan, before, after, exact):
rid = exact_binding(plan, validate_snapshot(plan, after), exact)
same_source(plan, before, after, exact)
old = indexed(before['reviews']); new = indexed(after['reviews'])
if set(new) - set(old) != {rid} or set(old) - set(new):
fail('review cardinality mismatch')
current = [r for r in after['reviews'] if r['user']['id'] == plan['reviewer']['id']
and r['commit_id'] == plan['target']['head'] and r['state'] in ['PENDING', 'APPROVED', 'REQUEST_CHANGES']
and not r['dismissed']]
if len(current) != 1 or current[0]['id'] != rid:
fail('competing current-head review')
projected = []
for old_id, review in old.items():
fresh = new[old_id]
if before['review_comments'][str(old_id)] != after['review_comments'][str(old_id)]:
fail('historical review comments changed')
if canonical(review) == canonical(fresh):
continue
permitted = {'official', 'dismissed', 'updated_at'}
if (review['user']['id'] != plan['reviewer']['id'] or review['state'] not in ['APPROVED', 'REQUEST_CHANGES']
or set(review) != set(fresh)
or canonical({k: v for k, v in review.items() if k not in permitted}) != canonical({k: v for k, v in fresh.items() if k not in permitted})):
fail('historical review immutable content changed')
provider = plan['provider_contract']
superseded = provider['supersede_same_author'] and review.get('official') is True and review.get('dismissed') is False
retouched = provider['retouch_dismissed'] and review.get('official') is False and review.get('dismissed') is True
if (not (superseded or retouched) or fresh.get('official') is not False or fresh.get('dismissed') is not True
or fresh.get('updated_at') not in {exact['submitted_at'], exact['updated_at']}):
fail('unauthorized historical review projection')
projected.append(old_id)
return {'verified': True, 'review_id': rid, 'state': exact['state'], 'head': plan['target']['head'],
'projected_review_ids': projected, 'review_records_added': 1,
'commit_file_order_normalized': before['commits'] != after['commits'], 'read_only': True}
class UncertainPublication(ValidationError):
"""A durable attempt exists; a second create is prohibited."""
class Journal:
def __init__(self, directory):
import os, stat
self.path = Path(directory)
if not self.path.is_absolute():
fail('journal must be an absolute trusted path')
for p in [self.path, *self.path.parents]:
if p.is_symlink():
fail('symlinked journal path')
self.path.mkdir(mode=0o700, exist_ok=True)
st = self.path.lstat()
if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o700:
fail('unsafe journal directory')
self.fd = None
def __enter__(self):
import fcntl, os, stat
self.fd = os.open(self.path / 'publication.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
try:
st = os.fstat(self.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 journal lock')
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BaseException:
os.close(self.fd); self.fd = None
raise
return self
def __exit__(self, *args):
import os
os.close(self.fd)
def read(self, name):
import os, stat
p = self.path / name
try:
fd = os.open(p, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
except FileNotFoundError:
return None
except OSError:
raise ValidationError('journal artifact unavailable') from None
with os.fdopen(fd, 'r', encoding='utf-8') as artifact:
st = os.fstat(artifact.fileno())
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 journal artifact')
return loads_json(artifact.read())
def write(self, name, value):
import os, tempfile
self.read(name) # Reject existing symlink/hardlink/foreign artifacts.
fd, tmp = tempfile.mkstemp(dir=self.path, prefix='.native-review-')
try:
with os.fdopen(fd, 'wb') as out:
out.write(canonical(value)); out.flush(); os.fsync(out.fileno())
os.replace(tmp, self.path / name)
directory = os.open(self.path, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
finally:
if os.path.exists(tmp):
os.unlink(tmp)
def candidates(plan, snapshot):
own = [r for r in snapshot['reviews'] if r['user']['id'] == plan['reviewer']['id']
and r['commit_id'] == plan['target']['head'] and r['state'] in ['PENDING', 'APPROVED', 'REQUEST_CHANGES']]
matching = [r for r in own if marker(plan) in r.get('body', '')]
if len(matching) > 1 or any(r not in matching for r in own):
fail('duplicate or conflicting current-head review')
if matching and (matching[0]['body'] != plan['body'] or matching[0]['state'] != build_payload(plan)['event']):
fail('conflicting canonical review')
return matching
def read_api(api, method, *args):
try:
return getattr(api, method)(*args)
except Exception:
raise ValidationError('authoritative API read unavailable; do not retry POST') from None
def check_lease(lease, plan):
try:
result = lease.check(plan)
except Exception:
raise ValidationError('held Issue lease invalid or unavailable') from None
if result is not None:
fail('unsupported lease check result')
def publish(plan, baseline, api, lease, journal_directory, *, declaration=None):
"""Use one trusted role transport and an already-held Issue lease; never retry POST."""
import copy
validate_snapshot(plan, baseline)
require_adoption(declaration, plan['target']['head'])
payload = build_payload(plan)
if any(not callable(getattr(api, k, None)) for k in ['identity', 'snapshot', 'create_review', 'read_review']):
fail('unsupported adapter capabilities')
if not callable(getattr(lease, 'check', None)):
fail('missing held Issue lease')
fingerprint = hashlib.sha256(canonical(plan)).hexdigest()
check_lease(lease, plan)
with Journal(journal_directory) as journal:
intent = journal.read('intent.json')
before = baseline
if intent is not None:
if type(intent) is not dict or intent.get('fingerprint') != fingerprint:
fail('journal belongs to a different publication')
before = journal.read('before.json')
if before is None or hashlib.sha256(canonical(before)).hexdigest() != intent.get('before_sha256'):
fail('journal baseline integrity mismatch')
validate_snapshot(plan, before)
expected_intent = {'schema': 'gitea-agent.native-review-intent/v1', 'fingerprint': fingerprint,
'before_sha256': hashlib.sha256(canonical(before)).hexdigest(),
'head': plan['target']['head'], 'event': payload['event'],
'body_sha256': hashlib.sha256(payload['body'].encode()).hexdigest()}
if canonical(intent) != canonical(expected_intent):
fail('journal intent schema or binding mismatch')
check_lease(lease, plan)
role_identity(read_api(api, 'identity'), plan['reviewer'])
fresh = validate_snapshot(plan, read_api(api, 'snapshot', plan['target']))
matching = candidates(plan, fresh)
if matching:
exact = read_api(api, 'read_review', plan['target'], matching[0]['id'])
if exact['id'] in indexed(before['reviews']):
same_source(plan, before, fresh)
if canonical(before['reviews']) != canonical(fresh['reviews']) or before['review_comments'] != fresh['review_comments']:
fail('no-op baseline drift')
rid = exact_binding(plan, fresh, exact)
result = {'verified': True, 'review_id': rid, 'state': exact['state'], 'head': plan['target']['head'], 'review_records_added': 0}
else:
result = verify_publication(plan, before, fresh, exact)
check_lease(lease, plan)
result.update(mode='no_op', read_only=False, wrote=False)
journal.write('verified.json', result)
return result
if intent is not None:
raise UncertainPublication('durable attempt has no authoritative match; do not retry POST')
same_source(plan, baseline, fresh)
if canonical(baseline['reviews']) != canonical(fresh['reviews']) or baseline['review_comments'] != fresh['review_comments']:
fail('review baseline drift')
check_lease(lease, plan)
# A second source read immediately precedes intent/create; do not mutate a stale baseline.
role_identity(read_api(api, 'identity'), plan['reviewer'])
latest = validate_snapshot(plan, read_api(api, 'snapshot', plan['target']))
same_source(plan, fresh, latest)
if canonical(fresh['reviews']) != canonical(latest['reviews']) or fresh['review_comments'] != latest['review_comments']:
fail('review baseline drift')
before = copy.deepcopy(latest)
journal.write('before.json', before)
journal.write('intent.json', {'schema': 'gitea-agent.native-review-intent/v1', 'fingerprint': fingerprint,
'before_sha256': hashlib.sha256(canonical(before)).hexdigest(),
'head': plan['target']['head'], 'event': payload['event'],
'body_sha256': hashlib.sha256(payload['body'].encode()).hexdigest()})
check_lease(lease, plan)
require_adoption(declaration, plan['target']['head'])
uncertain = False
response_id = None
try:
created = api.create_review(plan['target'], payload)
if type(created) is dict:
response_id = positive_id(created.get('id'))
else:
uncertain = True
except Exception:
# Never expose raw transport exception text (which may contain credentials).
uncertain = True
check_lease(lease, plan)
try:
role_identity(read_api(api, 'identity'), plan['reviewer'])
after = validate_snapshot(plan, read_api(api, 'snapshot', plan['target']))
matching = candidates(plan, after)
if len(matching) != 1:
raise UncertainPublication('create outcome not uniquely found; do not retry POST')
if response_id is not None and response_id != matching[0]['id']:
fail('create response and collection mismatch')
exact = read_api(api, 'read_review', plan['target'], matching[0]['id'])
result = verify_publication(plan, before, after, exact)
except ValidationError:
raise
except Exception:
raise UncertainPublication('authoritative readback unavailable; retained attempt; do not retry POST') from None
check_lease(lease, plan)
result.update(mode='reconciled' if uncertain else 'created', read_only=False, wrote=True)
journal.write('verified.json', result)
return result
class ApiAdapter:
"""REST bridge: request(path, payload=None) -> (JSON, headers, status).
The caller supplies one already-authenticated, no-redirect, TLS-verifying,
role-local transport. This class never loads/falls back to credentials.
"""
def __init__(self, request, *, origin, credential_source):
if not callable(request) or origin != ORIGIN + '/api/v1':
fail('invalid authenticated transport origin')
if credential_source not in ['$HERMES_HOME/.env:GITEA_TOKEN', 'role-scoped-external-secret:GITEA_TOKEN']:
fail('unverified role credential source')
self.request = request
self.origin = origin
self.credential_source = credential_source
def call(self, path, payload=None):
try:
data, headers, status = self.request(path, payload)
except Exception:
raise ValidationError('authenticated API request failed') from None
expected = [200, 201] if payload is not None else [200]
if type(status) is not int or status not in expected or not hasattr(headers, 'items'):
fail('authenticated API status/shape mismatch')
return data, {str(k).lower(): v for k, v in headers.items()}
def identity(self):
data, _ = self.call('/user')
if type(data) is not dict or data.get('login') != 'gitea-review-agent':
fail('unexpected authenticated role')
positive_id(data.get('id'))
return {'id': data['id'], 'login': data['login'], 'origin': self.origin,
'credential_source': self.credential_source}
def prefix(self, target):
for key in ['owner', 'repository']:
if type(target.get(key)) is not str or not re.fullmatch(r'[A-Za-z0-9_][A-Za-z0-9_.-]{0,254}', target[key]):
fail('invalid request target')
positive_id(target['issue']); positive_id(target['pull'])
return '/repos/' + target['owner'] + '/' + target['repository']
def collection(self, endpoint, key):
records = []
def fetch(page):
data, headers = self.call(endpoint + '?limit=50&page=' + str(page))
raw_total = headers.get('x-total-count')
if raw_total is not None and (type(raw_total) is not str or not re.fullmatch(r'\d+', raw_total)):
fail('invalid declared collection total')
total = int(raw_total) if raw_total is not None else None
if data is None and total == 0:
data = []
if type(data) is not list:
fail('invalid API collection')
link = headers.get('link')
if link is not None and type(link) is not str:
fail('invalid collection Link header')
more = True if link and re.search(r'rel=["\']?next', link) else None
if more is None and total is not None:
more = page * 50 < total
records.append({'page': page, 'count': len(data), 'declared_total': total, 'link': link})
envelope = {'items': data, 'has_more': more}
if total is not None:
envelope['total_count'] = total
return envelope
items = collect_pages(fetch, key, page_size=50, max_pages=1000)
check_pages(items, records)
return items, records
def snapshot(self, target):
identity = self.identity() # Always first authoritative read.
prefix = self.prefix(target)
repo, _ = self.call(prefix)
issue, _ = self.call(prefix + '/issues/' + str(target['issue']))
pr, _ = self.call(prefix + '/pulls/' + str(target['pull']))
spec_user, _ = self.call('/users/gitea-spec-agent')
if type(spec_user) is not dict or spec_user.get('login') != 'gitea-spec-agent':
fail('invalid spec identity')
out = {'identity': identity, 'spec_user': {'id': positive_id(spec_user.get('id')), 'login': spec_user['login']},
'repository': repo, 'issue': issue, 'pr': pr, 'pagination': {}, 'review_comments': {}}
pull = prefix + '/pulls/' + str(target['pull'])
endpoints = [('files', pull + '/files', 'filename'), ('commits', pull + '/commits', 'sha'),
('reviews', pull + '/reviews', 'id'),
('issue_comments', prefix + '/issues/' + str(target['issue']) + '/comments', 'id'),
('pr_comments', prefix + '/issues/' + str(target['pull']) + '/comments', 'id'),
('statuses', prefix + '/statuses/' + digest(target['head'], 40), 'id')]
for name, endpoint, key in endpoints:
out[name], out['pagination'][name] = self.collection(endpoint, key)
for review in out['reviews']:
rid = str(positive_id(review['id']))
out['review_comments'][rid], out['pagination']['review_comments_' + rid] = self.collection(pull + '/reviews/' + rid + '/comments', 'id')
return out
def create_review(self, target, payload):
if type(payload) is not dict or set(payload) != {'commit_id', 'body', 'event', 'comments'} or payload['comments'] != []:
fail('invalid create payload')
data, _ = self.call(self.prefix(target) + '/pulls/' + str(target['pull']) + '/reviews', payload)
return data
def read_review(self, target, review_id):
positive_id(review_id)
data, _ = self.call(self.prefix(target) + '/pulls/' + str(target['pull']) + '/reviews/' + str(review_id))
return data
def _validate_registered_source(plan, policy_path, deadline_monotonic):
import math, os, stat, subprocess, time
if type(deadline_monotonic) not in [int, float] or not math.isfinite(deadline_monotonic):
fail('invalid or expired Issue lease')
t = plan['target']
info = _support.validate_cache_paths(policy_path, t['owner'], t['repository'], t['issue'])
wt = Path(info['repository']) / 'worktrees' / f'issue-{t["issue"]}' / 'checkout'
opened = []
def close_opened():
while opened:
try: os.close(opened.pop())
except OSError: pass
def reject(message):
close_opened()
fail(message)
def open_dir(path):
parts = path.parts
fd = os.open(parts[0], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC)
opened.append(fd)
try:
for part in parts[1:]:
nxt = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=fd)
opened.append(nxt); os.close(fd); opened.remove(fd); fd = nxt
return fd
except BaseException:
close_opened(); raise
try:
worktrees_fd = open_dir(wt.parent.parent)
issue_fd = os.open(wt.parent.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=worktrees_fd)
opened.append(issue_fd)
checkout_fd = os.open(wt.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=issue_fd)
opened.append(checkout_fd)
for directory_fd in [worktrees_fd, issue_fd, checkout_fd]:
st = os.fstat(directory_fd)
if (not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid()
or st.st_mode & (stat.S_IWGRP | stat.S_IWOTH)):
reject('unsafe existing worktree path')
state_fd = os.open('state.json', os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=issue_fd)
opened.append(state_fd)
git_pointer_fd = os.open('.git', os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=checkout_fd)
opened.append(git_pointer_fd)
state_st = os.fstat(state_fd); git_st = os.fstat(git_pointer_fd)
if not stat.S_ISREG(state_st.st_mode) or state_st.st_uid != os.getuid() or stat.S_IMODE(state_st.st_mode) != 0o600:
reject('unsafe worktree state')
if not stat.S_ISREG(git_st.st_mode) or git_st.st_uid != os.getuid():
reject('invalid worktree Git pointer')
chunks = []
while chunk := os.read(state_fd, 65536):
chunks.append(chunk)
try:
state = loads_json(b''.join(chunks).decode('utf-8'))
except Exception:
close_opened()
raise
os.lseek(git_pointer_fd, 0, os.SEEK_SET)
pointer = os.read(git_pointer_fd, 4096).decode('utf-8').strip()
if not pointer.startswith('gitdir: '):
reject('invalid worktree Git pointer')
worktree_git_fd = open_dir(Path(pointer[8:]))
except OSError:
close_opened()
raise ValidationError('unsafe existing worktree path') from None
if (type(state) is not dict or state.get('schema') != 'gitea-agent.worktree/v1' or state.get('owner') != t['owner']
or state.get('repository') != t['repository'] or type(state.get('issue')) is not int
or state['issue'] != t['issue'] or state.get('created_by_role') != 'gitea-impl-agent'
or state.get('mode') != 'detached'):
reject('worktree registration identity mismatch')
git_binary = '/usr/bin/git'; binary_stat = Path(git_binary).lstat()
if not stat.S_ISREG(binary_stat.st_mode) or binary_stat.st_uid != 0 or binary_stat.st_mode & 0o022:
reject('untrusted system Git binary')
env = {'PATH': '/usr/bin:/bin', 'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_OPTIONAL_LOCKS': '0'}
git_dir_fd = open_dir(Path(info['repository_git']))
def git(*args):
try:
process = subprocess.run([git_binary, '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null',
'-c', 'safe.directory=/proc/self/fd/' + str(checkout_fd),
'--git-dir=/proc/self/fd/' + str(worktree_git_fd),
'--work-tree=/proc/self/fd/' + str(checkout_fd), *args], capture_output=True, text=True, env=env, timeout=15,
pass_fds=(checkout_fd, worktree_git_fd, git_dir_fd))
except Exception:
close_opened()
raise ValidationError('read-only Git check unavailable') from None
if process.returncode or len(process.stdout) > 1000000:
reject('read-only Git check failed')
return process.stdout
if git('rev-parse', 'HEAD').strip() != t['head']:
reject('worktree HEAD mismatch')
if git('status', '--porcelain=v1', '--untracked-files=all').strip():
reject('existing worktree is not clean')
if git('rev-parse', '--path-format=absolute', '--git-common-dir').strip() != info['repository_git']:
reject('worktree common Git directory mismatch')
records = git('--git-dir=/proc/self/fd/' + str(git_dir_fd), 'worktree', 'list', '--porcelain', '-z').split('\0\0')
matches = [r.split('\0') for r in records if r.startswith('worktree ' + str(wt) + '\0')]
if len(matches) != 1 or 'detached' not in matches[0] or not any(x == 'locked' or x.startswith('locked ') for x in matches[0]):
reject('worktree detached/locked registration mismatch')
close_opened()
if time.monotonic() >= deadline_monotonic:
fail('Issue lease expired during source checks')
class HeldIssueLease:
"""Validate a caller-owned Issue lock FD; never close/release the caller's FD."""
def __init__(self, issue_fd, policy_path, deadline_monotonic):
self.issue_fd = issue_fd; self.policy_path = policy_path; self.deadline_monotonic = deadline_monotonic
def check(self, plan):
import math, time
validate_plan(plan)
if (type(self.issue_fd) is not int or type(self.deadline_monotonic) not in [int, float]
or not math.isfinite(self.deadline_monotonic) or time.monotonic() >= self.deadline_monotonic):
fail('invalid or expired Issue lease')
t = plan['target']; info = _support.validate_cache_paths(self.policy_path, t['owner'], t['repository'], t['issue'])
lock = Path(info['locks']) / f'issue-{t["issue"]}.lock'
try:
if not _locks.validate_held_flock(self.issue_fd, lock):
fail('Issue lock is not already held by this process')
except (OSError, _locks.LockValidationError):
raise ValidationError('Issue lock unavailable') from None
_validate_registered_source(plan, self.policy_path, self.deadline_monotonic)
class BrokerIssueLease:
"""Validate an authenticated shared managed-lock broker Issue lease."""
def __init__(self, handle_path, policy_path, deadline_monotonic):
self.handle_path = handle_path; self.policy_path = policy_path; self.deadline_monotonic = deadline_monotonic
def check(self, plan):
import math, time
validate_plan(plan)
if (type(self.deadline_monotonic) not in [int, float] or not math.isfinite(self.deadline_monotonic)
or time.monotonic() >= self.deadline_monotonic):
fail('invalid or expired Issue lease')
t = plan['target']
try:
handle = _broker.read_handle_metadata(self.handle_path)
status = _broker.request_handle(self.handle_path, 'status')
except Exception:
raise ValidationError('broker Issue lease unavailable') from None
if (type(status) is not dict or status.get('status') not in ('held', 'repository-held')
or status.get('owner') != t['owner'] or status.get('repository') != t['repository']
or status.get('issue') != t['issue'] or status.get('role') != 'gitea-review-agent'
or type(status.get('pid')) is not int or status.get('pid') != handle.get('pid')
or type(handle.get('pid')) is not int or status.get('deadline') != handle.get('deadline')
or type(status.get('deadline')) not in [int, float]
or status.get('deadline') > self.deadline_monotonic or time.monotonic() >= status.get('deadline')):
fail('broker Issue lease identity mismatch')
_validate_registered_source(plan, self.policy_path, self.deadline_monotonic)
def main(argv=None):
import argparse, sys
parser = argparse.ArgumentParser(description='Read-only native review artifact preflight/replay. Live publication is the Python publish API.')
commands = parser.add_subparsers(dest='command', required=True)
pre = commands.add_parser('preflight')
pre.add_argument('--plan', required=True); pre.add_argument('--baseline', required=True)
verify = commands.add_parser('verify')
for name in ['plan', 'before', 'after', 'exact']:
verify.add_argument('--' + name, required=True)
args = parser.parse_args(argv)
def read(path):
return loads_json(Path(path).read_text(encoding='utf-8'))
try:
plan = read(args.plan)
if args.command == 'preflight':
snapshot = validate_snapshot(plan, read(args.baseline))
out = {'ready': True, 'read_only': True, 'artifact_validation_only': True,
'head': plan['target']['head'], 'event': build_payload(plan)['event'],
'reviewer_id': plan['reviewer']['id'], 'spec_review_id': plan['spec_review_id'],
'snapshot_sha256': hashlib.sha256(canonical(snapshot)).hexdigest()}
else:
out = verify_publication(plan, read(args.before), read(args.after), read(args.exact))
print(json.dumps(out, ensure_ascii=False))
return 0
except Exception:
# No arbitrary artifact or transport exception is echoed to the terminal.
print(json.dumps({'error': 'invalid or unavailable review artifacts', 'read_only': True}), file=sys.stderr)
return 1
if __name__ == '__main__':
raise SystemExit(main())