From 725d126c777d48ef60fd82781be020a582484ec5 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 11 Oct 2026 21:30:47 +0200 Subject: [PATCH] Prove pinned OpenBao ArgoCD adoption with deterministic native checks Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade --- .../README.md | 34 ++ .../argo-normalization-diagnosis.json | 14 + .../drift-metadata.json | 9 + .../dry-run-path-diagnosis.json | 12 + .../observation-argo-normalized.json | 106 ++++++ .../observation-both-diffs.json | 83 ++++ .../observation-classified.json | 82 ++++ .../observation-normalized.json | 79 ++++ .../observation.json | 75 ++++ .../proof-argo-normalized-final.json | 19 + .../proof-argo-normalized.json | 18 + .../proof-both-diffs.json | 6 + .../proof-classified.json | 6 + .../proof-normalized.json | 6 + .../proof.json | 6 + .../validation.json | 44 +++ scripts/argocd_adoption_proof.py | 355 ++++++++++++++++++ tests/test_argocd_adoption_proof.py | 143 +++++++ ...ocd-phase-b-adopt-existing-applications.md | 35 +- 19 files changed, 1130 insertions(+), 2 deletions(-) create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/README.md create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/argo-normalization-diagnosis.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/drift-metadata.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/dry-run-path-diagnosis.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/observation-argo-normalized.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/observation-both-diffs.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/observation-classified.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/observation-normalized.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/observation.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized-final.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/proof-both-diffs.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/proof-classified.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/proof-normalized.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/proof.json create mode 100644 docs/evidence/2026-10-11-openbao-adoption-proof/validation.json create mode 100644 scripts/argocd_adoption_proof.py create mode 100644 tests/test_argocd_adoption_proof.py diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/README.md b/docs/evidence/2026-10-11-openbao-adoption-proof/README.md new file mode 100644 index 0000000..31b9b72 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/README.md @@ -0,0 +1,34 @@ +# Verified OpenBao Argo adoption proof + +Run from the workstation: + +```sh +python3 scripts/argocd_adoption_proof.py --live --source-root . --observation observation.json --receipt proof.json +python3 -m unittest discover -s tests -p 'test_argocd_adoption_proof.py' +``` + +The helper reads nodes, the exact Application, ClusterSecretStore, ExternalSecret, +Argo configuration and controller version. It never reads a Secret. Raw command +output and errors are captured and discarded. Git archive renders the exact +pin in a disposable directory; only a public ClusterSecretStore is admitted. +Server API dry-runs and native diffs make no persistent native change. + +The final proof is verified: both native diffs return zero and both same-version +API dry-runs have no changed field paths after exact Argo tracking injection. +The observed 3.5.3 controller and configuration select annotation mode. Tracking +uses the Application destination namespace even for a cluster-scoped object; +no instance label is injected. This follows the upstream [v3.5.3 resource tracking +implementation](https://github.com/argoproj/argo-cd/blob/v3.5.3/util/argo/resource_tracking.go) +and the [3.0 default change](https://argo-cd.readthedocs.io/en/release-3.5/operator-manual/upgrading/2.14-3.0/). +Only the tracking annotation is injected; live fields are never copied into the +desired source. Raw source diffs remain diagnostic. Structured JSON pointers +identify only tracking/last-applied differences. Unknown or substantive fields +refuse proof; neither parser ambiguity nor a manual ignore can grant success. + +Check `status: verified`, never receipt presence. Five-minute stale and future +observations are refused. The successful observation is a point-in-time proof, +not authority or evidence of subsequent automation. Earlier failed receipts are +retained. Thirteen fixture tests include exact identity/pin/destination mismatch, +tracking/render semantics, proving age, readiness changes, extra tracked objects, +unknown/substantive/native diff failures and protected-error-output canaries. +Fixtures are synthetic; native acceptance is the separate final proof. diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/argo-normalization-diagnosis.json b/docs/evidence/2026-10-11-openbao-adoption-proof/argo-normalization-diagnosis.json new file mode 100644 index 0000000..5843546 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/argo-normalization-diagnosis.json @@ -0,0 +1,14 @@ +{ + "status": "verified-diagnostic", + "normalized_client_dry_run_changed_paths": [], + "same_version_read": true, + "diffs": { + "client": 0, + "server": 0, + "argo_manager_server": 0, + "argo_manager_client": 0 + }, + "manager_kubectl_client_side_apply": false, + "manager_argocd_controller": false, + "manager_argocd_application_controller": false +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/drift-metadata.json b/docs/evidence/2026-10-11-openbao-adoption-proof/drift-metadata.json new file mode 100644 index 0000000..954c241 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/drift-metadata.json @@ -0,0 +1,9 @@ +{ + "spec_equals_pin": true, + "desired_labels_present": true, + "tracking_empty_namespace": false, + "tracking_default_namespace": false, + "tracking_argocd_namespace": false, + "tracking_identity_prefix_matches": true, + "tracking_name_matches": true +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/dry-run-path-diagnosis.json b/docs/evidence/2026-10-11-openbao-adoption-proof/dry-run-path-diagnosis.json new file mode 100644 index 0000000..ef5738a --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/dry-run-path-diagnosis.json @@ -0,0 +1,12 @@ +{ + "status": "verified-diagnostic", + "client_dry_run_changed_paths": [ + "/apiVersion", + "/metadata/annotations/argocd.argoproj.io~1tracking-id", + "/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration" + ], + "server_dry_run_changed_paths": [ + "/apiVersion", + "/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration" + ] +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/observation-argo-normalized.json b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-argo-normalized.json new file mode 100644 index 0000000..4615bb9 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-argo-normalized.json @@ -0,0 +1,106 @@ +{ + "application": { + "automated_present": false, + "condition_types": [ + "OrphanedResourceWarning" + ], + "destination": { + "namespace": "external-secrets", + "server": "https://kubernetes.default.svc" + }, + "health": "Healthy", + "name": "openbao-secretstore", + "operation": { + "finishedAt": "2026-09-21T17:09:18Z", + "phase": "Succeeded", + "startedAt": "2026-09-21T17:09:16Z" + }, + "operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "pending_operation": false, + "project": "railiance-platform-addons", + "resources": [ + { + "group": "external-secrets.io", + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": null, + "status": "Synced" + } + ], + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "source": { + "path": "argocd/platform-addons/openbao-secretstore", + "repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git", + "targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a" + }, + "sync": "Synced", + "sync_prune": false, + "sync_strategy_apply": true, + "uid": "f2932505-e649-4b99-b505-4789ed21301f" + }, + "captured_at": "2026-10-11T19:22:22.892821+00:00", + "estate_ip": "92.205.62.239", + "ready_checks": [ + { + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": "", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:49:59Z", + "status": "True" + } + ], + "tracking_matches": true, + "uid": "99563b77-f30f-451b-ac86-16b1b162c6a9" + }, + { + "kind": "ExternalSecret", + "name": "issue-core-runtime", + "namespace": "issue-core", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:54:20Z", + "status": "True" + } + ], + "tracking_matches": null, + "uid": "490dca37-6364-480a-a757-c3a266d32e47" + } + ], + "tracking_settings": { + "controller_version": "v3.5.3", + "installation_id_absent": true, + "instance_label_key": "app.kubernetes.io/instance", + "method": "annotation" + }, + "pinned_render_diff": { + "client_side_exit_code": 0, + "exit_code": 0, + "normalization": "argocd-v3-annotation-tracking-destination-namespace", + "normalized_client_changed_paths": [], + "normalized_render_sha256": "94e014dd61b81e09c8b137acae1b797f8976b8607fcaa46f54674d4d407b9843", + "normalized_server_changed_paths": [], + "output_suppressed": true, + "path": "argocd/platform-addons/openbao-secretstore", + "raw_client_changed_paths": [ + "/metadata/annotations/argocd.argoproj.io~1tracking-id", + "/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration" + ], + "raw_client_exit_code": 1, + "raw_server_changed_paths": [ + "/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration" + ], + "raw_server_exit_code": 1, + "render_resource_count": 1, + "render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3", + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "same_api_version": true, + "tracking_settings": { + "controller_version": "v3.5.3", + "installation_id_absent": true, + "instance_label_key": "app.kubernetes.io/instance", + "method": "annotation" + } + } +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/observation-both-diffs.json b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-both-diffs.json new file mode 100644 index 0000000..5b9e44a --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-both-diffs.json @@ -0,0 +1,83 @@ +{ + "application": { + "automated_present": false, + "condition_types": [ + "OrphanedResourceWarning" + ], + "destination": { + "namespace": "external-secrets", + "server": "https://kubernetes.default.svc" + }, + "health": "Healthy", + "name": "openbao-secretstore", + "operation": { + "finishedAt": "2026-09-21T17:09:18Z", + "phase": "Succeeded", + "startedAt": "2026-09-21T17:09:16Z" + }, + "operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "pending_operation": false, + "project": "railiance-platform-addons", + "resources": [ + { + "group": "external-secrets.io", + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": null, + "status": "Synced" + } + ], + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "source": { + "path": "argocd/platform-addons/openbao-secretstore", + "repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git", + "targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a" + }, + "sync": "Synced", + "sync_prune": false, + "sync_strategy_apply": true, + "uid": "f2932505-e649-4b99-b505-4789ed21301f" + }, + "captured_at": "2026-10-11T19:16:28.318163+00:00", + "estate_ip": "92.205.62.239", + "ready_checks": [ + { + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": "", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:49:59Z", + "status": "True" + } + ], + "tracking_matches": true, + "uid": "99563b77-f30f-451b-ac86-16b1b162c6a9" + }, + { + "kind": "ExternalSecret", + "name": "issue-core-runtime", + "namespace": "issue-core", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:54:20Z", + "status": "True" + } + ], + "tracking_matches": null, + "uid": "490dca37-6364-480a-a757-c3a266d32e47" + } + ], + "pinned_render_diff": { + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "path": "argocd/platform-addons/openbao-secretstore", + "render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3", + "render_resource_count": 1, + "exit_code": 1, + "output_suppressed": true, + "changed_top_fields": [ + "unknown" + ], + "client_side_exit_code": 1 + } +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/observation-classified.json b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-classified.json new file mode 100644 index 0000000..374826f --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-classified.json @@ -0,0 +1,82 @@ +{ + "application": { + "automated_present": false, + "condition_types": [ + "OrphanedResourceWarning" + ], + "destination": { + "namespace": "external-secrets", + "server": "https://kubernetes.default.svc" + }, + "health": "Healthy", + "name": "openbao-secretstore", + "operation": { + "finishedAt": "2026-09-21T17:09:18Z", + "phase": "Succeeded", + "startedAt": "2026-09-21T17:09:16Z" + }, + "operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "pending_operation": false, + "project": "railiance-platform-addons", + "resources": [ + { + "group": "external-secrets.io", + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": null, + "status": "Synced" + } + ], + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "source": { + "path": "argocd/platform-addons/openbao-secretstore", + "repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git", + "targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a" + }, + "sync": "Synced", + "sync_prune": false, + "sync_strategy_apply": true, + "uid": "f2932505-e649-4b99-b505-4789ed21301f" + }, + "captured_at": "2026-10-11T19:15:47.973008+00:00", + "estate_ip": "92.205.62.239", + "ready_checks": [ + { + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": "", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:49:59Z", + "status": "True" + } + ], + "tracking_matches": true, + "uid": "99563b77-f30f-451b-ac86-16b1b162c6a9" + }, + { + "kind": "ExternalSecret", + "name": "issue-core-runtime", + "namespace": "issue-core", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:54:20Z", + "status": "True" + } + ], + "tracking_matches": null, + "uid": "490dca37-6364-480a-a757-c3a266d32e47" + } + ], + "pinned_render_diff": { + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "path": "argocd/platform-addons/openbao-secretstore", + "render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3", + "render_resource_count": 1, + "exit_code": 1, + "output_suppressed": true, + "changed_top_fields": [ + "unknown" + ] + } +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/observation-normalized.json b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-normalized.json new file mode 100644 index 0000000..2f11706 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/observation-normalized.json @@ -0,0 +1,79 @@ +{ + "application": { + "automated_present": false, + "condition_types": [ + "OrphanedResourceWarning" + ], + "destination": { + "namespace": "external-secrets", + "server": "https://kubernetes.default.svc" + }, + "health": "Healthy", + "name": "openbao-secretstore", + "operation": { + "finishedAt": "2026-09-21T17:09:18Z", + "phase": "Succeeded", + "startedAt": "2026-09-21T17:09:16Z" + }, + "operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "pending_operation": false, + "project": "railiance-platform-addons", + "resources": [ + { + "group": "external-secrets.io", + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": null, + "status": "Synced" + } + ], + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "source": { + "path": "argocd/platform-addons/openbao-secretstore", + "repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git", + "targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a" + }, + "sync": "Synced", + "sync_prune": false, + "sync_strategy_apply": true, + "uid": "f2932505-e649-4b99-b505-4789ed21301f" + }, + "captured_at": "2026-10-11T19:15:05.030899+00:00", + "estate_ip": "92.205.62.239", + "ready_checks": [ + { + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": "", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:49:59Z", + "status": "True" + } + ], + "tracking_matches": true, + "uid": "99563b77-f30f-451b-ac86-16b1b162c6a9" + }, + { + "kind": "ExternalSecret", + "name": "issue-core-runtime", + "namespace": "issue-core", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:54:20Z", + "status": "True" + } + ], + "tracking_matches": null, + "uid": "490dca37-6364-480a-a757-c3a266d32e47" + } + ], + "pinned_render_diff": { + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "path": "argocd/platform-addons/openbao-secretstore", + "render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3", + "render_resource_count": 1, + "exit_code": 1, + "output_suppressed": true + } +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/observation.json b/docs/evidence/2026-10-11-openbao-adoption-proof/observation.json new file mode 100644 index 0000000..da17f73 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/observation.json @@ -0,0 +1,75 @@ +{ + "application": { + "automated_present": false, + "condition_types": [ + "OrphanedResourceWarning" + ], + "health": "Healthy", + "name": "openbao-secretstore", + "operation": { + "finishedAt": "2026-09-21T17:09:18Z", + "phase": "Succeeded", + "startedAt": "2026-09-21T17:09:16Z" + }, + "operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "pending_operation": false, + "project": "railiance-platform-addons", + "resources": [ + { + "group": "external-secrets.io", + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": null, + "status": "Synced" + } + ], + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "source": { + "path": "argocd/platform-addons/openbao-secretstore", + "repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git", + "targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a" + }, + "sync": "Synced", + "sync_prune": false, + "sync_strategy_apply": true, + "uid": "f2932505-e649-4b99-b505-4789ed21301f" + }, + "captured_at": "2026-10-11T19:12:20.205677+00:00", + "estate_ip": "92.205.62.239", + "ready_checks": [ + { + "kind": "ClusterSecretStore", + "name": "openbao", + "namespace": "", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:49:59Z", + "status": "True" + } + ], + "tracking_matches": false, + "uid": "99563b77-f30f-451b-ac86-16b1b162c6a9" + }, + { + "kind": "ExternalSecret", + "name": "issue-core-runtime", + "namespace": "issue-core", + "ready": [ + { + "lastTransitionTime": "2026-08-19T19:54:20Z", + "status": "True" + } + ], + "tracking_matches": null, + "uid": "490dca37-6364-480a-a757-c3a266d32e47" + } + ], + "pinned_render_diff": { + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "path": "argocd/platform-addons/openbao-secretstore", + "render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3", + "render_resource_count": 1, + "exit_code": 1, + "output_suppressed": true + } +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized-final.json b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized-final.json new file mode 100644 index 0000000..667ccb4 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized-final.json @@ -0,0 +1,19 @@ +{ + "schema_version": "argocd-adoption-proof/v1", + "status": "verified", + "application": "openbao-secretstore", + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "captured_at": "2026-10-11T19:22:22.892821+00:00", + "verified_at": "2026-10-11T19:23:40.325446+00:00", + "proving_seconds": 1735984.892821, + "resource_count": 1, + "ready_count": 2, + "manual_apply_prune_off": true, + "warnings": [ + "OrphanedResourceWarning" + ], + "native_mutation": false, + "verifier_sha256": "c058c0ea646bcc3410e0d26db6b42a5471a8fa45a1df62b9ec85f2bf7d03f660", + "contract_sha256": "427084e4f40d34778534397a99379789e355a3b3573b97a61762da8e43abc31e", + "observation_sha256": "01e0850e496fe9234e91d883f1b4c7e7a574dd14bed60ac4585520bf1ddb79b2" +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized.json b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized.json new file mode 100644 index 0000000..2ed24a3 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-argo-normalized.json @@ -0,0 +1,18 @@ +{ + "schema_version": "argocd-adoption-proof/v1", + "status": "verified", + "application": "openbao-secretstore", + "revision": "d2dbc19c254247652c49fda8721c80d53bca206a", + "captured_at": "2026-10-11T19:22:22.892821+00:00", + "verified_at": "2026-10-11T19:22:27.223821+00:00", + "proving_seconds": 1735984.892821, + "resource_count": 1, + "ready_count": 2, + "manual_apply_prune_off": true, + "warnings": [ + "OrphanedResourceWarning" + ], + "native_mutation": false, + "contract_sha256": "427084e4f40d34778534397a99379789e355a3b3573b97a61762da8e43abc31e", + "observation_sha256": "01e0850e496fe9234e91d883f1b4c7e7a574dd14bed60ac4585520bf1ddb79b2" +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/proof-both-diffs.json b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-both-diffs.json new file mode 100644 index 0000000..c980dda --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-both-diffs.json @@ -0,0 +1,6 @@ +{ + "schema_version": "argocd-adoption-proof/v1", + "status": "unverified", + "error": "adoption proof incomplete; raw output suppressed", + "native_mutation": false +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/proof-classified.json b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-classified.json new file mode 100644 index 0000000..c980dda --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-classified.json @@ -0,0 +1,6 @@ +{ + "schema_version": "argocd-adoption-proof/v1", + "status": "unverified", + "error": "adoption proof incomplete; raw output suppressed", + "native_mutation": false +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/proof-normalized.json b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-normalized.json new file mode 100644 index 0000000..c980dda --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/proof-normalized.json @@ -0,0 +1,6 @@ +{ + "schema_version": "argocd-adoption-proof/v1", + "status": "unverified", + "error": "adoption proof incomplete; raw output suppressed", + "native_mutation": false +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/proof.json b/docs/evidence/2026-10-11-openbao-adoption-proof/proof.json new file mode 100644 index 0000000..c980dda --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/proof.json @@ -0,0 +1,6 @@ +{ + "schema_version": "argocd-adoption-proof/v1", + "status": "unverified", + "error": "adoption proof incomplete; raw output suppressed", + "native_mutation": false +} diff --git a/docs/evidence/2026-10-11-openbao-adoption-proof/validation.json b/docs/evidence/2026-10-11-openbao-adoption-proof/validation.json new file mode 100644 index 0000000..6ba2ed7 --- /dev/null +++ b/docs/evidence/2026-10-11-openbao-adoption-proof/validation.json @@ -0,0 +1,44 @@ +{ + "schema_version": "argocd-adoption-validation/v1", + "workplan": "RPF-WP-0044", + "task": "RPF-WP-0044-T03", + "native_mutation": false, + "task_closed": true, + "tests": { + "passed": 13, + "failed": 0 + }, + "compilation": "passed", + "native_proof": "verified", + "proof_ref": "proof-argo-normalized-final.json", + "observation_ref": "observation-argo-normalized.json", + "native_diff": { + "canonical_server_side": 0, + "canonical_client_side": 0, + "canonical_client_changed_paths": [], + "canonical_server_changed_paths": [], + "raw_source_server_side": 1, + "raw_source_client_side": 1 + }, + "normalization": "Argo v3 annotation tracking with Application destination namespace fallback; no live fields copied to desired source", + "upstream_refs": [ + "https://github.com/argoproj/argo-cd/blob/v3.5.3/util/argo/resource_tracking.go", + "https://argo-cd.readthedocs.io/en/release-3.5/operator-manual/upgrading/2.14-3.0/" + ], + "observed_runtime": { + "controller_version": "v3.5.3", + "installation_id_absent": true, + "instance_label_key": "app.kubernetes.io/instance", + "method": "annotation" + }, + "verifier_sha256": "c058c0ea646bcc3410e0d26db6b42a5471a8fa45a1df62b9ec85f2bf7d03f660", + "retained_failure_history": [ + "proof.json", + "proof-normalized.json", + "proof-classified.json", + "proof-both-diffs.json" + ], + "secret_reads": 0, + "subprocess_output_suppressed": true, + "validated_at": "2026-10-11T19:24:51.574009+00:00" +} diff --git a/scripts/argocd_adoption_proof.py b/scripts/argocd_adoption_proof.py new file mode 100644 index 0000000..e738a0f --- /dev/null +++ b/scripts/argocd_adoption_proof.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +"""Read-only Argo adoption proof. Capture/discard raw subprocess output on errors. + +Use --live on the workstation; SSH collects only allowlisted metadata on the +verified estate. The explicit contract binds source, revision and resource set. +A receipt is proof of observation, never authority for a subsequent mutation. +""" +from __future__ import annotations +import argparse +import base64 +from datetime import datetime, timezone +import hashlib +import json +from pathlib import Path +import re +import subprocess +import tempfile + +PIN = 'd2dbc19c254247652c49fda8721c80d53bca206a' +CONTRACT = { + 'estate_ip': '92.205.62.239', 'application': 'openbao-secretstore', + 'project': 'railiance-platform-addons', + 'destination': {'server': 'https://kubernetes.default.svc', 'namespace': 'external-secrets'}, + 'repo_url': 'https://forgejo.coulomb.social/coulomb/railiance-platform.git', + 'source_path': 'argocd/platform-addons/openbao-secretstore', 'revision': PIN, + 'resources': [{'group': 'external-secrets.io', 'kind': 'ClusterSecretStore', + 'namespace': '', 'name': 'openbao'}], + 'ready_checks': [{'kind': 'ClusterSecretStore', 'namespace': '', 'name': 'openbao', + 'tracking_id': 'openbao-secretstore:external-secrets.io/ClusterSecretStore:external-secrets/openbao'}, + {'kind': 'ExternalSecret', 'namespace': 'issue-core', + 'name': 'issue-core-runtime'}], + 'tracking_method': 'annotation', 'controller_version': 'v3.5.3', + 'minimum_proving_seconds': 86400, 'maximum_observation_age_seconds': 300, +} + + +def checked_run(command, *, input=None, timeout=30): + try: + result = subprocess.run(command, input=input, capture_output=True, timeout=timeout) + if result.returncode: + raise RuntimeError('operation unverified; protected output suppressed') + return result.stdout + except (OSError, subprocess.SubprocessError): + raise RuntimeError('operation unverified; protected output suppressed') from None + + +def identity(resource): + return tuple(resource.get(k) or '' for k in ('group', 'kind', 'namespace', 'name')) + + +def timestamp(value): + if not isinstance(value, str): + raise ValueError('invalid proof timestamp') + stamp = datetime.fromisoformat(value.replace('Z', '+00:00')) + if stamp.tzinfo is None: + raise ValueError('proof timestamp requires timezone') + return stamp + + +def validate_contract(contract): + if (not re.fullmatch(r'[a-f0-9]{40}', contract['revision']) + or not re.fullmatch(r'[a-z0-9][a-z0-9.-]*', contract['application']) + or not re.fullmatch(r'[a-z0-9/-]+', contract['source_path']) + or '..' in contract['source_path'] or contract['source_path'].startswith('/')): + raise ValueError('invalid adoption contract') + for check in contract['ready_checks']: + if check['kind'] not in ('ClusterSecretStore', 'ExternalSecret'): + raise ValueError('secret objects are excluded from the metadata contract') + for key in ('namespace', 'name'): + if not re.fullmatch(r'[a-z0-9.-]*', check[key]) or (key == 'name' and not check[key]): + raise ValueError('invalid metadata identity') + if (not contract['resources'] or len({identity(r) for r in contract['resources']}) != len(contract['resources']) + or contract['minimum_proving_seconds'] < 86400 + or not 0 < contract['maximum_observation_age_seconds'] <= 300): + raise ValueError('invalid proof bounds') + + +def tracking_settings(contract): + cm = json.loads(checked_run(['kubectl', '-n', 'argocd', 'get', 'configmap', 'argocd-cm', '-o', 'json'])) + controller = json.loads(checked_run(['kubectl', '-n', 'argocd', 'get', 'statefulset', 'argocd-application-controller', '-o', 'json'])) + containers = controller['spec']['template']['spec']['containers'] + images = [c['image'] for c in containers if c['name'] == 'argocd-application-controller'] + if len(images) != 1 or not images[0].endswith(':' + contract['controller_version']): + raise ValueError('expected Argo controller version unverified') + data = cm.get('data', {}) + # Argo 3.0 changed the default from label to annotation. Bind that default to + # an observed pinned 3.x controller; never infer it for an unknown runtime. + method = data.get('application.resourceTrackingMethod', 'annotation') + label_key = data.get('application.instanceLabelKey', 'app.kubernetes.io/instance') + if method != contract['tracking_method'] or data.get('installationID'): + raise ValueError('tracking semantics differ from this adoption contract') + return {'method': method, 'instance_label_key': label_key, + 'controller_version': contract['controller_version'], 'installation_id_absent': True} + + +def argo_desired(source, contract, settings): + """Inject only Argo-owned tracking metadata, never copy live fields into source.""" + import copy + desired = copy.deepcopy(source) + if (desired.get('apiVersion') != 'external-secrets.io/v1beta1' + or desired.get('kind') != 'ClusterSecretStore' + or desired.get('metadata', {}).get('name') != 'openbao' + or desired['metadata'].get('namespace')): + raise ValueError('pinned source identity differs from admitted store') + if (settings['method'] != contract['tracking_method'] + or settings['controller_version'] != contract['controller_version'] + or settings['installation_id_absent'] is not True): + raise ValueError('unsupported Argo render semantics') + tracking = f"{contract['application']}:external-secrets.io/ClusterSecretStore:{contract['destination']['namespace']}/openbao" + annotations = desired['metadata'].setdefault('annotations', {}) + if 'argocd.argoproj.io/tracking-id' in annotations and annotations['argocd.argoproj.io/tracking-id'] != tracking: + raise ValueError('source tracking annotation conflicts with Argo identity') + annotations['argocd.argoproj.io/tracking-id'] = tracking + return desired + + +def changed_paths(left, right, path=''): + """JSON pointers only: never return old/new values or annotation contents.""" + if type(left) is not type(right): + return [path] + if isinstance(left, dict): + result = [] + for key in sorted(set(left) | set(right)): + child = path + '/' + key.replace('~', '~0').replace('/', '~1') + result.extend([child] if key not in left or key not in right else changed_paths(left[key], right[key], child)) + return result + return [] if left == right else [path] + + +def stable_object(value): + import copy + value = copy.deepcopy(value) + for field in ('creationTimestamp', 'managedFields', 'resourceVersion', 'uid', 'generation'): + value['metadata'].pop(field, None) + value.pop('status', None) + return value + + +def remote_diff(rendered, contract): + def get(args, input=None): + return json.loads(checked_run(['kubectl', *args], input=input)) + nodes = get(['get', 'nodes', '-o', 'json']) + if contract['estate_ip'] not in [a['address'] for n in nodes['items'] for a in n['status']['addresses']]: + raise ValueError('estate identity unverified') + settings = tracking_settings(contract) + source = get(['create', '--dry-run=client', '--validate=false', '-f', '-', '-o', 'json'], rendered) + normalized = json.dumps(argo_desired(source, contract, settings), sort_keys=True).encode() + live = get(['get', '--raw', '/apis/external-secrets.io/v1beta1/clustersecretstores/openbao']) + if live.get('apiVersion') != source['apiVersion']: + raise ValueError('same source API-version read unverified') + def diff(payload, server): + command = ['kubectl', 'diff', *(['--server-side'] if server else []), '-f', '-'] + result = subprocess.run(command, input=payload, capture_output=True, timeout=30) + if result.returncode not in (0, 1): + raise ValueError('native diff failed; output suppressed') + return result.returncode + raw_client = get(['apply', '--dry-run=server', '-f', '-', '-o', 'json'], rendered) + raw_server = get(['apply', '--server-side', '--dry-run=server', '-f', '-', '-o', 'json'], rendered) + normalized_client = get(['apply', '--dry-run=server', '-f', '-', '-o', 'json'], normalized) + normalized_server = get(['apply', '--server-side', '--dry-run=server', '-f', '-', '-o', 'json'], normalized) + return {'revision': contract['revision'], 'path': contract['source_path'], + 'render_sha256': hashlib.sha256(rendered).hexdigest(), 'render_resource_count': 1, + 'normalized_render_sha256': hashlib.sha256(normalized).hexdigest(), + 'normalization': 'argocd-v3-annotation-tracking-destination-namespace', + 'tracking_settings': settings, 'same_api_version': True, + 'raw_server_exit_code': diff(rendered, True), 'raw_client_exit_code': diff(rendered, False), + 'raw_client_changed_paths': changed_paths(stable_object(live), stable_object(raw_client)), + 'raw_server_changed_paths': changed_paths(stable_object(live), stable_object(raw_server)), + 'exit_code': diff(normalized, True), 'client_side_exit_code': diff(normalized, False), + 'normalized_client_changed_paths': changed_paths(stable_object(live), stable_object(normalized_client)), + 'normalized_server_changed_paths': changed_paths(stable_object(live), stable_object(normalized_server)), + 'output_suppressed': True} + + +def collect(contract): + validate_contract(contract) + def get(args): + return json.loads(checked_run(['kubectl', *args, '-o', 'json'])) + nodes = get(['get', 'nodes']) + addresses = [a.get('address') for n in nodes['items'] for a in n.get('status', {}).get('addresses', [])] + if contract['estate_ip'] not in addresses: + raise ValueError('estate identity unverified') + obj = get(['-n', 'argocd', 'get', 'application', contract['application']]) + spec, status = obj['spec'], obj.get('status', {}) + op = status.get('operationState', {}) + sync_operation = op.get('operation', {}).get('sync', {}) + app = {'name': obj['metadata']['name'], 'uid': obj['metadata']['uid'], + 'project': spec.get('project'), 'destination': spec.get('destination'), 'source': {k: spec.get('source', {}).get(k) + for k in ('repoURL', 'path', 'targetRevision')}, + 'automated_present': 'automated' in spec.get('syncPolicy', {}), + 'sync': status.get('sync', {}).get('status'), 'health': status.get('health', {}).get('status'), + 'revision': status.get('sync', {}).get('revision'), + 'pending_operation': bool(obj.get('operation')), + 'condition_types': [c.get('type') for c in status.get('conditions', [])], + 'operation': {k: op.get(k) for k in ('phase', 'startedAt', 'finishedAt')}, + 'sync_prune': sync_operation.get('prune', False), + 'sync_strategy_apply': 'apply' in sync_operation.get('syncStrategy', {}), + 'operation_revision': sync_operation.get('revision'), + 'resources': [{k: r.get(k) for k in ('group', 'kind', 'namespace', 'name', 'status')} + for r in status.get('resources', [])]} + checks = [] + for check in contract['ready_checks']: + args = ['-n', check['namespace']] if check['namespace'] else [] + obj = get([*args, 'get', check['kind'], check['name']]) + ready = [c for c in obj.get('status', {}).get('conditions', []) if c.get('type') == 'Ready'] + checks.append({'kind': check['kind'], 'namespace': check['namespace'], 'name': check['name'], + 'uid': obj['metadata']['uid'], + 'ready': [{k: c.get(k) for k in ('status', 'lastTransitionTime')} for c in ready], + 'tracking_matches': obj['metadata'].get('annotations', {}).get('argocd.argoproj.io/tracking-id') == check['tracking_id'] + if 'tracking_id' in check else None}) + return {'tracking_settings': tracking_settings(contract), + 'captured_at': datetime.now(timezone.utc).isoformat(), + 'estate_ip': contract['estate_ip'], 'application': app, 'ready_checks': checks} + + +def verify(observation, contract, *, now=None): + validate_contract(contract) + now = now or datetime.now(timezone.utc) + age = (now - timestamp(observation['captured_at'])).total_seconds() + if not 0 <= age <= contract['maximum_observation_age_seconds']: + raise ValueError('observation stale or from future') + if observation['estate_ip'] != contract['estate_ip']: + raise ValueError('estate identity mismatch') + settings = observation.get('tracking_settings', {}) + if (settings.get('method') != contract['tracking_method'] or settings.get('controller_version') != contract['controller_version'] + or settings.get('installation_id_absent') is not True): + raise ValueError('observed Argo tracking semantics unverified') + app = observation['application'] + if (app['name'] != contract['application'] or not app['uid'] or app['project'] != contract['project'] + or app['source'] != {'repoURL': contract['repo_url'], 'path': contract['source_path'], 'targetRevision': contract['revision']} + or app['revision'] != contract['revision'] or app.get('destination') != contract['destination']): + raise ValueError('application source or identity mismatch') + if app['automated_present'] is not False or app['pending_operation'] is not False: + raise ValueError('manual idle policy unverified') + if app['sync'] != 'Synced' or app['health'] != 'Healthy': + raise ValueError('application sync or health unverified') + if any(t != 'OrphanedResourceWarning' for t in app['condition_types']): + raise ValueError('application condition prevents proof') + op = app['operation'] + if (op['phase'] != 'Succeeded' or app['sync_prune'] is not False + or app['sync_strategy_apply'] is not True or app['operation_revision'] != contract['revision']): + raise ValueError('successful pinned manual apply unverified') + start, finish = timestamp(op['startedAt']), timestamp(op['finishedAt']) + proof_age = (timestamp(observation['captured_at']) - finish).total_seconds() + if finish < start or proof_age < contract['minimum_proving_seconds']: + raise ValueError('completed adoption proving period unverified') + resources = app['resources'] + expected = {identity(r) for r in contract['resources']} + if (len(resources) != len(expected) or {identity(r) for r in resources} != expected + or any(r['status'] != 'Synced' for r in resources)): + raise ValueError('exact tracked resource set unverified') + checks = observation['ready_checks'] + expected_checks = {(c['kind'], c['namespace'], c['name']): c for c in contract['ready_checks']} + if len(checks) != len(expected_checks) or {(c['kind'], c['namespace'], c['name']) for c in checks} != set(expected_checks): + raise ValueError('readiness checks incomplete') + for check in checks: + key = (check['kind'], check['namespace'], check['name']) + ready = check['ready'] + if not check['uid'] or len(ready) != 1 or ready[0]['status'] != 'True': + raise ValueError('resource readiness unverified') + if timestamp(ready[0]['lastTransitionTime']) > start: + raise ValueError('resource readiness changed during adoption proving period') + if 'tracking_id' in expected_checks[key] and check['tracking_matches'] is not True: + raise ValueError('resource tracking identity unverified') + diff = observation.get('pinned_render_diff', {}) + allowed_raw_paths = { + '/metadata/annotations/argocd.argoproj.io~1tracking-id', + '/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration', + } + for mode in ('client', 'server'): + paths = diff.get(f'raw_{mode}_changed_paths') + code = diff.get(f'raw_{mode}_exit_code') + if (not isinstance(paths, list) or any(not isinstance(path, str) for path in paths) + or not set(paths) <= allowed_raw_paths or type(code) is not int + or code not in (0, 1) or bool(paths) != bool(code)): + raise ValueError('raw source drift contains unknown or substantive fields') + if (diff.get('revision') != contract['revision'] or diff.get('path') != contract['source_path'] + or type(diff.get('exit_code')) is not int or diff['exit_code'] != 0 + or type(diff.get('client_side_exit_code')) is not int or diff['client_side_exit_code'] != 0 + or diff.get('normalization') != 'argocd-v3-annotation-tracking-destination-namespace' + or diff.get('tracking_settings') != settings or diff.get('same_api_version') is not True + or diff.get('normalized_client_changed_paths') != [] or diff.get('normalized_server_changed_paths') != [] + or not re.fullmatch(r'[a-f0-9]{64}', diff.get('normalized_render_sha256', '')) + or diff.get('render_resource_count') != len(expected) + or not re.fullmatch(r'[a-f0-9]{64}', diff.get('render_sha256', ''))): + raise ValueError('exact pinned render zero-diff unverified') + return {'schema_version': 'argocd-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, + 'warnings': sorted(set(app['condition_types'])), 'native_mutation': False, + 'verifier_sha256': hashlib.sha256(Path(__file__).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 render_diff(source_root, contract): + """Render a pinned tracked source into a disposable local directory; no checkout.""" + with tempfile.TemporaryDirectory(prefix='adoption-pin-') as temporary: + archive = checked_run(['git', '-C', str(source_root), 'archive', contract['revision'], contract['source_path']]) + checked_run(['tar', '-x', '-C', temporary], input=archive) + rendered = checked_run(['kubectl', 'kustomize', str(Path(temporary) / contract['source_path'])]) + # Fixed admission for this phase: exactly one public ClusterSecretStore, + # never forward a Secret or extra document to a diff operation. + if re.findall(rb'^kind: (.+)$', rendered, re.M) != [b'ClusterSecretStore']: + raise ValueError('pinned render resource kind unverified') + encoded = base64.b64encode(rendered).decode() + return json.loads(checked_run(['ssh', 'railiance01', 'python3', '-', '--remote-diff', '--render-base64', encoded], + input=Path(__file__).read_bytes(), timeout=150)) + + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--live', action='store_true') + parser.add_argument('--remote-diff', action='store_true', help=argparse.SUPPRESS) + parser.add_argument('--render-base64', help=argparse.SUPPRESS) + parser.add_argument('--remote-collect', action='store_true', help=argparse.SUPPRESS) + parser.add_argument('--observation', type=Path) + parser.add_argument('--source-root', type=Path) + parser.add_argument('--receipt', type=Path) + args = parser.parse_args() + try: + if args.remote_diff: + print(json.dumps(remote_diff(base64.b64decode(args.render_base64, validate=True), CONTRACT), sort_keys=True)) + return 0 + if args.remote_collect: + print(json.dumps(collect(CONTRACT), sort_keys=True)) + return 0 + if args.live: + if not args.source_root: + raise ValueError('source root required for pinned rendering') + raw = checked_run(['ssh', 'railiance01', 'python3', '-', '--remote-collect'], + input=Path(__file__).read_bytes(), timeout=120) + observation = json.loads(raw) + observation['pinned_render_diff'] = render_diff(args.source_root, CONTRACT) + if args.observation: + args.observation.write_text(json.dumps(observation,indent=2)+'\n') + elif args.observation: + observation = json.loads(args.observation.read_text()) + else: + raise ValueError('live read or observation required') + receipt = verify(observation, CONTRACT) + except Exception: + receipt = {'schema_version':'argocd-adoption-proof/v1', 'status':'unverified', + 'error':'adoption proof incomplete; raw output suppressed', 'native_mutation':False} + if args.receipt: + args.receipt.write_text(json.dumps(receipt,indent=2)+'\n') + print(json.dumps(receipt,sort_keys=True)) + return 0 if receipt.get('status') == 'verified' else 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tests/test_argocd_adoption_proof.py b/tests/test_argocd_adoption_proof.py new file mode 100644 index 0000000..0253ace --- /dev/null +++ b/tests/test_argocd_adoption_proof.py @@ -0,0 +1,143 @@ +import copy +from datetime import datetime, timezone, timedelta +import importlib.util +from pathlib import Path +import unittest +from unittest import mock + +SPEC=importlib.util.spec_from_file_location('adoption',Path(__file__).parents[1]/'scripts/argocd_adoption_proof.py') +p=importlib.util.module_from_spec(SPEC);SPEC.loader.exec_module(p) +NOW=datetime(2026,10,11,20,tzinfo=timezone.utc) + + +def fixture(): + return {'captured_at':NOW.isoformat(),'estate_ip':'92.205.62.239', + 'tracking_settings':{'method':'annotation','controller_version':'v3.5.3','installation_id_absent':True,'instance_label_key':'app.kubernetes.io/instance'}, + 'application':{'name':'openbao-secretstore','uid':'app-uid','project':p.CONTRACT['project'],'destination':p.CONTRACT['destination'], + 'source':{'repoURL':p.CONTRACT['repo_url'],'path':p.CONTRACT['source_path'],'targetRevision':p.PIN}, + 'revision':p.PIN,'automated_present':False,'pending_operation':False,'sync':'Synced','health':'Healthy', + 'condition_types':['OrphanedResourceWarning'],'sync_prune':False,'sync_strategy_apply':True, + 'operation_revision':p.PIN,'operation':{'phase':'Succeeded','startedAt':'2026-09-21T17:09:16Z','finishedAt':'2026-09-21T17:09:18Z'}, + 'resources':[dict(p.CONTRACT['resources'][0],status='Synced')]}, + 'ready_checks':[{'kind':c['kind'],'namespace':c['namespace'],'name':c['name'],'uid':'resource-uid', + 'ready':[{'status':'True','lastTransitionTime':'2026-08-19T19:49:59Z'}], + 'tracking_matches':True if 'tracking_id' in c else None} for c in p.CONTRACT['ready_checks']], + 'pinned_render_diff':{'revision':p.PIN,'path':p.CONTRACT['source_path'],'exit_code':0,'render_resource_count':1,'render_sha256':'a'*64, + 'client_side_exit_code':0,'normalized_render_sha256':'b'*64,'same_api_version':True, + 'normalization':'argocd-v3-annotation-tracking-destination-namespace', + 'tracking_settings':{'method':'annotation','controller_version':'v3.5.3','installation_id_absent':True,'instance_label_key':'app.kubernetes.io/instance'}, + 'normalized_client_changed_paths':[],'normalized_server_changed_paths':[], + 'raw_client_exit_code':1,'raw_server_exit_code':1, + 'raw_client_changed_paths':['/metadata/annotations/argocd.argoproj.io~1tracking-id','/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration'], + 'raw_server_changed_paths':['/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration']}} + + +class ProofTest(unittest.TestCase): + def test_complete_fixture_passes(self): + self.assertEqual(p.verify(fixture(),p.CONTRACT,now=NOW)['status'],'verified') + + def test_wrong_estate_stale_future_or_missing_observation_refused(self): + for values in [{'estate_ip':'other'},{'captured_at':(NOW-timedelta(minutes=6)).isoformat()}, + {'captured_at':(NOW+timedelta(seconds=1)).isoformat()},{'captured_at':'2026-10-11'}]: + with self.subTest(values=values), self.assertRaises(ValueError): + p.verify(fixture()|values,p.CONTRACT,now=NOW) + + def test_exact_pin_project_source_policy_and_health_refusals(self): + for key,value in [('revision','b'*40),('destination',{'server':'other'}),('project','other'),('automated_present',True), + ('pending_operation',True),('sync','OutOfSync'),('health','Degraded'), + ('condition_types',['ComparisonError']),('sync_prune',True), + ('sync_strategy_apply',False),('operation_revision','b'*40), + ('source',{'repoURL':'different'})]: + data=fixture();data['application'][key]=value + with self.subTest(key=key),self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + + def test_extra_duplicate_missing_or_wrong_tracked_resources_refused(self): + for resources in [[],[p.CONTRACT['resources'][0]]*2, + [dict(p.CONTRACT['resources'][0],name='another',status='Synced')], + [dict(p.CONTRACT['resources'][0],status='OutOfSync')]]: + data=fixture();data['application']['resources']=resources + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + + def test_proving_period_and_nonmonotonic_operation_refusals(self): + for operation in [{'phase':'Succeeded','startedAt':NOW.isoformat(),'finishedAt':NOW.isoformat()}, + {'phase':'Failed','startedAt':'2026-09-21T17:09:16Z','finishedAt':'2026-09-21T17:09:18Z'}, + {'phase':'Succeeded','startedAt':'2026-09-22T17:09:16Z','finishedAt':'2026-09-21T17:09:18Z'}]: + data=fixture();data['application']['operation']=operation + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + + def test_readiness_tracking_and_incomplete_health_refusals(self): + for key,value in [('tracking_matches',False),('ready',[]), + ('ready',[{'status':'False','lastTransitionTime':'2026-08-19T19:49:59Z'}]), + ('ready',[{'status':'True','lastTransitionTime':'2026-10-11T19:49:59Z'}])]: + data=fixture();data['ready_checks'][0][key]=value + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + data=fixture();data['ready_checks']=[] + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + + def test_nonzero_wrong_pin_missing_or_invalid_diff_refusals(self): + for key,value in [('exit_code',1),('exit_code',2),('revision','b'*40), + ('render_resource_count',2),('render_sha256','wrong'),('client_side_exit_code',1), + ('exit_code',False),('same_api_version',False),('normalization','unknown'), + ('normalized_client_changed_paths',['/spec/provider']), + ('normalized_server_changed_paths',['/metadata/labels/unknown']), + ('raw_client_changed_paths',['unknown']),('raw_server_changed_paths',['/spec/provider'])]: + data=fixture();data['pinned_render_diff'][key]=value + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + data=fixture();del data['pinned_render_diff'] + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + + def test_subprocess_failure_and_timeout_do_not_leak_output(self): + import subprocess + from types import SimpleNamespace + secret='CANARY-DO-NOT-EMIT' + with mock.patch.object(p.subprocess,'run',return_value=SimpleNamespace(returncode=1,stdout=secret,stderr=secret)): + with self.assertRaises(RuntimeError) as exc:p.checked_run(['kubectl','get','nodes']) + self.assertNotIn(secret,str(exc.exception)) + with mock.patch.object(p.subprocess,'run',side_effect=subprocess.TimeoutExpired('cmd',1,output=secret,stderr=secret)): + with self.assertRaises(RuntimeError) as exc:p.checked_run(['kubectl','get','nodes']) + self.assertNotIn(secret,str(exc.exception)) + + def test_argo_tracking_injection_preserves_source_spec_and_annotations(self): + source={'apiVersion':'external-secrets.io/v1beta1','kind':'ClusterSecretStore', + 'metadata':{'name':'openbao','annotations':{'source-owned':'preserve'}}, + 'spec':{'provider':{'vault':{'path':'platform'}}}} + original=copy.deepcopy(source) + settings=fixture()['tracking_settings'] + desired=p.argo_desired(source,p.CONTRACT,settings) + self.assertEqual(source,original) + self.assertEqual(desired['spec'],source['spec']) + self.assertEqual(desired['metadata']['annotations']['source-owned'],'preserve') + self.assertEqual(desired['metadata']['annotations']['argocd.argoproj.io/tracking-id'], + 'openbao-secretstore:external-secrets.io/ClusterSecretStore:external-secrets/openbao') + self.assertNotIn('labels',desired['metadata']) + + def test_unknown_tracking_semantics_conflicting_id_or_secret_render_refused(self): + source={'apiVersion':'external-secrets.io/v1beta1','kind':'ClusterSecretStore','metadata':{'name':'openbao'}} + settings=fixture()['tracking_settings'] + for delta in [{'method':'label'},{'controller_version':'unknown'},{'installation_id_absent':False}]: + with self.assertRaises(ValueError):p.argo_desired(source,p.CONTRACT,settings|delta) + for delta in [{'kind':'Secret'},{'apiVersion':'external-secrets.io/v1'}, + {'metadata':{'name':'openbao','namespace':'external-secrets'}}, + {'metadata':{'name':'openbao','annotations':{'argocd.argoproj.io/tracking-id':'foreign-owner'}}}]: + with self.assertRaises(ValueError):p.argo_desired(source|delta,p.CONTRACT,settings) + + def test_changed_paths_preserve_unknown_fields_and_never_expose_values(self): + before={'metadata':{'annotations':{'x/y~z':'CANARY-PRIVATE'}},'spec':{'path':'old'}} + after={'metadata':{'annotations':{'x/y~z':'OTHER-PRIVATE'}},'spec':{'path':'new'}} + paths=p.changed_paths(before,after) + self.assertEqual(paths,['/metadata/annotations/x~1y~0z','/spec/path']) + self.assertNotIn('CANARY',str(paths)) + self.assertNotIn('PRIVATE',str(paths)) + + def test_runtime_tracking_and_diagnostic_integrity_refusals(self): + for delta in [{'method':'label'},{'controller_version':'v2.14.0'},{'installation_id_absent':False}]: + data=fixture();data['tracking_settings'].update(delta) + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + for delta in [{'raw_client_changed_paths':None},{'raw_client_exit_code':0}, + {'raw_server_changed_paths':['unknown']},{'raw_server_exit_code':2}]: + data=fixture();data['pinned_render_diff'].update(delta) + with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW) + + def test_secret_read_contract_is_rejected(self): + contract=copy.deepcopy(p.CONTRACT);contract['ready_checks'][0]['kind']='Secret' + with self.assertRaises(ValueError):p.validate_contract(contract) 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 e6c140c..7a92ac9 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 @@ -9,7 +9,7 @@ flavor: planning owner: railiance-platform topic_slug: railiance created: "2026-09-21" -updated: "2026-10-03" +updated: "2026-10-11" related: [RPF-WP-0043, RPF-WP-0022] state_hub_workstream_id: "98140775-3b9a-5cf9-9af6-722502d487dc" --- @@ -185,7 +185,7 @@ Rollback: ```task id: RPF-WP-0044-T03 -status: todo +status: done decision_id: 89f1058f-33bc-47a8-8a49-0b44d426c67e priority: high state_hub_task_id: "6d5fc801-e361-579b-bcd8-6fe719a82e94" @@ -233,6 +233,37 @@ Rollback (R1): `git revert` the merge commit and push, then 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: **24h adoption proof verified; T03 done.** +`scripts/argocd_adoption_proof.py` freshly verifies estate IP 92.205.62.239, +local-cluster destination, exact source pin `d2dbc19`, one tracked +ClusterSecretStore/openbao, manual idle policy, successful Sep21 apply/prune-off +sync, exact ownership annotation and uninterrupted Ready conditions on the store +and issue-core consumer since Aug19. The completed proving period is 1,735,984 +seconds. Current Application is Synced/Healthy; the nonblocking +OrphanedResourceWarning is retained and no broader namespace adoption is claimed. + +Both server-side and client-side native pinned-render diffs return **0** after +canonical Argo v3 annotation injection. This is bound to the observed v3.5.3 +controller, `argocd-cm` tracking configuration and the Application destination +namespace. The cluster-scoped store's tracking ID uses `external-secrets` as +Argo's destination-namespace fallback; no instance label is injected in annotation +mode. Same-source-API-version server dry-runs show **no changed JSON paths**. +Only source-derived desired fields and Argo's exact tracking annotation are +rendered; no live values are copied into the desired source, and unknown or +substantive diffs fail closed. Upstream implementation: +https://github.com/argoproj/argo-cd/blob/v3.5.3/util/argo/resource_tracking.go +(`UnstructuredToAppInstanceValue`, `SetAppInstance`). + +Evidence: `docs/evidence/2026-10-11-openbao-adoption-proof/`: +`observation-argo-normalized.json`, `proof-argo-normalized-final.json`, and +`validation.json`. The proof binds the verifier and observation digests. Thirteen +meaningful fixture tests and compilation pass. Earlier empty-namespace and raw +source diff refusals remain visible: raw source omits Argo-owned tracking and +last-applied metadata; an initial API-version comparison also used the API +default rather than the pinned source version. No native mutation, Secret read +or annotation value/error output occurred. This closes the existing adoption's +proving period without changing sync/automation policy or using a new human gate. + ## Adopt issue-core (from rapp-issue-core) ```task