#!/usr/bin/env python3 """Read-only activity-core promotion gate; signed owner acceptance is mandatory. D2 grants scoped activation after proof, never substitutes for release-principal custody, authenticated issuers or rollback. This checker cannot activate a worker, issue credentials, change Argo policy or admit its own caller as a trusted signer. """ from __future__ import annotations import argparse import base64 from datetime import datetime, timezone, timedelta import hashlib import json from pathlib import Path import re import subprocess PIN = 'a86a14332d76d92a7ed176ea34dca4429902c396' OWNER_TASK = 'ACTIVITY-WP-0041-T03' AUTHORITY = 'ACTIVITY-WP-0041-image-only-v1' APPLICATION_PATH = 'argocd/railiance01/applications/activity-core.application.yaml' RESOURCES = { ('', 'ConfigMap', 'activity-core', name) for name in ('actcore-runtime-config', 'actcore-external-activity-definitions', 'actcore-report-schemas', 'actcore-ops-service-inventory') } | {('', 'Service', 'activity-core', name) for name in ('actcore-api', 'actcore-worker-metrics')} | { ('apps', 'Deployment', 'activity-core', name) for name in ('actcore-api', 'actcore-worker', 'actcore-event-router') } CHECKS = { 'principal_scope_enforced', 'credential_custody_verified', 'revocation_denial_proven', 'independent_receipt_issuers_verified', 'authenticated_ci_digest_binding_proven', 'continuous_health_observer_active', 'live_and_rollback_retention_verified', 'image_only_admission_enforced', 'source_cas_and_single_flight_proven', 'restart_and_lost_response_recovery_proven', 'out_of_scope_denials_proven', 'forged_stale_mismatched_receipt_denials_proven', 'failed_health_rollback_proven', 'temporal_dispatch_registered', 'durable_audit_sink_verified', 'report_schedule_invariants_verified', } TARGETS = { 'platform_repository': 'coulomb/railiance-platform', 'branch': 'main', 'child_path': APPLICATION_PATH, 'child_field': '/spec/source/targetRevision', 'root_application': 'railiance-apps-root', 'root_sync_selects_only': 'activity-core', 'child_application': 'activity-core', 'prune': False, } def canonical(value): return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode() def stamp(value): result = datetime.fromisoformat(value.replace('Z', '+00:00')) if result.tzinfo is None: raise ValueError('timezone required') return result def run(argv, *, input=None): try: result = subprocess.run(argv, input=input, capture_output=True, timeout=100) if result.returncode: raise ValueError('metadata read unverified; protected output suppressed') return result.stdout except (OSError, subprocess.SubprocessError): raise ValueError('metadata read unverified; protected output suppressed') from None def collect(): def get(args): return json.loads(run(['kubectl', *args, '-o', 'json'])) nodes = get(['get', 'nodes']) if '92.205.62.239' not in [a['address'] for n in nodes['items'] for a in n['status']['addresses']]: raise ValueError('estate identity mismatch') namespace = get(['get', 'namespace', 'kube-system']) obj = get(['-n', 'argocd', 'get', 'application', 'activity-core']) spec, status = obj['spec'], obj.get('status', {}) expected_source = {'repoURL': 'https://forgejo.coulomb.social/coulomb/activity-core.git', 'path': 'k8s/gitops', 'targetRevision': PIN} if spec.get('source') != expected_source: raise ValueError('exact activity source unverified') op = status.get('operationState', {}) deployments = [] for name in ('actcore-api', 'actcore-worker', 'actcore-event-router'): deployment = get(['-n', 'activity-core', 'get', 'deployment', name]) generation = deployment['metadata']['generation']; ds = deployment.get('status', {}) desired = deployment['spec'].get('replicas', 1) deployments.append({'name': name, 'generation_observed': ds.get('observedGeneration') == generation, 'replicas': desired, 'ready': ds.get('readyReplicas', 0), 'updated': ds.get('updatedReplicas', 0), 'available': ds.get('availableReplicas', 0)}) project = get(['-n', 'argocd', 'get', 'appproject', 'activity-core'])['spec'] return {'captured_at': datetime.now(timezone.utc).isoformat(), 'estate_ip': '92.205.62.239', 'cluster_uid': namespace['metadata']['uid'], 'application_uid': obj['metadata']['uid'], 'source': expected_source, 'project': spec.get('project'), 'destination': spec.get('destination'), 'automated_present': 'automated' in spec.get('syncPolicy', {}), 'sync_options': spec.get('syncPolicy', {}).get('syncOptions', []), 'pending_operation': bool(obj.get('operation')), 'sync': status.get('sync', {}).get('status'), 'revision': status.get('sync', {}).get('revision'), 'health': status.get('health', {}).get('status'), 'operation': {k: op.get(k) for k in ('phase', 'finishedAt')}, 'condition_types': [c.get('type') for c in status.get('conditions', [])], 'resources': [{k: r.get(k) for k in ('group', 'kind', 'namespace', 'name', 'status')} for r in status.get('resources', [])], 'project_scope_matches': (project.get('sourceRepos') == ['https://forgejo.coulomb.social/coulomb/activity-core.git'] and project.get('destinations') == [{'server': 'https://kubernetes.default.svc', 'namespace': 'activity-core'}] and project.get('clusterResourceWhitelist', []) == [] and {(r.get('group') or '',r['kind']) for r in project.get('namespaceResourceWhitelist', [])} == {('', 'ConfigMap'),('', 'Service'),('apps','Deployment')}), 'deployments': deployments} def adoption_checks(observation, now): if (not 0 <= (now-stamp(observation['captured_at'])).total_seconds() <= 300 or observation['estate_ip'] != '92.205.62.239' or not observation['cluster_uid'] or not observation['application_uid']): raise ValueError('fresh exact estate observation required') if (observation['source'] != {'repoURL':'https://forgejo.coulomb.social/coulomb/activity-core.git','path':'k8s/gitops','targetRevision':PIN} or observation['project'] != 'activity-core' or observation['revision'] != PIN or observation['destination'] != {'server':'https://kubernetes.default.svc','namespace':'activity-core'} or observation['automated_present'] is not False or observation['pending_operation'] is not False or set(observation['sync_options']) != {'CreateNamespace=false','ApplyOutOfSyncOnly=true','PruneLast=true','FailOnSharedResource=true'} or observation['sync'] != 'Synced' or observation['health'] != 'Healthy' or observation['condition_types'] or observation['project_scope_matches'] is not True): raise ValueError('bounded manual adoption scope unverified') op = observation['operation'] if op['phase'] != 'Succeeded' or (stamp(observation['captured_at'])-stamp(op['finishedAt'])).total_seconds() < 86400: raise ValueError('24-hour adoption eligibility unverified') resources = observation['resources'] if (len(resources) != 9 or {tuple(r.get(k) or '' for k in ('group','kind','namespace','name')) for r in resources} != RESOURCES or any(r['status'] != 'Synced' for r in resources)): raise ValueError('nine-resource runtime inventory unverified') deployments = observation['deployments'] if (len(deployments) != 3 or {d['name'] for d in deployments} != {'actcore-api','actcore-worker','actcore-event-router'} or any(d['generation_observed'] is not True or type(d['replicas']) is not int or d['replicas'] < 1 or any(d[k] != d['replicas'] for k in ('ready','updated','available')) for d in deployments)): raise ValueError('deployment generation/readiness unverified') def owner_acceptance(envelope, trusted_key, trusted_principal, observation, now): from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey if set(envelope) != {'payload','signature'} or not trusted_principal or len(trusted_key) != 32: raise ValueError('trusted owner acceptance configuration required') payload = envelope['payload'] Ed25519PublicKey.from_public_bytes(trusted_key).verify(base64.b64decode(envelope['signature'],validate=True), canonical(payload)) if (payload.get('schema_version') != 'activity-core-release-acceptance/v1' or payload.get('owner_task') != OWNER_TASK or payload.get('issuer') != trusted_principal or payload.get('authority') != AUTHORITY or payload.get('application') != 'activity-core' or payload.get('cluster_uid') != observation['cluster_uid'] or payload.get('adoption_revision') != PIN or payload.get('targets') != TARGETS or not payload.get('release_principal') or not re.fullmatch('[a-f0-9]{40}',payload.get('broker_revision','')) or set(payload.get('checks',{})) != CHECKS or any(value is not True for value in payload['checks'].values())): raise ValueError('owner acceptance scope or machine proof incomplete') issued, expires = stamp(payload['issued_at']), stamp(payload['expires_at']) if not 0 <= (now-issued).total_seconds() <= 300 or not now < expires <= issued + timedelta(minutes=5): raise ValueError('owner acceptance stale or expired') health = payload['continuous_health'] if (health.get('revision') != PIN or health.get('continuous') is not True or type(health.get('max_gap_seconds')) is not int or not 0 < health['max_gap_seconds'] <= 90 or not 0 <= (now-stamp(health['observed_at'])).total_seconds() <= 90 or (stamp(health['observed_at'])-stamp(health['healthy_since'])).total_seconds() < 86400): raise ValueError('owner continuous observation proof incomplete') return payload def readiness(observation, *, envelope=None, trusted_key=None, trusted_principal=None, now=None): now = now or datetime.now(timezone.utc) base = {'schema_version':'activity-promotion-readiness/v1','task':'RPF-WP-0048-T02', 'owner_dependency':OWNER_TASK,'native_mutation':False,'automatic_sync_candidate':False, 'observed_at':now.isoformat(),'checker_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()} try: adoption_checks(observation,now) except Exception: return base | {'status':'unverified','reason':'adoption_metadata_unverified'} base |= {'adoption_24h_eligible':True,'adoption_snapshot_is_not_continuous_observation':True, 'observation_sha256':hashlib.sha256(canonical(observation)).hexdigest()} if envelope is None or trusted_key is None or trusted_principal is None: return base | {'status':'waiting','reason':'authenticated_owner_activation_acceptance_missing', 'depends_on':[OWNER_TASK],'needs_human':False} try: payload = owner_acceptance(envelope,trusted_key,trusted_principal,observation,now) except Exception: return base | {'status':'unverified','reason':'owner_activation_acceptance_unverified'} return base | {'status':'ready','owner_acceptance_sha256':hashlib.sha256(canonical(envelope)).hexdigest(), 'release_principal':payload['release_principal'],'broker_revision':payload['broker_revision'], 'scope':AUTHORITY,'rollback':'disable dedicated release dispatcher admission; retain manual child policy and prior source pin', 'abort_owner':'railiance-platform'} def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--live',action='store_true');parser.add_argument('--remote',action='store_true',help=argparse.SUPPRESS) parser.add_argument('--observation',type=Path);parser.add_argument('--receipt',type=Path) parser.add_argument('--owner-acceptance',type=Path) parser.add_argument('--trusted-owner-key',type=Path,help='operator-owned raw Ed25519 public key; never receipt-provided') parser.add_argument('--trusted-owner-principal',help='operator-owned issuer binding') args=parser.parse_args() try: if args.remote: print(json.dumps(collect(),sort_keys=True));return 0 if args.live: import sys;sys.path.insert(0,str(Path(__file__).resolve().parent));import kube_route observation=json.loads(run(kube_route.ssh_argv(['python3','-','--remote']),input=Path(__file__).read_bytes())) observation['remote_captured_at']=observation['captured_at'] observation['captured_at']=datetime.now(timezone.utc).isoformat() if args.observation:args.observation.write_text(json.dumps(observation,indent=2)+'\n') else:observation=json.loads(args.observation.read_text()) envelope=json.loads(args.owner_acceptance.read_text()) if args.owner_acceptance else None key=args.trusted_owner_key.read_bytes() if args.trusted_owner_key else None result=readiness(observation,envelope=envelope,trusted_key=key,trusted_principal=args.trusted_owner_principal) except Exception:result={'schema_version':'activity-promotion-readiness/v1','status':'unverified','reason':'protected_read_or_inputs_unverified','native_mutation':False} if args.receipt:args.receipt.write_text(json.dumps(result,indent=2)+'\n') print(json.dumps(result,sort_keys=True));return 0 if result['status']=='ready' else 1 if __name__=='__main__':raise SystemExit(main())