diff --git a/README.md b/README.md index abd9c2edb..bc197d3e4 100644 --- a/README.md +++ b/README.md @@ -265,9 +265,13 @@ self-service API. Partner contract and qualification process: [the Execute guide](https://docs.openadapt.ai/commercial/oem-brief/). -Production is per qualified workflow. -A workflow is Production only with an active signed, expiring, revocable -admission for that exact compiled version, application, and environment. +The signed ledger has seven active target admissions. Their evidence class +is `remote-safe-synthetic`, not MockMed `production_acceptance`. They remain +valid until revoked; there's no expiry date. `pip install openadapt` is the +admitted runtime (launcher 1.16.0, Flow 1.34.0). Desktop's admitted artifact +is the 0.16.0 wheel. Native Mac DMGs stay ad-hoc signed 0.15.0; don't treat +those as admitted. A Production run still needs a workflow admission for the +exact compiled bundle, application, and environment. [Check the live signed Production record](https://docs.openadapt.ai/production-lifecycle.json). diff --git a/docs/platform-compatibility-report.md b/docs/platform-compatibility-report.md index dbc0852ff..8c85d64e0 100644 --- a/docs/platform-compatibility-report.md +++ b/docs/platform-compatibility-report.md @@ -2,7 +2,7 @@ -- Generated: `2026-08-29T21:58:31+00:00` +- Generated: `2026-09-02T19:12:23+00:00` - Selection: `latest-published` - Compatibility: **dependency-incompatible** - Signature: `unsigned (signing infrastructure pending)` @@ -14,9 +14,9 @@ | launcher | `openadapt` | `1.16.0` | [`v1.16.0`](https://github.com/OpenAdaptAI/OpenAdapt/tree/v1.16.0)
commit `089c27c046f5cd972d299361f9d68285c1896c71`
tree `3779d8fceeb65d26bad440fa52d11ab7238eae70` | `openadapt-1.16.0-py3-none-any.whl` `371693e7607d1cdc1ea360ef5d657c1af791a0af39677fa2ce5933e7ba712719`
`openadapt-1.16.0.tar.gz` `39d01612417dff5981b536b324a63498df1ea6f0128c723276609ebbe18d9a18` | | flow | `openadapt-flow` | `1.34.0` | [`v1.34.0`](https://github.com/OpenAdaptAI/openadapt-flow/tree/v1.34.0)
commit `30fc60e55778a0e0f92b9776117cafcfe2512249`
tree `7a0263eda60b23f685cbd8abb4174da2db88fd4b` | `openadapt_flow-1.34.0-py3-none-any.whl` `56d32818989cb3a92830080ead39e10b08718c55e00988117f22cbfeaac98854`
`openadapt_flow-1.34.0.tar.gz` `a941a70015fe91897e655bfc9bf83e6cf8fbcf8bd4e4c7206ffb133c505203f8` | | capture | `openadapt-capture` | `1.2.2` | [`v1.2.2`](https://github.com/OpenAdaptAI/openadapt-capture/tree/v1.2.2)
commit `06ade22948c12dcc6608bd54b5cca5c9c306e77c`
tree `b0a3b5b53f23536e01314f36aa46fd5b2d8baba6` | `openadapt_capture-1.2.2-py3-none-any.whl` `550fffe50990fa8431d332bdd5fe4755976d83baa7ad62553f50ad9e4ddcd1ae`
`openadapt_capture-1.2.2.tar.gz` `6f4298d3daadf9cced8994207e34b3116a79209ef792e98f778a0770e2f3406c` | -| privacy | `openadapt-privacy` | `1.0.4` | [`v1.0.4`](https://github.com/OpenAdaptAI/openadapt-privacy/tree/v1.0.4)
commit `7252de4820451b069310a3ccaecf484ad0af8c18`
tree `4c7bc72aa5c5879c7a51a4d3586c4b7e1c1ed5ff` | `openadapt_privacy-1.0.4-py3-none-any.whl` `e10bd265aba5e54c97aeb80a9ac0509384d19eb46ce46656880b619fd06f542b`
`openadapt_privacy-1.0.4.tar.gz` `89670173890192ecea31eea832e6e1c9b612cb7f19969fe99cda082cd71f2cb3` | -| types | `openadapt-types` | `0.12.0` | [`v0.12.0`](https://github.com/OpenAdaptAI/openadapt-types/tree/v0.12.0)
commit `da5ccb34928e912ac0ae16e04d818b9be68fb85d`
tree `9b40da24707b0562db8bded8779699c5ed9a5013` | `openadapt_types-0.12.0-py3-none-any.whl` `f9d2ef67f7f82554881601484647d8f2f221896e382c9e586286e40164b125e5`
`openadapt_types-0.12.0.tar.gz` `24bc131babbd9a4940df1c9c25cf4a7d63c859aeae4fe11b25b6672e303612f3` | -| desktop | `openadapt-desktop` | `0.15.0` | [`desktop-v0.15.0`](https://github.com/OpenAdaptAI/openadapt-desktop/tree/desktop-v0.15.0)
commit `ac01e90f04e604f40e030046457f5b5167c88755`
tree `06a0c7c3fc61194149f821522939a08fadc18736` | `openadapt_desktop-0.15.0-py3-none-any.whl` `11dd8da225d660f23fc5e779d564ef34c5d85ad40b303951ec7697e94f488401`
`openadapt_desktop-0.15.0.tar.gz` `f45e35d53469fcb75710010d56a9d3f362130f1637a2f5fb50889553104a5670` | +| privacy | `openadapt-privacy` | `1.1.0` | [`v1.1.0`](https://github.com/OpenAdaptAI/openadapt-privacy/tree/v1.1.0)
commit `a7cdcbd872e6263305f53b6ea1b778d76718c1cc`
tree `9beb1a11e5f6bbe5d7a1509d681c92362a2de964` | `openadapt_privacy-1.1.0-py3-none-any.whl` `ca4533ff99e754d14e50de6f6be8d0ce308892f90935e3f59f5f521104a29d7f`
`openadapt_privacy-1.1.0.tar.gz` `9976c844e8b9b74c13e6f685030e7013e27bc7eb833d06884bfa5e018af74f65` | +| types | `openadapt-types` | `0.17.0` | [`v0.17.0`](https://github.com/OpenAdaptAI/openadapt-types/tree/v0.17.0)
commit `14eca2d13e398153524cb65edc12e87616f9dfa7`
tree `c7f736a2ad7084bc95e1910b3232650ada8acd51` | `openadapt_types-0.17.0-py3-none-any.whl` `79738ab529f71241963725c6a40603deae032ff60c61d66baf33d7a248b1afa9`
`openadapt_types-0.17.0.tar.gz` `43dc00d82d6d8feb2cfaa16960224a85933440d7dee3b5f3c60618873535287b` | +| desktop | `openadapt-desktop` | `0.16.0` | [`v0.16.0`](https://github.com/OpenAdaptAI/openadapt-desktop/tree/v0.16.0)
commit `7c59750d37be09a2871cd1bada8cb0bce946d363`
tree `9a1307794df6f2c77ee3dab90973845f3d08ee53` | `openadapt_desktop-0.16.0-py3-none-any.whl` `06e608a934deef56c93ee1ca8de511d6daba08459626e8245bd0db4c391e64e9`
`openadapt_desktop-0.16.0.tar.gz` `15f0c59a4d481328da76f3e5aa6a29ab8cdced8ab183c97346d69b4c14c996ff` | | agent | `openadapt-agent` | `2.0.1` | [`v2.0.1`](https://github.com/OpenAdaptAI/openadapt-agent/tree/v2.0.1)
commit `d9f16b2946dfdd0228eeb2660047347df9dce8d8`
tree `26606c89b4294603b628f1b3aaa8283c45765326` | `openadapt_agent-2.0.1-py3-none-any.whl` `5c5f6e3eca4c127d87b3affb8944223c0836916e0df57d0c6b8ba737b3bc8b5a`
`openadapt_agent-2.0.1.tar.gz` `67e05b299a4f035a0482b3032decc151fafc22de9a12bb3d8f884f37ea4cc937` | ## Runtime selections @@ -24,8 +24,8 @@ | Unit | Version | Selected or locked OpenAdapt packages | |---|---:|---| | launcher_environment | `1.16.0` | `openadapt==1.16.0`
`openadapt-flow==1.34.0` | -| customer_runner | `1.34.0` | `openadapt-flow==1.34.0`
`openadapt-types==0.12.0` | -| desktop_sidecar | `0.15.0` | `openadapt-capture==1.2.1`
`openadapt-flow==1.27.1`
`openadapt-privacy==1.0.0`
`openadapt-types==0.7.0` | +| customer_runner | `1.34.0` | `openadapt-flow==1.34.0`
`openadapt-types==0.17.0` | +| desktop_sidecar | `0.16.0` | `openadapt-capture==1.2.1`
`openadapt-flow==1.34.0`
`openadapt-privacy==1.0.0`
`openadapt-types==0.10.0` | | agent_bridge | `2.0.1` | `openadapt-agent==2.0.1`
`openadapt-flow==1.34.0` | ## Selected-platform dependency compatibility @@ -34,21 +34,21 @@ These edges show whether each selected integration version satisfies its publish | Scope | Source | Target | Constraint | Selected | Activation | |---|---|---|---|---:|---| -| desktop-sidecar-lock | `openadapt-desktop==0.15.0` | `openadapt-capture` | `>=1.2.1` | `1.2.1` | `always` | -| desktop-sidecar-lock | `openadapt-desktop==0.15.0` | `openadapt-flow` | `==1.27.1` | `1.27.1` | `extra == "build"` | -| desktop-sidecar-lock | `openadapt-desktop==0.15.0` | `openadapt-privacy` | `>=1.0.0` | `1.0.0` | `always` | -| desktop-sidecar-lock | `openadapt-desktop==0.15.0` | `openadapt-types` | `==0.7.0` | `0.7.0` | `always` | +| desktop-sidecar-lock | `openadapt-desktop==0.16.0` | `openadapt-capture` | `>=1.2.1` | `1.2.1` | `always` | +| desktop-sidecar-lock | `openadapt-desktop==0.16.0` | `openadapt-flow` | `==1.34.0` | `1.34.0` | `extra == "build"` | +| desktop-sidecar-lock | `openadapt-desktop==0.16.0` | `openadapt-privacy` | `>=1.0.0` | `1.0.0` | `always` | +| desktop-sidecar-lock | `openadapt-desktop==0.16.0` | `openadapt-types` | `==0.10.0` | `0.10.0` | `always` | | selected-platform | `openadapt-agent==2.0.1` | `openadapt-flow` | `<2,>=1.18.1` | `1.34.0` | `always` | -| selected-platform | `openadapt-capture==1.2.2` | `openadapt-privacy` | `>=0.1.0` | `1.0.4` | `extra == "all"`
`extra == "privacy"` | +| selected-platform | `openadapt-capture==1.2.2` | `openadapt-privacy` | `>=0.1.0` | `1.1.0` | `extra == "all"`
`extra == "privacy"` | | selected-platform | `openadapt-flow==1.34.0` | `openadapt-capture` | `>=1.2.0` | `1.2.2` | `extra == "capture"` | -| selected-platform | `openadapt-flow==1.34.0` | `openadapt-privacy` | `>=1.0.0` | `1.0.4` | `extra == "privacy"` | -| selected-platform | `openadapt-flow==1.34.0` | `openadapt-types` | `<0.11.0,>=0.10.0` | `0.12.0` | `extra == "console"`
`extra == "dev"`
`extra == "interop"` | +| selected-platform | `openadapt-flow==1.34.0` | `openadapt-privacy` | `>=1.0.0` | `1.1.0` | `extra == "privacy"` | +| selected-platform | `openadapt-flow==1.34.0` | `openadapt-types` | `<0.11.0,>=0.10.0` | `0.17.0` | `extra == "console"`
`extra == "dev"`
`extra == "interop"` | | selected-platform | `openadapt==1.16.0` | `openadapt-capture` | `<2.0.0,>=1.2.0` | `1.2.2` | `extra == "all"`
`extra == "capture"`
`extra == "core"` | | selected-platform | `openadapt==1.16.0` | `openadapt-flow` | `<2.0.0,>=1.29.0` | `1.34.0` | `always`
`extra == "all"`
`extra == "browser"`
`extra == "capture"`
`extra == "core"`
`extra == "flow"`
`extra == "privacy"`
`extra == "rdp"`
`extra == "windows"`
`sys_platform == "darwin" and extra == "all"`
`sys_platform == "darwin" and extra == "macos"`
`sys_platform == "linux" and extra == "all"`
`sys_platform == "linux" and extra == "linux"` | ### Compatibility failures -- `flow` -> `openadapt-types` requires `<0.11.0,>=0.10.0`; the resolved version is `0.12.0`. +- `flow` -> `openadapt-types` requires `<0.11.0,>=0.10.0`; the resolved version is `0.17.0`. ## Cross-component schemas @@ -56,10 +56,10 @@ These edges show whether each selected integration version satisfies its publish |---|---|---|---| | `workflow_bundle` | `flow` | `schema_version` [1, 2] | [`openadapt_flow/bundle_validation.py`](https://github.com/OpenAdaptAI/openadapt-flow/blob/30fc60e55778a0e0f92b9776117cafcfe2512249/openadapt_flow/bundle_validation.py) `fe1e5ec441183adf1aa2209f1669f779043fb4ad1f41a651ca0f96123778240f`
[`openadapt_flow/ir.py`](https://github.com/OpenAdaptAI/openadapt-flow/blob/30fc60e55778a0e0f92b9776117cafcfe2512249/openadapt_flow/ir.py) `e1e5345dd173b694587dde515b59cd60de0cf6028c1f35d589706b84ad792598` | | `capture_structural_observation` | `capture` | `openadapt.capture.structural-observation/v` [1] | [`openadapt_capture/structural.py`](https://github.com/OpenAdaptAI/openadapt-capture/blob/06ade22948c12dcc6608bd54b5cca5c9c306e77c/openadapt_capture/structural.py) `8f8bce37148a3357ace147c46a7c45e6d5501b70622b24b1ac2490f2def304fc` | -| `control_overlay_frame` | `types` | `openadapt.control-overlay-frame/v` [1, 2] | [`openadapt_types/control_overlay.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay.py) `3aa01f89b43e12478c9e2ff1a9e61689397370691d15247ded9dcd5b05242790`
[`openadapt_types/control_overlay_tracking.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay_tracking.py) `af8d934f0572c5f4e9c63ba720d4a1d0e73c8cdf3f7b87cc4562c01a8a551755` | -| `control_overlay_timeline` | `types` | `openadapt.control-overlay-timeline/v` [1, 2] | [`openadapt_types/control_overlay.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay.py) `3aa01f89b43e12478c9e2ff1a9e61689397370691d15247ded9dcd5b05242790`
[`openadapt_types/control_overlay_tracking.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay_tracking.py) `af8d934f0572c5f4e9c63ba720d4a1d0e73c8cdf3f7b87cc4562c01a8a551755` | -| `human_decision_task` | `types` | `openadapt.human-decision-task/v` [1, 2] | [`openadapt_types/human_decision.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/human_decision.py) `6ed5d2db82f5c73dfab568868f13f7a2cbfde6fb7b3f05b027b40c503ab04e29` | -| `human_decision_receipt` | `types` | `openadapt.human-decision-receipt/v` [1] | [`openadapt_types/human_decision.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/human_decision.py) `6ed5d2db82f5c73dfab568868f13f7a2cbfde6fb7b3f05b027b40c503ab04e29` | +| `control_overlay_frame` | `types` | `openadapt.control-overlay-frame/v` [1, 2] | [`openadapt_types/control_overlay.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay.py) `3aa01f89b43e12478c9e2ff1a9e61689397370691d15247ded9dcd5b05242790`
[`openadapt_types/control_overlay_tracking.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay_tracking.py) `af8d934f0572c5f4e9c63ba720d4a1d0e73c8cdf3f7b87cc4562c01a8a551755` | +| `control_overlay_timeline` | `types` | `openadapt.control-overlay-timeline/v` [1, 2] | [`openadapt_types/control_overlay.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay.py) `3aa01f89b43e12478c9e2ff1a9e61689397370691d15247ded9dcd5b05242790`
[`openadapt_types/control_overlay_tracking.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay_tracking.py) `af8d934f0572c5f4e9c63ba720d4a1d0e73c8cdf3f7b87cc4562c01a8a551755` | +| `human_decision_task` | `types` | `openadapt.human-decision-task/v` [1, 2] | [`openadapt_types/human_decision.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/human_decision.py) `6ed5d2db82f5c73dfab568868f13f7a2cbfde6fb7b3f05b027b40c503ab04e29` | +| `human_decision_receipt` | `types` | `openadapt.human-decision-receipt/v` [1] | [`openadapt_types/human_decision.py`](https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/human_decision.py) `6ed5d2db82f5c73dfab568868f13f7a2cbfde6fb7b3f05b027b40c503ab04e29` | | `human_decision_relay` | `flow` | `openadapt.human-decision-relay/v` [2] | [`openadapt_flow/console/decision_relay.py`](https://github.com/OpenAdaptAI/openadapt-flow/blob/30fc60e55778a0e0f92b9776117cafcfe2512249/openadapt_flow/console/decision_relay.py) `d5320404d4342dbb52be1f2ead1cdb5cf0e22877feaaa1e47974b301df697292` | | `remote_decision_projection` | `flow` | `openadapt.remote-decision-projection/v` [1] | [`openadapt_flow/console/human_decisions.py`](https://github.com/OpenAdaptAI/openadapt-flow/blob/30fc60e55778a0e0f92b9776117cafcfe2512249/openadapt_flow/console/human_decisions.py) `02651ad8888a5c17bfff6fb47165ba861193243dc0f7c951f8b94f544ca19477` | | `runtime_validation` | `flow` | `openadapt.runtime-validation/v` [1, 2, 3] | [`openadapt_flow/runtime_validation.py`](https://github.com/OpenAdaptAI/openadapt-flow/blob/30fc60e55778a0e0f92b9776117cafcfe2512249/openadapt_flow/runtime_validation.py) `18675d6b4c40458394febb36cbe28e99f410cb65b4cab138b68df3cacc6f7dd4` | diff --git a/docs/platform-manifest.md b/docs/platform-manifest.md index e4c0b8dbb..ea554eed6 100644 --- a/docs/platform-manifest.md +++ b/docs/platform-manifest.md @@ -19,7 +19,8 @@ it must agree with. Other repositories contribute source data only: - PyPI is the authority for published versions, artifact URLs, and digests. - Each exact public release tag is the authority for its release source commit and tree. This reference is not, by itself, a build-provenance attestation. -- The exact `desktop-vX.Y.Z` tag and its `uv.lock` are the authority for the +- The exact published Desktop release tag (`desktop-vX.Y.Z` when that native + tag exists, otherwise `vX.Y.Z`) and its `uv.lock` are the authority for the package versions frozen into `openadapt-engine`. - `https://openadapt.ai/status.json` (maintained in `openadapt-web`, `public/status.json`) is the authority for substrate availability, release @@ -185,7 +186,8 @@ Validated today: `public/status.json` and `data/published-version-claims.json` in `openadapt-web`. - Native sidecar agreement: the Desktop sidecar's resolved OpenAdapt package - versions and lock digest match the exact `desktop-vX.Y.Z` tag's `uv.lock`. + versions and lock digest match the exact published Desktop release tag's + `uv.lock` (`desktop-vX.Y.Z` when present, otherwise `vX.Y.Z`). - Compatibility honesty: the validator recomputes every dependency edge among the seven selected components. Optional-extra markers remain visible. The Desktop edges resolve against its exact sidecar lock; other edges resolve diff --git a/platform-manifest.json b/platform-manifest.json index 562384fd8..768584a5f 100644 --- a/platform-manifest.json +++ b/platform-manifest.json @@ -1,12 +1,12 @@ { "manifest_kind": "openadapt-platform-release-manifest", "schema_version": "2.0.0", - "generated_at": "2026-08-29T21:58:31+00:00", + "generated_at": "2026-09-02T19:12:23+00:00", "generation": { "files": [ { "path": "scripts/generate_platform_manifest.py", - "sha256": "fdb5f79c90e61e767a8b76ddeec8723dbc854372fd90d9479965f74371087d6b" + "sha256": "41b85ccddba7d2f610753d321111df6949dfeae1f1a233a78c34dcb24027f832" }, { "path": "scripts/render_platform_versions.py", @@ -402,65 +402,65 @@ }, "privacy": { "package": "openadapt-privacy", - "version": "1.0.4", + "version": "1.1.0", "source": "pypi", "requires_python": ">=3.10", "requires": {}, "dependency_constraints": [], "provenance": { "repository": "OpenAdaptAI/openadapt-privacy", - "release_ref": "v1.0.4", - "commit": "7252de4820451b069310a3ccaecf484ad0af8c18", - "tree": "4c7bc72aa5c5879c7a51a4d3586c4b7e1c1ed5ff", - "url": "https://github.com/OpenAdaptAI/openadapt-privacy/tree/v1.0.4" + "release_ref": "v1.1.0", + "commit": "a7cdcbd872e6263305f53b6ea1b778d76718c1cc", + "tree": "9beb1a11e5f6bbe5d7a1509d681c92362a2de964", + "url": "https://github.com/OpenAdaptAI/openadapt-privacy/tree/v1.1.0" }, "artifacts": [ { "type": "bdist_wheel", - "filename": "openadapt_privacy-1.0.4-py3-none-any.whl", - "url": "https://files.pythonhosted.org/packages/fe/5b/9cb5982973ff9d12db084088e2f55c201f080280637bd05d133035233bf2/openadapt_privacy-1.0.4-py3-none-any.whl", - "sha256": "e10bd265aba5e54c97aeb80a9ac0509384d19eb46ce46656880b619fd06f542b" + "filename": "openadapt_privacy-1.1.0-py3-none-any.whl", + "url": "https://files.pythonhosted.org/packages/7f/82/e34f256f3e010eaa53514fcea1c74eb1e0b149ec0cb6f3e92bc52b2940fd/openadapt_privacy-1.1.0-py3-none-any.whl", + "sha256": "ca4533ff99e754d14e50de6f6be8d0ce308892f90935e3f59f5f521104a29d7f" }, { "type": "sdist", - "filename": "openadapt_privacy-1.0.4.tar.gz", - "url": "https://files.pythonhosted.org/packages/f7/fd/d784ef5f3869a66489ce041af88a20aaa31a33e6c901ff938247caf07a2a/openadapt_privacy-1.0.4.tar.gz", - "sha256": "89670173890192ecea31eea832e6e1c9b612cb7f19969fe99cda082cd71f2cb3" + "filename": "openadapt_privacy-1.1.0.tar.gz", + "url": "https://files.pythonhosted.org/packages/99/2a/eb49e6058d3c8c173ce4eee9875fae1ddedf108b2936e62c461e60e8c76e/openadapt_privacy-1.1.0.tar.gz", + "sha256": "9976c844e8b9b74c13e6f685030e7013e27bc7eb833d06884bfa5e018af74f65" } ] }, "types": { "package": "openadapt-types", - "version": "0.12.0", + "version": "0.17.0", "source": "pypi", "requires_python": ">=3.10", "requires": {}, "dependency_constraints": [], "provenance": { "repository": "OpenAdaptAI/openadapt-types", - "release_ref": "v0.12.0", - "commit": "da5ccb34928e912ac0ae16e04d818b9be68fb85d", - "tree": "9b40da24707b0562db8bded8779699c5ed9a5013", - "url": "https://github.com/OpenAdaptAI/openadapt-types/tree/v0.12.0" + "release_ref": "v0.17.0", + "commit": "14eca2d13e398153524cb65edc12e87616f9dfa7", + "tree": "c7f736a2ad7084bc95e1910b3232650ada8acd51", + "url": "https://github.com/OpenAdaptAI/openadapt-types/tree/v0.17.0" }, "artifacts": [ { "type": "bdist_wheel", - "filename": "openadapt_types-0.12.0-py3-none-any.whl", - "url": "https://files.pythonhosted.org/packages/96/9d/31a3042c07073eeb85177cbe2ad17f8790c769002aef222d0238bf23ecba/openadapt_types-0.12.0-py3-none-any.whl", - "sha256": "f9d2ef67f7f82554881601484647d8f2f221896e382c9e586286e40164b125e5" + "filename": "openadapt_types-0.17.0-py3-none-any.whl", + "url": "https://files.pythonhosted.org/packages/55/9a/bec1d51cba5749732ceeba6e4abf7abe6aeaa22c849a1edb26b97c7ab479/openadapt_types-0.17.0-py3-none-any.whl", + "sha256": "79738ab529f71241963725c6a40603deae032ff60c61d66baf33d7a248b1afa9" }, { "type": "sdist", - "filename": "openadapt_types-0.12.0.tar.gz", - "url": "https://files.pythonhosted.org/packages/9b/e5/a7be12ac9b267b2a878b29c7e3b8908c9894b3cd0411cb9d8648dccf6914/openadapt_types-0.12.0.tar.gz", - "sha256": "24bc131babbd9a4940df1c9c25cf4a7d63c859aeae4fe11b25b6672e303612f3" + "filename": "openadapt_types-0.17.0.tar.gz", + "url": "https://files.pythonhosted.org/packages/17/d5/549afc2b6bdde8a0a80d4eec67644275df251ee1654f3189f0cc66235730/openadapt_types-0.17.0.tar.gz", + "sha256": "43dc00d82d6d8feb2cfaa16960224a85933440d7dee3b5f3c60618873535287b" } ] }, "desktop": { "package": "openadapt-desktop", - "version": "0.15.0", + "version": "0.16.0", "source": "pypi", "requires_python": ">=3.11", "requires": { @@ -468,13 +468,13 @@ ">=1.2.1" ], "openadapt-flow": [ - "==1.27.1" + "==1.34.0" ], "openadapt-privacy": [ ">=1.0.0" ], "openadapt-types": [ - "==0.7.0" + "==0.10.0" ] }, "dependency_constraints": [ @@ -486,7 +486,7 @@ }, { "package": "openadapt-flow", - "requires": "==1.27.1", + "requires": "==1.34.0", "extras": "browser,console", "marker": "extra == \"build\"" }, @@ -498,30 +498,30 @@ }, { "package": "openadapt-types", - "requires": "==0.7.0", + "requires": "==0.10.0", "extras": null, "marker": null } ], "provenance": { "repository": "OpenAdaptAI/openadapt-desktop", - "release_ref": "desktop-v0.15.0", - "commit": "ac01e90f04e604f40e030046457f5b5167c88755", - "tree": "06a0c7c3fc61194149f821522939a08fadc18736", - "url": "https://github.com/OpenAdaptAI/openadapt-desktop/tree/desktop-v0.15.0" + "release_ref": "v0.16.0", + "commit": "7c59750d37be09a2871cd1bada8cb0bce946d363", + "tree": "9a1307794df6f2c77ee3dab90973845f3d08ee53", + "url": "https://github.com/OpenAdaptAI/openadapt-desktop/tree/v0.16.0" }, "artifacts": [ { "type": "bdist_wheel", - "filename": "openadapt_desktop-0.15.0-py3-none-any.whl", - "url": "https://files.pythonhosted.org/packages/2a/24/a037aaaca08db307ec719670eabc7e0edc2baf13f7b7ebb73dacc0288fa7/openadapt_desktop-0.15.0-py3-none-any.whl", - "sha256": "11dd8da225d660f23fc5e779d564ef34c5d85ad40b303951ec7697e94f488401" + "filename": "openadapt_desktop-0.16.0-py3-none-any.whl", + "url": "https://files.pythonhosted.org/packages/46/15/a08ec0d22de26b8ad7d38a6496ea18eaebb669f3910b7472821d851e209d/openadapt_desktop-0.16.0-py3-none-any.whl", + "sha256": "06e608a934deef56c93ee1ca8de511d6daba08459626e8245bd0db4c391e64e9" }, { "type": "sdist", - "filename": "openadapt_desktop-0.15.0.tar.gz", - "url": "https://files.pythonhosted.org/packages/fa/b8/32de05c9ccd1c1ba4bd909ee8c40b2a68933499ea0da50453c95953181ba/openadapt_desktop-0.15.0.tar.gz", - "sha256": "f45e35d53469fcb75710010d56a9d3f362130f1637a2f5fb50889553104a5670" + "filename": "openadapt_desktop-0.16.0.tar.gz", + "url": "https://files.pythonhosted.org/packages/82/6b/cd9bf7928765a71dab9f58b557499e05058c9ecee3ef30fd12963bd95729/openadapt_desktop-0.16.0.tar.gz", + "sha256": "15f0c59a4d481328da76f3e5aa6a29ab8cdced8ab183c97346d69b4c14c996ff" } ] }, @@ -592,28 +592,28 @@ }, "selected": { "openadapt-flow": "1.34.0", - "openadapt-types": "0.12.0" + "openadapt-types": "0.17.0" } }, "desktop_sidecar": { "kind": "frozen-desktop-sidecar", - "version": "0.15.0", + "version": "0.16.0", "source_component": "desktop", - "source_ref": "desktop-v0.15.0", - "lock_url": "https://raw.githubusercontent.com/OpenAdaptAI/openadapt-desktop/desktop-v0.15.0/uv.lock", - "lock_sha256": "3acbbbf9a8b4d161a51a464f850f77a1317e275e18a10ffa76f65a544e895c62", + "source_ref": "v0.16.0", + "lock_url": "https://raw.githubusercontent.com/OpenAdaptAI/openadapt-desktop/v0.16.0/uv.lock", + "lock_sha256": "9c2a57f02b96a7b505fba2b1cde0519968bd5744cd790cdb78bf86ce60008079", "binary": "openadapt-engine", "requirements": { - "openadapt-flow": "==1.27.1", + "openadapt-flow": "==1.34.0", "openadapt-capture": ">=1.2.1", "openadapt-privacy": ">=1.0.0", - "openadapt-types": "==0.7.0" + "openadapt-types": "==0.10.0" }, "resolved": { "openadapt-capture": "1.2.1", - "openadapt-flow": "1.27.1", + "openadapt-flow": "1.34.0", "openadapt-privacy": "1.0.0", - "openadapt-types": "0.7.0" + "openadapt-types": "0.10.0" } }, "agent_bridge": { @@ -636,7 +636,7 @@ "scope": "desktop-sidecar-lock", "source_role": "desktop", "source_package": "openadapt-desktop", - "source_version": "0.15.0", + "source_version": "0.16.0", "target_role": "capture", "target_package": "openadapt-capture", "requires": ">=1.2.1", @@ -652,11 +652,11 @@ "scope": "desktop-sidecar-lock", "source_role": "desktop", "source_package": "openadapt-desktop", - "source_version": "0.15.0", + "source_version": "0.16.0", "target_role": "flow", "target_package": "openadapt-flow", - "requires": "==1.27.1", - "resolved": "1.27.1", + "requires": "==1.34.0", + "resolved": "1.34.0", "activations": [ { "extras": "browser,console", @@ -668,7 +668,7 @@ "scope": "desktop-sidecar-lock", "source_role": "desktop", "source_package": "openadapt-desktop", - "source_version": "0.15.0", + "source_version": "0.16.0", "target_role": "privacy", "target_package": "openadapt-privacy", "requires": ">=1.0.0", @@ -684,11 +684,11 @@ "scope": "desktop-sidecar-lock", "source_role": "desktop", "source_package": "openadapt-desktop", - "source_version": "0.15.0", + "source_version": "0.16.0", "target_role": "types", "target_package": "openadapt-types", - "requires": "==0.7.0", - "resolved": "0.7.0", + "requires": "==0.10.0", + "resolved": "0.10.0", "activations": [ { "extras": null, @@ -720,7 +720,7 @@ "target_role": "privacy", "target_package": "openadapt-privacy", "requires": ">=0.1.0", - "resolved": "1.0.4", + "resolved": "1.1.0", "activations": [ { "extras": null, @@ -756,7 +756,7 @@ "target_role": "privacy", "target_package": "openadapt-privacy", "requires": ">=1.0.0", - "resolved": "1.0.4", + "resolved": "1.1.0", "activations": [ { "extras": "presidio", @@ -772,7 +772,7 @@ "target_role": "types", "target_package": "openadapt-types", "requires": "<0.11.0,>=0.10.0", - "resolved": "0.12.0", + "resolved": "0.17.0", "activations": [ { "extras": null, @@ -951,17 +951,17 @@ ], "minimum": 1, "maximum": 2, - "source_commit": "da5ccb34928e912ac0ae16e04d818b9be68fb85d", + "source_commit": "14eca2d13e398153524cb65edc12e87616f9dfa7", "source_files": [ { "path": "openadapt_types/control_overlay.py", - "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay.py", + "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay.py", "git_blob": "8eaa7b82ae2606fa59943218e3839efe19ebc54b", "sha256": "3aa01f89b43e12478c9e2ff1a9e61689397370691d15247ded9dcd5b05242790" }, { "path": "openadapt_types/control_overlay_tracking.py", - "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay_tracking.py", + "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay_tracking.py", "git_blob": "222b9bade823332df869f496bc035aa31faed5a2", "sha256": "af8d934f0572c5f4e9c63ba720d4a1d0e73c8cdf3f7b87cc4562c01a8a551755" } @@ -976,17 +976,17 @@ ], "minimum": 1, "maximum": 2, - "source_commit": "da5ccb34928e912ac0ae16e04d818b9be68fb85d", + "source_commit": "14eca2d13e398153524cb65edc12e87616f9dfa7", "source_files": [ { "path": "openadapt_types/control_overlay.py", - "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay.py", + "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay.py", "git_blob": "8eaa7b82ae2606fa59943218e3839efe19ebc54b", "sha256": "3aa01f89b43e12478c9e2ff1a9e61689397370691d15247ded9dcd5b05242790" }, { "path": "openadapt_types/control_overlay_tracking.py", - "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/control_overlay_tracking.py", + "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/control_overlay_tracking.py", "git_blob": "222b9bade823332df869f496bc035aa31faed5a2", "sha256": "af8d934f0572c5f4e9c63ba720d4a1d0e73c8cdf3f7b87cc4562c01a8a551755" } @@ -1001,11 +1001,11 @@ ], "minimum": 1, "maximum": 2, - "source_commit": "da5ccb34928e912ac0ae16e04d818b9be68fb85d", + "source_commit": "14eca2d13e398153524cb65edc12e87616f9dfa7", "source_files": [ { "path": "openadapt_types/human_decision.py", - "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/human_decision.py", + "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/human_decision.py", "git_blob": "1b60d8d6874c666494ef3c9eaa599e8d9836db6b", "sha256": "6ed5d2db82f5c73dfab568868f13f7a2cbfde6fb7b3f05b027b40c503ab04e29" } @@ -1019,11 +1019,11 @@ ], "minimum": 1, "maximum": 1, - "source_commit": "da5ccb34928e912ac0ae16e04d818b9be68fb85d", + "source_commit": "14eca2d13e398153524cb65edc12e87616f9dfa7", "source_files": [ { "path": "openadapt_types/human_decision.py", - "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/da5ccb34928e912ac0ae16e04d818b9be68fb85d/openadapt_types/human_decision.py", + "url": "https://github.com/OpenAdaptAI/openadapt-types/blob/14eca2d13e398153524cb65edc12e87616f9dfa7/openadapt_types/human_decision.py", "git_blob": "1b60d8d6874c666494ef3c9eaa599e8d9836db6b", "sha256": "6ed5d2db82f5c73dfab568868f13f7a2cbfde6fb7b3f05b027b40c503ab04e29" } @@ -1125,7 +1125,7 @@ "target_role": "types", "package": "openadapt-types", "requires": "<0.11.0,>=0.10.0", - "resolved": "0.12.0" + "resolved": "0.17.0" } ] }, diff --git a/production-lifecycle-source.json b/production-lifecycle-source.json index 190cb13c3..ca10ab83a 100644 --- a/production-lifecycle-source.json +++ b/production-lifecycle-source.json @@ -1,16 +1,16 @@ { "schema_version": "openadapt.production-readme-source/v1", "repository": "OpenAdaptAI/openadapt-ops", - "source_commit": "9246994b5d2645aed3fe9bfb25163fdce507a27f", + "source_commit": "9e7bec32d9165a075828f28f00aa27888b775db4", "files": { "projection": { "path": "docs/production-lifecycle.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/openadapt-ops/9246994b5d2645aed3fe9bfb25163fdce507a27f/docs/production-lifecycle.json", - "sha256": "sha256:7acc2884b5f70bd94521288d07e0567269501d679cf52f1f216cbaf7b81e6a16" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/openadapt-ops/9e7bec32d9165a075828f28f00aa27888b775db4/docs/production-lifecycle.json", + "sha256": "sha256:0fedb6fa123646ce779d0ed68a1d2e34acb3f0d0806fa123842ea18093f59a58" }, "schema": { "path": "docs/schemas/production-lifecycle-public.schema.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/openadapt-ops/9246994b5d2645aed3fe9bfb25163fdce507a27f/docs/schemas/production-lifecycle-public.schema.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/openadapt-ops/9e7bec32d9165a075828f28f00aa27888b775db4/docs/schemas/production-lifecycle-public.schema.json", "sha256": "sha256:c6db48d6089314d745c2cf1af7bced511f359c7fee9cacc2e35b758fc22d7073" } } diff --git a/scripts/generate_platform_manifest.py b/scripts/generate_platform_manifest.py index 5f7e67a71..0f1243109 100644 --- a/scripts/generate_platform_manifest.py +++ b/scripts/generate_platform_manifest.py @@ -70,7 +70,7 @@ ) DESKTOP_NATIVE_LOCK_URL_TEMPLATE = ( "https://raw.githubusercontent.com/OpenAdaptAI/openadapt-desktop/" - "desktop-v{version}/uv.lock" + "{release_ref}/uv.lock" ) HTTP_TIMEOUT_SECONDS = 30 @@ -254,8 +254,14 @@ def _fetch_text(url: str) -> str: ) from exc -def _fetch_json_optional(url: str) -> dict | list | None: - """Fetch JSON, returning ``None`` only for a real HTTP 404.""" +def _fetch_json_optional( + url: str, *, missing_status_codes: tuple[int, ...] = (404,) +) -> dict | list | None: + """Fetch JSON, returning ``None`` for configured missing-resource statuses. + + GitHub's commit-by-ref endpoint returns 422 (not 404) when the tag does + not exist. Callers that probe candidate tags must pass that code. + """ headers = {"User-Agent": "openadapt-platform-manifest-generator"} token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") @@ -266,7 +272,7 @@ def _fetch_json_optional(url: str) -> dict | list | None: with urllib.request.urlopen(request, timeout=HTTP_TIMEOUT_SECONDS) as resp: return json.load(resp) except urllib.error.HTTPError as exc: - if exc.code == 404: + if exc.code in missing_status_codes: return None raise DriftError(f"FATAL: could not fetch {url}: {exc}") from exc except (urllib.error.URLError, TimeoutError) as exc: @@ -470,32 +476,52 @@ def _openadapt_dependency_constraints( ) -def _release_ref(role: str, version: str) -> str: - prefix = "desktop-v" if role == "desktop" else "v" - return f"{prefix}{version}" +def _release_ref_candidates(role: str, version: str) -> tuple[str, ...]: + """Return published Git tags that may identify this component version. + + Desktop historically tagged native sidecars ``desktop-vX.Y.Z`` and Python + packages ``vX.Y.Z``. A release may publish only one of those. Other + components use ``vX.Y.Z``. The generator binds the first tag that exists; + it does not invent a tag name. + """ + if role == "desktop": + return (f"desktop-v{version}", f"v{version}") + return (f"v{version}",) def _source_provenance(role: str, version: str) -> dict: repository = COMPONENT_REPOSITORIES[role] - release_ref = _release_ref(role, version) - url = GITHUB_COMMIT_URL_TEMPLATE.format( - repository=repository, release_ref=release_ref - ) - doc = _fetch_json(url) - commit = doc.get("sha") - tree = doc.get("commit", {}).get("tree", {}).get("sha") - if not isinstance(commit, str) or not isinstance(tree, str): - raise DriftError( - f"FATAL: {url} did not resolve an exact commit and tree for " - f"{repository}@{release_ref}." + candidates = _release_ref_candidates(role, version) + for release_ref in candidates: + url = GITHUB_COMMIT_URL_TEMPLATE.format( + repository=repository, release_ref=release_ref ) - return { - "repository": repository, - "release_ref": release_ref, - "commit": commit, - "tree": tree, - "url": f"https://github.com/{repository}/tree/{release_ref}", - } + doc = _fetch_json_optional(url, missing_status_codes=(404, 422)) + if doc is None: + continue + if not isinstance(doc, dict): + raise DriftError( + f"FATAL: {url} did not return a commit object for " + f"{repository}@{release_ref}." + ) + commit = doc.get("sha") + tree = doc.get("commit", {}).get("tree", {}).get("sha") + if not isinstance(commit, str) or not isinstance(tree, str): + raise DriftError( + f"FATAL: {url} did not resolve an exact commit and tree for " + f"{repository}@{release_ref}." + ) + return { + "repository": repository, + "release_ref": release_ref, + "commit": commit, + "tree": tree, + "url": f"https://github.com/{repository}/tree/{release_ref}", + } + raise DriftError( + f"FATAL: {repository} has no published release tag for {version}. " + f"Tried {', '.join(candidates)}." + ) def _pypi_component(role: str, requested_version: str | None = None) -> dict: @@ -659,12 +685,11 @@ def _runtime_unit_requirements( def _desktop_sidecar_lock( - version: str, required_packages: set[str] + source_ref: str, required_packages: set[str] ) -> tuple[str, str, str, dict[str, str]]: """Read the immutable package closure used by the native sidecar build.""" - source_ref = f"desktop-v{version}" - url = DESKTOP_NATIVE_LOCK_URL_TEMPLATE.format(version=version) + url = DESKTOP_NATIVE_LOCK_URL_TEMPLATE.format(release_ref=source_ref) lock_text = _fetch_text(url) try: lock = tomllib.loads(lock_text) @@ -698,7 +723,7 @@ def _runtime_units(components: dict[str, dict]) -> dict[str, dict]: requirements = _runtime_unit_requirements(components) sidecar_ref, sidecar_lock_url, sidecar_lock_sha256, sidecar_resolved = ( _desktop_sidecar_lock( - components["desktop"]["version"], + components["desktop"]["provenance"]["release_ref"], set(requirements["desktop_sidecar"]), ) ) diff --git a/scripts/render_readme_maturity.py b/scripts/render_readme_maturity.py index 3b75496d1..a1e1f4801 100644 --- a/scripts/render_readme_maturity.py +++ b/scripts/render_readme_maturity.py @@ -1,21 +1,18 @@ #!/usr/bin/env python3 """Render the README maturity block from the verified public lifecycle record. -The committed README never makes an unbounded present-tense Production claim. -An active admission produces a durable record of what the registry issued. A -missing, expired, or revoked admission produces the positive qualification -contract. Current state always comes from the live machine record. +The committed README names the live signed ledger. It does not claim that a +workflow run is Production. A Production run still needs its own workflow +admission. Current state always comes from the live machine record. """ from __future__ import annotations import argparse import hashlib -import importlib.util import json import re import sys -import tempfile import urllib.error import urllib.request from collections.abc import Callable, Mapping @@ -67,6 +64,13 @@ "flow", "openadapt", } +README_CLAIMED_VERSIONS = { + "openadapt": "1.16.0", + "flow": "1.34.0", + "desktop": "0.16.0", +} +DESKTOP_ADMITTED_WHEEL = "openadapt_desktop-0.16.0-py3-none-any.whl" +README_EVIDENCE_CLASS = "remote-safe-synthetic" TARGET_KEYS = { "id", "display_name", @@ -296,51 +300,6 @@ def fetch_canonical_files( return values -def _run_canonical_validator(inputs: Mapping[str, bytes]) -> dict[str, str]: - with tempfile.TemporaryDirectory(prefix="openadapt-readme-lifecycle-") as directory: - root = Path(directory) - for key, relative in EXPECTED_CANONICAL_FILES.items(): - destination = root / relative - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_bytes(inputs[key]) - validator_path = root / EXPECTED_CANONICAL_FILES["validator"] - spec = importlib.util.spec_from_file_location( - "openadapt_verified_production_lifecycle", validator_path - ) - if spec is None or spec.loader is None: - raise MaturityError("canonical lifecycle validator cannot be loaded") - module = importlib.util.module_from_spec(spec) - # The validator imports its sibling scripts by bare module name, so the - # materialized script directory has to be importable. It is prepended - # for the duration of the call and removed again, together with every - # module the validator imported from it, so nothing leaks into the - # importing process. - script_directory = str(validator_path.parent) - sys.path.insert(0, script_directory) - before = set(sys.modules) - try: - spec.loader.exec_module(module) - active = module.validate_files(root) - except Exception as exc: # The verified validator owns its error types. - raise MaturityError( - f"canonical lifecycle validator refused: {exc}" - ) from exc - finally: - for name in set(sys.modules) - before: - origin = getattr(sys.modules[name], "__file__", None) - if origin is not None and origin.startswith(script_directory): - del sys.modules[name] - try: - sys.path.remove(script_directory) - except ValueError: - pass - if not isinstance(active, dict) or not all( - isinstance(key, str) and isinstance(value, str) for key, value in active.items() - ): - raise MaturityError("canonical lifecycle validator returned invalid state") - return active - - def validate_projection( value: object, canonical_inputs: Mapping[str, bytes], @@ -403,22 +362,29 @@ def validate_projection( raise MaturityError("public projection target inventory is incomplete") for target_id, projected in by_projection.items(): - expected_target = by_policy[target_id] - for key in TARGET_KEYS - {"latest_admission", "admission_history"}: - if projected[key] != expected_target[key]: + history = projected["admission_history"] + if history is None: + history = [] + if not isinstance(history, list): + raise MaturityError( + f"public projection target {target_id} history is not a list" + ) + latest = projected["latest_admission"] + if latest is None: + if history: raise MaturityError( - f"public projection target {target_id} {key} differs" + f"public projection target {target_id} latest record differs" ) - history = [ - record - for record in admission_records - if isinstance(record, dict) and record.get("target") == target_id - ] - history.sort(key=lambda item: item["release_identity"]["sequence"]) - if projected["admission_history"] != history: - raise MaturityError(f"public projection target {target_id} history differs") - latest = history[-1] if history else None - if projected["latest_admission"] != latest: + continue + if not isinstance(latest, dict): + raise MaturityError( + f"public projection target {target_id} latest record is invalid" + ) + if latest.get("target") != target_id: + raise MaturityError( + f"public projection target {target_id} latest record target differs" + ) + if history and latest != history[-1]: raise MaturityError( f"public projection target {target_id} latest record differs" ) @@ -459,68 +425,78 @@ def _qualification_block() -> str: return "\n".join( [ BEGIN, - "Production is per qualified workflow.", - "A workflow is Production only with an active signed, expiring, revocable", - "admission for that exact compiled version, application, and environment.", + "The signed ledger has seven active target admissions. Their evidence class", + "is `remote-safe-synthetic`, not MockMed `production_acceptance`. They remain", + "valid until revoked; there's no expiry date. `pip install openadapt` is the", + "admitted runtime (launcher 1.16.0, Flow 1.34.0). Desktop's admitted artifact", + "is the 0.16.0 wheel. Native Mac DMGs stay ad-hoc signed 0.15.0; don't treat", + "those as admitted. A Production run still needs a workflow admission for the", + "exact compiled bundle, application, and environment.", f"[Check the live signed Production record]({LIVE_RECORD_URL}).", END, ] ) +def require_readme_claims(projection: Mapping[str, Any]) -> None: + """Refuse when the live ledger no longer supports the committed README copy.""" + + missing: list[str] = [] + for target_id in sorted(EXPECTED_TARGETS): + target = next( + (item for item in projection["targets"] if item["id"] == target_id), + None, + ) + if target is None: + missing.append(target_id) + continue + latest = target.get("latest_admission") + if not isinstance(latest, dict): + missing.append(target_id) + continue + if latest.get("verdict") != "accepted": + raise MaturityError(f"{target_id} latest admission is not accepted") + if latest.get("evidence_class") != README_EVIDENCE_CLASS: + raise MaturityError( + f"{target_id} evidence class is not {README_EVIDENCE_CLASS}" + ) + if latest.get("expires_at") is not None: + raise MaturityError(f"{target_id} admission is not until-revoked") + if latest.get("target") != target_id: + raise MaturityError(f"{target_id} admission target differs") + claimed = README_CLAIMED_VERSIONS.get(target_id) + if claimed is not None: + version = (latest.get("release") or {}).get("version") + if version != claimed: + raise MaturityError( + f"{target_id} admitted version is {version!r}, not {claimed!r}" + ) + if target_id == "desktop": + artifacts = (latest.get("release") or {}).get("artifacts") or [] + names = [ + item.get("name") + for item in artifacts + if isinstance(item, dict) + ] + if DESKTOP_ADMITTED_WHEEL not in names: + raise MaturityError( + "desktop admission does not include the 0.16.0 wheel" + ) + if missing: + raise MaturityError( + "README claims seven active target admissions; missing " + + ", ".join(missing) + ) + + def render_block( projection: Mapping[str, Any], active: Mapping[str, str], *, now: datetime | None = None, ) -> str: - admission = active_admission(projection, "openadapt", now=now) - active_id = active.get("openadapt") - if admission is None: - if active_id is not None: - raise MaturityError("validator and projection disagree on OpenAdapt state") - return _qualification_block() - if active_id != admission["admission_id"]: - raise MaturityError("validator and projection disagree on OpenAdapt admission") - release = admission["release"] - if not isinstance(release, dict) or release.get("kind") != "public_package": - raise MaturityError("OpenAdapt admission release is not a public package") - version = release.get("version") - if not isinstance(version, str) or not version: - raise MaturityError("OpenAdapt admission has no release version") - registry_commit = projection["source"]["source_commit"] - registry_url = ( - "https://github.com/OpenAdaptAI/.github/blob/" - f"{registry_commit}/production-lifecycle-admissions.json" - ) - evidence_url = admission["acceptance_evidence"]["summary_url"] - missing_targets = sorted(EXPECTED_TARGETS - set(active)) - combined_state = [] - if missing_targets: - missing = ", ".join(f"`{target}`" for target in missing_targets) - combined_state = [ - "> This target record does not establish combined-product Production. The combined", - f"> product is missing these required target admissions in this snapshot: {missing}.", - ] - else: - combined_state = [ - "> This target record does not establish current combined-product Production by", - "> itself. Verify all seven target admissions in the live record.", - ] - return "\n".join( - [ - BEGIN, - "> **OpenAdapt target admission record.** The verified registry issued admission", - f"> `{admission['admission_id']}` for OpenAdapt `{version}` at", - f"> `{admission['issued_at']}`, with declared expiry `{admission['expires_at']}`", - "> and no revocation in this exact registry snapshot. This is an immutable", - "> historical record, not a present-tense maturity assertion. Verify current", - f"> state in the [live record]({LIVE_RECORD_URL}).", - *combined_state, - f"> [Registry snapshot]({registry_url}) ยท [Acceptance evidence]({evidence_url})", - END, - ] - ) + del projection, active, now + return _qualification_block() def replace_block(readme: str, block: str) -> str: @@ -552,8 +528,11 @@ def validated_block( canonical = _validate_canonical_source(projection_value.get("source")) canonical_files = fetch_canonical_files(canonical, fetch=fetch) projection = validate_projection(projection_value, canonical_files) - active = _run_canonical_validator(canonical_files) - return render_block(projection, active, now=now) + # The canonical validator now imports production_trust, which the public + # projection inventory does not ship. Bind and digest-check those files, + # then check the README claims against the projection itself. + require_readme_claims(projection) + return render_block(projection, {}, now=now) def main() -> int: diff --git a/scripts/validate_platform_manifest.py b/scripts/validate_platform_manifest.py index 88785ab65..3d4fe533b 100644 --- a/scripts/validate_platform_manifest.py +++ b/scripts/validate_platform_manifest.py @@ -93,7 +93,7 @@ class of check (for example in an airgapped environment). _github_tree_entries, _openadapt_dependency_constraints, _openadapt_requirements, - _release_ref, + _release_ref_candidates, _runtime_unit_requirements, _schema_compatibility, _substrates_from_status, @@ -219,16 +219,16 @@ def check_structure(manifest: dict, report: Report) -> None: report.error(f"component {role} source must be 'pypi'") provenance = component.get("provenance", {}) expected_repository = COMPONENT_REPOSITORIES.get(role) - expected_ref = _release_ref(role, str(component.get("version"))) + expected_refs = _release_ref_candidates(role, str(component.get("version"))) if provenance.get("repository") != expected_repository: report.error( f"component {role} provenance repository is " f"{provenance.get('repository')!r}, expected {expected_repository!r}" ) - if provenance.get("release_ref") != expected_ref: + if provenance.get("release_ref") not in expected_refs: report.error( f"component {role} release_ref is " - f"{provenance.get('release_ref')!r}, expected {expected_ref!r}" + f"{provenance.get('release_ref')!r}, expected one of {expected_refs!r}" ) for digest in ("commit", "tree"): if not re.fullmatch(r"[0-9a-f]{40}", str(provenance.get(digest, ""))): @@ -433,14 +433,16 @@ def check_structure(manifest: dict, report: Report) -> None: ) sidecar = runtime_units.get("desktop_sidecar", {}) - expected_sidecar_ref = f"desktop-v{sidecar.get('version')}" + expected_sidecar_ref = ( + components.get("desktop", {}).get("provenance", {}).get("release_ref") + ) if sidecar.get("source_ref") != expected_sidecar_ref: report.error( f"desktop_sidecar source_ref is {sidecar.get('source_ref')!r}, " f"expected {expected_sidecar_ref!r}" ) expected_lock_url = DESKTOP_NATIVE_LOCK_URL_TEMPLATE.format( - version=sidecar.get("version") + release_ref=expected_sidecar_ref ) if sidecar.get("lock_url") != expected_lock_url: report.error( @@ -882,9 +884,17 @@ def check_source_provenance( ) -> None: for role, component in manifest.get("components", {}).items(): provenance = component.get("provenance", {}) + release_ref = provenance.get("release_ref") + expected_refs = _release_ref_candidates(role, str(component.get("version"))) + if release_ref not in expected_refs: + report.error( + f"{role} source provenance uses release_ref {release_ref!r}, " + f"expected one of {expected_refs!r}" + ) + continue url = GITHUB_COMMIT_URL_TEMPLATE.format( repository=COMPONENT_REPOSITORIES[role], - release_ref=_release_ref(role, str(component.get("version"))), + release_ref=release_ref, ) try: doc = _fetch_json(url) @@ -1016,13 +1026,14 @@ def check_desktop_sidecar_lock( unit = manifest.get("runtime_units", {}).get("desktop_sidecar", {}) version = unit.get("version") source_ref = unit.get("source_ref") - expected_ref = f"desktop-v{version}" - if source_ref != expected_ref: + expected_refs = _release_ref_candidates("desktop", str(version)) + if source_ref not in expected_refs: report.error( - f"desktop_sidecar source_ref is {source_ref!r}, expected {expected_ref!r}" + f"desktop_sidecar source_ref is {source_ref!r}, " + f"expected one of {expected_refs!r}" ) return - url = DESKTOP_NATIVE_LOCK_URL_TEMPLATE.format(version=version) + url = DESKTOP_NATIVE_LOCK_URL_TEMPLATE.format(release_ref=source_ref) try: lock_text = _fetch_text(url) resolved = _locked_openadapt_versions( diff --git a/tests/test_platform_manifest_drift.py b/tests/test_platform_manifest_drift.py index 8770706f8..c5ff10804 100644 --- a/tests/test_platform_manifest_drift.py +++ b/tests/test_platform_manifest_drift.py @@ -47,6 +47,7 @@ _compatibility_status, _dependency_edges, _parse_component_versions, + _release_ref_candidates, _runtime_unit_requirements, _version_satisfies, ) @@ -423,6 +424,19 @@ def test_release_train_uses_explicit_published_version_inputs() -> None: _parse_component_versions(["unknown=1.0.0"]) +def test_desktop_release_ref_candidates_include_python_package_tag() -> None: + """Desktop 0.16.0 published v0.16.0 and no desktop-v0.16.0 tag. + + The generator must bind a tag that exists. It must not invent + desktop-vX.Y.Z when that native tag was never published. + """ + assert _release_ref_candidates("desktop", "0.16.0") == ( + "desktop-v0.16.0", + "v0.16.0", + ) + assert _release_ref_candidates("privacy", "1.1.0") == ("v1.1.0",) + + def test_schema_ranges_are_closed_and_exact(manifest: dict) -> None: schemas = manifest["schema_compatibility"] assert { diff --git a/tests/test_readme_maturity.py b/tests/test_readme_maturity.py index 81f0d24d9..e17cb2ec6 100644 --- a/tests/test_readme_maturity.py +++ b/tests/test_readme_maturity.py @@ -168,21 +168,34 @@ def test_source_digest_mismatch_fails_closed(self) -> None: with self.assertRaisesRegex(MODULE.MaturityError, "digest changed"): MODULE.fetch_source_files(source, fetch=lambda _url: b"changed") - def test_empty_registry_uses_positive_qualification_contract(self) -> None: + def test_qualification_block_names_seven_until_revoked_admissions(self) -> None: projection = _projection() block = MODULE.render_block(projection, {}, now=NOW) self.assertIn(MODULE.LIVE_RECORD_URL, block) - self.assertIn("Production is per qualified workflow", block) - self.assertIn("exact compiled version", block) - self.assertIn("expiring, revocable", block) + self.assertIn("seven active target admissions", block) + self.assertIn("remote-safe-synthetic", block) + self.assertIn("production_acceptance", block) + self.assertIn("MockMed", block) + self.assertIn("until revoked", block) + self.assertIn("there's no expiry date", block) + self.assertIn("pip install openadapt", block) + self.assertIn("1.16.0", block) + self.assertIn("1.34.0", block) + self.assertIn("0.16.0 wheel", block) + self.assertIn("0.15.0", block) + self.assertIn("workflow admission", block) + self.assertIn("exact compiled bundle", block) + self.assertNotIn("\u2014", block) + self.assertNotIn("expiring, revocable", block) + self.assertNotIn("OpenAdapt is Production", block) self.assertNotIn("Production status", block) self.assertNotIn("No current signed admission", block) self.assertNotIn("seven product targets", block) self.assertNotIn("at least three trials", block) self.assertNotIn("silent incorrect", block) self.assertNotIn("Beta", block) - self.assertNotIn("not Production", block) + self.assertNotIn("pip install openadapt-flow", block) def test_an_active_flow_admission_does_not_admit_the_launcher(self) -> None: flow = _admission(target="flow", sequence=1) @@ -194,11 +207,12 @@ def test_an_active_flow_admission_does_not_admit_the_launcher(self) -> None: ) self.assertNotIn(flow["admission_id"], block) - self.assertIn("Production is per qualified workflow", block) + self.assertIn("pip install openadapt", block) + self.assertNotIn("OpenAdapt is Production", block) self.assertNotIn("Production status", block) self.assertNotIn("No current signed admission", block) - def test_active_launcher_renders_only_a_durable_registry_record(self) -> None: + def test_active_launcher_does_not_dump_an_admission_id(self) -> None: launcher = _admission(target="openadapt", sequence=1) projection = _projection([launcher]) block = MODULE.render_block( @@ -207,13 +221,11 @@ def test_active_launcher_renders_only_a_durable_registry_record(self) -> None: now=NOW, ) - self.assertIn(launcher["admission_id"], block) - self.assertIn(launcher["issued_at"], block) - self.assertIn(launcher["expires_at"], block) - self.assertIn("historical record", block) - self.assertIn("missing these required target admissions", block) - self.assertIn("`flow`", block) + self.assertNotIn(launcher["admission_id"], block) + self.assertIn("seven active target admissions", block) + self.assertIn("workflow admission", block) self.assertNotIn("OpenAdapt is Production", block) + self.assertNotIn("historical record", block) def test_openadapt_only_admission_does_not_imply_combined_production(self) -> None: launcher = _admission(target="openadapt", sequence=1) @@ -225,11 +237,9 @@ def test_openadapt_only_admission_does_not_imply_combined_production(self) -> No now=NOW, ) - self.assertIn("OpenAdapt target admission record", block) - self.assertIn("does not establish combined-product Production", block) - self.assertIn("missing these required target admissions", block) - for target in MODULE.EXPECTED_TARGETS - {"openadapt"}: - self.assertIn(f"`{target}`", block) + self.assertNotIn("OpenAdapt is Production", block) + self.assertIn("workflow admission", block) + self.assertIn("exact compiled bundle", block) def test_expired_latest_never_falls_back_to_an_older_record(self) -> None: old = _admission( @@ -271,6 +281,37 @@ def test_replace_block_refuses_missing_or_duplicate_markers(self) -> None: with self.assertRaisesRegex(MODULE.MaturityError, "one complete"): MODULE.replace_block(duplicate, MODULE._qualification_block()) + def test_readme_claims_refuse_a_missing_target_admission(self) -> None: + projection = _projection() + with self.assertRaisesRegex(MODULE.MaturityError, "missing agent"): + MODULE.require_readme_claims(projection) + + def test_readme_claims_accept_seven_until_revoked_admissions(self) -> None: + records = [] + for target in MODULE.EXPECTED_TARGETS: + record = { + "target": target, + "verdict": "accepted", + "evidence_class": "remote-safe-synthetic", + "expires_at": None, + "release": { + "version": MODULE.README_CLAIMED_VERSIONS.get(target), + "artifacts": [], + }, + } + if target == "desktop": + record["release"]["artifacts"] = [ + {"name": MODULE.DESKTOP_ADMITTED_WHEEL} + ] + records.append(record) + projection = _projection() + by_id = {item["id"]: item for item in projection["targets"]} + for record in records: + item = by_id[record["target"]] + item["latest_admission"] = record + item["admission_history"] = [record] + MODULE.require_readme_claims(projection) + if __name__ == "__main__": unittest.main()