railiance-platform/scripts/native_factory_acceptance.py
codex 426a0d5829
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Separate producer checks from attended audit readback
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-11 12:16:49 +02:00

274 lines
18 KiB
Python

#!/usr/bin/env python3
"""Prepare or execute the bounded, attended native factory sender acceptance."""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import io
import json
import os
from pathlib import Path
import secrets
import socket
import subprocess
import time
from urllib.error import HTTPError
from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler
import zipfile
from factory_audit_custody import contracts, desired, snapshot, receiver_check, require
from state_hub_preflight_lane import ROOT, LaneError, command, bao, data
IMAGE = 'forgejo.coulomb.social/coulomb/approval-engine@sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49'
RECEIVER = 'forgejo.coulomb.social/coulomb/audit-core@sha256:c82e0442de0fd181342916ae9cd5d6de41d859e1efda637bd93936c67873afa5'
SOURCES = {'approval-engine':('a0a602976eef818f36dde35f76f7f2e589bd051b','approval_engine'),
'informed-decision':('bda9381f070859ae997474cf44b86bf4eea9bb52','informed_decision')}
CONFIRM = 'VERIFY CCR-2026-0021 CCR-2026-0022 PRODUCERS'
def source_bundle(base):
buffer = io.BytesIO(); hashes = {}
with zipfile.ZipFile(buffer,'w',compression=zipfile.ZIP_DEFLATED) as out:
for repo,(commit,package) in SOURCES.items():
prefix = ['git','-C',str(base/repo)]
names = command(prefix+['ls-tree','-r','--name-only',commit,package]).stdout.decode().splitlines()
require(names and all(n.startswith(package+'/') and n.endswith('.py') for n in names), 'source_package_shape_changed')
for name in names:
body = command(prefix+['show',commit+':'+name]).stdout
hashes[name] = hashlib.sha256(body).hexdigest()
entry = zipfile.ZipInfo(name,date_time=(2026,9,11,0,0,0));entry.compress_type=zipfile.ZIP_DEFLATED
out.writestr(entry,body)
return buffer.getvalue(), hashes
def resources(run_id, sender, bundle, script):
suffix = run_id.removeprefix('factory-audit-');name='factory-audit-'+suffix
labels={'app.kubernetes.io/name':sender,'railiance.io/factory-audit-probe':run_id}
config={'sender':sender,'run_id':run_id,'event_id':run_id+'-'+sender,
'origin':'http://audit-core.audit-core.svc.cluster.local:8080',
'source_zip_sha256':hashlib.sha256(bundle).hexdigest()}
key='audit-token' if sender=='approval-engine' else 'token'
meta={'name':name,'namespace':sender,'labels':labels}
cm={'apiVersion':'v1','kind':'ConfigMap','metadata':meta,'immutable':True,
'data':{'producer.py':script.decode(),'config.json':json.dumps(config,sort_keys=True)},
'binaryData':{'source.zip':base64.b64encode(bundle).decode()}}
policy={'apiVersion':'networking.k8s.io/v1','kind':'NetworkPolicy','metadata':meta,
'spec':{'podSelector':{'matchLabels':{'railiance.io/factory-audit-probe':run_id}},
'policyTypes':['Ingress','Egress'],'ingress':[],
'egress':[{'to':[{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':'audit-core'}},
'podSelector':{'matchLabels':{'app.kubernetes.io/name':'audit-core','app.kubernetes.io/component':'receiver'}}}],
'ports':[{'protocol':'TCP','port':8080}]},
{'to':[{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':'kube-system'}},
'podSelector':{'matchLabels':{'k8s-app':'kube-dns'}}}],
'ports':[{'protocol':'UDP','port':53},{'protocol':'TCP','port':53}]}]}}
job={'apiVersion':'batch/v1','kind':'Job','metadata':meta,
'spec':{'backoffLimit':0,'activeDeadlineSeconds':150,'ttlSecondsAfterFinished':3600,
'template':{'metadata':{'labels':labels},'spec':{
'restartPolicy':'Never','automountServiceAccountToken':False,
'securityContext':{'runAsNonRoot':True,'runAsUser':10001,'runAsGroup':10001,
'fsGroup':10001,'seccompProfile':{'type':'RuntimeDefault'}},
'containers':[{'name':'probe','image':IMAGE,'imagePullPolicy':'IfNotPresent',
'command':['python','-I','/probe/producer.py'],
'securityContext':{'allowPrivilegeEscalation':False,'readOnlyRootFilesystem':True,
'capabilities':{'drop':['ALL']}},
'resources':{'requests':{'cpu':'25m','memory':'64Mi'},'limits':{'cpu':'500m','memory':'192Mi'}},
'volumeMounts':[{'name':'probe','mountPath':'/probe','readOnly':True},
{'name':'audit','mountPath':'/credential','readOnly':True},
{'name':'state','mountPath':'/state'}]}],
'volumes':[{'name':'probe','configMap':{'name':name,'defaultMode':0o444}},
{'name':'audit','secret':{'secretName':sender+'-audit','defaultMode':0o440,
'items':[{'key':key,'path':'token'}]}},
{'name':'state','emptyDir':{'sizeLimit':'32Mi'}}]}}}}
return [cm,policy,job]
def prepare(base, path):
bundle,hashes=source_bundle(base)
script=(ROOT/'scripts/native_factory_probe/producer.py').read_bytes()
run_id='factory-audit-'+datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')+'-'+secrets.token_hex(3)
packet={'schema':'platform.factory-native-acceptance.v1','run_id':run_id,'source_commits':SOURCES,
'source_hashes':hashes,'source_zip_sha256':hashlib.sha256(bundle).hexdigest(),
'probe_sha256':hashlib.sha256(script).hexdigest(),'objects':sum([resources(run_id,s,bundle,script) for s in SOURCES],[])}
with path.open('x') as f:json.dump(packet,f,indent=2);f.write('\n')
return {'packet':str(path),'sha256':hashlib.sha256(path.read_bytes()).hexdigest(),
'run_id':run_id,'objects':len(packet['objects']),'source_files':len(hashes)}
def validate_packet(path, sha256):
raw=path.read_bytes();require(hashlib.sha256(raw).hexdigest()==sha256,'packet_digest_changed')
packet=json.loads(raw);run_id=packet['run_id']
import re
require(re.fullmatch(r'factory-audit-[0-9]{14}-[0-9a-f]{6}',run_id),'invalid_run_id')
require(packet['source_commits']=={k:list(v) for k,v in SOURCES.items()},'source_pins_changed')
objects=packet['objects'];require(len(objects)==6,'exact_resources_required')
bundle=base64.b64decode(objects[0]['binaryData']['source.zip'],validate=True)
script=(ROOT/'scripts/native_factory_probe/producer.py').read_bytes()
require(hashlib.sha256(bundle).hexdigest()==packet['source_zip_sha256'],'bundle_digest_changed')
require(hashlib.sha256(script).hexdigest()==packet['probe_sha256'],'probe_digest_changed')
with zipfile.ZipFile(io.BytesIO(bundle)) as z:
require(set(z.namelist())==set(packet['source_hashes']),'source_inventory_changed')
require(all(hashlib.sha256(z.read(n)).hexdigest()==h for n,h in packet['source_hashes'].items()),'source_hash_changed')
require(objects==sum([resources(run_id,s,bundle,script) for s in SOURCES],[]),'resource_scope_changed')
return packet
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self,*args):return None
def read_http(origin, token, path):
require(path=='/v1/integrity' or path.startswith('/v1/events/factory-audit-'),'reader_path_not_scoped_to_probe')
request=Request(origin+path,headers={'Authorization':'Bearer '+token})
try:response=build_opener(ProxyHandler({}),NoRedirect()).open(request,timeout=10)
except HTTPError as exc:response=exc
with response:
raw=response.read(524289);require(len(raw)<=524288,'readback_too_large')
require(response.status==200,'operator_readback_failed')
return json.loads(raw)
def operator_identity(rows):
# Select an already existing independent, read-only, full-tenant operator.
candidates=[r for r in rows if r.get('may_read') is True and r.get('may_write',True) is False
and r.get('tenants')==['*'] and r['name'] not in SOURCES]
named=[r for r in candidates if 'operator' in r['name']]
if named:candidates=named
require(len(candidates)==1,'independent_operator_reader_ambiguous_or_absent')
return candidates[0]
def jobs(packet, kube, receipt, save):
"""Consume only the producer's already-delivered credential inside its Job."""
receipt['receiver_before']=receiver_check(kube,RECEIVER)
for lane in contracts(approved=True):
policies=data(command(kube+['-n',lane['name'],'get','networkpolicy','-o','json']))
require(not policies['items'],'existing_egress_policy_requires_review')
es=data(command(kube+['-n',lane['name'],'get','externalsecret',lane['secret'],'-o','json']))
require(any(c['type']=='Ready' and c['status']=='True' for c in es['status']['conditions']),'producer_projection_not_ready')
receipt.update(phase='native_jobs',operator_registry_reads=0);save()
created=[]
try:
for obj in packet['objects']:
# Create is exclusive; never replace another operation's object.
native=data(command(kube+['create','-f','-','-o','json'],payload=obj))
created.append({k:native[k] for k in ['apiVersion','kind','metadata']})
receipt['created_objects']=[{'kind':x['kind'],'name':x['metadata']['name'],'namespace':x['metadata']['namespace'],'uid':x['metadata']['uid']} for x in created];save()
receipt['producers']=[]
for sender in SOURCES:
name=packet['run_id'];pods=[]
for _ in range(80):
job=data(command(kube+['-n',sender,'get','job',name,'-o','json']))
if job.get('status',{}).get('succeeded')==1:break
if job.get('status',{}).get('failed'):
raise LaneError('native_probe_job_failed_'+sender)
time.sleep(2)
else:raise LaneError('native_probe_job_timeout_'+sender)
pods=data(command(kube+['-n',sender,'get','pods','-l','job-name='+name,'-o','json']))['items']
require(len(pods)==1 and pods[0]['status']['phase']=='Succeeded','probe_pod_not_succeeded')
raw=command(kube+['-n',sender,'logs',pods[0]['metadata']['name'],'-c','probe']).stdout
require(len(raw)<=8192,'probe_receipt_too_large')
producer=json.loads(raw)
require(producer['status']=='passed' and producer['sender']==sender
and producer['http_statuses']==[202,200] and producer['source_zip_sha256']==packet['source_zip_sha256'], 'probe_receipt_mismatch')
require(any(o['uid']==job['metadata']['uid'] and o.get('controller') is True for o in pods[0]['metadata'].get('ownerReferences',[])), 'probe_pod_owner_mismatch')
require(producer['event_id']==packet['run_id']+'-'+sender,'probe_event_id_mismatch')
producer.update(pod_uid=pods[0]['metadata']['uid'],image=IMAGE,
runtime_image_id=pods[0]['status']['containerStatuses'][0]['imageID'])
receipt['producers'].append(producer);save()
receipt.update(status='native_sender_checks_passed_pending_independent_readback',phase='sender_checks_complete');save()
finally:
# Delete only this run's objects with UID preconditions, Jobs first.
for obj in reversed(created):
meta=obj['metadata'];kind=obj['kind'];plural={'Job':'jobs','ConfigMap':'configmaps','NetworkPolicy':'networkpolicies'}[kind]
prefix='/api/v1' if obj['apiVersion']=='v1' else '/apis/'+obj['apiVersion']
uri=prefix+'/namespaces/'+meta['namespace']+'/'+plural+'/'+meta['name']
command(kube+['delete','--raw='+uri,'-f','/dev/stdin'],payload={'apiVersion':'v1','kind':'DeleteOptions','propagationPolicy':'Foreground','preconditions':{'uid':meta['uid']}})
if kind=='Job':
# Keep the probe's isolation policy until every dependent pod is gone.
command(kube+['-n',meta['namespace'],'wait','--for=delete','job/'+meta['name'],'--timeout=45s'])
receipt['cleanup_requested']=True;save()
def readback(packet, kube, receipt, save):
"""Operator-only archive readback; never rerun jobs or expose the registry."""
require(receipt['status']=='native_sender_checks_passed_pending_independent_readback'
and receipt['cleanup_requested'] is True and receipt['run_id']==packet['run_id'], 'successful_sender_receipt_required')
require({p['sender'] for p in receipt['producers']}==set(SOURCES) and len(receipt['producers'])==2,'exact_producer_receipts_required')
for producer in receipt['producers']:
require(producer['event_id']==packet['run_id']+'-'+producer['sender']
and producer['source_zip_sha256']==packet['source_zip_sha256']
and producer['status']=='passed' and producer['http_statuses']==[202,200],'producer_receipt_mismatch')
receipt['receiver_at_readback']=receiver_check(kube,RECEIVER);save()
identity=data(bao(['token','lookup','-format=json']))['data']
require('platform-admin' in identity['policies'] and 'root' not in identity['policies'],'attended_platform_admin_required')
version,_,rows,_=snapshot()
for lane in contracts(approved=True):
registered=next(r for r in rows if r['name']==lane['name'])
require(registered==desired(lane,registered['tokens'][0]),'sender_registry_scope_changed')
receipt['reader_candidates']=[{k:r.get(k) for k in ['name','may_read','may_write','tenants']} for r in rows if r.get('may_read') is True];save()
reader=operator_identity(rows);reader_token=(reader.get('tokens') or [reader['token']])[0]
receipt.update(registry_version=version,reader_name=reader['name'],phase='independent_readback');save()
with socket.socket() as sock:sock.bind(('127.0.0.1',0));port=sock.getsockname()[1]
forward=subprocess.Popen(kube+['-n','audit-core','port-forward','--address=127.0.0.1','service/audit-core',str(port)+':8080'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
try:
for _ in range(40):
if forward.poll() is not None:raise LaneError('port_forward_failed')
try:
with socket.create_connection(('127.0.0.1',port),timeout=.2):break
except OSError:time.sleep(.2)
else:raise LaneError('port_forward_not_ready')
origin='http://127.0.0.1:'+str(port)
for producer in receipt['producers']:
record=read_http(origin,reader_token,'/v1/events/'+producer['event_id'])
require(record['event_id']==producer['event_id'] and record['source']==producer['sender']
and record['tenant']=='tenant:platform','independent_readback_scope_mismatch')
detail=record['details']['data']
require(detail.get('synthetic') is True or detail.get('details',{}).get('synthetic') is True,'probe_not_marked_synthetic')
producer['independent_readback']={'event_id':record['event_id'],'accepted_at':record['accepted_at'],
'source':record['source'],'tenant':record['tenant'],'synthetic':True};save()
receipt['integrity_after']=read_http(origin,reader_token,'/v1/integrity')
require(receipt['integrity_after']['intact'] is True,'chain_not_intact_after_probes')
require(receipt['integrity_after']['events']>=2,'two_probe_events_missing')
receipt.update(status='native_producer_delivery_verified_pending_bearer_revocation_and_service_admission',phase='complete');save()
finally:
forward.terminate();forward.wait(timeout=10)
def main():
p=argparse.ArgumentParser(description=__doc__);sub=p.add_subparsers(dest='action',required=True)
prep=sub.add_parser('prepare');prep.add_argument('--source-root',type=Path,required=True);prep.add_argument('--packet',type=Path,required=True)
for name in ['run','jobs','readback']:
native=sub.add_parser(name);native.add_argument('--packet',type=Path,required=True);native.add_argument('--packet-sha256',required=True)
native.add_argument('--kubeconfig',required=True);native.add_argument('--server',required=True);native.add_argument('--receipt',type=Path,required=True);native.add_argument('--confirm',required=True)
if name=='readback':native.add_argument('--producer-receipt',type=Path,required=True)
a=p.parse_args()
if a.action=='prepare':print(json.dumps(prepare(a.source_root,a.packet)));return 0
receipt={'schema':'platform.factory-native-acceptance-receipt.v1','status':'refused','credential_values_emitted':False,'started_at':datetime.now(timezone.utc).isoformat()};fd=None
try:
packet=validate_packet(a.packet,a.packet_sha256)
require(a.confirm==CONFIRM,'exact_confirmation_required')
if a.action!='jobs':
require(Path.home().parent.name=='.warden-attended-login' and not os.getenv('BAO_TOKEN') and not os.getenv('VAULT_TOKEN'),'attended_warden_envelope_required')
if a.action=='readback':
prior=json.loads(a.producer_receipt.read_text())
require(prior['packet_sha256']==a.packet_sha256 and prior['run_id']==packet['run_id'],'producer_packet_mismatch')
receipt.update(prior)
receipt['readback_started_at']=datetime.now(timezone.utc).isoformat()
fd=os.open(a.receipt,os.O_RDWR|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
def save():
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0);os.write(fd,(json.dumps(receipt,indent=2)+'\n').encode());os.fsync(fd)
receipt.update(run_id=packet['run_id'],packet_sha256=a.packet_sha256,source_commits=packet['source_commits']);save()
kube=['kubectl','--kubeconfig',a.kubeconfig,'--server',a.server,'--request-timeout=20s']
if a.action in {'run','jobs'}:jobs(packet,kube,receipt,save)
if a.action in {'run','readback'}:readback(packet,kube,receipt,save)
receipt['completed_at']=datetime.now(timezone.utc).isoformat();save();return 0
except BaseException as exc:
receipt.update(status='refused',error=str(exc) if isinstance(exc,LaneError) else 'contained_native_acceptance_failed')
if fd is not None:save()
return 1
finally:
if fd is not None:os.close(fd)
if __name__=='__main__':raise SystemExit(main())