diff --git a/argocd/railiance01/bootstrap/01-railiance-tenants-project.yaml b/argocd/railiance01/bootstrap/01-railiance-tenants-project.yaml index 3aadff6..9b4ea7f 100644 --- a/argocd/railiance01/bootstrap/01-railiance-tenants-project.yaml +++ b/argocd/railiance01/bootstrap/01-railiance-tenants-project.yaml @@ -12,6 +12,7 @@ spec: - https://forgejo.coulomb.social/coulomb/*.git # Emergency rollback only (T12 Option A through 2026-08-07) - https://gitea.coulomb.social/coulomb/*.git + - ssh://git@forgejo.coulomb.social:30022/coulomb/rapp-issue-core.git destinations: - server: https://kubernetes.default.svc namespace: "*" diff --git a/docs/activity-core-release-admission.md b/docs/activity-core-release-admission.md index 99f15f0..b87fd04 100644 --- a/docs/activity-core-release-admission.md +++ b/docs/activity-core-release-admission.md @@ -54,3 +54,45 @@ receipts, concurrent release, restart, failed health and successful rollback. Run destructive failure fixtures in an isolated environment; production must not be intentionally broken to manufacture proof. Admit only after tests and the observation gate pass. Access, budget or scope expansion stays a monthly decision. + +## Deterministic platform readiness gate + +`python3 scripts/activity_promotion_readiness.py --live --observation observation.json +--receipt readiness.json` verifies only the current bounded adoption and reports +an explicit owner-proof wait until ACTIVITY-WP-0041-T03 acceptance exists. This +read-only check neither creates a principal nor activates a release worker. D2 +removes the founder reapproval; the identity/custody and runtime proofs above +remain required. ACTIVITY-WP-0042 scheduling acceptance does not supply them. + +Later activation checks require operator-owned `--trusted-owner-key` (raw +32-byte Ed25519 public key) and `--trusted-owner-principal`, supplied separately +from `--owner-acceptance`. No signer key supplied inside a receipt is trusted. +The acceptance envelope has exactly `payload` and `signature` (base64 Ed25519 +signature of sorted compact JSON). Its payload uses +`schema_version: activity-core-release-acceptance/v1`, binds the existing +`owner_task: ACTIVITY-WP-0041-T03`, issuer, dedicated release principal, +`authority: ACTIVITY-WP-0041-image-only-v1`, application, exact estate cluster UID, +adoption revision and full broker implementation revision. It expires within +five minutes; the operator owns signer admission/revocation and actual native +evidence verification. An agent must not synthesize this production acceptance +from test fixtures or a producer's booleans. + +The `targets` mapping is the exact single platform child +`targetRevision` path on `main`, selective root sync of only activity-core, +child activity-core and prune false, as named in the checker. `checks` must +contain exactly its named acceptance checks, all actual owner-verified: bounded +identity/custody/revocation/denials, independent authenticated receipts/CI digest +binding, continuous observer, image-only scope, source CAS/single-flight, +restart/lost-response recovery, retention, real failed-health rollback, Temporal +registration, durable audit sink and report/schedule invariants. +`continuous_health` binds the adoption revision and a current actual 24h sampled +interval with at most 90-second gaps. A source timestamp or snapshot is not this +proof. Signatures authenticate the accepted owner's assertions; the owner still +must ground them in actual runtime receipts. + +Native adoption mismatch or malformed/forged/stale/widened owner acceptance +returns unverified. Missing owner acceptance/trust binding returns waiting on +the existing owner task, with its cross-owner coordination case. Only ready +returns exit code zero. No automatic-sync source candidate is generated: +ACTIVITY-WP-0041's current broker contract still relies on explicit selective +manual child/root sync and forbids an automated child policy. diff --git a/docs/evidence/2026-10-11-activity-promotion-readiness/README.md b/docs/evidence/2026-10-11-activity-promotion-readiness/README.md new file mode 100644 index 0000000..7baede8 --- /dev/null +++ b/docs/evidence/2026-10-11-activity-promotion-readiness/README.md @@ -0,0 +1,30 @@ +# Activity-core bounded promotion readiness + +```sh +python3 scripts/activity_promotion_readiness.py --live --observation observation.json --receipt readiness.json +python3 -m unittest discover -s tests -p 'test_activity_promotion_readiness.py' +``` + +The current result is waiting for ACTIVITY-WP-0041-T03 authenticated owner +acceptance. Live scoped adoption, elapsed 24h and Deployment readiness pass; +no automation/identity/principal has been activated. A pair of snapshots or an +old successful-sync timestamp cannot prove the owner's gap-sensitive observer. +Raw subprocess output/errors and credential/Secret objects are never emitted. +The initial strict future-timestamp refusal is retained. The revised collector +records both the remote timestamp and the local observer's receipt time, avoiding +unjustified clock tolerance or backfilled health. + +For later activation, the operator supplies an independent trusted owner public +key/principal and signed native acceptance. The checker never loads a public key +from the submitted receipt, creates an identity, grants authority or changes a +child/root spec. Exact fields and source-derived proof requirements are recorded +in `docs/activity-core-release-admission.md`. Only `status: ready` exits zero; +waiting/unverified cannot activate anything. Generated fixture signatures in the +nine tests are not native acceptance evidence. The receipt and coordination case +accompany executable source changes, not an evidence-only checkpoint. + +Abort owner: railiance-platform. Rollback of preparation: revert this gate source +change. Later bounded dispatcher activation requires its declared owner rollback +(disable admission and recover the prior pinned source through the reviewed +broker); no native activation or credential scope expansion follows from this +preparation. Existing Application remains manually reconciled, prune disabled. diff --git a/docs/evidence/2026-10-11-activity-promotion-readiness/coordination-case.json b/docs/evidence/2026-10-11-activity-promotion-readiness/coordination-case.json new file mode 100644 index 0000000..3ea727b --- /dev/null +++ b/docs/evidence/2026-10-11-activity-promotion-readiness/coordination-case.json @@ -0,0 +1,31 @@ +{ + "schema_version": "cross-owner-wait-receipt/v0.1", + "scenario_type": "cross_owner_wait", + "case_id": "RPF-WP-0048-T02--ACTIVITY-WP-0041-T03", + "state": "waiting", + "reason": "authenticated-owner-activation-acceptance-missing", + "beneficiary_workplan_id": "RPF-WP-0048", + "beneficiary_task_id": "RPF-WP-0048-T02", + "beneficiary_repo": "railiance-platform", + "obligor_workplan_id": "ACTIVITY-WP-0041", + "obligor_task_id": "ACTIVITY-WP-0041-T03", + "obligor_repo": "activity-core", + "owner_observation_refs": [ + "activity-core/docs/release-broker.md", + "activity-core/docs/gitops-release.md", + "activity-core/docs/platform-recovery-scheduling-plan.md" + ], + "needs_human": false, + "automatic_wakeup": false, + "foreign_task_changed": false, + "checkpoint": { + "summary": "Live scoped adoption eligible; authenticated bounded-release owner proof missing. Deterministic fail-closed readiness gate implemented.", + "files_changed": [ + "scripts/activity_promotion_readiness.py", + "tests/test_activity_promotion_readiness.py", + "workplans/RPF-WP-0048-activity-core-gitops-adoption.md" + ], + "next_action": "ACTIVITY-WP-0041-T03 owner supplies native identity/custody/issuers/continuous observation/rollback acceptance, then platform reruns gate before activation.", + "blocked_reason": "ACTIVITY-WP-0041-T03 authenticated owner activation acceptance absent." + } +} diff --git a/docs/evidence/2026-10-11-activity-promotion-readiness/observation-before-local-measurement.json b/docs/evidence/2026-10-11-activity-promotion-readiness/observation-before-local-measurement.json new file mode 100644 index 0000000..862bdd9 --- /dev/null +++ b/docs/evidence/2026-10-11-activity-promotion-readiness/observation-before-local-measurement.json @@ -0,0 +1,124 @@ +{ + "application_uid": "cd179999-852d-4d70-9017-29e867475bff", + "automated_present": false, + "captured_at": "2026-10-11T19:44:18.628842+00:00", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "condition_types": [], + "deployments": [ + { + "available": 1, + "generation_observed": true, + "name": "actcore-api", + "ready": 1, + "replicas": 1, + "updated": 1 + }, + { + "available": 1, + "generation_observed": true, + "name": "actcore-worker", + "ready": 1, + "replicas": 1, + "updated": 1 + }, + { + "available": 1, + "generation_observed": true, + "name": "actcore-event-router", + "ready": 1, + "replicas": 1, + "updated": 1 + } + ], + "destination": { + "namespace": "activity-core", + "server": "https://kubernetes.default.svc" + }, + "estate_ip": "92.205.62.239", + "health": "Healthy", + "operation": { + "finishedAt": "2026-10-04T14:13:25Z", + "phase": "Succeeded" + }, + "pending_operation": false, + "project": "activity-core", + "project_scope_matches": true, + "resources": [ + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-external-activity-definitions", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-ops-service-inventory", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-report-schemas", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-runtime-config", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "Service", + "name": "actcore-api", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "Service", + "name": "actcore-worker-metrics", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": "apps", + "kind": "Deployment", + "name": "actcore-api", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": "apps", + "kind": "Deployment", + "name": "actcore-event-router", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": "apps", + "kind": "Deployment", + "name": "actcore-worker", + "namespace": "activity-core", + "status": "Synced" + } + ], + "revision": "a86a14332d76d92a7ed176ea34dca4429902c396", + "source": { + "path": "k8s/gitops", + "repoURL": "https://forgejo.coulomb.social/coulomb/activity-core.git", + "targetRevision": "a86a14332d76d92a7ed176ea34dca4429902c396" + }, + "sync": "Synced", + "sync_options": [ + "CreateNamespace=false", + "ApplyOutOfSyncOnly=true", + "PruneLast=true", + "FailOnSharedResource=true" + ] +} diff --git a/docs/evidence/2026-10-11-activity-promotion-readiness/observation.json b/docs/evidence/2026-10-11-activity-promotion-readiness/observation.json new file mode 100644 index 0000000..07fb2dc --- /dev/null +++ b/docs/evidence/2026-10-11-activity-promotion-readiness/observation.json @@ -0,0 +1,125 @@ +{ + "application_uid": "cd179999-852d-4d70-9017-29e867475bff", + "automated_present": false, + "captured_at": "2026-10-11T19:44:50.828034+00:00", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "condition_types": [], + "deployments": [ + { + "available": 1, + "generation_observed": true, + "name": "actcore-api", + "ready": 1, + "replicas": 1, + "updated": 1 + }, + { + "available": 1, + "generation_observed": true, + "name": "actcore-worker", + "ready": 1, + "replicas": 1, + "updated": 1 + }, + { + "available": 1, + "generation_observed": true, + "name": "actcore-event-router", + "ready": 1, + "replicas": 1, + "updated": 1 + } + ], + "destination": { + "namespace": "activity-core", + "server": "https://kubernetes.default.svc" + }, + "estate_ip": "92.205.62.239", + "health": "Healthy", + "operation": { + "finishedAt": "2026-10-04T14:13:25Z", + "phase": "Succeeded" + }, + "pending_operation": false, + "project": "activity-core", + "project_scope_matches": true, + "resources": [ + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-external-activity-definitions", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-ops-service-inventory", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-report-schemas", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "ConfigMap", + "name": "actcore-runtime-config", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "Service", + "name": "actcore-api", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": null, + "kind": "Service", + "name": "actcore-worker-metrics", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": "apps", + "kind": "Deployment", + "name": "actcore-api", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": "apps", + "kind": "Deployment", + "name": "actcore-event-router", + "namespace": "activity-core", + "status": "Synced" + }, + { + "group": "apps", + "kind": "Deployment", + "name": "actcore-worker", + "namespace": "activity-core", + "status": "Synced" + } + ], + "revision": "a86a14332d76d92a7ed176ea34dca4429902c396", + "source": { + "path": "k8s/gitops", + "repoURL": "https://forgejo.coulomb.social/coulomb/activity-core.git", + "targetRevision": "a86a14332d76d92a7ed176ea34dca4429902c396" + }, + "sync": "Synced", + "sync_options": [ + "CreateNamespace=false", + "ApplyOutOfSyncOnly=true", + "PruneLast=true", + "FailOnSharedResource=true" + ], + "remote_captured_at": "2026-10-11T19:44:52.074977+00:00" +} diff --git a/docs/evidence/2026-10-11-activity-promotion-readiness/readiness-clock-refusal.json b/docs/evidence/2026-10-11-activity-promotion-readiness/readiness-clock-refusal.json new file mode 100644 index 0000000..5644229 --- /dev/null +++ b/docs/evidence/2026-10-11-activity-promotion-readiness/readiness-clock-refusal.json @@ -0,0 +1,11 @@ +{ + "schema_version": "activity-promotion-readiness/v1", + "task": "RPF-WP-0048-T02", + "owner_dependency": "ACTIVITY-WP-0041-T03", + "native_mutation": false, + "automatic_sync_candidate": false, + "observed_at": "2026-10-11T19:44:17.229169+00:00", + "checker_sha256": "ed159e8128c0fbb2741fcff17e5b2eebcee279057da3c1c6bce285e48b233882", + "status": "unverified", + "reason": "adoption_metadata_unverified" +} diff --git a/docs/evidence/2026-10-11-activity-promotion-readiness/readiness.json b/docs/evidence/2026-10-11-activity-promotion-readiness/readiness.json new file mode 100644 index 0000000..0b26f11 --- /dev/null +++ b/docs/evidence/2026-10-11-activity-promotion-readiness/readiness.json @@ -0,0 +1,18 @@ +{ + "schema_version": "activity-promotion-readiness/v1", + "task": "RPF-WP-0048-T02", + "owner_dependency": "ACTIVITY-WP-0041-T03", + "native_mutation": false, + "automatic_sync_candidate": false, + "observed_at": "2026-10-11T19:44:50.828539+00:00", + "checker_sha256": "93986567cc6ea01b646cebd2e47eb4a5f19ad43b54ecfe420079a45b60ce30f3", + "adoption_24h_eligible": true, + "adoption_snapshot_is_not_continuous_observation": true, + "observation_sha256": "a66b0d128292e24f7964ca90602166ff8b0d88799f31f605dd9fdf88f0b53c25", + "status": "waiting", + "reason": "authenticated_owner_activation_acceptance_missing", + "depends_on": [ + "ACTIVITY-WP-0041-T03" + ], + "needs_human": false +} diff --git a/docs/evidence/2026-10-11-activity-promotion-readiness/validation.json b/docs/evidence/2026-10-11-activity-promotion-readiness/validation.json new file mode 100644 index 0000000..ab35f75 --- /dev/null +++ b/docs/evidence/2026-10-11-activity-promotion-readiness/validation.json @@ -0,0 +1,32 @@ +{ + "schema_version": "activity-promotion-validation/v1", + "task": "RPF-WP-0048-T02", + "task_closed": false, + "task_status": "wait", + "depends_on": [ + "ACTIVITY-WP-0041-T03" + ], + "needs_human": false, + "native_mutation": false, + "child_source_changed": false, + "tests": { + "passed": 9, + "failed": 0 + }, + "compilation": "passed", + "live_checks": "adoption_24h_eligible", + "activation": "waiting-for-authenticated-owner-proof", + "readiness_ref": "readiness.json", + "coordination_case_ref": "coordination-case.json", + "checker_sha256": "93986567cc6ea01b646cebd2e47eb4a5f19ad43b54ecfe420079a45b60ce30f3", + "source_contracts": { + "activity-core/docs/release-broker.md": "bbc917f9b2a533b65d41deb79324b6bd43f8ad14e83587387aa9078fff238fb1", + "activity-core/docs/gitops-release.md": "b1ce725b881a4d506e1129ad330451b75d65d81a3db2a63351867710bbd89da7", + "activity-core/docs/platform-recovery-scheduling-plan.md": "3b86a1bddae65abd56697360303ae9f56a969c98ae98d757f1ed689aaa241698" + }, + "new_attended_items": [], + "new_red_batch_items": [], + "failed_clock_observation_retained": "readiness-clock-refusal.json", + "timestamp_method": "local observer measured receipt after SSH return; remote timestamp retained separately, never used to backfill continuous health", + "validated_at": "2026-10-11T19:47:19.802659+00:00" +} diff --git a/docs/evidence/2026-10-11-issue-core-credential/contract-needs-owner-proof.json b/docs/evidence/2026-10-11-issue-core-credential/contract-needs-owner-proof.json new file mode 100644 index 0000000..3f06597 --- /dev/null +++ b/docs/evidence/2026-10-11-issue-core-credential/contract-needs-owner-proof.json @@ -0,0 +1,19 @@ +{ + "work_record": "RPF-WP-0044-T04", + "scope_approval_ref": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "repository": "coulomb/rapp-issue-core", + "read_only": true, + "openbao_path": "platform/operators/argocd/repositories/rapp-issue-core", + "destination": "argocd/rapp-issue-core-repository", + "project": "railiance-tenants", + "url": "ssh://git@forgejo.coulomb.social:30022/coulomb/rapp-issue-core.git", + "wrap_ttl_seconds": 60, + "new_network_scope": false, + "owner_review_ref": null, + "ssh_egress_existing_proof_ref": null, + "runtime_identity_ref": null, + "rollback_ref": null, + "ssh_known_hosts_sha256": null, + "expires_at": "2026-10-12T23:59:59+00:00", + "execution_mode": "attended-founder-bounded" +} diff --git a/docs/evidence/2026-10-11-issue-core-credential/installed-cli-proof.json b/docs/evidence/2026-10-11-issue-core-credential/installed-cli-proof.json new file mode 100644 index 0000000..3087f03 --- /dev/null +++ b/docs/evidence/2026-10-11-issue-core-credential/installed-cli-proof.json @@ -0,0 +1,21 @@ +{ + "synthetic_only": true, + "no_actual_credentials_or_services": true, + "installed_cli_combined_path_stdin_value_preserved": true, + "installed_cli_cas0_body_verified": true, + "list404_class": { + "exit": 2, + "stdout": "{}", + "stderr_empty": true + }, + "consumed_unwrap_error_class": { + "exit": 2, + "http_code": 400, + "exact_error": "wrapping token is not valid or does not exist" + }, + "help_confirmed": [ + "kv put field=- stdin semantics", + "cas=0 create-only", + "unwrap with no token argument uses current authenticated token" + ] +} diff --git a/docs/evidence/2026-10-11-issue-core-credential/owner-review.json b/docs/evidence/2026-10-11-issue-core-credential/owner-review.json new file mode 100644 index 0000000..0aa4813 --- /dev/null +++ b/docs/evidence/2026-10-11-issue-core-credential/owner-review.json @@ -0,0 +1,36 @@ +{ + "schema": "argocd-issue-core-fixed-command-review/v1", + "task": "RPF-WP-0044-T04", + "approval_ref": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "second_founder_approval_required": false, + "source_scope_already_approved": "Dedicated Argo reader, only coulomb/rapp-issue-core read-only, exact operator custody path", + "command": "BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 warden access openbao-platform-admin-login --exec -- python3 scripts/provision_argocd_issue_core_repository.py --contract --contract-sha256 --journal ", + "runtime": "Existing attended Platform-admin login encloses nested warden forgejo-admin-api-token --exec; silent outer fds suppress proxy inherited fetch errors; credential injection stays inside child. Kubectl destination is fixed SSH railiance01 native API, never workstation kubecontext.", + "runtime_authority_limits": "Current admin lane is interim proxy, not native issuer. It requires caller OpenBao identity, and operatorpath create/read/update capability plus parent metadata list. Source plan autonomous label is not runtime proof. No exact delegated operatorpath identity found in source; absent such a proven identity the outer OIDC/MFA login is Orange. This command is explicitly attended-founder-bounded, not autopilot. Root capability is accepted only in this declared attended mode; refs are manual review attestations, not native approval tokens.", + "preflight": [ + "exact contract references existing D2 and owner review, expiry and hash", + "node public IP exact 92.205.62.239 before trust", + "actual Argo SSH host CM hash matches trusted provider pin for [forgejo.coulomb.social]:30022", + "existing SSH egress proof, no new network scope", + "AppProject existing sourceRepos admits exact SSH repository", + "operatorpath create/read/update capabilities and fresh custody", + "fresh exact destination Secret and no matching provider deploykey" + ], + "custody": "tmpfs generated ed25519 private key -> provider receives public key only -> OpenBao CAS0 exact SSH_PRIVATE_KEY -> 60-second response wrap -> exactly one unwrap with child token env -> Secret create encrypted SSH stdin, no last-applied annotation Installed Bao CLI actual synthetic HTTP fixture confirmed combined-path SSH_PRIVATE_KEY=- stdin and CAS0 request, LIST404 -> exit2 empty JSON {}, and unwrap400 specific native error.", + "receipt": "Fixed non-secret stages, provider key ID, public-key hash, Bao write/unwrap request IDs and wrap accessor, custody version. All raw provider/CLI stdout/stderr discarded. Successful issuance says delivered_not_adopted; Argo adoption/health remains independent. Exact wrap creation path verified/retained, enabled audit-device metadata/count checked before grant, consumed-token replay must return native OpenBao 400 specific invalid-wrapping-token error; transport/403/unrelated 400 are not proof.", + "replay": "Exclusive O_NOFOLLOW journal; any preexisting journal refuses. Pending/ambiguous stages retained uncertain_do_not_replay. Do not rerun; reconcile IDs/custody/destination first. No automatic second key, unwrap, retry, deletion or broad credential fallback.", + "rollback": "Owner verifies journal provider key ID + exact fixed repo and public hash, revokes only that deploykey through same admin exec lane. Remove only exact matched Secret version, soft-delete only exact new operatorpath custody version, restore Application previous Git transport/revision and manual sync; never delete shared policy/store or another repo key. Revocation/removal is separate reviewed owner recovery; issuer performs no destructive fallback.", + "limitations": [ + "No native SSH-key expiry; maximum 180-day lifecycle rotation remains owner task.", + "No automatic rollback command executes.", + "No actual credential read, issue, unwrap, Secret create, or adoption executed.", + "Contract hostpin, egress, ownerreview/runtime/rollback refs must be filled and reviewed before command runs." + ], + "checks": { + "unit_tests": 9, + "passed": true, + "installed_cli_synthetic_put_stdin_cas0": true, + "installed_cli_synthetic_list404_signature_verified": true, + "installed_cli_synthetic_unwrap400_signature_verified": true + } +} diff --git a/docs/evidence/2026-10-11-issue-core-transport/transport-plan.json b/docs/evidence/2026-10-11-issue-core-transport/transport-plan.json new file mode 100644 index 0000000..e36173d --- /dev/null +++ b/docs/evidence/2026-10-11-issue-core-transport/transport-plan.json @@ -0,0 +1,41 @@ +{ + "schema": "argocd-public-transport/v1", + "task": "RPF-WP-0044-T04", + "approval_ref": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "abort_owner": "railiance-platform with railiance-enablement", + "diff_ref": "source-manifest.json", + "rollback": "Run same helper --action rollback using reviewed plan; guarded exact after-to-before fields only. Revert corresponding owner source patches too.", + "namespace": "argocd", + "target": "railiance01", + "baseline_sha256": "28074ce931b8362d0aab101317aef2decae69af527f5b0ee0699b5877ff89bb3", + "public_authority_observation_sha256": "264a71f4bf8672dfb3ac5d75ed7a7f467ae17d2f6c19aaf55893ff13718e3093", + "provider_pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "repo_server_tcp_30022_verified": true, + "resources": [ + { + "kind": "configmap", + "name": "argocd-ssh-known-hosts-cm", + "uid": "e1481984-dc5d-4fcc-9f2a-118c054865b7", + "path": "/data/ssh_known_hosts", + "before": "# This file was automatically generated by hack/update-ssh-known-hosts.sh. DO NOT EDIT\n[ssh.github.com]:443 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\n[ssh.github.com]:443 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl\n[ssh.github.com]:443 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\nbitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE=\nbitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO\nbitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M=\ngithub.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\ngithub.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\ngitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=\ngitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf\ngitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9\nssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H\nvs-ssh.visualstudio.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H\n", + "after": "# This file was automatically generated by hack/update-ssh-known-hosts.sh. DO NOT EDIT\n[ssh.github.com]:443 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\n[ssh.github.com]:443 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl\n[ssh.github.com]:443 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\nbitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE=\nbitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO\nbitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M=\ngithub.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\ngithub.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\ngitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=\ngitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf\ngitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9\nssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H\nvs-ssh.visualstudio.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H\n[forgejo.coulomb.social]:30022 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC7jadWwHX2j1aeXSonkdwP8CI5j1a+ihzj6kq7697h+8B5X2SpYBvwOi2brYmgoiccyapB2aowmctMLLTtrhMaARtvnZPeoFgPhBrdHaUhHdmTeSg7MbRBu5D8yYTt3CwDxF1+MUFE+NuRhCXRHQLbLCqTLlU9hgZV4xuqsv71lvNHzcF3vMcolTBtdVzTsgCfeV9J8wQMXTC7rFfVuRi5fC6m994KN+vt94VHIZcZwFgjdCEZ4uo7U07Fdp3MT6WMqgLftc9ewBL/p+i5BtzVkF725d5QMx6TL6vLrC7X8lrkUWQL+oy5CeUrnOnsyiuwbYwlFVLsdypa2WbdlHtkllBXVD2IdTxeu038BAjWBXkN1WCGEXWJvj829I8FzObnO+QoOsbwdmGSTFqBX7+2vSymbEHtNcS0/0jk5xUvdnvFRvkzDvhQjMgiF8LIVCk4zWTBeWPQ5r5lLooFn8B1hmsgqHVSd2DXMPUMOuTQRTdxpNicNkBlwEQSwFYSCf3vL+x2e2pIx4YvkLD+1bEwNwB8HIuzF1Vex7Cjbo07pTwxhTJ52ZFQNkja/LqyAzO0bOEb9MF6KIxXKS/qL5C2cf8kHB5eXbiAwbv3jkxVzBiYPEMJYxO0pJKaKK8Cvs8cT2MYsxQkly1MENsvf15eOHqg26CPz/PiruHkiifhXw==\n", + "owner": "railiance-enablement" + }, + { + "kind": "appproject", + "name": "railiance-tenants", + "uid": "f899f3ec-2c55-4cd0-9cb0-b2b4c6eb9be2", + "path": "/spec/sourceRepos", + "before": [ + "https://forgejo.coulomb.social/coulomb/*.git", + "https://gitea.coulomb.social/coulomb/*.git" + ], + "after": [ + "https://forgejo.coulomb.social/coulomb/*.git", + "https://gitea.coulomb.social/coulomb/*.git", + "ssh://git@forgejo.coulomb.social:30022/coulomb/rapp-issue-core.git" + ], + "owner": "railiance-platform" + } + ] +} diff --git a/docs/evidence/2026-10-11-target-revenue-adoption-proof/observation.json b/docs/evidence/2026-10-11-target-revenue-adoption-proof/observation.json new file mode 100644 index 0000000..cb6f79a --- /dev/null +++ b/docs/evidence/2026-10-11-target-revenue-adoption-proof/observation.json @@ -0,0 +1,159 @@ +{ + "application": { + "automated_present": false, + "condition_types": [ + "OrphanedResourceWarning" + ], + "destination": { + "namespace": "target-revenue", + "server": "https://kubernetes.default.svc" + }, + "health": "Healthy", + "name": "target-revenue", + "operation": { + "finishedAt": "2026-09-28T14:23:27Z", + "phase": "Succeeded", + "startedAt": "2026-09-28T14:23:26Z" + }, + "operation_revision": "a3a8a27a8cda32ae3c7b55353ee16a131c50a0a0", + "pending_operation": false, + "project": "railiance-tenants", + "resources": [ + { + "group": null, + "kind": "Service", + "name": "target-revenue", + "namespace": "target-revenue", + "status": "Synced" + }, + { + "group": "apps", + "kind": "Deployment", + "name": "target-revenue", + "namespace": "target-revenue", + "status": "Synced" + }, + { + "group": "external-secrets.io", + "kind": "ExternalSecret", + "name": "target-revenue-runtime", + "namespace": "target-revenue", + "status": "Synced" + }, + { + "group": "networking.k8s.io", + "kind": "Ingress", + "name": "target-revenue", + "namespace": "target-revenue", + "status": "Synced" + }, + { + "group": "postgresql.cnpg.io", + "kind": "Cluster", + "name": "target-revenue-pg", + "namespace": "target-revenue", + "status": "Synced" + } + ], + "revision": "a3a8a27a8cda32ae3c7b55353ee16a131c50a0a0", + "source": { + "path": "k8s/railiance", + "repoURL": "https://forgejo.coulomb.social/coulomb/target-revenue.git", + "targetRevision": "a3a8a27a8cda32ae3c7b55353ee16a131c50a0a0" + }, + "sync": "Synced", + "sync_prune": false, + "sync_strategy_apply": true, + "sync_strategy_force": false, + "uid": "42285c08-97c6-452f-ad8a-4f46d9691bc9" + }, + "captured_at": "2026-10-11T19:51:12.812423+00:00", + "estate_ip": "92.205.62.239", + "live_hooks": [], + "pinned_render_diff": { + "client_side_exit_code": 0, + "normalized_render_sha256": "3d08814b968978d634f4ecb444db31d24ff19d15ae12d8411dacb6e1448859ff", + "output_suppressed": true, + "path": "k8s/railiance", + "render_resource_count": 5, + "render_sha256": "c728d28ac2b7fd61be00e65d356d53a6b729573fdd5a15d420efb86b7c89f50a", + "revision": "a3a8a27a8cda32ae3c7b55353ee16a131c50a0a0", + "skipped_hooks": [ + "target-revenue-bootstrap-binky", + "target-revenue-migrate" + ] + }, + "ready_checks": [ + { + "available_replicas": 1, + "condition": "Available", + "generation": 6, + "instances": null, + "kind": "Deployment", + "name": "target-revenue", + "namespace": "target-revenue", + "observed_generation": 6, + "ready": [ + { + "lastTransitionTime": "2026-08-15T22:30:36Z", + "status": "True" + } + ], + "ready_instances": null, + "replicas": 1, + "tracking_matches": true, + "uid": "c6b7a312-968a-4d10-894d-d4f6b7b11205" + }, + { + "available_replicas": null, + "condition": "Ready", + "generation": 2, + "instances": null, + "kind": "ExternalSecret", + "name": "target-revenue-runtime", + "namespace": "target-revenue", + "observed_generation": null, + "ready": [ + { + "lastTransitionTime": "2026-09-05T16:39:12Z", + "status": "True" + } + ], + "ready_instances": null, + "replicas": null, + "tracking_matches": true, + "uid": "cde6e839-a964-443d-a19d-32cc5b53a9cb" + }, + { + "available_replicas": null, + "condition": "Ready", + "generation": 1, + "instances": 1, + "kind": "Cluster", + "name": "target-revenue-pg", + "namespace": "target-revenue", + "observed_generation": null, + "ready": [ + { + "lastTransitionTime": "2026-08-05T15:51:45Z", + "status": "True" + } + ], + "ready_instances": 1, + "replicas": null, + "tracking_matches": true, + "uid": "333e9409-0a9c-47fa-bb73-6c7309f0a099" + } + ], + "tracking_settings": { + "controller_version": "v3.5.3", + "installation_id_absent": true, + "instance_label_key": "app.kubernetes.io/instance", + "method": "annotation" + }, + "public_health": { + "url": "https://revenue.coulomb.social/healthz", + "status": 200 + }, + "metadata_captured_at": "2026-10-11T19:51:13.501350+00:00" +} diff --git a/docs/evidence/2026-10-11-target-revenue-adoption-proof/receipt.json b/docs/evidence/2026-10-11-target-revenue-adoption-proof/receipt.json new file mode 100644 index 0000000..059bf3f --- /dev/null +++ b/docs/evidence/2026-10-11-target-revenue-adoption-proof/receipt.json @@ -0,0 +1,24 @@ +{ + "schema_version": "argocd-workload-adoption-proof/v1", + "status": "verified", + "application": "target-revenue", + "revision": "a3a8a27a8cda32ae3c7b55353ee16a131c50a0a0", + "captured_at": "2026-10-11T19:51:12.812423+00:00", + "verified_at": "2026-10-11T19:51:12.813305+00:00", + "proving_seconds": 1142865.812423, + "resource_count": 5, + "ready_count": 3, + "manual_apply_prune_off": true, + "hooks_skipped": [ + "target-revenue-bootstrap-binky", + "target-revenue-migrate" + ], + "hooks_absent": true, + "child_automation_enabled": false, + "public_health_status": 200, + "native_mutation": false, + "verifier_sha256": "cf5d2113425d3e014f937371af31d9bcb11671ea036c1b8b8a65aaac47f954b4", + "tracking_helper_sha256": "c058c0ea646bcc3410e0d26db6b42a5471a8fa45a1df62b9ec85f2bf7d03f660", + "contract_sha256": "9946a3f3b5b7612ba9d6352b4dc795f2e5e0385d7d296cc4bf14411e1d7805dd", + "observation_sha256": "d77a033160e62b75f9f7df384222f8cd11ea6e8303f02e83909b4f25d201d1dd" +} diff --git a/scripts/activity_promotion_readiness.py b/scripts/activity_promotion_readiness.py new file mode 100644 index 0000000..8306a8c --- /dev/null +++ b/scripts/activity_promotion_readiness.py @@ -0,0 +1,210 @@ +#!/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: + observation=json.loads(run(['ssh','railiance01','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()) diff --git a/scripts/argocd_public_transport.py b/scripts/argocd_public_transport.py new file mode 100644 index 0000000..5d577f5 --- /dev/null +++ b/scripts/argocd_public_transport.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Guarded public-only Argo transport prerequisite; no Secret or credential calls.""" +import argparse +import copy +from datetime import datetime,timezone +import hashlib +import json +import os +from pathlib import Path +import re +import shlex +import subprocess + +URL='ssh://git@forgejo.coulomb.social:30022/coulomb/rapp-issue-core.git' +D2='89f1058f-33bc-47a8-8a49-0b44d426c67e' +TARGETS=[('configmap','argocd-ssh-known-hosts-cm','/data/ssh_known_hosts','railiance-enablement'),('appproject','railiance-tenants','/spec/sourceRepos','railiance-platform')] + +class Refused(Exception):pass + +def require(ok,reason): + if not ok:raise Refused(reason) + +def digest(value):return hashlib.sha256(json.dumps(value,sort_keys=True).encode()).hexdigest() + +def run(args,body=None): + try: + result=subprocess.run(['ssh','-o','BatchMode=yes','-o','ConnectTimeout=10','railiance01',shlex.join(['kubectl','--request-timeout=15s',*args])],input=body,text=True,capture_output=True,timeout=60) + except Exception:raise Refused('command_outcome_unverified') from None + require(result.returncode==0,'command_outcome_unverified') + return result.stdout + +def value(obj,path): + for key in path.strip('/').split('/'):obj=obj[key] + return obj + +def replace(obj,path,wanted): + keys=path.strip('/').split('/');parent=obj + for key in keys[:-1]:parent=parent[key] + parent[keys[-1]]=wanted + +def normalized(obj): + obj=copy.deepcopy(obj) + for field in ('managedFields','resourceVersion','generation'):obj.get('metadata',{}).pop(field,None) + return obj + +def validate(plan): + require(plan.get('schema')=='argocd-public-transport/v1' and plan.get('task')=='RPF-WP-0044-T04' and plan.get('approval_ref')==D2,'plan_authority') + require(plan.get('target')=='railiance01' and plan.get('namespace')=='argocd' and plan.get('repo_server_tcp_30022_verified') is True,'plan_target') + require(all(isinstance(plan.get(k),str) and plan[k] for k in ('diff_ref','rollback','abort_owner','baseline_sha256','public_authority_observation_sha256')),'plan_receipt_contract') + require(len(plan.get('resources',[]))==2,'target_count') + for item,fixed in zip(plan['resources'],TARGETS): + require(tuple(item.get(k) for k in ('kind','name','path','owner'))==fixed,'fixed_target') + require(bool(re.fullmatch(r'[a-f0-9-]{36}',item.get('uid',''))),'target_uid') + cm,project=plan['resources'] + require(isinstance(cm['before'],str) and cm['before'].endswith('\n') and isinstance(cm['after'],str),'host_field_type') + added=cm['after'][len(cm['before']):] + require(cm['after'].startswith(cm['before']) and bool(re.fullmatch(r'\[forgejo\.coulomb\.social\]:30022 (ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256) [A-Za-z0-9+/=]+\n',added)),'host_addition_scope') + require('[forgejo.coulomb.social]:30022 ' not in cm['before'],'host_preimage_already_admitted') + require(isinstance(project['before'],list) and all(isinstance(v,str) for v in project['before']) and URL not in project['before'] and project['after']==project['before']+[URL],'repository_addition_scope') + +def patch(item,live,rollback=False): + before,after=(item['after'],item['before']) if rollback else (item['before'],item['after']) + require(live['metadata']['uid']==item['uid'],'target_uid_changed') + require(value(live,item['path'])==before,'target_preimage_changed') + require(isinstance(live['metadata'].get('resourceVersion'),str),'resource_version_required') + return [{'op':'test','path':'/metadata/uid','value':item['uid']},{'op':'test','path':'/metadata/resourceVersion','value':live['metadata']['resourceVersion']},{'op':'test','path':item['path'],'value':before},{'op':'replace','path':item['path'],'value':after}] + +def perform(plan,action,receipt,checkpoint=lambda:None): + validate(plan) + nodes=json.loads(run(['get','nodes','-o','json']))['items'] + require(len(nodes)==1 and any(a.get('address')=='92.205.62.239' for a in nodes[0]['status']['addresses']),'wrong_estate') + read=lambda item:json.loads(run(['-n','argocd','get',item['kind'],item['name'],'-o','json'])) + rollback=action=='rollback' + # Validate both targets before the first write, avoiding an avoidable partial change. + live=[read(item) for item in plan['resources']] + for item,obj in zip(plan['resources'],live): + require(obj['metadata']['uid']==item['uid'],'target_uid_changed') + if action=='verify':require(value(obj,item['path'])==item['after'],'verification_field_changed') + elif rollback and value(obj,item['path'])==item['before']:continue + else:patch(item,obj,rollback) + for item,obj in zip(plan['resources'],live): + record={'kind':item['kind'],'name':item['name'],'uid':item['uid'],'field':item['path'],'before_sha256':digest(item['before']),'after_sha256':digest(item['after'])};receipt['resources'].append(record) + if action=='verify':record['verified']=True;continue + if rollback and value(obj,item['path'])==item['before']:record['already_restored']=True;continue + body=json.dumps(patch(item,obj,rollback));args=['-n','argocd','patch',item['kind'],item['name'],'--type=json','--patch-file=/dev/stdin'] + admitted=json.loads(run(args+['--dry-run=server','-o','json'],body)) + wanted=copy.deepcopy(obj);replace(wanted,item['path'],item['before'] if rollback else item['after']) + require(normalized(admitted)==normalized(wanted),'dry_run_modified_other_fields');record['server_dry_run']=True + if action=='dry-run':continue + receipt['pending_target']=item['kind']+'/'+item['name'];receipt['mutation_attempted']=True;checkpoint() + run(args+['-o','name'],body) + checked=read(item) + require(checked['metadata']['uid']==item['uid'] and normalized(checked)==normalized(wanted),'postchange_projection_unverified') + record['changed_verified']=True;receipt['pending_target']=None;checkpoint() + receipt['passed']=True;return receipt + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--plan',type=Path,required=True);parser.add_argument('--plan-sha256',required=True) + parser.add_argument('--action',choices=('dry-run','execute','verify','rollback'),default='dry-run') + parser.add_argument('--receipt',type=Path,required=True) + args=parser.parse_args() + receipt={'schema':'argocd-public-transport-receipt/v1','task':'RPF-WP-0044-T04','approval_ref':D2,'action':args.action,'passed':False,'mutation_attempted':False,'resources':[],'secret_or_credential_calls':False,'captured_at':datetime.now(timezone.utc).isoformat()} + # Exclusive receipt is a replay guard. Never overwrite another attempt. + fd=None + try: + require(not args.receipt.exists(),'receipt_exists_do_not_replay') + fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + raw=args.plan.read_bytes();require(hashlib.sha256(raw).hexdigest()==args.plan_sha256,'plan_changed') + def checkpoint(): + os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0);os.write(fd,(json.dumps(receipt,sort_keys=True)+'\n').encode());os.fsync(fd) + receipt['plan_sha256']=args.plan_sha256;checkpoint();perform(json.loads(raw),args.action,receipt,checkpoint) + except Refused as error:receipt['failure']=str(error) + except Exception:receipt['failure']='internal_outcome_unverified' + finally: + if fd is not None: + os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0);os.write(fd,(json.dumps(receipt,sort_keys=True)+'\n').encode());os.fsync(fd);os.close(fd) + # Only whitelist receipt fields are emitted. Every raw child error stays captured. + print(json.dumps(receipt,sort_keys=True));return 0 if receipt['passed'] else 1 + +if __name__=='__main__':raise SystemExit(main()) diff --git a/scripts/provision_argocd_issue_core_repository.py b/scripts/provision_argocd_issue_core_repository.py new file mode 100644 index 0000000..2fb3a0d --- /dev/null +++ b/scripts/provision_argocd_issue_core_repository.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Silent fixed-scope D2 repository deploy-key issuer/destination actuator. + +Run through the existing Platform attended login and Forgejo admin --exec lane. +No lane grants itself authority. The reviewed contract seals transport/host trust +and existing egress; the command performs no RBAC, policy or network expansion. +No implicit recovery: an existing journal, ambiguous result or drift stops replay. +""" +import argparse +import base64 +from datetime import datetime, timezone +import hashlib +import fnmatch +import json +import os +import re +from pathlib import Path +import stat +import shlex +import subprocess +import sys +import tempfile +import urllib.error +import urllib.request + +REPO='coulomb/rapp-issue-core' +BAO='platform/operators/argocd/repositories/rapp-issue-core' +URL='ssh://git@forgejo.coulomb.social:30022/coulomb/rapp-issue-core.git' +SECRET='rapp-issue-core-repository' +TITLE='argocd-rapp-issue-core-readonly-D2' +D2='89f1058f-33bc-47a8-8a49-0b44d426c67e' +FIXED={'work_record':'RPF-WP-0044-T04','scope_approval_ref':D2,'repository':REPO,'read_only':True,'openbao_path':BAO,'destination':'argocd/'+SECRET,'project':'railiance-tenants','url':URL,'wrap_ttl_seconds':60,'new_network_scope':False,'execution_mode':'attended-founder-bounded'} + + +class Refused(Exception):pass + + +def require(ok,reason): + if not ok:raise Refused(reason) + + +def captured(argv,body=None,env=None): + try: + result=subprocess.run(argv,input=body,env=env,capture_output=True,text=True,timeout=60) + except Exception: + raise Refused('command_unverified') from None + return result + + +def run(argv,body=None,env=None): + result=captured(argv,body,env) + require(result.returncode==0,'command_unverified') + return result.stdout + + +def metadata_children(): + result=captured(['bao','list','-format=json','platform/metadata/operators/argocd/repositories/']) + # Installed CLI represents an empty/404 LIST as exit 2, JSON {}, no stderr. + if result.returncode==2 and result.stdout.strip()=='{}' and not result.stderr.strip():return [] + require(result.returncode==0,'custody_metadata_unverified') + children=json.loads(result.stdout);require(isinstance(children,list),'custody_metadata_unverified');return children + + +def replay_denied(env): + result=captured(['bao','unwrap','-format=json'],env=env) + text=result.stderr or '' + return result.returncode==2 and not result.stdout.strip() and bool(re.search(r'Code:\s*400\b',text)) and bool(re.search(r'^\* wrapping token is not valid or does not exist\s*$',text,re.M)) + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self,*args,**kwargs):raise Refused('redirect_refused') + + +def api(token,method,path,payload=None,expected=(200,)): + require(path.startswith('/repos/'+REPO+'/') or path=='/repos/'+REPO,'provider_path_refused') + request=urllib.request.Request('https://forgejo.coulomb.social/api/v1'+path,method=method, + data=None if payload is None else json.dumps(payload).encode(), + headers={'Authorization':'token '+token,'Content-Type':'application/json'}) + try: + with urllib.request.build_opener(NoRedirect()).open(request,timeout=30) as response: + require(response.status in expected,'provider_status_refused') + raw=response.read(1024*1024+1);require(len(raw)<=1024*1024,'provider_body_bound') + return json.loads(raw) if raw else None + except Refused:raise + except Exception:raise Refused('provider_outcome_unverified') from None + + +def validate_contract(contract): + require(isinstance(contract,dict),'contract_type') + for key,value in FIXED.items(): + require(type(contract.get(key)) is type(value) and contract[key]==value,'contract_scope') + for key in ('owner_review_ref','ssh_egress_existing_proof_ref','runtime_identity_ref','rollback_ref'): + require(isinstance(contract.get(key),str) and contract[key].strip(),'missing_owner_proof') + require(isinstance(contract.get('ssh_known_hosts_sha256'),str) and len(contract['ssh_known_hosts_sha256'])==64,'host_pin_required') + require(datetime.now(timezone.utc) start: + raise ValueError('readiness changed during proving period') + if c['kind'] == 'Deployment' and (c['observed_generation'] != c['generation'] or not c['replicas'] or c['available_replicas'] != c['replicas']): + raise ValueError('deployment availability incomplete') + if c['kind'] == 'Cluster' and (not c['instances'] or c['ready_instances'] != c['instances']): + raise ValueError('CNPG readiness incomplete') + diff = observation['pinned_render_diff'] + if (diff.get('revision') != contract['revision'] or diff.get('path') != contract['source_path'] or type(diff.get('client_side_exit_code')) is not int + or diff['client_side_exit_code'] != 0 or diff.get('render_resource_count') != len(expected) + or diff.get('skipped_hooks') != sorted(contract['hooks']) or diff.get('output_suppressed') is not True + or any(not re.fullmatch(r'[a-f0-9]{64}', diff.get(k, '')) for k in ('render_sha256', 'normalized_render_sha256'))): + raise ValueError('pinned normalized client zero-diff unverified') + if observation.get('live_hooks') != [] or observation.get('public_health') != {'url': contract['health_url'], 'status': 200}: + raise ValueError('hook absence or public health unverified') + return {'schema_version': 'argocd-workload-adoption-proof/v1', 'status': 'verified', 'application': contract['application'], + 'revision': contract['revision'], 'captured_at': observation['captured_at'], 'verified_at': now.isoformat(), + 'proving_seconds': proof_age, 'resource_count': len(expected), 'ready_count': len(checks), + 'manual_apply_prune_off': True, 'hooks_skipped': sorted(contract['hooks']), 'hooks_absent': True, + 'child_automation_enabled': False, 'public_health_status': 200, 'native_mutation': False, + 'verifier_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'tracking_helper_sha256': hashlib.sha256(Path(__file__).with_name('argocd_adoption_proof.py').read_bytes()).hexdigest(), + 'contract_sha256': hashlib.sha256(json.dumps(contract, sort_keys=True).encode()).hexdigest(), + 'observation_sha256': hashlib.sha256(json.dumps(observation, sort_keys=True).encode()).hexdigest()} + + +def remote_program(common, rendered): + import base64 + return ('import sys,types,base64,json\n' + 'm=types.ModuleType("argocd_adoption_proof");sys.modules[m.__name__]=m\n' + + 'exec(compile(base64.b64decode(' + repr(base64.b64encode(common).decode()) + '),"argocd_adoption_proof.py","exec"),m.__dict__)\n' + + 'n={"__name__":"workload_proof"};exec(compile(base64.b64decode(' + repr(base64.b64encode(Path(__file__).read_bytes()).decode()) + + '),"workload_adoption_proof.py","exec"),n)\n' + + 'try:\n print(json.dumps(n["collect"](base64.b64decode(' + repr(base64.b64encode(rendered).decode()) + '),n["CONTRACT"]),sort_keys=True))\n' + + 'except Exception:\n raise SystemExit("adoption observation incomplete; protected output suppressed")\n').encode() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--live', action='store_true') + parser.add_argument('--source-root', type=Path) + parser.add_argument('--observation', type=Path, required=True) + parser.add_argument('--receipt', type=Path, required=True) + args = parser.parse_args() + try: + if args.live: + if not args.source_root: + raise ValueError('source root required') + import tempfile + with tempfile.TemporaryDirectory(prefix='workload-adoption-pin-') as temp: + archive = checked_run(['git', '-C', str(args.source_root), 'archive', CONTRACT['revision'], CONTRACT['source_path']]) + checked_run(['tar', '-x', '-C', temp], input=archive) + rendered = checked_run(['kubectl', 'kustomize', str(Path(temp) / CONTRACT['source_path'])]) + common = Path(__file__).with_name('argocd_adoption_proof.py').read_bytes() + observation = json.loads(checked_run(['ssh', 'railiance01', 'python3', '-'], input=remote_program(common, rendered), timeout=180)) + with urlopen(CONTRACT['health_url'], timeout=30) as response: + observation['public_health'] = {'url': response.geturl(), 'status': response.status} + observation['metadata_captured_at'] = observation['captured_at'] + observation['captured_at'] = datetime.now(timezone.utc).isoformat() + args.observation.write_text(json.dumps(observation, indent=2) + '\n') + else: + observation = json.loads(args.observation.read_text()) + receipt = verify(observation, CONTRACT) + except Exception: + receipt = {'schema_version': 'argocd-workload-adoption-proof/v1', 'status': 'unverified', + 'error': 'adoption proof incomplete; protected output suppressed', 'native_mutation': False} + args.receipt.write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps(receipt, sort_keys=True)) + return 0 if receipt['status'] == 'verified' else 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tests/probe_bao_cli_synthetic.py b/tests/probe_bao_cli_synthetic.py new file mode 100644 index 0000000..70cb4c4 --- /dev/null +++ b/tests/probe_bao_cli_synthetic.py @@ -0,0 +1,22 @@ +import hashlib,json,os,subprocess,threading +from http.server import BaseHTTPRequestHandler,HTTPServer +captured=[] +class Handler(BaseHTTPRequestHandler): + def log_message(self,*args):pass + def do_GET(self): + if '/metadata/' in self.path: + self.send_response(404);self.end_headers();self.wfile.write(b'{"errors":[]}');return + self.send_response(200);self.end_headers();self.wfile.write(b'{"data":{"path":"platform/","type":"kv","options":{"version":"2"}}}') + def do_POST(self):self.do_PUT() + def do_PUT(self): + body=self.rfile.read(int(self.headers.get('Content-Length','0')));captured.append((self.command,self.path,json.loads(body or b'{}'))) + if self.path=='/v1/sys/wrapping/unwrap':self.send_response(400);self.end_headers();self.wfile.write(b'{"errors":["wrapping token is not valid or does not exist"]}') + else:self.send_response(200);self.end_headers();self.wfile.write(b'{"data":{"version":1}}') + def do_LIST(self):self.send_response(404);self.end_headers();self.wfile.write(b'{"errors":[]}') +server=HTTPServer(('127.0.0.1',0),Handler);threading.Thread(target=server.serve_forever,daemon=True).start() +env={**os.environ,'BAO_ADDR':'http://127.0.0.1:'+str(server.server_port),'VAULT_ADDR':'http://127.0.0.1:'+str(server.server_port),'BAO_TOKEN':'SYNTHETIC_NON_SECRET_FIXTURE','VAULT_TOKEN':'SYNTHETIC_NON_SECRET_FIXTURE'} +put=subprocess.run(['bao','kv','put','-format=json','-cas=0','platform/operators/argocd/repositories/rapp-issue-core','SSH_PRIVATE_KEY=-','REPOSITORY=coulomb/rapp-issue-core'],input='SYNTHETIC_KEY_FIXTURE\n',text=True,env=env,capture_output=True) +listing=subprocess.run(['bao','list','-format=json','platform/metadata/operators/argocd/repositories/'],env=env,capture_output=True,text=True) +unwrap=subprocess.run(['bao','unwrap','-format=json'],env=env,capture_output=True,text=True) +server.shutdown() +print(json.dumps({'synthetic_only':True,'installed_cli_kv_put_exit':put.returncode,'stdin_field_exact':any(item[1]=='/v1/platform/data/operators/argocd/repositories/rapp-issue-core' and item[2].get('data',{}).get('SSH_PRIVATE_KEY')=='SYNTHETIC_KEY_FIXTURE\n' and item[2].get('options',{}).get('cas')==0 for item in captured),'list_404_exit':listing.returncode,'list_404_safe_error':(listing.stdout+listing.stderr).strip(),'unwrap_400_exit':unwrap.returncode,'unwrap_400_safe_error':(unwrap.stdout+unwrap.stderr).strip()},indent=2)) diff --git a/tests/test_activity_promotion_readiness.py b/tests/test_activity_promotion_readiness.py new file mode 100644 index 0000000..b48dbe1 --- /dev/null +++ b/tests/test_activity_promotion_readiness.py @@ -0,0 +1,127 @@ +import base64 +from datetime import datetime, timezone, timedelta +import importlib.util +from pathlib import Path +import unittest +from unittest import mock +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +SPEC=importlib.util.spec_from_file_location('readiness',Path(__file__).parents[1]/'scripts/activity_promotion_readiness.py') +p=importlib.util.module_from_spec(SPEC);SPEC.loader.exec_module(p) +NOW=datetime(2026,10,11,20,tzinfo=timezone.utc) + + +def observation(): + return {'captured_at':NOW.isoformat(),'estate_ip':'92.205.62.239','cluster_uid':'cluster-uid','application_uid':'app-uid', + 'source':{'repoURL':'https://forgejo.coulomb.social/coulomb/activity-core.git','path':'k8s/gitops','targetRevision':p.PIN}, + 'project':'activity-core','revision':p.PIN,'destination':{'server':'https://kubernetes.default.svc','namespace':'activity-core'}, + 'automated_present':False,'pending_operation':False, + 'sync_options':['CreateNamespace=false','ApplyOutOfSyncOnly=true','PruneLast=true','FailOnSharedResource=true'], + 'sync':'Synced','health':'Healthy','condition_types':[],'project_scope_matches':True, + 'operation':{'phase':'Succeeded','finishedAt':'2026-10-04T14:13:25Z'}, + 'resources':[dict(zip(('group','kind','namespace','name'),r),status='Synced') for r in sorted(p.RESOURCES)], + 'deployments':[{'name':name,'generation_observed':True,'replicas':1,'ready':1,'updated':1,'available':1} + for name in ('actcore-api','actcore-worker','actcore-event-router')]} + + +def payload(): + return {'schema_version':'activity-core-release-acceptance/v1','owner_task':p.OWNER_TASK,'issuer':'owner-acceptance-principal', + 'authority':p.AUTHORITY,'application':'activity-core','cluster_uid':'cluster-uid','adoption_revision':p.PIN, + 'targets':dict(p.TARGETS),'release_principal':'activity-core-release-broker','broker_revision':'a'*40, + 'checks':{c:True for c in p.CHECKS},'issued_at':NOW.isoformat(),'expires_at':(NOW+timedelta(minutes=4)).isoformat(), + 'continuous_health':{'revision':p.PIN,'continuous':True,'max_gap_seconds':90,'observed_at':NOW.isoformat(), + 'healthy_since':(NOW-timedelta(days=1)).isoformat()}} + + +class GateTest(unittest.TestCase): + def setUp(self): + self.private=Ed25519PrivateKey.generate() + self.public=self.private.public_key().public_bytes(Encoding.Raw,PublicFormat.Raw) + + def envelope(self,data=None): + data=data or payload() + return {'payload':data,'signature':base64.b64encode(self.private.sign(p.canonical(data))).decode()} + + def evaluate(self,envelope=None,obs=None,key=None): + return p.readiness(obs or observation(),envelope=envelope or self.envelope(),trusted_key=key or self.public, + trusted_principal='owner-acceptance-principal',now=NOW) + + def test_complete_signed_fixture_passes_without_activation(self): + result=self.evaluate() + self.assertEqual(result['status'],'ready') + self.assertFalse(result['native_mutation']) + self.assertFalse(result['automatic_sync_candidate']) + + def test_snapshot_and_elapsed_time_cannot_substitute_for_owner_proof(self): + result=p.readiness(observation(),now=NOW) + self.assertEqual(result['status'],'waiting') + self.assertEqual(result['depends_on'],['ACTIVITY-WP-0041-T03']) + self.assertFalse(result['needs_human']) + self.assertTrue(result['adoption_snapshot_is_not_continuous_observation']) + + def test_every_acceptance_check_required(self): + for check in p.CHECKS: + for missing in (False,None,'true'): + data=payload();data['checks'][check]=missing + with self.subTest(check=check,value=missing):self.assertEqual(self.evaluate(self.envelope(data))['status'],'unverified') + + def test_unknown_key_forged_tampered_or_self_introduced_trust_refused(self): + envelope=self.envelope();envelope['payload']['release_principal']='attacker' + self.assertEqual(self.evaluate(envelope)['status'],'unverified') + unknown=Ed25519PrivateKey.generate().public_key().public_bytes(Encoding.Raw,PublicFormat.Raw) + self.assertEqual(self.evaluate(key=unknown)['status'],'unverified') + envelope=self.envelope();envelope['public_key']=self.public.hex() + self.assertEqual(self.evaluate(envelope)['status'],'unverified') + envelope=self.envelope();envelope['signature']='invalid-base64!' + self.assertEqual(self.evaluate(envelope)['status'],'unverified') + + def test_wrong_scope_identity_cluster_commit_or_expired_owner_proof_refused(self): + for key,value in [('authority','admin'),('application','other'),('cluster_uid','other'), + ('adoption_revision','b'*40),('owner_task','other'),('issuer','producer'), + ('broker_revision','main'),('release_principal',''), + ('issued_at',(NOW-timedelta(minutes=6)).isoformat()), + ('issued_at',(NOW+timedelta(seconds=1)).isoformat()), + ('expires_at',NOW.isoformat())]: + data=payload();data[key]=value + with self.subTest(key=key):self.assertEqual(self.evaluate(self.envelope(data))['status'],'unverified') + for key,value in [('prune',True),('root_sync_selects_only','*'),('child_path','other')]: + data=payload();data['targets'][key]=value + with self.subTest(target=key):self.assertEqual(self.evaluate(self.envelope(data))['status'],'unverified') + + def test_continuous_sampling_requires_owner_signed_fresh_bounded_interval(self): + for key,value in [('continuous',False),('max_gap_seconds',91),('max_gap_seconds',True), + ('revision','b'*40),('healthy_since',(NOW-timedelta(hours=23)).isoformat()), + ('observed_at',(NOW-timedelta(seconds=91)).isoformat()), + ('observed_at',(NOW+timedelta(seconds=1)).isoformat())]: + data=payload();data['continuous_health'][key]=value + with self.subTest(key=key):self.assertEqual(self.evaluate(self.envelope(data))['status'],'unverified') + + def test_exact_live_adoption_scope_and_freshness_refusals(self): + for key,value in [('estate_ip','other'),('project_scope_matches',False),('project','other'), + ('automated_present',True),('pending_operation',True),('revision','b'*40), + ('health','Degraded'),('sync','OutOfSync'),('condition_types',['ComparisonError']), + ('captured_at',(NOW-timedelta(minutes=6)).isoformat()), + ('captured_at',(NOW+timedelta(seconds=1)).isoformat()), + ('resources',[]),('deployments',[])]: + data=observation();data[key]=value + with self.subTest(key=key):self.assertEqual(self.evaluate(obs=data)['status'],'unverified') + data=observation();data['resources'].append(data['resources'][0]) + self.assertEqual(self.evaluate(obs=data)['status'],'unverified') + for key,value in [('ready',0),('generation_observed',False),('updated',0),('replicas',0)]: + data=observation();data['deployments'][0][key]=value + with self.subTest(deployment=key):self.assertEqual(self.evaluate(obs=data)['status'],'unverified') + + def test_no_signature_verification_attempt_without_trusted_operator_config(self): + self.assertEqual(p.readiness(observation(),envelope=self.envelope(),now=NOW)['status'],'waiting') + + def test_protected_subprocess_errors_and_timeouts_never_leak_canary(self): + import subprocess + from types import SimpleNamespace + canary='SECRET-CANARY-NOT-OUTPUT' + with mock.patch.object(p.subprocess,'run',return_value=SimpleNamespace(returncode=1,stdout=canary,stderr=canary)): + with self.assertRaises(ValueError) as exc:p.run(['kubectl','get','nodes']) + self.assertNotIn(canary,str(exc.exception)) + with mock.patch.object(p.subprocess,'run',side_effect=subprocess.TimeoutExpired('cmd',1,output=canary,stderr=canary)): + with self.assertRaises(ValueError) as exc:p.run(['kubectl','get','nodes']) + self.assertNotIn(canary,str(exc.exception)) diff --git a/tests/test_issue_core_credential_executor.py b/tests/test_issue_core_credential_executor.py new file mode 100644 index 0000000..f939fe5 --- /dev/null +++ b/tests/test_issue_core_credential_executor.py @@ -0,0 +1,92 @@ +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch +spec=importlib.util.spec_from_file_location('issuer',Path(__file__).resolve().parents[1]/'scripts/provision_argocd_issue_core_repository.py') +issuer=importlib.util.module_from_spec(spec);spec.loader.exec_module(issuer) + +class IssuerTests(unittest.TestCase): + def contract(self):return {**issuer.FIXED,'owner_review_ref':'fixed-review','ssh_egress_existing_proof_ref':'fixed-egress','runtime_identity_ref':'attended-platform','rollback_ref':'fixed-rollback','ssh_known_hosts_sha256':'a'*64,'expires_at':'2099-01-01T00:00:00+00:00'} + def test_existing_d2_covers_exact_scope_but_broadening_refused(self): + issuer.validate_contract(self.contract()) + for key,value in [('repository','coulomb/*'),('read_only',False),('scope_approval_ref','self-approved'),('destination','argocd/broad-creds'),('new_network_scope',True)]: + data=self.contract();data[key]=value + with self.assertRaises(issuer.Refused):issuer.validate_contract(data) + def test_wrong_estate_prevents_credentials_and_grants(self): + calls=[] + def runner(args,*a,**kw): + calls.append(args);return json.dumps({'items':[{'status':{'addresses':[{'address':'192.0.2.1'}]}}]}) + with patch.dict(os.environ,{'API_TOKEN':'SYNTHETIC_TEST_ONLY'}),patch.object(issuer,'run',runner),patch.object(issuer,'api') as api: + with self.assertRaises(issuer.Refused):issuer.execute(self.contract(),Path('/tmp/unused-fixture-journal')) + api.assert_not_called() + self.assertEqual(len(calls),1) + def test_provider_cannot_redirect_or_use_other_repo(self): + with self.assertRaises(issuer.Refused):issuer.api('synthetic','GET','/repos/coulomb/other/keys') + with self.assertRaises(issuer.Refused):issuer.NoRedirect().redirect_request(None,None,None,None,None,None) + def test_secret_has_exact_scope_no_apply_annotation(self): + body=issuer.secret_payload('SYNTHETIC_PRIVATE_CANARY') + self.assertEqual(body['metadata']['namespace'],'argocd');self.assertEqual(body['metadata']['name'],issuer.SECRET) + self.assertNotIn('kubectl.kubernetes.io/last-applied-configuration',body['metadata']['annotations']) + self.assertNotIn('password',body['stringData']);self.assertEqual(body['stringData']['url'],issuer.URL) + def test_silent_main_does_not_echo_invalid_contract_or_raw_child_errors(self): + with tempfile.TemporaryDirectory() as tmp: + p=Path(tmp)/'contract.json';p.write_text('SECRET_CANARY') + result=subprocess.run(['python3',str(Path(__file__).resolve().parents[1]/'scripts/provision_argocd_issue_core_repository.py'),'--contract',str(p),'--contract-sha256','a'*64,'--journal',str(Path(tmp)/'journal.json')],capture_output=True,text=True) + self.assertEqual(result.returncode,1);self.assertEqual(result.stdout,'');self.assertEqual(result.stderr,'');self.assertEqual(json.loads((Path(tmp)/'journal.json').read_text())['failure'],'contract_changed') + def test_synthetic_end_to_end_uses_one_unwrap_and_exact_destination(self): + commands=[];public='ssh-ed25519 SYNTHETIC_PUBLIC fixture';private='SYNTHETIC_PRIVATE_CANARY';known='[forgejo.coulomb.social]:30022 ssh-ed25519 SYNTHETIC_HOST\n' + contract=self.contract();contract['ssh_known_hosts_sha256']=__import__('hashlib').sha256(known.encode()).hexdigest() + with tempfile.TemporaryDirectory() as tmp: + journal=Path(tmp)/'journal.json';keydir=Path(tmp)/'keys';keydir.mkdir() + class FakeTmp: + def __enter__(self):return str(keydir) + def __exit__(self,*args):return False + def runner(argv,body=None,env=None): + commands.append((argv,body,env));text=' '.join(argv) + if 'get nodes' in text:return json.dumps({'items':[{'status':{'addresses':[{'address':'92.205.62.239'}]}}]}) + if 'get configmap' in text:return json.dumps({'data':{'ssh_known_hosts':known}}) + if 'get appproject' in text:return json.dumps({'spec':{'sourceRepos':[issuer.URL]}}) + if argv[:3]==['bao','token','capabilities']:return 'create, read, update' + if argv[:3]==['bao','audit','list']:return json.dumps({'file/':{'type':'file'}}) + if 'get secret' in text:return '' if '--ignore-not-found' in text else 'secret/'+issuer.SECRET + if argv[0]=='ssh-keygen': + path=Path(argv[-1]);path.write_text(private);path.chmod(0o600);Path(str(path)+'.pub').write_text(public);return '' + if argv[:3]==['bao','kv','put']:return json.dumps({'request_id':'write-fixture','data':{'version':1}}) + if argv[:3]==['bao','kv','get']:return json.dumps({'wrap_info':{'ttl':60,'token':'SYNTHETIC_WRAP','accessor':'fixture-accessor','creation_path':'platform/data/operators/argocd/repositories/rapp-issue-core'}}) + if argv[:2]==['bao','unwrap']: + self.assertEqual(env['BAO_TOKEN'],'SYNTHETIC_WRAP');return json.dumps({'request_id':'unwrap-fixture','data':{'data':{'SSH_PRIVATE_KEY':private,'PROVIDER_KEY_ID':'7'}}}) + return 'secret/'+issuer.SECRET + def capture(argv,body=None,env=None): + commands.append((argv,body,env)) + if argv[:2]==['bao','list']:return subprocess.CompletedProcess(argv,2,'{}','') + return subprocess.CompletedProcess(argv,2,'','Code: 400. Errors:\n\n* wrapping token is not valid or does not exist\n') + def provider(token,method,path,payload=None,expected=(200,)): + if method=='POST': + self.assertEqual(payload,{'title':issuer.TITLE,'key':public,'read_only':True});return {'id':7,'key':public,'read_only':True} + return [] if '/keys?' in path else {'full_name':issuer.REPO,'private':True} + with patch.dict(os.environ,{'API_TOKEN':'SYNTHETIC_ADMIN'}),patch.object(issuer,'run',runner),patch.object(issuer,'captured',capture),patch.object(issuer,'api',provider),patch.object(issuer.tempfile,'TemporaryDirectory',lambda **kw:FakeTmp()):issuer.execute(contract,journal) + receipt=json.loads(journal.read_text());self.assertEqual(receipt['state'],'delivered_not_adopted');self.assertNotIn(private,journal.read_text());self.assertNotIn('SYNTHETIC_WRAP',journal.read_text()) + self.assertEqual(sum(argv[:2]==['bao','unwrap'] for argv,body,env in commands),2);self.assertTrue(receipt['consumed_wrap_replay_denied_400']);self.assertEqual(receipt['wrap_creation_path'],'platform/data/operators/argocd/repositories/rapp-issue-core') + for argv,body,env in commands: + self.assertNotIn(private,' '.join(argv));self.assertNotIn('SYNTHETIC_WRAP',' '.join(argv));self.assertNotIn(' apply ',' '.join(argv));self.assertNotIn(' patch ',' '.join(argv)) + + def test_empty_metadata_list_404_is_accepted_other_errors_refused(self): + with patch.object(issuer,'captured',return_value=subprocess.CompletedProcess([],2,'{}','')):self.assertEqual(issuer.metadata_children(),[]) + for value in [subprocess.CompletedProcess([],2,'{}','SECRET_CANARY'),subprocess.CompletedProcess([],1,'','Code: 403'),subprocess.CompletedProcess([],2,'','network unreachable')]: + with patch.object(issuer,'captured',return_value=value): + with self.assertRaisesRegex(issuer.Refused,'^custody_metadata_unverified$'):issuer.metadata_children() + def test_replay_denial_requires_specific_400_not_transport_or_forbidden(self): + good='Error unwrapping: Error making API request.\nCode: 400. Errors:\n\n* wrapping token is not valid or does not exist\n' + for code,text,expected in [(2,good,True),(2,good.replace('400','403'),False),(2,'dial tcp: connection refused SECRET_CANARY',False),(1,good,False),(2,'Code: 400\n* unrelated failure',False)]: + with patch.object(issuer,'captured',return_value=subprocess.CompletedProcess([],code,'',text)):self.assertEqual(issuer.replay_denied({}),expected) + + def test_raw_command_output_timeout_and_error_suppressed(self): + for result in [subprocess.CompletedProcess(['fixture'],1,'SECRET_CANARY','SECRET_CANARY'),subprocess.TimeoutExpired(['fixture'],60,output='SECRET_CANARY',stderr='SECRET_CANARY')]: + with patch.object(issuer.subprocess,'run',side_effect=result if isinstance(result,Exception) else None,return_value=result): + with self.assertRaisesRegex(issuer.Refused,'^command_unverified$'):issuer.run(['fixture']) + +if __name__=='__main__':unittest.main() diff --git a/tests/test_public_transport.py b/tests/test_public_transport.py new file mode 100644 index 0000000..d3755a3 --- /dev/null +++ b/tests/test_public_transport.py @@ -0,0 +1,88 @@ +import copy +import importlib.util +import json +from pathlib import Path +import subprocess +import unittest +from unittest.mock import patch +spec=importlib.util.spec_from_file_location('transport',Path(__file__).resolve().parents[1]/'scripts/argocd_public_transport.py') +t=importlib.util.module_from_spec(spec);spec.loader.exec_module(t) + +class TransportTests(unittest.TestCase): + def plan(self):return json.loads((Path(__file__).resolve().parents[1]/'docs/evidence/2026-10-11-issue-core-transport/transport-plan.json').read_text()) + def objects(self,plan): + result={} + for item in plan['resources']: + obj={'metadata':{'uid':item['uid'],'resourceVersion':'17','labels':{'unrelated':'retain'}},'data':{},'spec':{}} + t.replace(obj,item['path'],copy.deepcopy(item['before']));result[item['kind']]=obj + return result + def runner(self,objects,calls,modify_dry=False): + def run(args,body=None): + calls.append((args,body)) + if args[:2]==['get','nodes']:return json.dumps({'items':[{'status':{'addresses':[{'address':'92.205.62.239'}]}}]}) + kind=args[3] + if args[2]=='get':return json.dumps(objects[kind]) + obj=copy.deepcopy(objects[kind]) + for op in json.loads(body): + if op['op']=='test':self.assertEqual(t.value(obj,op['path']),op['value']) + else:t.replace(obj,op['path'],op['value']) + if '--dry-run=server' in args: + if modify_dry:obj['metadata']['labels']['unrelated']='changed' + return json.dumps(obj) + objects[kind]=obj;return kind+'/'+args[4] + return run + def receipt(self):return {'resources':[],'mutation_attempted':False} + def test_dry_run_changes_no_live_values(self): + plan=self.plan();objects=self.objects(plan);before=copy.deepcopy(objects);calls=[] + with patch.object(t,'run',self.runner(objects,calls)):r=t.perform(plan,'dry-run',self.receipt()) + self.assertEqual(objects,before);self.assertTrue(r['passed']);self.assertFalse(r['mutation_attempted']);self.assertEqual(sum(args[2]=='patch' for args,body in calls if len(args)>2),2) + def test_execute_and_guarded_rollback_preserve_other_fields(self): + plan=self.plan();objects=self.objects(plan);before=copy.deepcopy(objects);calls=[];checkpoints=[];receipt=self.receipt() + with patch.object(t,'run',self.runner(objects,calls)): + t.perform(plan,'execute',receipt,lambda:checkpoints.append(copy.deepcopy(receipt))) + for item in plan['resources']:self.assertEqual(t.value(objects[item['kind']],item['path']),item['after']) + self.assertTrue(any(x.get('pending_target') for x in checkpoints)) + t.perform(plan,'verify',self.receipt());t.perform(plan,'rollback',self.receipt()) + self.assertEqual(objects,before) + for args,body in calls: + if body: + ops=json.loads(body);self.assertEqual([op['op'] for op in ops],['test','test','test','replace']);self.assertEqual(ops[0]['path'],'/metadata/uid');self.assertEqual(ops[1]['path'],'/metadata/resourceVersion') + def test_other_target_preimage_drift_stops_before_first_patch(self): + plan=self.plan();objects=self.objects(plan);objects['appproject']['spec']['sourceRepos'].append('ssh://unreviewed/repo');calls=[] + with patch.object(t,'run',self.runner(objects,calls)): + with self.assertRaisesRegex(t.Refused,'^target_preimage_changed$'):t.perform(plan,'execute',self.receipt()) + self.assertFalse(any(args[2]=='patch' for args,body in calls if len(args)>2)) + def test_partial_second_write_is_recorded_and_rollback_can_restore_first(self): + plan=self.plan();objects=self.objects(plan);before=copy.deepcopy(objects);calls=[];runner=self.runner(objects,calls);receipt=self.receipt();snapshots=[] + def failing(args,body=None): + if len(args)>3 and args[2]=='patch' and args[3]=='appproject' and '--dry-run=server' not in args:raise t.Refused('command_outcome_unverified') + return runner(args,body) + with patch.object(t,'run',failing): + with self.assertRaises(t.Refused):t.perform(plan,'execute',receipt,lambda:snapshots.append(copy.deepcopy(receipt))) + self.assertEqual(receipt['pending_target'],'appproject/railiance-tenants');self.assertTrue(receipt['resources'][0]['changed_verified']);self.assertTrue(snapshots[-1]['mutation_attempted']) + with patch.object(t,'run',runner):t.perform(plan,'rollback',self.receipt()) + self.assertEqual(objects,before) + + def test_target_uid_change_refused(self): + plan=self.plan();objects=self.objects(plan);objects['configmap']['metadata']['uid']='foreign' + with patch.object(t,'run',self.runner(objects,[])): + with self.assertRaisesRegex(t.Refused,'^target_uid_changed$'):t.perform(plan,'execute',self.receipt()) + def test_server_admission_extra_change_refused(self): + plan=self.plan();objects=self.objects(plan);before=copy.deepcopy(objects) + with patch.object(t,'run',self.runner(objects,[],True)): + with self.assertRaisesRegex(t.Refused,'^dry_run_modified_other_fields$'):t.perform(plan,'execute',self.receipt()) + self.assertEqual(objects,before) + def test_broader_repo_or_host_replacement_refused(self): + for kind in ('host','repo'): + plan=self.plan() + if kind=='host':plan['resources'][0]['after']='unreviewed replacement\n' + else:plan['resources'][1]['after'].append('ssh://forgejo.coulomb.social/coulomb/*') + with self.assertRaises(t.Refused):t.validate(plan) + def test_wrong_estate_and_raw_canary_refused(self): + with patch.object(t,'run',return_value=json.dumps({'items':[{'status':{'addresses':[{'address':'192.0.2.8'}]}}]})) as run: + with self.assertRaisesRegex(t.Refused,'^wrong_estate$'):t.perform(self.plan(),'execute',self.receipt()) + self.assertEqual(run.call_count,1) + with patch.object(t.subprocess,'run',return_value=subprocess.CompletedProcess([],1,'SECRET_CANARY','SECRET_CANARY')): + with self.assertRaisesRegex(t.Refused,'^command_outcome_unverified$'):t.run(['get','nodes']) + +if __name__=='__main__':unittest.main() diff --git a/tests/test_workload_adoption_proof.py b/tests/test_workload_adoption_proof.py new file mode 100644 index 0000000..904e895 --- /dev/null +++ b/tests/test_workload_adoption_proof.py @@ -0,0 +1,114 @@ +import copy +from datetime import datetime, timedelta, timezone +import importlib.util +import json +from pathlib import Path +import sys +import pytest + +SCRIPTS = Path(__file__).resolve().parents[1] / 'scripts' +if not SCRIPTS.exists(): SCRIPTS = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPTS)) +import workload_adoption_proof as proof + +NOW = datetime(2026, 10, 11, 20, 0, tzinfo=timezone.utc) + + +def observation(): + c = proof.CONTRACT + old = (NOW - timedelta(days=2)).isoformat() + settings = {'method': 'annotation', 'controller_version': 'v3.5.3', 'installation_id_absent': True} + return {'captured_at': NOW.isoformat(), 'metadata_captured_at': NOW.isoformat(), 'estate_ip': c['estate_ip'], 'tracking_settings': settings, + 'application': {'name': c['application'], 'uid': 'application-uid', 'project': c['project'], 'destination': c['destination'], + 'source': {'repoURL': c['repo_url'], 'path': c['source_path'], 'targetRevision': c['revision']}, 'revision': c['revision'], + 'automated_present': False, 'pending_operation': False, 'sync': 'Synced', 'health': 'Healthy', 'condition_types': [], + 'operation': {'phase': 'Succeeded', 'startedAt': old, 'finishedAt': old}, 'sync_prune': False, + 'sync_strategy_apply': True, 'sync_strategy_force': False, 'operation_revision': c['revision'], 'resources': [{**r, 'status': 'Synced'} for r in c['resources']]}, + 'ready_checks': [{**check, 'uid': 'resource-uid', 'tracking_matches': True, + 'ready': [{'status': 'True', 'lastTransitionTime': old}], 'generation': 2, 'observed_generation': 2, + 'replicas': 1, 'available_replicas': 1, 'instances': 1, 'ready_instances': 1} for check in c['ready_checks']], + 'live_hooks': [], 'public_health': {'url': c['health_url'], 'status': 200}, + 'pinned_render_diff': {'revision': c['revision'], 'path': c['source_path'], 'client_side_exit_code': 0, + 'render_resource_count': len(c['resources']), 'skipped_hooks': sorted(c['hooks']), 'output_suppressed': True, + 'render_sha256': 'a' * 64, 'normalized_render_sha256': 'b' * 64}} + + +def test_verified_receipt_is_manual_adoption_only(): + receipt = proof.verify(observation(), proof.CONTRACT, now=NOW) + assert receipt['status'] == 'verified' + assert receipt['resource_count'] == 5 + assert receipt['ready_count'] == 3 + assert receipt['proving_seconds'] == 172800 + assert receipt['native_mutation'] is False + assert receipt['child_automation_enabled'] is False + + +@pytest.mark.parametrize('field,value', [('automated_present', True), ('pending_operation', True), ('sync_prune', True), + ('sync_strategy_apply', False), ('sync_strategy_force', True), ('revision', 'f'*40), ('health', 'Progressing'), ('condition_types', ['ComparisonError'])]) +def test_wrong_runtime_gate_fails(field, value): + obj = observation(); obj['application'][field] = value + with pytest.raises(ValueError): proof.verify(obj, proof.CONTRACT, now=NOW) + + +@pytest.mark.parametrize('mutation', ['missing-resource', 'extra-hook-resource', 'resource-not-synced', 'hook-live', + 'short-proving', 'stale-observation', 'future-observation', 'metadata-stale', 'client-diff', 'missing-check', 'ready-false', + 'new-ready-transition', 'deployment-generation', 'deployment-replicas', 'cnpg-instances', 'tracking', 'health-url', 'health-status']) +def test_missing_or_wrong_proof_fails(mutation): + obj = observation() + if mutation == 'missing-resource': obj['application']['resources'].pop() + elif mutation == 'extra-hook-resource': obj['application']['resources'].append({'group': 'batch', 'kind': 'Job', 'namespace': 'target-revenue', 'name': proof.CONTRACT['hooks'][0], 'status': 'Synced'}) + elif mutation == 'resource-not-synced': obj['application']['resources'][0]['status'] = 'OutOfSync' + elif mutation == 'hook-live': obj['live_hooks'] = [proof.CONTRACT['hooks'][0]] + elif mutation == 'short-proving': obj['application']['operation']['finishedAt'] = (NOW-timedelta(hours=23)).isoformat() + elif mutation == 'stale-observation': obj['captured_at'] = (NOW-timedelta(minutes=6)).isoformat() + elif mutation == 'future-observation': obj['captured_at'] = (NOW+timedelta(seconds=1)).isoformat() + elif mutation == 'metadata-stale': obj['metadata_captured_at'] = (NOW-timedelta(minutes=6)).isoformat() + elif mutation == 'client-diff': obj['pinned_render_diff']['client_side_exit_code'] = 1 + elif mutation == 'missing-check': obj['ready_checks'].pop() + elif mutation == 'ready-false': obj['ready_checks'][1]['ready'][0]['status'] = 'False' + elif mutation == 'new-ready-transition': obj['ready_checks'][1]['ready'][0]['lastTransitionTime'] = NOW.isoformat() + elif mutation == 'deployment-generation': obj['ready_checks'][0]['observed_generation'] = 1 + elif mutation == 'deployment-replicas': obj['ready_checks'][0]['available_replicas'] = 0 + elif mutation == 'cnpg-instances': obj['ready_checks'][2]['ready_instances'] = 0 + elif mutation == 'tracking': obj['ready_checks'][0]['tracking_matches'] = False + elif mutation == 'health-url': obj['public_health']['url'] = 'https://unexpected.test/healthz' + elif mutation == 'health-status': obj['public_health']['status'] = 503 + with pytest.raises(ValueError): proof.verify(obj, proof.CONTRACT, now=NOW) + + +def sources(): + docs = [] + for r in proof.CONTRACT['resources']: + docs.append({'apiVersion': r['group']+'/v1' if r['group'] else 'v1', 'kind': r['kind'], + 'metadata': {'name': r['name'], 'namespace': r['namespace']}}) + for name in proof.CONTRACT['hooks']: + docs.append({'apiVersion': 'batch/v1', 'kind': 'Job', 'metadata': {'name': name, 'namespace': 'target-revenue', 'annotations': {'argocd.argoproj.io/hook': 'Sync'}}}) + return docs + + +def test_normalization_skips_hooks_and_only_injects_tracking_metadata(): + docs = sources(); original = copy.deepcopy(docs) + settings = observation()['tracking_settings'] + normalized = proof.normalize(docs, proof.CONTRACT, settings) + assert len(normalized) == 5 + assert docs == original + assert not any(r['kind'] == 'Job' for r in normalized) + assert normalized[0]['metadata']['annotations']['argocd.argoproj.io/tracking-id'] == 'target-revenue:/Service:target-revenue/target-revenue' + + +@pytest.mark.parametrize('bad', ['secret', 'extra-resource', 'wrong-hook', 'missing-hook', 'duplicate-resource', 'conflicting-tracking']) +def test_normalization_fails_closed(bad): + docs = sources() + if bad == 'secret': docs.append({'apiVersion': 'v1', 'kind': 'Secret', 'metadata': {'name': 'secret', 'namespace': 'target-revenue'}}) + elif bad == 'extra-resource': docs.append({'apiVersion': 'v1', 'kind': 'Service', 'metadata': {'name': 'extra', 'namespace': 'target-revenue'}}) + elif bad == 'wrong-hook': docs[-1]['metadata']['annotations']['argocd.argoproj.io/hook'] = 'PreSync' + elif bad == 'missing-hook': docs.pop() + elif bad == 'duplicate-resource': docs.append(copy.deepcopy(docs[0])) + elif bad == 'conflicting-tracking': docs[0]['metadata']['annotations'] = {'argocd.argoproj.io/tracking-id': 'other'} + with pytest.raises(ValueError): proof.normalize(docs, proof.CONTRACT, observation()['tracking_settings']) + + +def test_kubectl_stream_and_list_decoding(): + docs = sources() + assert proof.decode_objects(('\n'.join(json.dumps(r) for r in docs)).encode()) == docs + assert proof.decode_objects(json.dumps({'kind': 'List', 'items': docs}).encode()) == docs diff --git a/workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md b/workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md index 7a92ac9..82b99fb 100644 --- a/workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md +++ b/workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md @@ -268,36 +268,25 @@ proving period without changing sync/automation policy or using a new human gate ```task id: RPF-WP-0044-T04 -status: todo +status: wait +needs_human: true +intervention_note: "Orange attended Platform OpenBao OIDC/MFA login for the already D2-approved exact repository reader; no second source-scope approval." decision_id: 89f1058f-33bc-47a8-8a49-0b44d426c67e priority: high state_hub_task_id: "0e619658-0453-51ed-b9a0-bed28b5544fd" ``` -2026-10-11: requalified after founder decision D2 (ArgoCD owns production) resolved — see the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. +D2 already authorizes this exact private repository read-only reader and the ordered adoption. No renewed founder go-ahead is required. The remaining human act is the existing Platform OpenBao attended OIDC/MFA login; route it through the single attended-window checklist. -**Waits on the founder's go-ahead**, and on two preconditions: +Source preparation now disables wrapper direct deployment/rollback and verifies Argo ownership using the exact HTTPS or SSH source URL. The fixed credential executor is `scripts/provision_argocd_issue_core_repository.py`, reviewed in `docs/evidence/2026-10-11-issue-core-credential/owner-review.json`. It creates one repository-only read-only SSH deploy key, stores it at `platform/operators/argocd/repositories/rapp-issue-core`, response-wraps for 60 seconds, unwraps once, proves consumed-token denial, and creates only `argocd/rapp-issue-core-repository`. Tests and installed Bao synthetic checks pass; native issuance/adoption have not run. The contract example is incomplete and cannot authorize execution. -- `rapp-issue-core` agrees (hub message) that ArgoCD owns production and its - `make deploy` stops being a production path; it would fight self-heal. - Image releases become commits to `manifests/30-runtime.yaml`. -- A CCR for the ArgoCD repository credential, source at - `platform/operators/argocd/repositories/rapp-issue-core`, read-only deploy - token. Applied with `make argocd-repo-apply`; no value is handled in this - plan. - -Zero diff against live for both the raw `manifests/` and the rendered output -(digest `sha256:a56c80cc…b92cf`). All kinds are in the `railiance-tenants` -whitelist. Live check: `rapp-issue-core make verify-live`. Workload restore: -`rapp-issue-core make rollback`. - -2026-10-03: human gate routed to hub decision 89f1058f-33bc-47a8-8a49-0b44d426c67e (D2: ArgoCD owns production: go-ahead sequence for railiance-apps-root, issue-core, policy-nexus and activity-core automation) by CUST-WP-0075; resolving that decision raises C-40 here. +Public host trust and exact project admission are prepared in owner source and the bounded `scripts/argocd_public_transport.py`; its server dry-run passed. Preserve each exact attempt and any partial failure. Finish those prerequisites, seal the owner-reviewed contract, use the attended login, then pin the newly committed wrapper source in the Application, inspect client-side native diff, manually sync with prune/force disabled, and run the bounded owner verifier. Image releases and rollback use Git revisions plus Argo; direct `make rollback` is disabled. Never adopt the stale wrapper revision that predates the ExternalSecret metadata guard. ## Adopt target-revenue ```task id: RPF-WP-0044-T05 -status: todo +status: done decision_id: 89f1058f-33bc-47a8-8a49-0b44d426c67e priority: medium state_hub_task_id: "d418068a-6fb0-5416-aac5-d23c93924d9c" @@ -321,6 +310,24 @@ target-revenue commit. 2026-10-03: human gate routed to hub decision 89f1058f-33bc-47a8-8a49-0b44d426c67e (D2: ArgoCD owns production: go-ahead sequence for railiance-apps-root, issue-core, policy-nexus and activity-core automation) by CUST-WP-0075; resolving that decision raises C-40 here. + +2026-10-11: **manual adoption proving complete**. Fresh read-only proof at 2026-10-11T19:51:12.812423+00:00 +verified exact pin `a3a8a27a8cda32ae3c7b55353ee16a131c50a0a0`, manual apply strategy, +prune/force off and no pending operation. The last successful pinned apply finished +2026-09-28T14:23:27Z; 1142865 seconds of proving exceeds 24 hours. +All five non-hook objects are tracked and Synced; Deployment Available (generation observed), +ExternalSecret Ready and CNPG Ready (1/1). ArgoCD remains Synced/Healthy; normalized native +client-side diff is zero and `https://revenue.coulomb.social/healthz` returns 200. Both declared +Sync-hook Jobs are absent and excluded from the apply-strategy diff. No live mutation or +Secret read was performed. Receipt: `docs/evidence/2026-10-11-target-revenue-adoption-proof/receipt.json`; +sanitized observation alongside it. Reusable check: `scripts/workload_adoption_proof.py`; +35 new failure/normalization tests plus 13 existing OpenBao adoption tests pass. + +This closes T05 adoption/proving only. Child automated sync stays off. The existing +target-revenue owner handoff for hook re-execution safety remains a prerequisite to future +automation; it is not silently resolved and no new residual is invented here. The observed +OrphanedResourceWarning is non-blocking; other Application condition types fail verification. + ## Adopt external-secrets (highest risk) ```task diff --git a/workplans/RPF-WP-0048-activity-core-gitops-adoption.md b/workplans/RPF-WP-0048-activity-core-gitops-adoption.md index dfcedea..c7ffd93 100644 --- a/workplans/RPF-WP-0048-activity-core-gitops-adoption.md +++ b/workplans/RPF-WP-0048-activity-core-gitops-adoption.md @@ -8,7 +8,7 @@ status: blocked owner: codex topic_slug: railiance created: "2026-09-27" -updated: "2026-10-03" +updated: "2026-10-11" state_hub_workstream_id: "b8cf2fc2-60c2-5d4e-a60a-55f1304d9f54" --- @@ -40,9 +40,9 @@ health, resource inventory, schedules and original deployment generations. ```task id: RPF-WP-0048-T02 -status: todo +status: wait decision_id: 89f1058f-33bc-47a8-8a49-0b44d426c67e -depends_on: [ACTIVITY-WP-0041] +depends_on: [ACTIVITY-WP-0041-T03] priority: high state_hub_task_id: "63b44949-39f8-52fd-ab63-e618b67ef992" ``` @@ -58,6 +58,36 @@ activating bounded routine promotion. Destructive pruning remains disabled. 2026-10-03: human gate routed to hub decision 89f1058f-33bc-47a8-8a49-0b44d426c67e (D2: ArgoCD owns production: go-ahead sequence for railiance-apps-root, issue-core, policy-nexus and activity-core automation) by CUST-WP-0075; resolving that decision raises C-40 here. +2026-10-11: qualified once as an **owner-proof wait**, not a founder gate. +`scripts/activity_promotion_readiness.py` now checks the exact live source pin +`a86a143`, local estate/cluster identity, narrow AppProject, nine tracked runtime +objects, manual/no-prune policy, successful Oct4 sync, elapsed 24h eligibility and +all three Deployment generations/readiness. Native read-only checks pass. Elapsed +time and healthy snapshots are explicitly not gap-sensitive continuous-observation +proof. The gate requires authenticated owner acceptance for the dedicated release +principal, custody/revocation/denials, independently trusted receipts, real +continuous sampling, source CAS/single-flight/restart, retention, Temporal/audit +sink/invariants and successful failed-health rollback before returning ready. +Nine fixture tests (including forged trust and substantive scope failures) pass. +No release identity or trusted signer is created by this gate. + +Owner evidence is still absent: `activity-core/docs/release-broker.md` explicitly +records its modules as not activated in production, and +`activity-core/docs/gitops-release.md` retains the actual release-identity and +rollback requirements. ACTIVITY-WP-0042's accepted scheduling boundary remains +scheduling-only; `docs/platform-recovery-scheduling-plan.md` does not admit a +release principal or new mutating recovery operation. D2 resolves the founder +question but does not supply those machine proofs. Therefore wait on the existing +ACTIVITY-WP-0041-T03 once; no new `needs_human` flag or per-item permission request. + +Cross-owner case and checkpoint: +`docs/evidence/2026-10-11-activity-promotion-readiness/coordination-case.json`. +Read-only receipt: the same directory's `readiness.json` and `validation.json`. +No worker was injected or foreign task changed. Next action: owner supplies +authenticated native acceptance and trusted issuer binding, then rerun the gate +before bounded dispatcher activation. No child source/Argo automation candidate +was changed, and this wait does not authorize direct automated sync. + ## Make registry retention aware of digest-pinned releases ```task