diff --git a/argocd/railiance01/drafts/external-secrets.application.yaml b/argocd/railiance01/drafts/external-secrets.application.yaml index eebc9e1..2e5c5ea 100644 --- a/argocd/railiance01/drafts/external-secrets.application.yaml +++ b/argocd/railiance01/drafts/external-secrets.application.yaml @@ -1,5 +1,5 @@ # DRAFT for railiance01 (RPF-WP-0044). Not synced by any root: move to -# ../applications/ only in this app's adoption task, with the founder's go-ahead. +# ../applications/ only after predecessor automated proof under accepted D2. # No automated sync, no finalizer. CRDs survive Application pruning/deletion. apiVersion: argoproj.io/v1alpha1 kind: Application diff --git a/intakes/RPF-IN-0011-activity-core-release-custody.md b/intakes/RPF-IN-0011-activity-core-release-custody.md new file mode 100644 index 0000000..957d650 --- /dev/null +++ b/intakes/RPF-IN-0011-activity-core-release-custody.md @@ -0,0 +1,48 @@ +--- +id: RPF-IN-0011 +kind: intake +type: intake +title: "Prepare dedicated activity-core image-only release principal and custody" +repo: railiance-platform +lane: supervised +status: open +owner: railiance-platform +requested_by: activity-core +origin: handoff +origin_ref: ACTIVITY-WP-0041-T03 +priority: high +created: "2026-10-11" +updated: "2026-10-11" +tags: [security, cross-owner, release, credential-custody] +--- + +Prepare and return an exact owner workplan/task binding for the already D2-authorized +`ACTIVITY-WP-0041-image-only-v1` release scope. This intake is independent of the +RPF48 gate that awaits ACT41 acceptance. It requests owner implementation and native +acceptance, not another founder approval or a secret value. RPF-WP-0036-T03 is +backup freshness work and does not supply this release identity/custody commitment. + +Fixed intended Kubernetes identity is +`system:serviceaccount:activity-core:activity-core-release-broker` on estate +92.205.62.239, cluster UID a553c742-0115-43d4-99a4-a5ca56fe0786. Native read-only +metadata found no dedicated account, only default/activity-core-eso, with no matching +namespace/ArgoCD role bindings. ESO projection custody cannot be borrowed for release. +Platform must choose and independently attest the fixed Git publication identity +and exclusive broker custody, plus distinct build/review/health/retention issuer +principals and Ed25519 key custody/rotation; no local fixture key is acceptance. + +Activity-core source supplies bounded health GET, 60-second durable observer, +private config/credential drift checks, actual Kubernetes username and eleven +allow/deny checks, dedicated Temporal worker, exact one-field Git publication, +targeted non-pruning Argo operations, durable rollback and sanitized audit sink. +See activity-core docs/release-runtime-owner-handoff.md and this repo's + docs/activity-core-release-admission.md for exact boundaries. Keep privileged +transport behind this reviewed broker because Application RBAC and repository-write +credentials alone cannot enforce the one-field scope. + +Return source-bound owner acceptance and actual Git/Kubernetes allow/deny, custody, +revocation, independent signed receipts, continuous observer/ledger, Temporal +registration, normal release and failed-health rollback proof. No native activation +is claimed. If preparation requires attended admin authentication, stage the exact +reviewed custody command first, then place that login in the orange checklist. +Only a genuinely expanded action/access/custody scope goes to the red daily batch. diff --git a/scripts/eso_adoption_readiness.py b/scripts/eso_adoption_readiness.py new file mode 100644 index 0000000..6585ea6 --- /dev/null +++ b/scripts/eso_adoption_readiness.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Local, non-executing ESO adoption readiness gate. No credential or live calls.""" +import argparse +from datetime import datetime,timezone +import hashlib +import json +from pathlib import Path +import re +import yaml + +D2='89f1058f-33bc-47a8-8a49-0b44d426c67e' + +def require(ok,reason): + if not ok:raise ValueError(reason) + +def fresh(receipt,now): + stamp=datetime.fromisoformat(receipt['captured_at'].replace('Z','+00:00')) + require(stamp.tzinfo is not None and 0<=(now-stamp).total_seconds()<=300,'predecessor_proof_stale') + +def source_check(app): + require(app.get('kind')=='Application' and app.get('metadata',{}).get('name')=='external-secrets' and app['metadata'].get('namespace')=='argocd','source_identity') + spec=app['spec'];source=spec['source'];require(spec.get('project')=='railiance-platform-addons' and spec.get('destination')=={'server':'https://kubernetes.default.svc','namespace':'external-secrets'},'source_destination') + require(source.get('repoURL')=='https://charts.external-secrets.io' and source.get('chart')=='external-secrets' and source.get('targetRevision')=='0.16.1' and source['helm'].get('releaseName')=='external-secrets','source_chart_pin') + values=yaml.safe_load(source['helm']['values']);require(values.get('installCRDs') is True,'install_crds') + options=values.get('crds',{}).get('annotations',{}).get('argocd.argoproj.io/sync-options','') + require(set(options.split(','))=={'Prune=false','Delete=false'},'crd_prune_delete_protection') + sync=spec.get('syncPolicy',{});require('automated' not in sync and not app['metadata'].get('finalizers'),'manual_no_finalizer') + require('ServerSideApply=true' in sync.get('syncOptions',[]),'server_side_apply') + require(not any(x in sync.get('syncOptions',[]) for x in ('Force=true','Replace=true')),'no_force_replace') + return {'chart_pin':'0.16.1','crd_prune_delete_protected':True,'manual_sync_only':True,'server_side_apply':True} + +def issue_check(receipt,now): + require(receipt.get('schema_version')=='rapp-issue-core-adoption/v2' and receipt.get('passed') is True,'issue_core_adoption_missing') + fresh(receipt,now);checks=receipt['checks'];require(checks.get('estate_node_ip') is True and checks.get('argo_synced_healthy') is True and checks.get('argo_tracking') is True and checks.get('external_secret_ready') is True and checks.get('private_service') is True,'issue_core_adoption_incomplete') + require(bool(re.fullmatch(r'[0-9a-f]{40}',checks.get('source_revision',''))),'issue_core_immutable_revision') + require(all(checks.get(k)==v for k,v in {'healthz_status':200,'unauthenticated_ingest_status':401,'authenticated_ingest_status':201,'authenticated_close_status':200}.items()),'issue_core_controlled_smoke_missing') + +def target_check(receipt,now): + require(receipt.get('schema_version')=='argocd-workload-adoption-proof/v1' and receipt.get('status')=='verified' and receipt.get('application')=='target-revenue','target_revenue_adoption_missing') + fresh(receipt,now);require(receipt.get('manual_apply_prune_off') is True and receipt.get('child_automation_enabled') is False,'target_revenue_adoption_incomplete') + +def assess(app,issue,target,now=None): + now=now or datetime.now(timezone.utc);missing=[];checks={} + for name,fn,value in [('source',source_check,app),('RPF-WP-0044-T04',issue_check,issue),('RPF-WP-0044-T05',target_check,target)]: + try: + result=fn(value) if name=='source' else fn(value,now) + checks[name]=result if name=='source' else True + except Exception:missing.append(name) + return {'schema':'eso-adoption-readiness/v1','task':'RPF-WP-0044-T06','approval_ref':D2,'source_ready':bool(checks.get('source')),'ready_for_fresh_native_render_diff':not missing,'missing_or_invalid':missing,'checks':checks,'next_action':'fresh_native_render_diff' if not missing else 'wait_for_predecessor_automated_proof','founder_approval_required':False,'native_mutation':False,'live_or_credential_calls':False} + +def main(): + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--draft',required=True,type=Path);parser.add_argument('--issue-core-proof',type=Path);parser.add_argument('--target-revenue-proof',type=Path) + args=parser.parse_args() + def load(path): + try:return json.loads(path.read_text()) if path else {} + except Exception:return {} + try:app=yaml.safe_load(args.draft.read_text());receipt=assess(app,load(args.issue_core_proof),load(args.target_revenue_proof)) + except Exception:receipt={'ready_for_fresh_native_render_diff':False,'failure':'source_unverified','native_mutation':False,'live_or_credential_calls':False} + print(json.dumps(receipt,sort_keys=True));return 0 if receipt['ready_for_fresh_native_render_diff'] else 2 + +if __name__=='__main__':raise SystemExit(main()) diff --git a/tests/test_eso_adoption_readiness.py b/tests/test_eso_adoption_readiness.py new file mode 100644 index 0000000..70cf303 --- /dev/null +++ b/tests/test_eso_adoption_readiness.py @@ -0,0 +1,38 @@ +import copy +from datetime import datetime,timezone,timedelta +import importlib.util +import json +from pathlib import Path +import unittest +import yaml +spec=importlib.util.spec_from_file_location('eso_gate',Path(__file__).resolve().parents[1]/'scripts/eso_adoption_readiness.py');gate=importlib.util.module_from_spec(spec);spec.loader.exec_module(gate) +NOW=datetime(2026,10,11,21,0,tzinfo=timezone.utc) + +class ReadinessTests(unittest.TestCase): + def app(self):return yaml.safe_load((Path(__file__).resolve().parents[1]/'argocd/railiance01/drafts/external-secrets.application.yaml').read_text()) + def issue(self):return {'schema_version':'rapp-issue-core-adoption/v2','passed':True,'captured_at':NOW.isoformat(),'checks':{'estate_node_ip':True,'argo_synced_healthy':True,'argo_tracking':True,'external_secret_ready':True,'private_service':True,'source_revision':'a'*40,'healthz_status':200,'unauthenticated_ingest_status':401,'authenticated_ingest_status':201,'authenticated_close_status':200}} + def target(self):return {'schema_version':'argocd-workload-adoption-proof/v1','status':'verified','application':'target-revenue','captured_at':NOW.isoformat(),'manual_apply_prune_off':True,'child_automation_enabled':False} + def test_prepared_source_cannot_skip_issue_core_dependency(self): + result=gate.assess(self.app(),{},self.target(),NOW);self.assertTrue(result['source_ready']);self.assertFalse(result['ready_for_fresh_native_render_diff']);self.assertEqual(result['missing_or_invalid'],['RPF-WP-0044-T04']);self.assertFalse(result['founder_approval_required']) + def test_complete_fresh_predecessors_allow_diff_only(self): + result=gate.assess(self.app(),self.issue(),self.target(),NOW);self.assertTrue(result['ready_for_fresh_native_render_diff']);self.assertFalse(result['native_mutation']);self.assertEqual(result['next_action'],'fresh_native_render_diff') + def test_stale_future_wrong_estate_or_missing_smoke_cannot_authorize_adoption(self): + for key,value in [('captured_at',(NOW-timedelta(minutes=6)).isoformat()),('captured_at',(NOW+timedelta(seconds=1)).isoformat())]: + issue=self.issue();issue[key]=value;self.assertFalse(gate.assess(self.app(),issue,self.target(),NOW)['ready_for_fresh_native_render_diff']) + for key,value in [('estate_node_ip',False),('argo_tracking',False),('authenticated_close_status',None),('source_revision','main')]: + issue=self.issue();issue['checks'][key]=value;self.assertFalse(gate.assess(self.app(),issue,self.target(),NOW)['ready_for_fresh_native_render_diff']) + def test_crd_guards_manual_policy_and_no_replace_required(self): + for mutation in ('guard','automation','replace','chart'): + app=self.app() + if mutation=='guard':app['spec']['source']['helm']['values']='installCRDs: true\n' + elif mutation=='automation':app['spec']['syncPolicy']['automated']={} + elif mutation=='replace':app['spec']['syncPolicy']['syncOptions'].append('Replace=true') + else:app['spec']['source']['targetRevision']='latest' + result=gate.assess(app,self.issue(),self.target(),NOW);self.assertFalse(result['source_ready']);self.assertFalse(result['ready_for_fresh_native_render_diff']) + def test_invalid_canary_fields_never_echo_values(self): + app=self.app();app['spec']['source']['helm']['values']='SECRET_CANARY';issue=self.issue();issue['private_value']='SECRET_CANARY';result=gate.assess(app,issue,{},NOW);self.assertNotIn('SECRET_CANARY',json.dumps(result)) + def test_target_revenue_proof_must_be_exact_application_and_prune_off(self): + for key,value in [('application','foreign-app'),('manual_apply_prune_off',False),('child_automation_enabled',True),('status','unverified')]: + target=self.target();target[key]=value;self.assertFalse(gate.assess(self.app(),self.issue(),target,NOW)['ready_for_fresh_native_render_diff']) + +if __name__=='__main__':unittest.main()