diff --git a/evidence-registry.json b/evidence-registry.json index 77055be..9e9d3d0 100644 --- a/evidence-registry.json +++ b/evidence-registry.json @@ -4,9 +4,9 @@ "repository": "OpenAdaptAI/.github", "repository_id": "858454062", "repository_owner_id": "132681217", - "revision": 7, - "previous_registry_head_sha256": "sha256:ae6b1c8f7ad3f5d12fc9b0c8abfad42950f74c73c0f602f645cd08e6dbdbd9f4", - "registry_head_sha256": "sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90", + "revision": 8, + "previous_registry_head_sha256": "sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", "signer_registry": { "schema_version": "openadapt.qualification-signer-registry-pointer/v1", "object_path": "production-evidence/signer-registries/sha256/7a/7a81bf3d213c74673f3c6b5fa179234cbee534c9432aaea6ae09e455562f96b6.qualification-signer-registry.json", @@ -925,6 +925,28 @@ "semantic_identity_sha256": "sha256:65c5bf4d7b8e0159a1ede34440bd80c0287f4448027c54c44e6a09214ac9bf88", "subject_sha256": "sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977", "registry_entry_sha256": "sha256:691ab41408c5eb46bfb0653b17fcb87455733898d1acb33b175bc510af0ebc62" + }, + { + "kind": "qualification-release", + "object_schema_version": "openadapt.qualification-release/v2", + "object_path": "production-evidence/objects/sha256/0e/0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51.qualification-release.json", + "object_sha256": "sha256:0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51", + "size_bytes": 7641, + "object_media_type": "application/vnd.openadapt.qualification-release+json;version=2", + "semantic_identity_sha256": "sha256:bf7608421fb7e6dbf0c660ebdc20b25877703fd019b9c5f27d193a717d08ca5e", + "subject_sha256": null, + "registry_entry_sha256": "sha256:56394709d93aa546e5c619915e48473f420f479777f0bfb526d9d727e5d4eb26" + }, + { + "kind": "qualification-release-sigstore-bundle", + "object_schema_version": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_path": "production-evidence/objects/sha256/11/114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910.qualification-release-sigstore-bundle.json", + "object_sha256": "sha256:114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910", + "size_bytes": 2741, + "object_media_type": "application/vnd.dev.sigstore.bundle.v0.3+json", + "semantic_identity_sha256": "sha256:3ecfd6d66f69d57c3d349c29ae1b813389fc17ba7976d19d0e09463cab25809b", + "subject_sha256": "sha256:0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51", + "registry_entry_sha256": "sha256:20b9769f47d82e1a0be905e7e1e0c91086645ee4db16745a05e9f39f22d90278" } ] } diff --git a/local-candidates/flow-1.35.1-measured/README.md b/local-candidates/flow-1.35.1-measured/README.md index bede392..013a15f 100644 --- a/local-candidates/flow-1.35.1-measured/README.md +++ b/local-candidates/flow-1.35.1-measured/README.md @@ -1,13 +1,67 @@ -# Flow 1.35.1 measured release candidate - -This unsigned candidate passes the six-class evidence checks for the exact -local wheel. It is ready for release review. Publication and admission remain -false. - -The source commit is `aed32758b7342c61787a3b56fe940e1e9f2d648a`. The retained +# Flow 1.35.1 measured release evidence + +Flow 1.35.1 is published on PyPI and GitHub from source +`44e99a48ebf048c18892aa17cef6ae594db0d0c2`. Both publishers supply these exact +artifacts: + +| Artifact | SHA-256 | +| --- | --- | +| `openadapt_flow-1.35.1-py3-none-any.whl` | `sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d` | +| `openadapt_flow-1.35.1.tar.gz` | `sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca` | + +The September 9 campaign measured the published wheel with one sealed synthetic +reference workflow and 18 fresh trial groups. Each of the six classes has three +groups: healthy, safe halt, uncertain delivery, idempotency replay, declared +attended, and governed repair. All 18 groups passed their declared checks. +The measured manifest is +`sha256:8167536c8cc0c6dcdae8a54a2843b3a71920baa7eaf401bbc9149ecc1e29b996`; +it inventories 831 artifacts, including all 27 native phase reports. + +The 18 primary outcomes are 12 `VERIFIED`, three `HALTED_BEFORE_EFFECT`, and +three `RECONCILIATION_REQUIRED`. Retained database and input records supply the +effect checks. Silent incorrect success, over-halt, unsafe effect, blind retry, +replay dispatch, and unplanned intervention counts are zero. All 27 native +phase reports record zero model calls. Native external network-call counts +remain `unknown`. The outcome categories don't define an aggregate success +percentage. + +This campaign covers one task, one sealed bundle, synthetic pixels, local +SQLite, and the exact fixture geometry. The operator and reviewer principals +are simulated. It doesn't establish customer qualification, actual human +approval, broader display support, hosted execution, or product-wide Production. + +Workflow admission +`sha256:fd6ec7bcddbd9a40bcb7f9999d4fc9c91513cef9255b62e857918bf1ad0ffb5d` +is registered through [PR #42](https://github.com/OpenAdaptAI/.github/pull/42) +at main `f0fb9cc812c0d1653b1ec2998749b078f244308e`. It binds the measured +workflow to the published wheel. The product release admission is a separate +step. + +Release admission +`sha256:279d6e0a9728be2d6466106b07a23a45cb6dd8acf0c4f4bcc2047d09468cabb1` +binds this exact published release. Its signed object is +`sha256:0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51`. +The issuer checked the complete registered evidence chain and current trust +before issuance. Its required expiry field is `null` under the existing +until-revoked authority. Current use still requires the canonical verifier to +check the release identity, artifact bytes, active authority, and revocation +state. A stored admission alone doesn't establish current validity. + +The acceptance path uses the existing `already-published-pypi` contract with +exact artifact and tag controls. GitHub release `385710075` reports +`immutable:false`. The repository's immutable-release setting applies to future +releases; it doesn't make this existing release immutable. + +## Historical September 8 candidate study + +The unsigned candidate from September 8 passed the six-class evidence checks +for its exact local wheel and was ready for release review. Publication and +admission were still false. + +Its source commit was `aed32758b7342c61787a3b56fe940e1e9f2d648a`. The retained manifest digest is `sha256:2cd3b47a9c1fc3d81b1207e7f64441d82ad7f0f62babf49d8ffde6ae2ac2c7ca`. -The verifier checks all 1,617 inventoried artifacts and 36 selected trial groups. +The verifier checked all 1,617 inventoried artifacts and 36 selected trial groups. | Class | Selected groups | Required outcome | | --- | ---: | --- | @@ -18,27 +72,29 @@ The verifier checks all 1,617 inventoried artifacts and 36 selected trial groups | Declared attended | 3 | Bound synthetic decision, live revalidation, and verified final effect | | Governed repair | 3 | Reviewed and approved candidate, complete campaigns, retained proof, and verified canary | -Each of the 12 selected task-condition cells has three groups. The 42 complete -raw groups retain 54 native phase reports. The six excluded groups are the three -moderate-display trials, whose declared contract permits a verified effect or a -safe halt, and three retained-write timeout trials, whose complete effect proof -permits `VERIFIED`. The selected uncertain-delivery class requires -`RECONCILIATION_REQUIRED`. Two incomplete repair instrumentation/restart attempts -remain retained and excluded. Classes are distinct, but outcome indicators can -overlap across phases; these counts don't define an overall success percentage. - -The scope is synthetic pixels, local SQLite, and synthetic reviewer inputs. -The actual repair refreshes the typed field's OCR anchor at step 002. The -hidden-Save halt is a separate refusal check. No customer qualification or -actual human approval is established. Compact scaled identity regions and the -excluded moderate-display identity-tokenization case can still refuse. Their -exact evidence remains in the manifest; no threshold was relaxed. - -The source, wheel, sdist, and measured evidence require review before the -protected-main qualification and publication steps. A changed published wheel -requires new exact-byte evidence. The existing issuer must then bind the -reviewed release and workflow contracts before any signed admission or live -projection changes. The published Flow admission still names 1.34.0. +Each of the 12 selected task-condition cells had three groups. The 42 complete +raw groups retained 54 native phase reports. The six excluded groups were the +three moderate-display trials, whose declared contract permitted a verified +effect or a safe halt, and three retained-write timeout trials, whose complete +effect proof permitted `VERIFIED`. The selected uncertain-delivery class +required `RECONCILIATION_REQUIRED`. Two incomplete repair +instrumentation/restart attempts were retained and excluded. Classes were +distinct, but outcome indicators could overlap across phases; these counts +don't define an overall success percentage. + +The scope was synthetic pixels, local SQLite, and synthetic reviewer inputs. +The repair refreshed the typed field's OCR anchor at step 002. The hidden-Save +halt was a separate refusal check. The study established no customer +qualification or actual human approval. Compact scaled identity regions and +the excluded moderate-display identity-tokenization case could still refuse. +Their exact evidence remains in the historical manifest; no threshold was +relaxed. + +At that point, the source, wheel, sdist, and measured evidence still required +review before protected-main qualification and publication. A changed published +wheel required new exact-byte evidence. The issuer still had to bind the +reviewed release and workflow contracts before signed admission or live +projection changes. The published Flow admission then named 1.34.0. ## Issue a measured admission @@ -80,12 +136,17 @@ Add `--stage-registry /path/to/evidence-registry.json` to append the signed pair through the existing staging tool. Commit the pair before using its reference. The script doesn't commit or push. +Use five sequential registry PRs: receipt, workflow, manifest, summary, then +release and the matching ledgers. Stage one signed pair per PR, with one registry +revision increment and the actual previous registry head. Merge each phase +before preparing the next phase's registered references. In the release PR, +commit the registered pair first, then append its exact release reference and +the workflow reference to the ledgers in a later commit with the registry +unchanged. Preserve all referenced storage commits in the merge. + Use current `.github` main for receipt, workflow, release, and outer signing -sources. The receipt must reach `.github` main before workflow issuance, and the -summary must reach main before release issuance. Intermediate workflow and -manifest storage references can name committed branch objects; preserve those -commits in the reviewed merge. Acceptance uses the reviewed `openadapt-evals` -main source. A storage commit never substitutes for an issuer source. +sources. Acceptance uses the reviewed `openadapt-evals` main source. A storage +commit never substitutes for an issuer source. Collect fresh publication staging after the campaign, receipt, and workflow are complete, just before preparing the acceptance manifest. Preserve the diff --git a/production-evidence/objects/sha256/0e/0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51.qualification-release.json b/production-evidence/objects/sha256/0e/0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51.qualification-release.json new file mode 100644 index 0000000..a9419ff --- /dev/null +++ b/production-evidence/objects/sha256/0e/0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51.qualification-release.json @@ -0,0 +1 @@ +{"admission_id_sha256":"sha256:279d6e0a9728be2d6466106b07a23a45cb6dd8acf0c4f4bcc2047d09468cabb1","artifact_inventory_sha256":"sha256:642f2fb785203048678cdad07c38a990701d6a312d8f1bba9e1b097d49569707","authority_state_sha256":"sha256:df5079141df68877c4b06f05b9fb9fdf279307b7629eb1f2dce12def4bd5e2fa","claim_scope":"production_flow","evidence_class":"remote-safe-synthetic","expires_at":null,"issued_at":"2026-09-09T18:59:59Z","issuer":{"environment":"production-release-admission","ref":"refs/heads/main","repository":"OpenAdaptAI/.github","repository_id":"858454062","repository_owner_id":"132681217","source_commit":"26e98be1866f342e2cb8bae53f57254b3ad715cd","workflow":".github/workflows/issue-production-release-admission.yml"},"not_before":"2026-09-09T18:59:59Z","production_acceptance_summary_bundle_reference":{"kind":"production-acceptance-summary-sigstore-bundle","object_media_type":"application/vnd.dev.sigstore.bundle.v0.3+json","object_path":"production-evidence/objects/sha256/45/4528137325a9fcc6a07b1dc6a4e4eb6ac542d153b7660aff0f58d1ab5803d52e.production-acceptance-summary-sigstore-bundle.json","object_schema_version":"application/vnd.dev.sigstore.bundle.v0.3+json","object_sha256":"sha256:4528137325a9fcc6a07b1dc6a4e4eb6ac542d153b7660aff0f58d1ab5803d52e","registry_entry_sha256":"sha256:691ab41408c5eb46bfb0653b17fcb87455733898d1acb33b175bc510af0ebc62","registry_head_sha256":"sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90","registry_revision":7,"registry_source_commit":"26e98be1866f342e2cb8bae53f57254b3ad715cd","repository":"OpenAdaptAI/.github","repository_id":"858454062","repository_owner_id":"132681217","schema_version":"openadapt.production-evidence-object-reference/v2","semantic_identity_sha256":"sha256:65c5bf4d7b8e0159a1ede34440bd80c0287f4448027c54c44e6a09214ac9bf88","size_bytes":2789,"subject_sha256":"sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977"},"production_acceptance_summary_reference":{"kind":"production-acceptance-summary","object_media_type":"application/vnd.openadapt.production-lifecycle-evidence-summary+json;version=3","object_path":"production-evidence/objects/sha256/30/30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977.production-acceptance-summary.json","object_schema_version":"openadapt.production-lifecycle-evidence-summary/v3","object_sha256":"sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977","registry_entry_sha256":"sha256:8b4d7accdc374e3859c552e934f1164ccb3c05b0ddbc711f855c4ad5e3bcd3cb","registry_head_sha256":"sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90","registry_revision":7,"registry_source_commit":"26e98be1866f342e2cb8bae53f57254b3ad715cd","repository":"OpenAdaptAI/.github","repository_id":"858454062","repository_owner_id":"132681217","schema_version":"openadapt.production-evidence-object-reference/v2","semantic_identity_sha256":"sha256:06b6ad85b2a88ab5f829a0d1b895d50a65e07c072f3fab1c8329b3320068133a","size_bytes":14525,"subject_sha256":null},"publication_policy_sha256":"sha256:ed5ab69fe8127604ca674263cbc8c774b7cfd10754e90bfaa9b71ba573bce1d1","publication_staging":{"assets":[{"asset_id":"553219190","kind":"python-wheel","media_type":"application/zip","name":"openadapt_flow-1.35.1-py3-none-any.whl","publish_destinations":["github-release","pypi"],"sha256":"sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d","size_bytes":2129416,"uploader_id":"321543906","uploader_login":"openadapt-release[bot]"},{"asset_id":"553219189","kind":"python-sdist","media_type":"application/gzip","name":"openadapt_flow-1.35.1.tar.gz","publish_destinations":["github-release","pypi"],"sha256":"sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca","size_bytes":21135866,"uploader_id":"321543906","uploader_login":"openadapt-release[bot]"}],"draft":false,"draft_release_id":"385710075","immutable_releases":{"enabled":true,"enforced_by_owner":false},"immutable_releases_sha256":"sha256:07649aafb167237fecc138f5e93b48ddce5a69f4060da7130e3c78e59fd48581","observed_at":"2026-09-09T18:40:27Z","prerelease":false,"publication_mode":"already-published-pypi","pypi_files":[{"filename":"openadapt_flow-1.35.1-py3-none-any.whl","packagetype":"bdist_wheel","sha256":"sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d","size_bytes":2129416,"yanked":false},{"filename":"openadapt_flow-1.35.1.tar.gz","packagetype":"sdist","sha256":"sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca","size_bytes":21135866,"yanked":false}],"release_app_bot_user_id":"321543906","release_app_id":"4730708","release_app_installation_id":"156835568","release_author_login":"openadapt-release[bot]","repository":"OpenAdaptAI/openadapt-flow","repository_id":"1291376938","schema_version":"openadapt.production-release-staging-evidence/v1","tag":"v1.35.1","tag_ref_state":{"exists":true,"ref":"refs/tags/v1.35.1"},"tag_ref_state_sha256":"sha256:2efe31dddf8f57f3ba0986b364e1ee4caf962f0172faa902662b747edaa84fa0","tag_rulesets":[{"bypass_actors":[{"actor_id":"4730708","actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"exclude":[],"include":["refs/tags/v*"]}},"enforcement":"active","name":"OpenAdapt policy: release tag creation","repository":"OpenAdaptAI/openadapt-flow","repository_id":"1291376938","role":"creation_authority","rules":[{"type":"creation"}],"ruleset_id":"22667117","schema_version":"openadapt.production-release-tag-ruleset/v1","target":"tag"},{"bypass_actors":[],"conditions":{"ref_name":{"exclude":[],"include":["refs/tags/v*"]}},"enforcement":"active","name":"OpenAdapt policy: immutable release tags","repository":"OpenAdaptAI/openadapt-flow","repository_id":"1291376938","role":"immutability","rules":[{"type":"deletion"},{"type":"non_fast_forward"},{"type":"update"}],"ruleset_id":"22667137","schema_version":"openadapt.production-release-tag-ruleset/v1","target":"tag"}],"tag_rulesets_sha256":"sha256:6d002021c7ade6bd0abd4c81ad3fbd8421be7d87116cc7d5a840951a1a2fe0fb","target_commitish":"44e99a48ebf048c18892aa17cef6ae594db0d0c2"},"publication_staging_sha256":"sha256:ade70ba59e8ca9827d3f090559f02b31cdbc7f1198dbf526b9b483510ac0b6b1","release":{"artifacts":[{"kind":"python-sdist","media_type":"application/gzip","name":"openadapt_flow-1.35.1.tar.gz","publish_destinations":["github-release","pypi"],"sha256":"sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca","size_bytes":21135866},{"kind":"python-wheel","media_type":"application/zip","name":"openadapt_flow-1.35.1-py3-none-any.whl","publish_destinations":["github-release","pypi"],"sha256":"sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d","size_bytes":2129416}],"deployment_id":null,"deployment_sha256":null,"kind":"package","schema_version":"openadapt.production-release-candidate/v1","source_commit":"44e99a48ebf048c18892aa17cef6ae594db0d0c2","source_repository":"OpenAdaptAI/openadapt-flow","source_repository_id":"1291376938","tag":"v1.35.1","version":"1.35.1"},"release_identity":{"channel":"production","previous_admission_sha256":"sha256:d2983b86d5428c892421b191f6943a63cf4f9a0b1629e324d099f5df6fa5dcda","schema_version":"openadapt.monotonic-production-release/v1","sequence":2},"release_sha256":"sha256:32cae0917121cbcf657347ca023da15b5b3151db6656526df896e93e3377d49e","revocation_state_sha256":"sha256:250ab430e467f5e584ebd2b1832976b42186b698da71d627ee1724c2b1da2457","schema_version":"openadapt.qualification-release/v2","signer_registry_sha256":"sha256:e243a23243b24986ed08812e284a1bd8c4993814149f02bcba79cc520e10ca14","target":"flow","verdict":"accepted"} diff --git a/production-evidence/objects/sha256/11/114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910.qualification-release-sigstore-bundle.json b/production-evidence/objects/sha256/11/114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910.qualification-release-sigstore-bundle.json new file mode 100644 index 0000000..7144d97 --- /dev/null +++ b/production-evidence/objects/sha256/11/114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910.qualification-release-sigstore-bundle.json @@ -0,0 +1 @@ +{"dsseEnvelope":{"payload":"eyJhdXRob3JpdHlfc3RhdGVfc2hhMjU2Ijoic2hhMjU2OmRmNTA3OTE0MWRmNjg4NzdjNGIwNmYwNWI5ZmI5ZmRmMjc5MzA3Yjc2MjllYjFmMmRjZTEyZGVmNGJkNWUyZmEiLCJleHBpcmVzX2F0IjpudWxsLCJpc3N1ZWRfYXQiOiIyMDI2LTA5LTA5VDE4OjU5OjU5WiIsImtleV9pZCI6Im9hLXB1YmxpYy10cnVzdC1lZDI1NTE5LTljZjRiY2EyMTRjMDFkNzkiLCJub3RfYmVmb3JlIjoiMjAyNi0wOS0wOVQxODo1OTo1OVoiLCJvYmplY3Rfa2luZCI6InF1YWxpZmljYXRpb24tcmVsZWFzZSIsIm9iamVjdF9tZWRpYV90eXBlIjoiYXBwbGljYXRpb24vdm5kLm9wZW5hZGFwdC5xdWFsaWZpY2F0aW9uLXJlbGVhc2UranNvbjt2ZXJzaW9uPTIiLCJvYmplY3Rfc2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucXVhbGlmaWNhdGlvbi1yZWxlYXNlL3YyIiwib2JqZWN0X3NoYTI1NiI6InNoYTI1NjowZWRkMTlkNTFkNDQ5ZTNlYWJhMjE0NjMxMTdmYjE5MTQ4NTUzYTgxN2UwM2M4YzViODA0OTUzYTI2YmNiYjUxIiwib2JqZWN0X3NpemVfYnl0ZXMiOjc2NDEsInJlcXVlc3RfaWRfc2hhMjU2Ijoic2hhMjU2OjMxMzk0YWNiNjQ3YjhjN2FkN2FiNWFmNGQ5NmY4MTZmNzRiMGQ0MWJjZjIyZTEyZTM4ODU2M2ZmZWNmOGY4NTQiLCJyZXZvY2F0aW9uX3N0YXRlX3NoYTI1NiI6InNoYTI1NjoyNTBhYjQzMGU0NjdmNWU1ODRlYmQyYjE4MzI5NzZiNDIxODZiNjk4ZGE3MWQ2MjdlZTE3MjRjMmIxZGEyNDU3Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wdWJsaWMtdHJ1c3Qtc2lnbmluZy1zdGF0ZW1lbnQvdjEiLCJzZW1hbnRpY19pZGVudGl0eV9zaGEyNTYiOiJzaGEyNTY6YmY3NjA4NDIxZmI3ZTZkYmYwYzY2MGViZGMyMGIyNTg3NzcwM2ZkMDE5YjljNWYyN2QxOTNhNzE3ZDA4Y2E1ZSIsInNpZ25hdHVyZV9wcm9maWxlIjoic29mdHdhcmUtZWQyNTUxOS1kc3NlLXYxIiwic2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6InNoYTI1NjplMjQzYTIzMjQzYjI0OTg2ZWQwODgxMmUyODRhMWJkOGM0OTkzODE0MTQ5ZjAyYmNiYTc5Y2M1MjBlMTBjYTE0Iiwic2lnbmluZ19hdXRob3JpdHkiOnsiZW52aXJvbm1lbnQiOiJzeW50aGV0aWMtcXVhbGlmaWNhdGlvbi1ldmlkZW5jZS1kZWNpc2lvbiIsImtleV9vcmlnaW4iOiJzb2Z0d2FyZSIsInJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInJlcG9zaXRvcnkiOiJPcGVuQWRhcHRBSS8uZ2l0aHViIiwicmVwb3NpdG9yeV9pZCI6Ijg1ODQ1NDA2MiIsInJlcG9zaXRvcnlfb3duZXJfaWQiOiIxMzI2ODEyMTciLCJzb3VyY2VfY29tbWl0IjoiMjZlOThiZTE4NjZmMzQyZTJjYjhiYWU1M2Y1NzI1NGIzYWQ3MTVjZCIsIndvcmtmbG93IjoiLmdpdGh1Yi93b3JrZmxvd3Mvc2lnbi1wcm9kdWN0aW9uLWV2aWRlbmNlLXNvZnR3YXJlLnltbCJ9LCJzb3VyY2VfaXNzdWVyIjp7ImVudmlyb25tZW50IjoicHJvZHVjdGlvbi1yZWxlYXNlLWFkbWlzc2lvbiIsInJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInJlcG9zaXRvcnkiOiJPcGVuQWRhcHRBSS8uZ2l0aHViIiwicmVwb3NpdG9yeV9pZCI6Ijg1ODQ1NDA2MiIsInJlcG9zaXRvcnlfb3duZXJfaWQiOiIxMzI2ODEyMTciLCJzb3VyY2VfY29tbWl0IjoiMjZlOThiZTE4NjZmMzQyZTJjYjhiYWU1M2Y1NzI1NGIzYWQ3MTVjZCIsIndvcmtmbG93IjoiLmdpdGh1Yi93b3JrZmxvd3MvaXNzdWUtcHJvZHVjdGlvbi1yZWxlYXNlLWFkbWlzc2lvbi55bWwifX0K","payloadType":"application/vnd.openadapt.production-public-trust-signing-statement+json;version=1","signatures":[{"keyid":"oa-public-trust-ed25519-9cf4bca214c01d79","sig":"ocKaUaQeGk73b00RpeRYKVrU6zA2665MFmFRbzo8HtTzyZF0GV4AJOV6hakEtYDZIWE7P+GqYZ47mSHo8EElCQ=="}]},"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{"hint":"oa-public-trust-ed25519-9cf4bca214c01d79"}}} diff --git a/production-lifecycle-admissions.json b/production-lifecycle-admissions.json index 062bd5d..95eb4a3 100644 --- a/production-lifecycle-admissions.json +++ b/production-lifecycle-admissions.json @@ -128,6 +128,24 @@ "semantic_identity_sha256": "sha256:bc95f40cda0e9374b38d4a05d4a473b7b9e533f61be8e04b9138c0d684efe1b7", "size_bytes": 7531, "subject_sha256": null + }, + { + "kind": "qualification-release", + "object_media_type": "application/vnd.openadapt.qualification-release+json;version=2", + "object_path": "production-evidence/objects/sha256/0e/0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51.qualification-release.json", + "object_schema_version": "openadapt.qualification-release/v2", + "object_sha256": "sha256:0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51", + "registry_entry_sha256": "sha256:56394709d93aa546e5c619915e48473f420f479777f0bfb526d9d727e5d4eb26", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "2ed288e8bd47dd1809f883cc75e3f5ce34348692", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:bf7608421fb7e6dbf0c660ebdc20b25877703fd019b9c5f27d193a717d08ca5e", + "size_bytes": 7641, + "subject_sha256": null } ] } diff --git a/production-workflow-admissions.json b/production-workflow-admissions.json index fb244e4..827be33 100644 --- a/production-workflow-admissions.json +++ b/production-workflow-admissions.json @@ -128,6 +128,24 @@ "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", "semantic_identity_sha256": "sha256:920214d3bb3378e5cdab572c67ac04d13de3b2f19f99ade5e73f7e394282776e", "subject_sha256": null + }, + { + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/94/947224523757df41127be021fc66adee5746f07ff1a0e913319c5461e430e34d.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:947224523757df41127be021fc66adee5746f07ff1a0e913319c5461e430e34d", + "registry_entry_sha256": "sha256:8bf275e7e38161fdbc66084cad6f2365c4283d363b9097b01c5f2587aadc8a73", + "registry_head_sha256": "sha256:2de6be9baa56e46d4c705a19a790c1637130968f90dd2ab6cbd26678d53f2891", + "registry_revision": 5, + "registry_source_commit": "f0fb9cc812c0d1653b1ec2998749b078f244308e", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:7181ef7c2f3a74d1a8e6b8289a544efd40d4e85b8b590855d3dff2715b9ed2d1", + "size_bytes": 8058, + "subject_sha256": null } ] } diff --git a/scripts/validate_production_lifecycle.py b/scripts/validate_production_lifecycle.py index 48cc4c6..fbec969 100755 --- a/scripts/validate_production_lifecycle.py +++ b/scripts/validate_production_lifecycle.py @@ -1702,7 +1702,7 @@ def _validate_v2_workflow_admission( ) -> tuple[dict[str, Any], bool]: """Validate one registered qualification-admission/v4 ledger row. - remote-safe-synthetic tutorial rows are retained and checked. One + remote-safe-synthetic reference rows are retained and checked. One active row is enough to list a workflow admission. It does not qualify a product release, and it is not a MockMed production_acceptance flip. @@ -1743,10 +1743,6 @@ def _validate_v2_workflow_admission( raise LifecycleError( f"workflow admission {index} evidence class is not remote-safe-synthetic" ) - if admission["bundle_version"] != "0.0.0-synthetic": - raise LifecycleError( - f"workflow admission {index} is not the synthetic tutorial bundle" - ) if admission["expires_at"] is not None: raise LifecycleError(f"workflow admission {index} expiry must be until-revoked") if admission.get("evals_production_acceptance") is not False and ( diff --git a/tests/test_production_lifecycle.py b/tests/test_production_lifecycle.py index ae2b642..1d5d371 100644 --- a/tests/test_production_lifecycle.py +++ b/tests/test_production_lifecycle.py @@ -10,7 +10,7 @@ import sys import types import unittest -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from pathlib import Path from unittest import mock @@ -1105,18 +1105,68 @@ def test_schema_files_are_valid_json(self) -> None: class PublishedPolicyTests(unittest.TestCase): """Bind the validator to the policy document the repository publishes.""" + BASELINE_RELEASE_OBJECTS = ( + "sha256:790122a25c87e456c6e45d25ebf5cd029b21b8511265b062fd9129b74aa1dd82", + "sha256:a339d74d2a28281dee2daacbf378bd9b03f1cca7884980e7e8355345b7195d64", + "sha256:999ffd46284b5aabc32eca6b39c0cd9b01a689b2ec3679c6b8a77d42e78c722c", + "sha256:ab90ad9cd7a5b284d2f2cb59f6feb4477062cb2a7fd4ab089cd177001dedd25d", + "sha256:962514bb387e79cd31fa00a7f5d3fe2476a4dd89ee508095a183de3ba8cc70b0", + "sha256:549d2abc0f18d3e14d505698865d7226bf6cdf8356300322ee42d00b717ce054", + "sha256:5370cba3f92802fd27b3f364a746c219d9c651e9992b87d25a4d180386744f82", + ) + + BASELINE_WORKFLOW_OBJECTS = ( + "sha256:dcdb32a762aca87fbb1a7c9df5d346403b167ec5850d35fe47fd64d942684a04", + "sha256:34b8a1157cfd72d820cac0c49e3679959174083ccbb83cf0371925cc72226fdb", + "sha256:4b505945969b876b8af9d9686416214d191b78cd3472e313ce3776a5e550efcb", + "sha256:71b69bc2339fd3202eae960c2cd7fbef4963e6736fa5ad6e24d7879e53f8da7d", + "sha256:46e925b68d8313880756b38fcbf17d8bf02af1fb5b57692e63cc972479088a3e", + "sha256:71470304eb13b4d9aed64c0d26c9d31d1bb8a4eb463955be0b720e1aed523e61", + "sha256:6d0c77a7831103e1959406513f7b53fca7fb0709c872056461dc8b6be06a784e", + ) + + def published_objects(self, ledger_name: str) -> list[dict]: + ledger = json.loads((ROOT / ledger_name).read_text(encoding="utf-8")) + registry = lifecycle.evidence_registry + entries = registry.validate_registry( + json.loads((ROOT / "evidence-registry.json").read_text(encoding="utf-8")) + ) + result = [] + for reference in ledger["admissions"]: + registry.require_registered(entries, reference=reference, label=ledger_name) + raw = (ROOT / reference["object_path"]).read_bytes() + self.assertEqual(digest_bytes(raw), reference["object_sha256"]) + self.assertEqual(len(raw), reference["size_bytes"]) + result.append(json.loads(raw)) + return result + + def published_now(self) -> datetime: + # Test the checked-in snapshot after its actual activation times. This + # deterministic clock does not change the validator's live clock. + objects = [ + value + for name in ( + "production-lifecycle-admissions.json", + "production-workflow-admissions.json", + ) + for value in self.published_objects(name) + ] + return max( + trust.require_timestamp(value[field], "published admission " + field) + for value in objects + for field in ("issued_at", "not_before") + ) + timedelta(seconds=1) + def test_published_policy_is_accepted(self) -> None: - # Remaining-six rows are issued 2026-09-02T19:22:47Z. Use a clock - # after that instant. This is not a MockMed production_acceptance flip. - published_now = datetime(2026, 9, 2, 19, 30, 0, tzinfo=timezone.utc) + published_now = self.published_now() active = lifecycle.validate_files(ROOT, now=published_now) - release = json.loads( + release = max( ( - ROOT - / "production-evidence/objects/sha256/79/" - "790122a25c87e456c6e45d25ebf5cd029b21b8511265b062fd9129b74aa1dd82" - ".qualification-release.json" - ).read_text(encoding="utf-8") + value + for value in self.published_objects("production-lifecycle-admissions.json") + if value["target"] == "flow" + ), + key=lambda value: value["release_identity"]["sequence"], ) self.assertEqual(release["evidence_class"], "remote-safe-synthetic") self.assertEqual(release["target"], "flow") @@ -1129,7 +1179,7 @@ def test_published_policy_is_accepted(self) -> None: def test_seven_synthetic_target_admissions_are_product_production( self, ) -> None: - published_now = datetime(2026, 9, 2, 19, 30, 0, tzinfo=timezone.utc) + published_now = self.published_now() active = lifecycle.validate_files(ROOT, now=published_now) self.assertEqual(len(active), 7) self.assertEqual(set(active), set(lifecycle.EXPECTED_TARGETS)) @@ -1144,7 +1194,7 @@ def test_seven_synthetic_target_admissions_are_product_production( def test_published_workflow_ledger_lists_synthetic_tutorial_admissions( self, ) -> None: - published_now = datetime(2026, 9, 2, 19, 30, 0, tzinfo=timezone.utc) + published_now = self.published_now() active = lifecycle.validate_files(ROOT, now=published_now) self.assertEqual(len(active), 7) ledger = json.loads( @@ -1154,7 +1204,13 @@ def test_published_workflow_ledger_lists_synthetic_tutorial_admissions( ledger["schema_version"], "openadapt.production-workflow-admissions/v1" ) self.assertEqual(ledger["policy_sha256"], lifecycle.RETAINED_POLICY_SHA256) - self.assertEqual(len(ledger["admissions"]), 7) + self.assertEqual( + tuple( + row["object_sha256"] + for row in ledger["admissions"][: len(self.BASELINE_WORKFLOW_OBJECTS)] + ), + self.BASELINE_WORKFLOW_OBJECTS, + ) kinds = {row["kind"] for row in ledger["admissions"]} self.assertEqual(kinds, {"qualification-admission"}) tutorial = json.loads( @@ -1188,7 +1244,13 @@ def test_release_ledger_keeps_the_flow_object_reference(self) -> None: ledger = json.loads( (ROOT / "production-lifecycle-admissions.json").read_text(encoding="utf-8") ) - self.assertEqual(len(ledger["admissions"]), 7) + self.assertEqual( + tuple( + row["object_sha256"] + for row in ledger["admissions"][: len(self.BASELINE_RELEASE_OBJECTS)] + ), + self.BASELINE_RELEASE_OBJECTS, + ) flow = ledger["admissions"][0] self.assertEqual(flow["kind"], "qualification-release") self.assertEqual( diff --git a/tests/test_registered_workflow_versions.py b/tests/test_registered_workflow_versions.py new file mode 100644 index 0000000..b367f0f --- /dev/null +++ b/tests/test_registered_workflow_versions.py @@ -0,0 +1,111 @@ +"""Version checks on retained, signed workflow admissions.""" + +from __future__ import annotations + +import copy +import json +import sys +import tempfile +import unittest +from datetime import datetime, timedelta +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import production_trust as trust # noqa: E402 +import validate_production_lifecycle as lifecycle # noqa: E402 + + +class RegisteredWorkflowVersionTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.registry = json.loads((ROOT / "evidence-registry.json").read_bytes()) + cls.entries = lifecycle.evidence_registry.validate_registry( + cls.registry, root=ROOT + ) + rows = json.loads((ROOT / "production-workflow-admissions.json").read_bytes())[ + "admissions" + ] + # These are the real historical and measured reference objects. Their + # original signatures must verify; a generated test key cannot prove it. + expected = { + "sha256:dcdb32a762aca87fbb1a7c9df5d346403b167ec5850d35fe47fd64d942684a04": ( + "0.0.0-synthetic" + ), + "sha256:947224523757df41127be021fc66adee5746f07ff1a0e913319c5461e430e34d": ( + "1.35.1-reference.1" + ), + } + cls.cases = [ + ( + row, + json.loads((ROOT / row["object_path"]).read_bytes()), + expected[row["object_sha256"]], + ) + for row in rows + if row["object_sha256"] in expected + ] + if len(cls.cases) != len(expected): + raise AssertionError("the two retained signed workflow rows are required") + cls.now = max( + datetime.fromisoformat(value[field].replace("Z", "+00:00")) + for _, value, _ in cls.cases + for field in ("issued_at", "not_before") + ) + timedelta(seconds=1) + + def validate_row(self, row, root=None): + return lifecycle._validate_v2_workflow_admission( + row, + index=0, + root=ROOT if root is None else root, + registry_document=self.registry, + registry_entries=self.entries, + now=self.now, + ) + + def test_actual_registered_old_and_reference_versions_verify(self): + for row, expected_object, version in self.cases: + with self.subTest(version=version): + admission, active = self.validate_row(row) + self.assertEqual(admission, expected_object) + self.assertEqual(admission["bundle_version"], version) + self.assertTrue(active) + + def test_malformed_versions_keep_the_canonical_schema_refusal(self): + for _, original, version in self.cases: + for invalid in (None, True, 1, "", "../private", "a" * 65): + with self.subTest(version=version, invalid=invalid): + changed = copy.deepcopy(original) + changed["bundle_version"] = invalid + with self.assertRaisesRegex( + trust.TrustError, "bundle version is not canonical" + ): + trust.validate_qualification_admission(changed, now=self.now) + + def test_changed_valid_version_keeps_the_admission_digest_refusal(self): + for _, original, version in self.cases: + with self.subTest(version=version): + changed = copy.deepcopy(original) + changed["bundle_version"] = "1.35.1-reference.2" + with self.assertRaisesRegex( + trust.TrustError, "qualification admission id is invalid" + ): + trust.validate_qualification_admission(changed, now=self.now) + + def test_changed_signed_version_keeps_the_registered_bytes_refusal(self): + for row, original, version in self.cases: + with self.subTest(version=version), tempfile.TemporaryDirectory() as tmp: + changed = copy.deepcopy(original) + changed["bundle_version"] = "1.35.1-reference.2" + path = Path(tmp) / row["object_path"] + path.parent.mkdir(parents=True) + path.write_bytes(lifecycle.evidence_registry.canonical(changed) + b"\n") + with self.assertRaisesRegex( + lifecycle.LifecycleError, "bytes differ from the registry" + ): + self.validate_row(row, root=Path(tmp)) + + +if __name__ == "__main__": + unittest.main()