#!/usr/bin/env python3 """Persist and deliver an execution report; never mark integration accepted.""" import argparse import hashlib import json from pathlib import Path import re import sys import time from control import Client,APIError from worker import atomic_json,session_lock def deliver_file(client,path,now=None): now=time.time() if now is None else now with session_lock('task-report:'+str(path.resolve())): record=json.loads(path.read_text(encoding='utf-8')) if record['status']!='pending' or record.get('next_retry',0)>now: return record try: receipt=client.call('/api/task-reports',record['payload']) except Exception as exc: record['attempts']=record.get('attempts',0)+1 terminal=isinstance(exc,APIError) and exc.status in (400,403,409) record['status']='blocked' if terminal or record['attempts']>=10 else 'pending' record['next_retry']=now+min(60,2**record['attempts']) record['error']='Report delivery requires inspection' if record['status']=='blocked' else 'Temporary delivery interruption' atomic_json(path,record) return record record.update(status='accepted',receipt=receipt,accepted_at=now) record.pop('error',None) atomic_json(path,record) return record def submit(cfg,payload): root=Path(cfg['outbox_dir']).expanduser().resolve() root.mkdir(parents=True,exist_ok=True,mode=0o700) canonical=json.dumps(payload,sort_keys=True,ensure_ascii=True) report_id=payload.get('report_id') or 'report-'+hashlib.sha256(canonical.encode()).hexdigest()[:32] if not isinstance(report_id,str) or not re.fullmatch(r'[A-Za-z0-9_.:-]{1,128}',report_id): raise ValueError('Invalid report id') payload={**payload,'report_id':report_id} path=root/(hashlib.sha256(report_id.encode()).hexdigest()+'.json') if path.is_symlink():raise ValueError('Report file cannot be a symlink') with session_lock('task-report:'+str(path)): if path.exists(): record=json.loads(path.read_text(encoding='utf-8')) if record['payload']!=payload:raise ValueError('Report id already used for different content') else: atomic_json(path,{'status':'pending','created':time.time(),'payload':payload,'attempts':0}) return deliver_file(Client(cfg['control']),path) def flush(cfg): root=Path(cfg['outbox_dir']).expanduser().resolve() root.mkdir(parents=True,exist_ok=True,mode=0o700) client=Client(cfg['control']) counts={'pending':0,'accepted':0,'blocked':0,'superseded':0} records=[] for path in root.glob('*.json'): if not re.fullmatch(r'[a-f0-9]{64}\.json',path.name):continue if path.is_symlink():raise ValueError('Unexpected report symlink') records.append((path,json.loads(path.read_text(encoding='utf-8')))) completed={} for _,record in records: payload=record['payload'] if payload['stage']=='execution_completed' and record['status'] in ('pending','accepted'): key=(payload['task_id'],payload['session_id']) completed[key]=max(completed.get(key,0),record['created']) for path,record in records: payload=record['payload'];key=(payload['task_id'],payload['session_id']) if record['status']=='pending' and payload['stage']!='execution_completed' and record['created']