diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index 8c29c39..298e83e 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -73,6 +73,9 @@ jobs:
# The curated Concepts and Ecosystem sections replace it. Re-enable only
# if a maintained, product-accurate architecture page is reintroduced.
+ - name: Validate published version claims
+ run: uv run python scripts/check_published_version_claims.py --offline
+
- name: Run tests against generated source
run: uv run pytest tests/ -q
diff --git a/README.md b/README.md
index 80a6c52..a49216f 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,19 @@ uv run mkdocs build --strict
uv run python scripts/validate_docs.py
```
+The managed-runtime artifact tuple has one editable source:
+`docs/published-version-claims.json`. Change the claim's `version`, release
+commit, archive hashes, source-manifest evidence, and `verified_on` fields once,
+then render all marked pages:
+
+```bash
+uv run python scripts/render_published_version_claims.py
+uv run python scripts/render_published_version_claims.py --check
+```
+
+Do not edit the marked page values directly. The offline version-claim gate and
+the documentation deployment both fail when the committed render is stale.
+
## Tests
```bash
diff --git a/docs/concepts/backends.md b/docs/concepts/backends.md
index c3acf92..04f70f8 100644
--- a/docs/concepts/backends.md
+++ b/docs/concepts/backends.md
@@ -28,8 +28,10 @@ the highest-fidelity signal each surface offers.
### Web: Playwright
-A headless Chromium driven by Playwright drives the web substrate, and every
-example in these docs uses it. It exposes a full structured layer:
+Playwright drives the web substrate. Flow can launch Chromium or attach to one
+existing signed-in local Chromium tab through a loopback CDP endpoint. Both
+entry modes use the same recorder, compiler format, and governed runtime. The
+browser exposes a full structured layer:
- **Structural rung**: reads the DOM element under a point, so resolution and
[identity](identity-gate.md) can use stable selectors and structured text
@@ -37,10 +39,25 @@ example in these docs uses it. It exposes a full structured layer:
- **Structural postconditions**: URL change, title change, new-tab opened.
- **CI-friendly**: no OS permissions, no display server; the whole record →
compile → replay loop runs in a container.
+- **Existing-session recording**: attach mode preserves a dedicated browser
+ profile that has already completed sign-in, SSO, or 2FA. Flow refuses remote
+ endpoints and ambiguous same-origin tabs. It does not navigate or close the
+ external browser. It records viewport and monitor-scale transitions as new
+ per-event coordinate baselines. An idle transition rebaselines and continues.
+ An action that overlaps an unverified transition aborts the recording and
+ publishes no complete metadata.
It shares the same bundle, resolution ladder, and identity gate as every other
substrate; nothing about the safety model is specific to it.
+The Chrome extension code in `openadapt-capture` does not define a second
+compiler or direct replay path. An extension acquisition path must use Flow's
+shared event and evidence schema, capture-time secret exclusion, authenticated
+session identity, acknowledged ordered delivery, and exact event/frame/viewport
+binding before Flow accepts its recording. Source-time secret exclusion, DOM
+identity, field geometry, and exact event/frame binding stay inside the
+compiler contract.
+
### Desktop: Windows (UIA)
The public `WindowsBackend` now narrows the in-session boundary to typed
diff --git a/docs/concepts/deployment-matrix.md b/docs/concepts/deployment-matrix.md
index 4761f4b..5b18bab 100644
--- a/docs/concepts/deployment-matrix.md
+++ b/docs/concepts/deployment-matrix.md
@@ -108,11 +108,11 @@ synthetic. A missing production dependency makes the affected operation
unavailable rather than substituting a simulated success.
The retained hosted-recorder qualification used a Flow 1.8.0 worker. The
-current Cloud managed-runtime manifest pins Flow 1.31.0 at release commit
-`2d225dea9a0ad29ca84ce1b037cc0ac671367e28`. Its wheel SHA-256 is
-`81133db1528ad1bb1f26e3fcb6aea61b0651db6d905cf2e4943e8383c1f3d29c` and
+current Cloud managed-runtime manifest pins Flow 1.31.0 at release commit
+2d225dea9a0ad29ca84ce1b037cc0ac671367e28. Its wheel SHA-256 is
+81133db1528ad1bb1f26e3fcb6aea61b0651db6d905cf2e4943e8383c1f3d29c and
its source SHA-256 is
-`cf1fc356d14d267df82be188de3e9a3575734f18f46ef91ac8075438cc731540`.
+cf1fc356d14d267df82be188de3e9a3575734f18f46ef91ac8075438cc731540.
The pin proves configured artifact identity. It does not prove that the build is
deployed or that a hosted workflow passed acceptance. Public readiness checks
live mode, authentication, database, private storage, runner, compiler,
diff --git a/docs/desktop/connect-to-cloud.md b/docs/desktop/connect-to-cloud.md
index e8c2343..1c2ec50 100644
--- a/docs/desktop/connect-to-cloud.md
+++ b/docs/desktop/connect-to-cloud.md
@@ -6,8 +6,13 @@ description: >-
# Connect the desktop app to a cloud workspace
-OpenAdapt Desktop is a **Beta supporting surface**. The local loop (record, compile, replay, teach) works offline and needs no
-account. A **cloud workspace** (`app.openadapt.ai`) adds an org-wide dashboard:
+OpenAdapt Desktop supplies the local record, compile, replay, and teach loop.
+It works offline and needs no account.
+
+**Desktop Production contract:**
+Production requires an active signed admission for this exact release or deployment.
+
+A **cloud workspace** (`app.openadapt.ai`) adds an org-wide dashboard:
pushed workflows are visible and runs needing attention are triaged. This page
covers the path: sign in → mint an ingest token → connect the desktop app → push
a recording → see it in the dashboard.
diff --git a/docs/desktop/install.md b/docs/desktop/install.md
index 21e1aaf..de13c88 100644
--- a/docs/desktop/install.md
+++ b/docs/desktop/install.md
@@ -22,11 +22,14 @@ you explicitly push it to a [cloud workspace](connect-to-cloud.md).
Get the installer from
[openadapt.ai/download](https://openadapt.ai/download). The page detects your OS
-and architecture and offers the right build. Public Beta release
-`desktop-v0.15.0` ships the complete Windows, macOS, and Linux installer set with
+and architecture and offers the right build. Release `desktop-v0.15.0` ships
+the complete Windows, macOS, and Linux installer set with
`SHA256SUMS`, a CycloneDX SBOM, per-platform metadata, and build-provenance
attestations.
+**Current Desktop release admission:**
+Production requires an active signed admission for this exact release or deployment.
+
| OS | Installer |
|---|---|
| Windows | `.msi` or `.exe` |
diff --git a/docs/ecosystem/index.md b/docs/ecosystem/index.md
index 663bf14..7cc6d7c 100644
--- a/docs/ecosystem/index.md
+++ b/docs/ecosystem/index.md
@@ -1,67 +1,70 @@
-# Package and repository lifecycle
+# Product components and release admission
-OpenAdapt the product (the [demonstration compiler](../concepts/demonstration-compiler.md))
-is what most people need. Underneath it sits a set of open-source libraries and
-infrastructure the compiler and its research build on. This section is for
-contributors and integrators who use those pieces directly.
+OpenAdapt is one product. Install `openadapt` and use `openadapt flow …`. The
+public repositories below supply the product's launcher, compiler, recorder,
+desktop cockpit, agent bridge, and trust surfaces.
-!!! note "Product first"
- If you want to compile and run a workflow, start with
- [Get started](../get-started/index.md). The libraries below are the building
- blocks behind OpenAdapt, not the way most users interact with it. Each links
- to its source repository, where its own README is the source of truth.
+Each release has its own evidence state. The state cells below state the
+Production requirement. In the browser, a cell changes to **Production** only
+when the current canonical admission record contains a live, non-revoked
+admission for that exact release. The browser validates the live record against
+the hash in this documentation deployment. It does not use an older admission
+when the newest admission expires or is revoked. The site-wide product label
+also checks current PyPI metadata for
+`openadapt`, `openadapt-flow`, `openadapt-capture`, `openadapt-desktop`, and
+`openadapt-agent`. Each default version must equal its active admitted release,
+and that version must retain unyanked wheel and source artifacts.
-The end-user identity is deliberately singular: install `openadapt` and run
-`openadapt flow …`. The `openadapt-flow` repository is where contributors
-inspect and change the engine. See [Versions and compatibility](../reference/compatibility.md)
-for the tested package ranges.
+[Read the Production admission contract](../reference/production-lifecycle.md)
-## Lifecycle labels
+## Product components
-These labels describe the public role of a repository, not the quality of every
-module inside it:
+| Component | Current release admission | Public role |
+|---|---|---|
+| [OpenAdapt](https://github.com/OpenAdaptAI/OpenAdapt) | Production requires an active signed admission for this exact release or deployment. | Installer, meta-package, and unified `openadapt flow` command. |
+| [OpenAdapt Flow](https://github.com/OpenAdaptAI/openadapt-flow) | Production requires an active signed admission for this exact release or deployment. | Canonical demonstration compiler and governed runtime for Browser, native Windows, native macOS, native Linux, RDP, and Citrix/VDI. |
+| [OpenAdapt Cloud](https://app.openadapt.ai/) | Production requires an active signed admission for this exact release or deployment. | Managed control plane for organizations, exact-hash admission, browser runners, reports, billing, and usage. |
+| [OpenAdapt Desktop](https://github.com/OpenAdaptAI/openadapt-desktop) | Production requires an active signed admission for this exact release or deployment. | Windows, macOS, and Linux cockpit for recording, compilation, qualification, replay, and local review. |
+| [OpenAdapt Agent](https://github.com/OpenAdaptAI/openadapt-agent) | Production requires an active signed admission for this exact release or deployment. | Governed bridge from MCP clients and Agent Skills to exact Flow bundles. |
+| [OpenAdapt Capture](https://github.com/OpenAdaptAI/openadapt-capture) | Production requires an active signed admission for this exact release or deployment. | Canonical native recorder for screen, mouse, keyboard, timing, window scope, and media. Windows UIA evidence is emitted with the captured action. The shared observer protocol defines the macOS Accessibility and Linux AT-SPI integration boundary. RDP and Citrix remain pixel-observed at the remote boundary. |
+| OpenAdapt documentation | Production requires an active signed admission for this exact release or deployment. | Version-bound product, operation, security, and qualification documentation at this site. |
-- **Beta**: an active product or component on the supported product path.
-- **Experimental**: an active integration whose contract is still being proven.
-- **Research**: evidence or model work, not required by deterministic replay.
-- **Deprecated**: retained for history or migration; no new integrations.
+Browser recording stays in Flow because DOM identity, field geometry,
+source-time secret masking, and Playwright execution use one browser session
+contract. Flow can launch Chromium or attach to one selected local tab. The
+Chrome extension code in the Capture repository does not define a second
+compiler or direct replay path. An extension acquisition path must satisfy the
+same authenticated event, evidence, masking, and frame-binding contract before
+Flow accepts its recording.
-## Product and optional components
+## Trust and interoperability libraries
-| Repository | Lifecycle | Public role |
-|---|---|---|
-| [OpenAdapt](https://github.com/OpenAdaptAI/OpenAdapt) | **Beta** | Installer/meta-package and unified `openadapt flow` dispatcher. |
-| [openadapt-flow](https://github.com/OpenAdaptAI/openadapt-flow) | **Beta** | Canonical compiler and governed runtime. Drives web, native Windows, native macOS, native Linux, RDP, and Citrix/VDI as first-class substrates behind one backend protocol. |
-| [OpenAdapt Cloud](https://app.openadapt.ai/) | **Beta** | Proprietary live control plane for the managed subscription: organizations, exact-hash admission, runner orchestration, reports, billing, and usage. |
-| [openadapt-desktop](https://github.com/OpenAdaptAI/openadapt-desktop) | **Beta** | Public `desktop-v0.15.0` provides Windows MSI/NSIS, macOS arm64/x64 DMG, and Linux AppImage/DEB installers. Every installer path is installed, launched, and uninstalled in the native release workflow; the release includes exact checksums, a CycloneDX SBOM, platform metadata, and build-provenance attestations. |
-| [openadapt-agent](https://github.com/OpenAdaptAI/openadapt-agent) | **Experimental** | Active v2 bridge that exposes governed Flow bundles to MCP clients and Agent Skills. The pre-v2 model-driven execution wrapper is the deprecated line; the repository itself is active. |
-| [openadapt-capture](https://github.com/OpenAdaptAI/openadapt-capture) | **Beta** | Canonical native screen, mouse, keyboard, timing, and window-scoped recorder behind Flow's Windows, macOS, Linux, RDP, and Citrix recording paths. Capture 1.1 retains Windows UIA evidence at action time; remote sessions remain externally black-box. Browser recording remains inside Flow's Playwright listener. |
-| [openadapt-privacy](https://github.com/OpenAdaptAI/openadapt-privacy) | **Experimental** | Optional PII/PHI scrubbing used on configured persist, log, and upload paths. |
-| [openadapt-types](https://github.com/OpenAdaptAI/openadapt-types) | **Experimental** | Shared interoperability schemas; contributor-facing, not an end-user product. |
+| Repository | Public role |
+|---|---|
+| [openadapt-privacy](https://github.com/OpenAdaptAI/openadapt-privacy) | Optional PII/PHI sanitization for configured persistence, logs, and artifact egress. |
+| [openadapt-types](https://github.com/OpenAdaptAI/openadapt-types) | Shared interoperability schemas for contributors and integrators. |
-## Research components
+## Evaluation and model development
-| Repository | Lifecycle | Public role |
-|---|---|---|
-| [openadapt-ml](https://github.com/OpenAdaptAI/openadapt-ml) | **Research** | VLM training, inference, and demonstration-conditioning work. Not required by the healthy compiler path. |
-| [openadapt-evals](https://github.com/OpenAdaptAI/openadapt-evals) | **Research** | GUI-agent and demonstration-conditioning evaluation infrastructure. |
-| [openadapt-grounding](https://github.com/OpenAdaptAI/openadapt-grounding) | **Research** | UI grounding experiments and model adapters. |
-| [openadapt-retrieval](https://github.com/OpenAdaptAI/openadapt-retrieval) | **Research** | Demonstration retrieval experiments. |
+| Repository | Public role |
+|---|---|
+| [openadapt-ml](https://github.com/OpenAdaptAI/openadapt-ml) | Optional VLM training, inference, and demonstration-conditioning work. The healthy compiler path does not require it. |
+| [openadapt-evals](https://github.com/OpenAdaptAI/openadapt-evals) | GUI workflow and demonstration-conditioning evaluation infrastructure. |
+| [openadapt-grounding](https://github.com/OpenAdaptAI/openadapt-grounding) | UI grounding mechanisms and model adapters. |
+| [openadapt-retrieval](https://github.com/OpenAdaptAI/openadapt-retrieval) | Demonstration retrieval mechanisms. |
## How the pieces fit
```mermaid
flowchart LR
- C[openadapt-capture
optional recording] --> F[[openadapt-flow
canonical compiler]]
+ C[openadapt-capture
native recording] --> F[[openadapt-flow
canonical compiler]]
+ B[Playwright browser
recording] --> F
F --> R[Deterministic
replay bundle]
- A[openadapt-agent
MCP + Agent Skills bridge] --> F
- ML[openadapt-ml
grounding / identity models] -.optional on-prem appliance.-> F
- E[openadapt-evals
benchmarks] -.measures.-> F
+ A[openadapt-agent
governed bridge] --> F
+ ML[optional models] -.resolve under drift.-> F
+ E[openadapt-evals] -.measures.-> F
```
-The compiler is the product. Capture feeds it native and remote demonstrations, the ML layer
-can supply optional on-prem models, Agent gives MCP clients and Agent Skills a
-governed route into Flow bundles, and evals measures adjacent research. This page
-intentionally does not expose internal developer tools as product components.
-See [Qualification evidence](../get-started/what-works-today.md) for integrated
-feature and backend coverage.
+See [Qualification evidence](../get-started/what-works-today.md) for bounded
+feature and backend results. Evidence for one task and environment does not
+extend to another workflow.
diff --git a/docs/get-started/first-workflow.md b/docs/get-started/first-workflow.md
index 6c1b762..f62cac0 100644
--- a/docs/get-started/first-workflow.md
+++ b/docs/get-started/first-workflow.md
@@ -108,6 +108,12 @@ profiles require it explicitly.)
Perform the task once. When you are done, press ++ctrl+c++ or close the browser
window to finish. The recording is written to `rec/`.
+!!! tip "Keep an existing SSO or 2FA session"
+ Start a dedicated Chromium profile with a local remote-debugging port. Add
+ `--browser-cdp-endpoint http://127.0.0.1:9222` to attach the same recorder
+ to its one open same-origin tab. Flow does not navigate or close that
+ browser. See [Record your own app](../guides/record-your-app.md#use-an-existing-signed-in-chromium-session).
+
!!! tip "Record a clean demonstration"
Do the task the way you want it replayed: one clear path, no dead ends. The
compiler treats your demonstration as evidence of intent, so a tidy run
diff --git a/docs/get-started/what-works-today.md b/docs/get-started/what-works-today.md
index 2ffad47..d7f7031 100644
--- a/docs/get-started/what-works-today.md
+++ b/docs/get-started/what-works-today.md
@@ -55,7 +55,7 @@ customer-controlled runtime connected to the same governance model.
| Surface | Status | What is actually demonstrated | Important boundary |
|---|---|---|---|
| `openadapt` installer and `openadapt flow` dispatcher | **Supported** | Installs the compiler and exposes the unified command surface. | The standalone `openadapt-flow` package remains the canonical engine and may be installed directly. |
-| Record -> compile -> lint -> certify -> replay -> report on a browser | **Supported** | Runs end to end in CI against the bundled app and end to end against a real third-party app. | A clean run is not automatically safe. Identity coverage, risk classification, postconditions, and effect contracts must be audited per bundle. |
+| Record -> compile -> lint -> certify -> replay -> report on a browser | **Supported** | Runs end to end in CI against the bundled app and end to end against a real third-party app. The local attach recorder also passes 3 real Chromium record-and-compile trials with source-time password exclusion and external-browser survival checks. | A clean run is not automatically safe. Identity coverage, risk classification, postconditions, and effect contracts must be audited per bundle. Attach mode is loopback-only Chromium and requires a dedicated browser process started with remote debugging. An extension acquisition path must satisfy Flow's authenticated shared event, evidence, secret-masking, and frame-binding contract before Flow accepts its recording. |
| Deterministic target re-resolution and saved heal diffs | **Supported** | Theme, movement, and rename drift are covered by the bundled drift matrix. | Scale/reflow and tenant-specific state can still halt. The base bundle is not silently promoted; save and review a healed bundle explicitly. |
| `lint` and `certify` | **Supported** | Report coverage gaps and refuse bundles that violate the selected policy. | Certification is opt-in and only enforces what the policy names. An uncertified bundle remains runnable with `replay`. |
| Fail-closed `run` admission gate | **Supported** | The shipped gate checks certification, identity/effect coverage, approval fallback, encryption, and manifest integrity before executing. | Development escape hatches exist, and passing the gate does not validate the backend or prove a workflow safe. |
@@ -68,11 +68,11 @@ customer-controlled runtime connected to the same governance model.
| Native Linux backend | **Supported** | Required current-main job [`linux-atspi-x11`](https://github.com/OpenAdaptAI/openadapt-flow/actions/runs/30059807758/job/89378981573) at exact Flow commit `3de5fc67acf3024a621f812c5a6ed9be07fac335` ran one fresh GTK3 process per trial on Ubuntu 24.04 X11/AT-SPI. It confirmed 3/3 exact-file effects, 3/3 ambiguous-target refusals, and 3/3 stale-target refusals, with 0 silent incorrect successes, 0 over-halts, 0 operator interventions, and 0 model calls. | Native receipts prove AT-SPI delivery only; independent exact file bytes or confirmed absence prove effects. This is bounded to the in-tree GTK3 fixture and CI Xvfb image; it does not establish Wayland or arbitrary third-party application support. |
| RDP backend | **Supported** | Two bounded results exercise complementary RDP paths. Aardwolf 0.2.14 over a Parallels Windows 11 VM completed 3/3 Windows Run-dialog file effects with independent guest-tools readback. The accepted full governed lifecycle at mechanism commit `6031fde` recorded, compiled, and replayed a synthetic note workflow through real FreeRDP3-transported pixels/input: 3/3 healthy effects and 3/3 drift safe-halts, with zero model calls, silent incorrect successes, false completions, drift writes, or healthy over-halts. Consequential remote actions now acquire a fresh frame, re-resolve target and identity, and use a one-shot input lease that refuses changed session context, pixels, dimensions, or readiness before delivery. Review the [Aardwolf/Windows report](https://github.com/OpenAdaptAI/openadapt-flow/blob/6610d24cebba27918b8ea507b2f05a094057ac85/benchmark/rdp/results_82a658a_20260718.sanitized.json), the [FreeRDP lifecycle report](https://github.com/OpenAdaptAI/openadapt-flow/blob/affedc5f1f0de533a0744deaa8e30a203c91c6b3/benchmark/rdp_ladder/results.json), [Flow PR #142](https://github.com/OpenAdaptAI/openadapt-flow/pull/142), [Flow PR #177](https://github.com/OpenAdaptAI/openadapt-flow/pull/177), and [Flow PR #238](https://github.com/OpenAdaptAI/openadapt-flow/pull/238). | The Windows batch qualifies its exact task, snapshot, transport, and oracle. The FreeRDP batch qualifies a synthetic Linux Tk task over a real RDP round trip; it is not Aardwolf, a Windows-app qualification, Citrix ICA/HDX, or WAN-captured drift. The two-phase mechanism is separately covered by runtime and backend refusal tests; it does not enlarge those bounded application results. |
| Citrix / VDI backend | **Supported** | The dedicated `--backend citrix` path selects the exact Citrix Workspace/Viewer owner, optionally binds an exact title, requires a readiness marker for governed `run`, carries the closed target into durable resume, and uses the shared pixel identity, effect, policy, and halt contracts. Consequential input reacquires the exact client window, focus, geometry, readiness, fresh pixels, resolved target, and record identity, then refuses any change before the first input edge. The accepted no-DOM qualification completed 3/3 healthy effects and 3/3 severe-drift safe-halts with 0 model calls, silent incorrect successes, false completions, healthy over-halts, or drift writes. Review the [immutable report](https://github.com/OpenAdaptAI/openadapt-flow/blob/f6faac5b900b78cbda5980de0e983a9f987285ac/benchmark/citrix_workspace/results.json), [Flow PR #183](https://github.com/OpenAdaptAI/openadapt-flow/pull/183), and [Flow PR #238](https://github.com/OpenAdaptAI/openadapt-flow/pull/238). | The accepted artifact explicitly records `code_readiness_accepted: true` and `ica_hdx_accepted: false`. It qualifies the shipped Citrix Workspace-window backend contract over a no-DOM canvas stand-in, not a counted real ICA/HDX batch. The exact ICA/HDX client, codec, latency, DPI, lock/readiness, input, identity, and effect matrix is a separate per-deployment qualification boundary. |
-| Desktop authoring GUI and tray | **Supported / Beta** | Public `desktop-v0.15.0` provides Windows MSI/NSIS, macOS arm64/x64 DMG, and Linux AppImage/DEB installers. All six installer paths were installed, launched, and uninstalled in the native release workflow; the release includes `SHA256SUMS`, a CycloneDX SBOM, per-platform metadata, and build-provenance attestations. | Windows/Linux remain unsigned and macOS is ad-hoc signed, so verify checksums and provenance before overriding the OS publisher warning. The app and CLI drive the same released compiler/runtime. |
+| Desktop authoring GUI and tray | **Native installers** | Public `desktop-v0.15.0` provides Windows MSI/NSIS, macOS arm64/x64 DMG, and Linux AppImage/DEB installers. All six installer paths were installed, launched, and uninstalled in the native release workflow; the release includes `SHA256SUMS`, a CycloneDX SBOM, per-platform metadata, and build-provenance attestations. The current release admission is derived from the [Production lifecycle record](../reference/production-lifecycle.md). | Windows/Linux remain unsigned and macOS is ad-hoc signed, so verify checksums and provenance before overriding the OS publisher warning. The app and CLI drive the same released compiler/runtime. |
| Hosted CLI connectivity | **Supported / public offer** | `login`, exact-hash artifact preparation/upload, one-time runtime validation, bound replacement activation, and `report-break` connect the local engine to the live control plane. | Upload requires destination policy and an approved sanitized derivative; checkout never bypasses an egress refusal. |
| Artifact sanitation and local review | **Supported / launch gate** | The sanitized-derivative pipeline inventories, transforms, rescans, manifests, hashes, and supports local review/approval. | The raw original remains sensitive; unknown or unresolved content is refused; runtime observations can reintroduce PHI/PII. |
| Cross-engine hosted validation | **Supported / launch gate** | `validate-hosted` binds an approved recording and bundle, compiler provenance, strict lint, policy certification, derived risk class, and successful replay report to a one-time Cloud challenge. | It is operator self-attestation signed with the ingest token, not an independently observed certification. Exact deployment policy, risk-class, and deployed compiler-version allowlists still apply. |
-| Hosted browser recorder and runtime health | **Supported / bounded launch component** | A retained non-simulated hosted session on `openadapt-flow` 1.8.0 produced frames and input evidence, assembled a compileable recording, finalized one workflow idempotently, enforced resource limits, and cleaned up ephemeral qualification data. The current managed-runtime manifest pins the Flow 1.31.0 runner/compiler artifact identity. Authenticated live health separately checks live mode, auth, database, storage, runner, compiler, runtime-validation trust, runtime boundary, bundle protection, recorder, callbacks, scheduler, human-decision Web Push, retention, security events, secrets, validation policy, and billing. | Explicitly initiated, public-HTTPS, non-regulated authoring only. Raw observations remain private inside the declared hosted boundary. A runtime pin does not prove live deployment or hosted acceptance. Readiness proves deployed dependencies and service identity, not a customer workflow qualification or SLA. |
+| Hosted browser recorder and runtime health | **Supported / bounded launch component** | A retained non-simulated hosted session on `openadapt-flow` 1.8.0 produced frames and input evidence, assembled a compileable recording, finalized one workflow idempotently, enforced resource limits, and cleaned up ephemeral qualification data. The current managed-runtime manifest pins the Flow 1.31.0 runner/compiler artifact identity. Authenticated live health separately checks live mode, auth, database, storage, runner, compiler, runtime-validation trust, runtime boundary, bundle protection, recorder, callbacks, scheduler, human-decision Web Push, retention, security events, secrets, validation policy, and billing. | Explicitly initiated, public-HTTPS, non-regulated authoring only. Raw observations remain private inside the declared hosted boundary. A runtime pin does not prove live deployment or hosted acceptance. Readiness proves deployed dependencies and service identity, not a customer workflow qualification or SLA. |
| Hosted dashboard/control plane | **Supported / public offer** | Authentication, organizations, exact-hash bundle ingest, immutable run admission, browser runner orchestration, structural reports, replacement activation, billing, and metering form the managed lifecycle. | Production uses live dependencies and fails unavailable rather than substituting mock behavior. |
| Hosted execution | **Supported / public offer** | Live Stripe Checkout connects onboarding and subscription entitlements to managed browser execution; the runner verifies exact admitted bundle bytes and authenticated callbacks. | The public subscription covers approved browser workflows. Other substrates use separately scoped deployments and commercial terms. Checkout does not create an SLA or certification. |
| Air-gapped on-prem package | **Supported** | A local queue, systemd unit, minimized hash-chained audit log, and air-gap checks are provided. | Full-disk encryption and operational hardening remain operator/deployment responsibilities. |
diff --git a/docs/guides/hosted.md b/docs/guides/hosted.md
index c7b0f8c..b4876b9 100644
--- a/docs/guides/hosted.md
+++ b/docs/guides/hosted.md
@@ -35,14 +35,18 @@ switching, and sign-out.
## What the subscription includes
-| Surface | Launch status | Boundary |
+**Current Cloud release admission:**
+Production requires an active signed admission for this exact release or deployment.
+[Review the admission contract](../reference/production-lifecycle.md)
+
+| Surface | Availability | Boundary |
|---|---|---|
-| Local browser record -> compile -> managed execute | **Beta / public offer** | Governed authoring and validation remain local; managed execution uses the qualified browser substrate. |
-| Hosted browser record -> compileable workflow | **Beta / bounded launch component** | The retained non-simulated provider qualification used `openadapt-flow` 1.8.0; the current managed-runtime manifest pins Flow 1.31.0 artifact identity. A runtime pin does not prove live deployment or hosted workflow acceptance. This is a separate raw-observation boundary, not the reviewed-derivative upload lane. |
-| Account, organization, onboarding | **Beta / public offer** | Checkout and sign-in bind the subscription to an isolated organization. |
-| Structural run history and reports | **Beta / public offer** | Safety depends on the workflow's configured identity, effect, and policy checks. Repair and validation remain local. |
-| Checkout, portal, entitlements, metering | **Beta / public offer** | Live Stripe Checkout, signed webhooks, entitlements, usage, and the billing portal form one managed subscription contract. |
-| Self-hosted browser execution | **Beta** | No hosted account required. |
+| Local browser record -> compile -> managed execute | **Included in the managed subscription** | Governed authoring and validation remain local; managed execution uses the qualified browser substrate. |
+| Hosted browser record -> compileable workflow | **Available for approved sessions** | The retained non-simulated provider qualification used `openadapt-flow` 1.8.0; the current managed-runtime manifest pins Flow 1.31.0 artifact identity. A runtime pin does not prove live deployment or hosted workflow acceptance. This is a separate raw-observation boundary, not the reviewed-derivative upload lane. |
+| Account, organization, onboarding | **Included in the managed subscription** | Checkout and sign-in bind the subscription to an isolated organization. |
+| Structural run history and reports | **Included in the managed subscription** | Safety depends on the workflow's configured identity, effect, and policy checks. Repair and validation remain local. |
+| Checkout, portal, entitlements, metering | **Included in the managed subscription** | Live Stripe Checkout, signed webhooks, entitlements, usage, and the billing portal form one managed subscription contract. |
+| Self-hosted browser execution | **Available locally** | No hosted account required. |
| Windows UIA | **Supported / scoped deployment** | The exact in-tree WinForms matrix passed 3/3 with an independent SQLite oracle and 3/3 stale/ambiguity refusals. Windows subscriptions and deployments are ordered separately from the public browser offer and qualified per workflow. |
| Native macOS | **Supported / scoped deployment** | One macOS 15.7.3 arm64 host produced 3/3 exact-byte TextEdit effects plus a two-window ambiguity refusal. Native macOS subscriptions and deployments are ordered separately from the public browser offer and qualified per workflow. |
| Native Linux | **Supported / scoped deployment** | The required Ubuntu 24.04 X11/AT-SPI current-main lane completed 3/3 exact-file effects and refused 3/3 ambiguous plus 3/3 stale targets, with zero silent incorrect successes, over-halts, interventions, or model calls. Native Linux deployments run locally or in a customer-controlled boundary and are qualified per workflow. |
@@ -64,7 +68,7 @@ demo. A qualified hosted browser session produced PNG frames, accepted and
retained input evidence, assembled a native recording, created one compileable
workflow idempotently, enforced its resource limits, and removed the ephemeral
qualification data. That retained qualification used an `openadapt-flow` 1.8.0
-worker. The managed-runtime manifest pins the Flow 1.31.0 artifact identity. A
+worker. The managed-runtime manifest pins the Flow 1.31.0 artifact identity. A
pin does not prove that the build is live or that a hosted workflow passed
acceptance. The public readiness endpoint separately verifies the configured
live dependencies, including authentication, storage, runner, compiler,
diff --git a/docs/guides/record-your-app.md b/docs/guides/record-your-app.md
index ea578bf..d807bcb 100644
--- a/docs/guides/record-your-app.md
+++ b/docs/guides/record-your-app.md
@@ -8,9 +8,9 @@ target flags in place of `--url`.
## Record
-On the web substrate, `record --backend web --url` opens a headed browser on your app and
-watches what you do: clicks, typing, key presses, and scrolls. It writes the same
-recording format `compile` consumes.
+On the web substrate, `record --backend web --url` opens a headed browser on
+your app and watches what you do: clicks, typing, key presses, and scrolls. It
+writes the same recording format `compile` consumes.
```bash
openadapt flow record --backend web --url https://your.app --out rec
@@ -23,6 +23,100 @@ the window to finish.
Add `--headless` to run the browser without a window, for scripted recording
in a pipeline.
+### Use an existing signed-in Chromium session
+
+Flow can attach the same Playwright recorder to one existing local Chromium
+tab. Use this mode when the browser profile has already completed sign-in, SSO,
+or 2FA.
+
+Start Chromium with a dedicated debugging profile. Do not enable remote
+debugging on a sensitive general-purpose browser profile.
+
+=== "macOS"
+
+ ```bash
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
+ --remote-debugging-address=127.0.0.1 \
+ --remote-debugging-port=9222 \
+ --user-data-dir="$HOME/Library/Application Support/OpenAdapt/ChromeRecorderProfile"
+ ```
+
+=== "Linux"
+
+ ```bash
+ google-chrome \
+ --remote-debugging-address=127.0.0.1 \
+ --remote-debugging-port=9222 \
+ --user-data-dir="${XDG_DATA_HOME:-$HOME/.local/share}/openadapt/chrome-recorder-profile"
+ ```
+
+=== "Windows PowerShell"
+
+ ```powershell
+ & "$env:ProgramFiles\Google\Chrome\Application\chrome.exe" `
+ --remote-debugging-address=127.0.0.1 `
+ --remote-debugging-port=9222 `
+ --user-data-dir="$env:LOCALAPPDATA\OpenAdapt\ChromeRecorderProfile"
+ ```
+
+Open the application in that browser. Then run:
+
+```bash
+openadapt flow record --backend web --url https://your.app \
+ --browser-cdp-endpoint http://127.0.0.1:9222 --out rec
+```
+
+Flow selects the sole open tab on the `--url` origin. It refuses an ambiguous
+selection. If the browser has two or more tabs on that origin, add the exact
+current URL with `--browser-page-url`. Flow does not navigate or close the
+attached browser.
+
+The endpoint must be on localhost or a loopback IP address, with an explicit
+port and no credentials, query, or fragment. Attach mode cannot combine with
+`--headless`. The external browser owns its display mode.
+
+The declared app URL, each observed page URL, document titles, selectors,
+accessible names, row identity text, typed values, and screenshots can contain
+sensitive data. Flow treats the complete recording as sensitive. Diagnostic
+messages omit URL query and fragment values. Recorded URLs retain structural
+origin, path, and parameter names. Flow drops a parameter value when its name
+matches a declared secret field. It withholds a complete URL or title when it
+cannot prove that reporting the text is safe.
+
+Password fields and fields declared with `--secret` bind a private input
+session and capture-time screenshot mask. Literal secret values stay in the
+page closure. Flow reports captured page text exactly, or withholds it and
+records why. It never rewrites captured text or substitutes a placeholder.
+Selector, role, accessible-name, row-identity, and receiving-field evidence use
+the same exact-or-withheld rule. A withheld identity cannot silently disarm an
+identity check.
+
+The `--out` path must not exist. Flow records into a new temporary sibling and
+publishes the output atomically only after final capture and detach. A refusal
+removes the temporary output and does not change an existing recording.
+
+Keep the selected tab on the declared application origin. You can resize the
+tab or move its window between monitors while no action is in progress. Flow
+observes viewport and device-scale changes. It waits for a stable CSS-pixel
+frame and binds later events to the new coordinate space. The recording keeps
+the viewport history and the exact before and after viewport for each event.
+
+Flow refuses a cross-origin navigation, a selected-tab close, a new popup or
+tab, or an event whose exact top-level frame binding is unavailable. It also
+refuses an action that overlaps a resize or monitor-scale transition. The last
+refusal is necessary because no exact pre-action frame exists in the new
+coordinate space. An overlapping action aborts the recording and publishes no
+complete metadata. When you resize between actions, stop interacting until the
+new frame is stable. Recording then continues automatically.
+
+The Chrome extension code in `openadapt-capture` is not a second compiler or
+replay runtime. An extension acquisition path must use the shared Flow event
+and evidence schema, capture-time secret exclusion, authenticated
+browser/profile/tab/document/session identity, an acknowledged ordered event
+stream, and exact frame, event, and viewport binding before Flow can accept its
+recording. Direct extension replay does not replace Flow's identity, policy,
+fresh-frame, effect, and refusal gates.
+
## Compile and replay
```bash
diff --git a/docs/guides/run-reports.md b/docs/guides/run-reports.md
index 3c4afdd..64a2838 100644
--- a/docs/guides/run-reports.md
+++ b/docs/guides/run-reports.md
@@ -29,10 +29,12 @@ For each step, the report records:
verified against the system of record, with its verdict.
- **Model calls**: any call to a grounding or state-verification model, so the $0
property is observable, not assumed.
-- **Outcome**: success, or a halt naming the violated expectation. Every
- terminal outcome (`VERIFIED`, `COMPLETED_UNVERIFIED`, `HALTED`, `FAILED`,
- `ROLLED_BACK`), the transaction refinement, and every typed halt reason are
- defined in [Run outcomes and halt reasons](../reference/run-outcomes.md).
+- **Outcome**: the exact terminal state and the violated expectation, when one
+ exists. A transaction ends as `VERIFIED`, `HALTED_BEFORE_EFFECT`,
+ `RECONCILIATION_REQUIRED`, `FAILED_PLATFORM`, `CANCELED`, `REJECTED_POLICY`,
+ `COMPLETED_UNVERIFIED`, or `ROLLED_BACK`. Only `VERIFIED` is a successful
+ production outcome. Every typed halt reason is defined in
+ [Run outcomes and halt reasons](../reference/run-outcomes.md).
## The identity-coverage line
diff --git a/docs/guides/security-and-data-handling.md b/docs/guides/security-and-data-handling.md
index ec50753..00c5a0d 100644
--- a/docs/guides/security-and-data-handling.md
+++ b/docs/guides/security-and-data-handling.md
@@ -283,7 +283,7 @@ lives, not what is enforced.
| **On-prem appliance** | A host inside your network, with a local queue, hash-chained audit log, optional LAN-only VLM box, and operator-pulled signed updates verified against a pinned vendor public key. | Nothing at run time; updates enter on removable media, signature-verified, with blue/green install and rollback. ([Deploy on-prem](deploy-on-prem.md), [ON_PREM.md](https://github.com/OpenAdaptAI/openadapt-flow/blob/main/docs/ON_PREM.md)) |
| **Hosted control plane + local execution** | Governed authoring, validation, and repair remain local; the control plane manages accounts, workflow versions, run history, and billing. | Operator-approved sanitized derivatives, minimized halt descriptors, and hash-bound attestations, verified against published schemas. ([The hosted option](hosted.md)) |
-Hosted managed *browser* execution is a separate Beta launch-candidate lane
+Hosted managed *browser* execution is a separate managed boundary
with its own declared boundary for public, non-regulated targets. See
[Hosted browser execution](hosted.md) for its current status and scope.
diff --git a/docs/guides/security-review.md b/docs/guides/security-review.md
index 6974b3a..4d38f12 100644
--- a/docs/guides/security-review.md
+++ b/docs/guides/security-review.md
@@ -125,14 +125,14 @@ release-signing authority, recovery drill, and maintenance window.
## Hosted service and substrate qualification
-Managed browser execution is a public Beta service with live Stripe Checkout,
+Managed browser execution is a public service with live Stripe Checkout,
onboarding, organization isolation, browser runner orchestration, artifacts,
reports, teaching, billing, and usage metering. Production explicitly selects
live dependencies; a missing runner, storage, or billing dependency returns an
operational failure and never substitutes mock success. Mock mode remains for
development and is visibly synthetic. The retained non-simulated hosted-recorder
qualification was run on Flow 1.8.0. The current managed-runtime manifest pins
-Flow 1.31.0 artifact identity; that pin does not prove live deployment or hosted
+Flow 1.31.0 artifact identity; that pin does not prove live deployment or hosted
workflow acceptance. The public readiness endpoint separately verifies live
mode, authentication, database migrations, private storage, runner, compiler,
runtime-validation trust, runtime boundary, bundle protection, recorder,
@@ -141,6 +141,10 @@ events, secret encryption, runtime-validation allowlists, and live billing
configuration. Readiness proves those dependencies and contracts are configured
and reachable; it is not a customer workflow qualification or an SLA.
+**Current Cloud release admission:**
+Production requires an active signed admission for this exact release or deployment.
+[Review the admission contract](../reference/production-lifecycle.md)
+
Windows UIA, native macOS, native Linux, RDP, and Citrix/VDI are first-class substrates,
ordered as scoped deployments and qualified per workflow in their real
environment. The published qualification evidence to date: Windows UIA passed
diff --git a/docs/index.md b/docs/index.md
index 6c30835..176049b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -19,6 +19,10 @@ model tier, records the repair, and halts instead of guessing when verification
fails.
+**Current product release admission:**
+Production requires active signed admissions for every required release and deployment, and execution accepts only active qualified workflow versions.
+[How release admission works](reference/production-lifecycle.md)
+
[Try it locally](get-started/index.md){ .md-button .md-button--primary }
[Read the concepts](concepts/demonstration-compiler.md){ .md-button }
[Evaluate a workflow](https://openadapt.ai/qualify){ .md-button }
@@ -174,9 +178,13 @@ the target outline nor the capsule is verification evidence.
Cloud uses the same application, mode, and view choices, then adds an openIMIS
deep dive with the compiled graph, contracts, six retained Standard-profile
-results, and byte-inventoried evidence links. **VERIFIED** means the complete
-declared contract passed; **HALTED** means the run stopped rather than claim an
-outcome it could not prove.
+results, and byte-inventoried evidence links. A transaction ends as
+`VERIFIED`, `HALTED_BEFORE_EFFECT`, `RECONCILIATION_REQUIRED`,
+`FAILED_PLATFORM`, `CANCELED`, `REJECTED_POLICY`, `COMPLETED_UNVERIFIED`, or
+`ROLLED_BACK`. Only `VERIFIED` is a successful production outcome.
+`RECONCILIATION_REQUIRED` means that an effect can have occurred and OpenAdapt
+cannot yet prove its final state. OpenAdapt does not retry that transaction
+blindly. See [Run outcomes and halt reasons](reference/run-outcomes.md).
[Watch the shared real-application demo](https://openadapt.ai/how-it-works){ .md-button .md-button--primary }
[Inspect the Cloud evidence deep dive](https://app.openadapt.ai/demo#footage){ .md-button }
@@ -218,10 +226,11 @@ bounded runs ended after exhausting their action budget without writing a
patient.
All three environments used synthetic data on one local host and ran through
-the **Browser (Playwright)** substrate, whose reference path is **Beta**. The
-healthcare row is distinct from the shared-public-demo OpenEMR field result
-above. Frappe Lending and openIMIS are API-rich references, and neither is
-evidence for a legacy Windows/Citrix system.
+the **Browser (Playwright)** substrate. These results qualify only the named
+tasks and environments. The healthcare row is distinct from the
+shared-public-demo OpenEMR field result above. Frappe Lending and openIMIS are
+API-rich references, and neither is evidence for a legacy Windows/Citrix
+system.
The public [aggregate report](https://github.com/OpenAdaptAI/openadapt-flow/tree/main/benchmark/agent_arm_verticals)
retains the method, run counts, outcomes, failure taxonomy, and caveats. Raw
diff --git a/docs/javascripts/production-lifecycle.js b/docs/javascripts/production-lifecycle.js
new file mode 100644
index 0000000..1d67052
--- /dev/null
+++ b/docs/javascripts/production-lifecycle.js
@@ -0,0 +1,460 @@
+/*
+ * Derive public Production labels from the current canonical admission record.
+ * Static page text stays useful when the record is unavailable. A page can only
+ * gain a Production label after this module validates the complete live record.
+ */
+(function productionLifecycleModule(root, factory) {
+ "use strict";
+
+ const api = factory();
+ if (typeof module === "object" && module.exports) {
+ module.exports = api;
+ }
+ root.OpenAdaptProductionLifecycle = api;
+
+ if (root.document && typeof root.fetch === "function") {
+ const refresh = () => api.refresh(root.document, root.fetch.bind(root));
+ if (root.document.readyState === "loading") {
+ root.document.addEventListener("DOMContentLoaded", refresh, { once: true });
+ } else {
+ refresh();
+ }
+ if (root.document$ && typeof root.document$.subscribe === "function") {
+ root.document$.subscribe(refresh);
+ }
+ }
+})(typeof globalThis === "undefined" ? this : globalThis, function factory() {
+ "use strict";
+
+ const TARGET_REQUIREMENT =
+ "Production requires an active signed admission for this exact release or deployment.";
+ const PRODUCT_REQUIREMENT =
+ "Production requires active signed admissions for every required release and deployment, and execution accepts only active qualified workflow versions.";
+ const PROJECTION_URL = "/production-lifecycle.json";
+ const ADMISSIONS_URL =
+ "https://raw.githubusercontent.com/OpenAdaptAI/.github/main/production-lifecycle-admissions.json";
+ const EXPECTED_TARGETS = Object.freeze({
+ agent: ["qualified_agent_bridge_release", "public_package"],
+ capture: ["qualified_native_recorder_release", "public_package"],
+ cloud: ["qualified_workflow_control_plane_deployment", "private_deployment"],
+ desktop: ["qualified_native_workflow_desktop_release", "public_package"],
+ docs: ["production_documentation_deployment", "public_deployment"],
+ flow: ["qualified_workflow_runtime_release", "public_package"],
+ openadapt: ["qualified_workflow_launcher_release", "public_package"],
+ });
+ const TARGET_IDS = Object.freeze(Object.keys(EXPECTED_TARGETS).sort());
+ const PYPI_PROJECTS = Object.freeze({
+ agent: "openadapt-agent",
+ capture: "openadapt-capture",
+ desktop: "openadapt-desktop",
+ flow: "openadapt-flow",
+ openadapt: "openadapt",
+ });
+ const EXPECTED_SOURCE_FILES = Object.freeze({
+ admissions: "production-lifecycle-admissions.json",
+ admissions_schema: "schemas/production-lifecycle-admissions.schema.json",
+ evidence_manifest_schema:
+ "schemas/production-lifecycle-evidence-manifest.schema.json",
+ evidence_summary_schema:
+ "schemas/production-lifecycle-evidence-summary.schema.json",
+ lifecycle: "repository-lifecycle.yml",
+ policy: "production-lifecycle-policy.json",
+ policy_schema: "schemas/production-lifecycle-policy.schema.json",
+ validator: "scripts/validate_production_lifecycle.py",
+ });
+ const HEX40 = /^[0-9a-f]{40}$/;
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
+ const VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
+ let requestSequence = 0;
+ let refreshGeneration = 0;
+
+ function isObject(value) {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+ }
+
+ function hasExactKeys(value, keys) {
+ return (
+ isObject(value) &&
+ Object.keys(value).sort().join("\u0000") === [...keys].sort().join("\u0000")
+ );
+ }
+
+ function isHttpsUrl(value) {
+ if (typeof value !== "string") return false;
+ try {
+ const parsed = new URL(value);
+ return (
+ parsed.protocol === "https:" &&
+ parsed.username === "" &&
+ parsed.password === ""
+ );
+ } catch (_error) {
+ return false;
+ }
+ }
+
+ function parseTimestamp(value) {
+ if (typeof value !== "string" || !TIMESTAMP.test(value)) return null;
+ const parsed = Date.parse(value);
+ if (!Number.isFinite(parsed)) return null;
+ return new Date(parsed).toISOString() === value.replace(/Z$/, ".000Z")
+ ? parsed
+ : null;
+ }
+
+ function sameJson(left, right) {
+ return JSON.stringify(left) === JSON.stringify(right);
+ }
+
+ function validateSource(source) {
+ if (
+ !hasExactKeys(source, ["schema_version", "repository", "source_commit", "files"]) ||
+ source.schema_version !== "openadapt.production-lifecycle-source/v1" ||
+ source.repository !== "OpenAdaptAI/.github" ||
+ !HEX40.test(source.source_commit) ||
+ !isObject(source.files)
+ ) {
+ return false;
+ }
+ if (!hasExactKeys(source.files, Object.keys(EXPECTED_SOURCE_FILES))) return false;
+ const prefix = `https://raw.githubusercontent.com/OpenAdaptAI/.github/${source.source_commit}/`;
+ return Object.entries(EXPECTED_SOURCE_FILES).every(([key, path]) => {
+ const item = source.files[key];
+ return (
+ hasExactKeys(item, ["path", "url", "sha256"]) &&
+ item.path === path &&
+ item.url === `${prefix}${path}` &&
+ DIGEST.test(item.sha256)
+ );
+ });
+ }
+
+ function validateProjection(projection) {
+ if (
+ !isObject(projection) ||
+ projection.schema_version !== "openadapt.public-production-lifecycle/v1" ||
+ projection.$schema !== "schemas/production-lifecycle-public.schema.json" ||
+ !validateSource(projection.source) ||
+ !Number.isInteger(projection.policy_revision) ||
+ projection.policy_revision < 1 ||
+ !Number.isInteger(projection.maximum_admission_days) ||
+ projection.maximum_admission_days < 1 ||
+ !hasExactKeys(projection.derivation, [
+ "mode",
+ "static_production_state",
+ "expired_or_revoked_latest_behavior",
+ "fallback_to_older_release",
+ ]) ||
+ projection.derivation.mode !== "latest_signed_admission_at_read_time" ||
+ projection.derivation.static_production_state !== false ||
+ projection.derivation.expired_or_revoked_latest_behavior !== "no_production" ||
+ projection.derivation.fallback_to_older_release !== false ||
+ !Array.isArray(projection.targets) ||
+ projection.targets.length !== TARGET_IDS.length
+ ) {
+ return null;
+ }
+
+ const targets = new Map();
+ for (const target of projection.targets) {
+ if (
+ !hasExactKeys(target, [
+ "id",
+ "display_name",
+ "lifecycle_scope",
+ "lifecycle_subject",
+ "source_repository",
+ "release_kind",
+ "required_claim_scope",
+ "required_artifact_kinds",
+ "package_index_project",
+ "artifact_authority_by_kind",
+ "latest_admission",
+ "admission_history",
+ ]) ||
+ !TARGET_IDS.includes(target.id) ||
+ targets.has(target.id)
+ ) {
+ return null;
+ }
+ const [claimScope, releaseKind] = EXPECTED_TARGETS[target.id];
+ if (
+ target.required_claim_scope !== claimScope ||
+ target.release_kind !== releaseKind ||
+ typeof target.display_name !== "string" ||
+ !["repository", "public_surface"].includes(target.lifecycle_scope) ||
+ typeof target.lifecycle_subject !== "string" ||
+ !/^OpenAdaptAI\/[A-Za-z0-9._-]+$/.test(target.source_repository) ||
+ !Array.isArray(target.required_artifact_kinds) ||
+ !isObject(target.artifact_authority_by_kind) ||
+ !Array.isArray(target.admission_history)
+ ) {
+ return null;
+ }
+ const ordered = [...target.admission_history].sort(
+ (left, right) =>
+ left?.release_identity?.sequence - right?.release_identity?.sequence,
+ );
+ if (!sameJson(ordered, target.admission_history)) return null;
+ let previousSequence = 0;
+ for (const admission of ordered) {
+ const sequence = admission?.release_identity?.sequence;
+ if (!Number.isInteger(sequence) || sequence <= previousSequence) return null;
+ previousSequence = sequence;
+ }
+ const latest = ordered.length ? ordered[ordered.length - 1] : null;
+ if (!sameJson(latest, target.latest_admission)) return null;
+ targets.set(target.id, target);
+ }
+ if (!TARGET_IDS.every((targetId) => targets.has(targetId))) return null;
+ return targets;
+ }
+
+ function validateLiveAdmissions(value, projection, targets) {
+ if (
+ !hasExactKeys(value, ["$schema", "schema_version", "policy_sha256", "admissions"]) ||
+ value.$schema !== "schemas/production-lifecycle-admissions.schema.json" ||
+ value.schema_version !== "openadapt.production-lifecycle-admissions/v1" ||
+ value.policy_sha256 !== projection.source.files.policy.sha256 ||
+ !Array.isArray(value.admissions)
+ ) {
+ return false;
+ }
+ const byTarget = new Map(TARGET_IDS.map((id) => [id, []]));
+ for (const admission of value.admissions) {
+ if (!isObject(admission) || !byTarget.has(admission.target)) return false;
+ byTarget.get(admission.target).push(admission);
+ }
+ for (const targetId of TARGET_IDS) {
+ const live = byTarget
+ .get(targetId)
+ .sort((left, right) => left?.release_identity?.sequence - right?.release_identity?.sequence);
+ if (!sameJson(live, targets.get(targetId).admission_history)) return false;
+ }
+ return true;
+ }
+
+ function deriveTarget(target, projection, now = Date.now()) {
+ const admission = target.latest_admission;
+ if (!isObject(admission)) return null;
+ const [claimScope, releaseKind] = EXPECTED_TARGETS[target.id];
+ const identity = admission.release_identity;
+ const release = admission.release;
+ const evidence = admission.acceptance_evidence;
+ const issuedAt = parseTimestamp(admission.issued_at);
+ const expiresAt = parseTimestamp(admission.expires_at);
+ if (
+ admission.target !== target.id ||
+ admission.claim_scope !== claimScope ||
+ admission.policy_revision !== projection.policy_revision ||
+ !isObject(identity) ||
+ identity.schema_version !== "openadapt.monotonic-production-release/v1" ||
+ identity.channel !== "production" ||
+ !Number.isInteger(identity.sequence) ||
+ identity.sequence < 1 ||
+ !isObject(release) ||
+ release.kind !== releaseKind ||
+ !isObject(evidence) ||
+ !isHttpsUrl(evidence.summary_url) ||
+ !DIGEST.test(evidence.summary_sha256) ||
+ issuedAt === null ||
+ expiresAt === null ||
+ issuedAt > now ||
+ expiresAt <= now ||
+ expiresAt <= issuedAt ||
+ expiresAt - issuedAt > projection.maximum_admission_days * 86400000 ||
+ admission.revoked_at !== null
+ ) {
+ return null;
+ }
+
+ let releaseLabel;
+ if (releaseKind === "public_package" && VERSION.test(release.version)) {
+ releaseLabel = `release ${release.version}`;
+ } else if (
+ releaseKind === "public_deployment" &&
+ typeof release.deployment_id === "string" &&
+ release.deployment_id.length > 0
+ ) {
+ releaseLabel = `deployment ${release.deployment_id}`;
+ } else if (
+ releaseKind === "private_deployment" &&
+ typeof release.deployment_release_id === "string" &&
+ release.deployment_release_id.length > 0
+ ) {
+ releaseLabel = `deployment release ${release.deployment_release_id}`;
+ } else {
+ return null;
+ }
+ return Object.freeze({
+ targetId: target.id,
+ releaseLabel,
+ releaseVersion:
+ releaseKind === "public_package" ? release.version : null,
+ summaryUrl: evidence.summary_url,
+ });
+ }
+
+ async function sha256(bytes) {
+ if (!globalThis.crypto?.subtle) throw new Error("SHA-256 is unavailable");
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
+ return (
+ "sha256:" +
+ [...new Uint8Array(digest)]
+ .map((value) => value.toString(16).padStart(2, "0"))
+ .join("")
+ );
+ }
+
+ function cacheBustedUrl(url, now) {
+ requestSequence += 1;
+ const separator = url.includes("?") ? "&" : "?";
+ return `${url}${separator}openadapt_lifecycle_request=${now}-${requestSequence}`;
+ }
+
+ async function verifyDefaultInstallAuthority(activeTargets, fetchImpl, now) {
+ try {
+ const checks = await Promise.all(
+ Object.entries(PYPI_PROJECTS).map(async ([targetId, project]) => {
+ const active = activeTargets.get(targetId);
+ if (!active || typeof active.releaseVersion !== "string") return false;
+ const url = cacheBustedUrl(
+ `https://pypi.org/pypi/${project}/json`,
+ now,
+ );
+ const response = await fetchImpl(url, {
+ cache: "no-store",
+ credentials: "omit",
+ });
+ if (!response.ok) return false;
+ const metadata = await response.json();
+ if (
+ !isObject(metadata) ||
+ !isObject(metadata.info) ||
+ metadata.info.version !== active.releaseVersion ||
+ !isObject(metadata.releases)
+ ) {
+ return false;
+ }
+ const files = metadata.releases[active.releaseVersion];
+ if (!Array.isArray(files)) return false;
+ const unyankedKinds = new Set(
+ files
+ .filter((file) => isObject(file) && file.yanked === false)
+ .map((file) => file.packagetype),
+ );
+ return unyankedKinds.has("bdist_wheel") && unyankedKinds.has("sdist");
+ }),
+ );
+ return checks.every(Boolean);
+ } catch (_error) {
+ return false;
+ }
+ }
+
+ async function load(fetchImpl, now = Date.now()) {
+ try {
+ const projectionRequestUrl = cacheBustedUrl(PROJECTION_URL, now);
+ const admissionsRequestUrl = cacheBustedUrl(ADMISSIONS_URL, now);
+ const [projectionResponse, admissionsResponse] = await Promise.all([
+ fetchImpl(projectionRequestUrl, {
+ cache: "no-store",
+ credentials: "same-origin",
+ }),
+ fetchImpl(admissionsRequestUrl, { cache: "no-store", credentials: "omit" }),
+ ]);
+ if (!projectionResponse.ok || !admissionsResponse.ok) return null;
+ const projection = await projectionResponse.json();
+ const targets = validateProjection(projection);
+ if (!targets) return null;
+ const admissionsBytes = await admissionsResponse.arrayBuffer();
+ const expectedDigest = projection.source.files.admissions.sha256;
+ if ((await sha256(admissionsBytes)) !== expectedDigest) return null;
+ const liveAdmissions = JSON.parse(new TextDecoder().decode(admissionsBytes));
+ if (!validateLiveAdmissions(liveAdmissions, projection, targets)) return null;
+
+ const activeTargets = new Map();
+ for (const [targetId, target] of targets) {
+ const active = deriveTarget(target, projection, now);
+ if (active) activeTargets.set(targetId, active);
+ }
+ return Object.freeze({
+ activeTargets,
+ defaultInstallVerified: await verifyDefaultInstallAuthority(
+ activeTargets,
+ fetchImpl,
+ now,
+ ),
+ });
+ } catch (_error) {
+ return null;
+ }
+ }
+
+ function renderTarget(element, active) {
+ element.textContent = "";
+ if (!active) {
+ element.textContent = TARGET_REQUIREMENT;
+ return;
+ }
+ element.append("Production — ", active.releaseLabel, " — ");
+ const link = element.ownerDocument.createElement("a");
+ link.href = active.summaryUrl;
+ link.rel = "noopener noreferrer";
+ link.textContent = "acceptance evidence";
+ element.append(link);
+ }
+
+ function render(document, state) {
+ const active =
+ state?.activeTargets instanceof Map ? state.activeTargets : new Map();
+ for (const element of document.querySelectorAll("[data-openadapt-production-target]")) {
+ renderTarget(element, active.get(element.dataset.openadaptProductionTarget));
+ }
+ const productIsProduction =
+ state?.defaultInstallVerified === true &&
+ TARGET_IDS.every((targetId) => active.has(targetId));
+ for (const element of document.querySelectorAll("[data-openadapt-production-product]")) {
+ element.textContent = productIsProduction ? "Production" : PRODUCT_REQUIREMENT;
+ }
+ }
+
+ async function refreshWithLoader(document, loader) {
+ refreshGeneration += 1;
+ const generation = refreshGeneration;
+ let state = null;
+ try {
+ state = await loader();
+ } catch (_error) {
+ state = null;
+ }
+ if (generation !== refreshGeneration) return false;
+ render(document, state);
+ return true;
+ }
+
+ async function refresh(document, fetchImpl, now = Date.now()) {
+ return refreshWithLoader(document, () => load(fetchImpl, now));
+ }
+
+ return Object.freeze({
+ ADMISSIONS_URL,
+ PRODUCT_REQUIREMENT,
+ PROJECTION_URL,
+ PYPI_PROJECTS,
+ TARGET_IDS,
+ TARGET_REQUIREMENT,
+ cacheBustedUrl,
+ deriveTarget,
+ load,
+ refresh,
+ refreshWithLoader,
+ render,
+ renderTarget,
+ sha256,
+ validateLiveAdmissions,
+ validateProjection,
+ verifyDefaultInstallAuthority,
+ });
+});
diff --git a/docs/published-version-claims.json b/docs/published-version-claims.json
index d6162c2..ba0919b 100644
--- a/docs/published-version-claims.json
+++ b/docs/published-version-claims.json
@@ -22,6 +22,10 @@
" historical A version some retained measurement was taken on.",
" Frozen forever; bumping it would falsify the record.",
"",
+ "rendered_locations:",
+ " Pages whose marked values are generated from one structured claim.",
+ " Change the claim fields once, then run the deterministic renderer.",
+ "",
"Adding a 'published X.Y.Z' style sentence to docs/ without a matching",
"pypi-latest entry here fails the check."
],
@@ -43,28 +47,41 @@
"kind": "pinned-deployment",
"package": "openadapt-flow",
"version": "1.31.0",
- "evidence": "openadapt-cloud origin/main 4e0257a1299cc0869af3e3f664a6ebfa0b59db1 runner/runtime-version.json pins openadapt_flow 1.31.0 at release commit 2d225dea9a0ad29ca84ce1b037cc0ac671367e28 (wheel sha256 81133db1528ad1bb1f26e3fcb6aea61b0651db6d905cf2e4943e8383c1f3d29c; sdist sha256 cf1fc356d14d267df82be188de3e9a3575734f18f46ef91ac8075438cc731540). This records the configured artifact pin, not live deployment or hosted acceptance.",
+ "source_manifest_repository": "OpenAdaptAI/openadapt-cloud",
+ "source_manifest_commit": "4e0257a1299cc0869af3e3f664a6ebfa0b59db1",
+ "source_manifest_path": "runner/runtime-version.json",
+ "release_commit": "2d225dea9a0ad29ca84ce1b037cc0ac671367e28",
+ "wheel_sha256": "81133db1528ad1bb1f26e3fcb6aea61b0651db6d905cf2e4943e8383c1f3d29c",
+ "sdist_sha256": "cf1fc356d14d267df82be188de3e9a3575734f18f46ef91ac8075438cc731540",
+ "evidence": "The structured source-manifest fields bind the Cloud runtime manifest. The structured release commit and archive hashes bind the configured Flow artifacts. This records the configured artifact pin, not live deployment or hosted acceptance.",
"verified_on": "2026-08-18",
- "locations": [
+ "rendered_locations": [
{
"file": "docs/concepts/deployment-matrix.md",
- "context": "managed-runtime manifest pins Flow 1.31.0 at release"
+ "values": {
+ "version": 1,
+ "release_commit": 1,
+ "wheel_sha256": 1,
+ "sdist_sha256": 1
+ }
},
{
"file": "docs/guides/security-review.md",
- "context": "managed-runtime manifest pins\nFlow 1.31.0 artifact identity"
- },
- {
- "file": "docs/guides/hosted.md",
- "context": "managed-runtime manifest pins Flow 1.31.0 artifact identity"
+ "values": {
+ "version": 1
+ }
},
{
"file": "docs/guides/hosted.md",
- "context": "managed-runtime manifest pins the Flow 1.31.0 artifact identity"
+ "values": {
+ "version": 2
+ }
},
{
"file": "docs/get-started/what-works-today.md",
- "context": "managed-runtime manifest pins the Flow 1.31.0 runner/compiler artifact identity"
+ "values": {
+ "version": 1
+ }
}
]
},
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index a8648fd..776b0d9 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -109,10 +109,12 @@ openadapt flow record --backend web --url https://your.app --out rec
| Flag | Description |
|---|---|
| `--url` | URL of the app to record against. **Required for `--backend web`** (the default); other substrates target through the [backend selector](#backend) instead. |
+| `--browser-cdp-endpoint URL` | Attach the Playwright recorder to an already-running local Chromium browser. The endpoint must use localhost or a loopback IP address and an explicit port. Flow binds a tab on the `--url` origin and does not navigate or close the browser. An idle resize or monitor-scale change starts a new exact per-event viewport baseline. |
+| `--browser-page-url URL` | Exact current URL used when two or more open tabs match the `--url` origin. Requires `--browser-cdp-endpoint`. |
| `--out` (required) | Recording output directory |
| `--secret FIELD` | Mark a typed field (by name or id) as a **secret**: never persisted, injected at replay from `OPENADAPT_FLOW_SECRET_`. `input[type=password]` is always secret. Repeatable. |
| `--param FIELD` | Record a typed field as a **parameter**: its demonstrated value becomes the default, overridable at replay with `--param`. Repeatable. |
-| `--headless` | Run the browser headless (scripted or CI recording) |
+| `--headless` | Run a browser launched by Flow headless (scripted or CI recording). It cannot be combined with `--browser-cdp-endpoint`; the attached browser controls its own display mode. |
## demo-record
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index f5b7cf2..3be165d 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -59,6 +59,13 @@ matching Chromium build downloads lazily on the first web action. Native
desktop, RDP, and Citrix commands neither import Playwright nor trigger a
browser download.
+The recorder can also attach to a Chromium process that you started with a
+local remote-debugging port and a dedicated user-data directory. Pass
+`--browser-cdp-endpoint`; no environment variable enables this path. The
+endpoint is loopback-only, and Flow never stores it in recording metadata.
+The recorder stores the attached tab's viewport history. It rebaselines after
+an idle tab resize or monitor-scale change.
+
| Variable | Purpose |
|---|---|
| `OPENADAPT_FLOW_NO_AUTO_INSTALL` | Disables automatic browser provisioning. Set it when you manage the browser yourself (e.g. you ran `playwright install chromium` ahead of time in a controlled image). |
diff --git a/docs/reference/index.md b/docs/reference/index.md
index 76178a2..5194537 100644
--- a/docs/reference/index.md
+++ b/docs/reference/index.md
@@ -33,8 +33,8 @@ configuration. Package topology is secondary: the canonical engine is
- [__Package and repository lifecycle__](../ecosystem/index.md)
- The retained package index, with current product, experimental, research,
- and deprecated repository roles.
+ The retained package index, with product, trust, interoperability,
+ evaluation, model-development, and historical roles.
- [__Documentation source of truth__](documentation-governance.md)
diff --git a/docs/reference/production-lifecycle.md b/docs/reference/production-lifecycle.md
index 68d5dd1..3e11ceb 100644
--- a/docs/reference/production-lifecycle.md
+++ b/docs/reference/production-lifecycle.md
@@ -7,15 +7,36 @@ policy.
## Qualified workflow
-A qualified workflow is one exact compiled workflow version that passed its
-declared qualification contract on its bound execution environment. Its signed
-identity binds the workflow bundle, runtime release, dependency set,
-environment, input schema, policy, identity checks, effect checks, and
-verification rules.
+In this Production contract, a qualified workflow is one exact, sealed compiled
+workflow version with an active signed admission. It passed its declared
+acceptance cases on its bound execution environment. It is not a workflow
+category and it is not a manual allowlist entry. The admission binds the
+organization and workflow identity, bundle version and digest, admitted runtime
+release, dependency set, application and environment, input and action
+contracts, policy, identity checks, effect checks, verification rules, evidence
+authority, issue time, expiry time, and revocation state.
-A Production runtime accepts only an exact qualified workflow identity. It
-refuses an absent, expired, revoked, or mismatched qualification. A change to a
-workflow version or any bound input requires a new qualification.
+For example, qualification can cover one invoice-entry bundle against one
+declared application version, runner image, input schema, policy, and
+independent saved-record check. It does not automatically cover another
+application version, a changed bundle, or a different effect verifier.
+
+The evidence must name the task, environment, condition, oracle, and failure
+taxonomy. It must include at least three trials per task per condition. It must
+report explicit silent-incorrect-success and over-halt counts, including zero
+counts. A target-specific acceptance policy can require more evidence.
+
+A Production run requires this exact qualified workflow identity. The run gate
+must refuse an absent, expired, revoked, or mismatched qualification. A change
+to a workflow version or any bound contract value, including the input schema,
+requires a new qualification. Live input values that satisfy the admitted
+schema do not each require requalification.
+
+Workflow qualification and product release admission are separate contracts.
+Workflow qualification proves the named business workflow in its environment.
+Release admission proves that an exact OpenAdapt component or deployment passed
+the target-specific product acceptance policy. A runtime can have a current
+Production admission and still refuse an unqualified customer workflow.
## Release admission
@@ -53,5 +74,12 @@ contains the exact source commit, input hashes, policy, and admission history.
It does not store a static `production: true` flag. A consumer must use the
pinned validator and derive the state at read time.
+This site derives each target label from that exact current admission. It shows
+the product-wide Production label only when all seven targets have an active
+admission and the current default versions of the five public packages match
+those admitted releases on PyPI. Each version must retain an unyanked wheel and
+source distribution. A record mismatch, expiry, revocation, version drift, or
+authority outage suppresses the Production label.
+
Runnable does not mean admitted. An installation, release, or successful demo
cannot create Production state without this complete evidence contract.
diff --git a/mkdocs.yml b/mkdocs.yml
index 2b39868..e6699b4 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -61,6 +61,9 @@ theme:
extra_css:
- stylesheets/brand.css
+extra_javascript:
+ - javascripts/production-lifecycle.js
+
plugins:
- search
@@ -188,7 +191,7 @@ nav:
- The bundle format: reference/bundle-format.md
- Configuration and env vars: reference/configuration.md
- Glossary: reference/glossary.md
- - Package and repository lifecycle: ecosystem/index.md
+ - Product components and release admission: ecosystem/index.md
- Documentation source of truth: reference/documentation-governance.md
- What's New: whats-new.md
- Changelog: changelog.md
diff --git a/repos.yml b/repos.yml
index 0bb16b1..5f2661b 100644
--- a/repos.yml
+++ b/repos.yml
@@ -1,7 +1,7 @@
# Repositories aggregated into the docs site.
#
# Public entries feed the aggregated Changelog and the What's New digest.
-# Private product repositories remain in the registry for lifecycle context but
+# Private product repositories remain in the registry for release-feed context but
# opt out of public release-feed aggregation.
# They intentionally do NOT set `doc_page`: the docs site presents a single
# curated Ecosystem page (docs/ecosystem/index.md) rather than mirroring each
@@ -16,19 +16,16 @@ repos:
- name: OpenAdapt
github: OpenAdaptAI/OpenAdapt
category: core
- lifecycle: beta
changelog: true
- name: openadapt-flow
github: OpenAdaptAI/openadapt-flow
category: core
- lifecycle: beta
changelog: true
- name: openadapt-cloud
github: OpenAdaptAI/openadapt-cloud
category: core
- lifecycle: beta
# The control-plane repository is private. The repository-scoped token used
# to build the public docs cannot read its release or pull-request feeds,
# and granting that token cross-repository access would widen the trust
@@ -40,53 +37,44 @@ repos:
- name: openadapt-desktop
github: OpenAdaptAI/openadapt-desktop
category: core
- lifecycle: beta
changelog: true
- name: openadapt-agent
github: OpenAdaptAI/openadapt-agent
category: core
- lifecycle: experimental
changelog: true
- name: openadapt-ml
github: OpenAdaptAI/openadapt-ml
category: core
- lifecycle: research
changelog: true
- name: openadapt-evals
github: OpenAdaptAI/openadapt-evals
category: core
- lifecycle: research
changelog: true
- name: openadapt-capture
github: OpenAdaptAI/openadapt-capture
category: core
- lifecycle: beta
changelog: true
- name: openadapt-privacy
github: OpenAdaptAI/openadapt-privacy
category: library
- lifecycle: experimental
changelog: true
- name: openadapt-types
github: OpenAdaptAI/openadapt-types
category: library
- lifecycle: experimental
changelog: true
- name: openadapt-grounding
github: OpenAdaptAI/openadapt-grounding
category: library
- lifecycle: research
changelog: true
- name: openadapt-retrieval
github: OpenAdaptAI/openadapt-retrieval
category: library
- lifecycle: research
changelog: true
diff --git a/scripts/check_published_version_claims.py b/scripts/check_published_version_claims.py
old mode 100644
new mode 100755
index 35c2ec7..ec6d3e4
--- a/scripts/check_published_version_claims.py
+++ b/scripts/check_published_version_claims.py
@@ -16,15 +16,18 @@
1. Every registered claim location still exists and still contains its exact
recorded context. A reword that turns a pinned or historical number back
into a publication claim therefore cannot land silently.
-2. No authored page contains a "published X.Y.Z"-shaped sentence that is not
+2. Every generated claim marker matches the claim registry's structured
+ artifact tuple. A release changes that one record and runs the deterministic
+ renderer; a stale or unregistered output fails.
+3. No authored page contains a "published X.Y.Z"-shaped sentence that is not
registered as ``pypi-latest``. This is the specific sentence shape that went
wrong; adding another one now requires declaring that it must track PyPI.
-3. ``docs/changelog.md`` parses and every tracked repository has releases.
+4. ``docs/changelog.md`` parses and every tracked repository has releases.
Network checks (run daily; PyPI is the authority):
-4. Every ``pypi-latest`` claim's version equals PyPI's newest release.
-5. The newest ``vX.Y.Z`` entry in each tracked section of ``docs/changelog.md``
+5. Every ``pypi-latest`` claim's version equals PyPI's newest release.
+6. The newest ``vX.Y.Z`` entry in each tracked section of ``docs/changelog.md``
equals PyPI's newest release for that package. The changelog is the docs'
always-live statement of "what the current release is", so it is guarded
structurally rather than by phrase.
@@ -52,6 +55,8 @@
import urllib.request
from pathlib import Path
+from render_published_version_claims import render_version_claims
+
ROOT = Path(__file__).resolve().parents[1]
REGISTRY_PATH = ROOT / "docs" / "published-version-claims.json"
CHANGELOG_PATH = ROOT / "docs" / "changelog.md"
@@ -111,7 +116,8 @@ def check_claim_locations(registry: dict, report: Report, root: Path = ROOT) ->
"""Every registered claim must still say what the registry says it says."""
for claim in registry.get("claims", []):
locations = claim.get("locations") or []
- if not locations:
+ rendered_locations = claim.get("rendered_locations") or []
+ if not locations and not rendered_locations:
report.error(
f"claim {claim.get('id')!r} registers no locations; a claim "
"nothing points at cannot be kept honest"
@@ -320,6 +326,9 @@ def main() -> int:
report = Report()
check_claim_locations(registry, report)
+ render_errors, _ = render_version_claims(registry, check=True)
+ for error in render_errors:
+ report.error(error)
scan_for_unregistered_claims(registry, report)
changelog_claims = check_changelog_structure(registry, report)
diff --git a/scripts/render_published_version_claims.py b/scripts/render_published_version_claims.py
new file mode 100755
index 0000000..066de7d
--- /dev/null
+++ b/scripts/render_published_version_claims.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+"""Render shared documentation artifact values from the claim registry.
+
+``docs/published-version-claims.json`` is the only editable source for a
+rendered version claim. Authored pages keep invisible, inline markers around
+the generated value so the Markdown remains readable on GitHub and MkDocs.
+
+Usage:
+ python scripts/render_published_version_claims.py
+ python scripts/render_published_version_claims.py --check
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import sys
+from collections import Counter
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+REGISTRY_PATH = ROOT / "docs" / "published-version-claims.json"
+
+NAME = re.compile(r"[A-Za-z0-9_.-]+")
+MARKER = re.compile(
+ r""
+ r"(?P[^<\r\n]+?)"
+ r""
+)
+CLAIM_COMMENT = re.compile(
+ r").)*version-claim(?:(?!-->).)*-->",
+ re.DOTALL | re.IGNORECASE,
+)
+FIELD_PATTERNS = {
+ "version": re.compile(r"\d+\.\d+\.\d+"),
+ "release_commit": re.compile(r"[0-9a-f]{40}"),
+ "wheel_sha256": re.compile(r"[0-9a-f]{64}"),
+ "sdist_sha256": re.compile(r"[0-9a-f]{64}"),
+}
+
+
+def load_registry(path: Path = REGISTRY_PATH) -> dict:
+ return json.loads(path.read_text(encoding="utf-8"))
+
+
+def render_version_claims(
+ registry: dict,
+ *,
+ root: Path = ROOT,
+ check: bool = False,
+) -> tuple[list[str], list[Path]]:
+ """Validate markers and render their claim versions.
+
+ The function validates the complete marker inventory before it writes a
+ file. This prevents a malformed or unregistered marker from producing a
+ partial update.
+ """
+
+ errors: list[str] = []
+ claims: dict[str, dict] = {}
+ expected: Counter[tuple[str, str, str]] = Counter()
+
+ for claim in registry.get("claims", []):
+ claim_id = claim.get("id")
+ if not isinstance(claim_id, str) or not NAME.fullmatch(claim_id):
+ errors.append("a rendered version claim has no valid id")
+ continue
+ if claim_id in claims:
+ errors.append(f"duplicate claim id {claim_id!r}")
+ continue
+ claims[claim_id] = claim
+
+ rendered_locations = claim.get("rendered_locations") or []
+ for location in rendered_locations:
+ file_name = location.get("file")
+ if not isinstance(file_name, str) or not file_name.startswith("docs/"):
+ errors.append(
+ f"claim {claim_id!r} has invalid rendered file {file_name!r}"
+ )
+ continue
+ values = location.get("values")
+ if not isinstance(values, dict) or not values:
+ errors.append(
+ f"claim {claim_id!r} has no rendered values for {file_name}"
+ )
+ continue
+ for field, count in values.items():
+ if not isinstance(field, str) or not NAME.fullmatch(field):
+ errors.append(
+ f"claim {claim_id!r} has invalid rendered field {field!r}"
+ )
+ continue
+ value = claim.get(field)
+ if not isinstance(value, str) or not value:
+ errors.append(
+ f"claim {claim_id!r} has no value for rendered field "
+ f"{field!r}"
+ )
+ pattern = FIELD_PATTERNS.get(field)
+ if pattern is not None and (
+ not isinstance(value, str) or not pattern.fullmatch(value)
+ ):
+ errors.append(
+ f"claim {claim_id!r} field {field!r} has invalid value "
+ f"{value!r}"
+ )
+ if not isinstance(count, int) or isinstance(count, bool) or count < 1:
+ errors.append(
+ f"claim {claim_id!r} has invalid marker count {count!r} "
+ f"for {file_name} field {field!r}"
+ )
+ continue
+ expected[(file_name, claim_id, field)] += count
+
+ docs_dir = root / "docs"
+ actual: Counter[tuple[str, str, str]] = Counter()
+ source_by_path: dict[Path, str] = {}
+ for path in sorted(docs_dir.rglob("*.md")):
+ text = path.read_text(encoding="utf-8")
+ source_by_path[path] = text
+ matches = list(MARKER.finditer(text))
+ if len(CLAIM_COMMENT.findall(text)) != len(matches) * 2:
+ errors.append(
+ f"{path.relative_to(root)} has an incomplete or malformed "
+ "version-claim marker pair"
+ )
+ relative = path.relative_to(root).as_posix()
+ for match in matches:
+ actual[(relative, match.group("id"), match.group("field"))] += 1
+
+ for key in sorted(set(expected) | set(actual)):
+ expected_count = expected[key]
+ actual_count = actual[key]
+ if expected_count != actual_count:
+ file_name, claim_id, field = key
+ errors.append(
+ f"claim {claim_id!r} field {field!r}: {file_name} has "
+ f"{actual_count} rendered marker(s); the registry requires "
+ f"{expected_count}"
+ )
+
+ if errors:
+ return errors, []
+
+ rendered_by_path: dict[Path, str] = {}
+ for path, text in source_by_path.items():
+ if not CLAIM_COMMENT.search(text):
+ continue
+
+ def replace(match: re.Match[str]) -> str:
+ claim_id = match.group("id")
+ field = match.group("field")
+ value = str(claims[claim_id][field])
+ return (
+ f"{value}"
+ f""
+ )
+
+ rendered_by_path[path] = MARKER.sub(replace, text)
+
+ changed = [
+ path
+ for path, rendered in rendered_by_path.items()
+ if source_by_path[path] != rendered
+ ]
+ if check and changed:
+ rendered_names = ", ".join(
+ path.relative_to(root).as_posix() for path in changed
+ )
+ errors.append(
+ "rendered version claims are stale in "
+ f"{rendered_names}; run "
+ "`python scripts/render_published_version_claims.py`"
+ )
+ return errors, []
+
+ if not check:
+ for path in changed:
+ path.write_text(rendered_by_path[path], encoding="utf-8")
+ return [], changed
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="Fail when committed rendered values differ from the registry.",
+ )
+ args = parser.parse_args()
+
+ try:
+ registry = load_registry()
+ errors, changed = render_version_claims(registry, check=args.check)
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
+ print(
+ f"ERROR: could not render published version claims: {exc}",
+ file=sys.stderr,
+ )
+ return 1
+
+ for error in errors:
+ print(f"ERROR: {error}", file=sys.stderr)
+ if errors:
+ return 1
+ if args.check:
+ print("OK: rendered version claims match the registry.")
+ elif changed:
+ print(f"Updated {len(changed)} documentation file(s).")
+ else:
+ print("Rendered version claims are already current.")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validate_docs.py b/scripts/validate_docs.py
index d42a5ae..f26f69b 100644
--- a/scripts/validate_docs.py
+++ b/scripts/validate_docs.py
@@ -1,6 +1,7 @@
"""Validate the generated docs site."""
import pathlib
+import re
import subprocess
import sys
@@ -43,6 +44,12 @@
"`openadapt-capture >=1.1.0`",
"Production deployments should pin the exact versions",
),
+ "reference/production-lifecycle.md": (
+ "at least three trials per task per condition",
+ "silent-incorrect-success",
+ "over-halt",
+ "active signed admission",
+ ),
"packages/openadapt.md": (
"redirect_to: /ecosystem/",
"pip install openadapt",
@@ -52,11 +59,18 @@
REQUIRED_NAV_PAGES = set(REQUIRED_PUBLIC_PAGES) - {"packages/openadapt.md"}
STALE_PRELAUNCH_MARKERS = {
+ "Available for qualification",
"Beta launch candidate",
"full paid production lifecycle remains pending",
"not a public availability statement",
}
+STATIC_MATURITY_PATTERN = re.compile(
+ r"\b(?:beta|experimental)\b|\breference path\b|\bbrowser-only\b",
+ re.IGNORECASE,
+)
+HISTORICAL_DOC_PATHS = {"changelog.md", "whats-new.md"}
+
def check_empty_pages(docs_dir=None):
"""Check for empty or stub-only doc pages."""
@@ -151,6 +165,20 @@ def check_product_docs_contract(docs_dir=None, mkdocs_file=None):
if marker in public_text:
issues.append(f"Stale prelaunch copy: {marker}")
+ # Product maturity is a read-time result from the signed lifecycle record.
+ # A static label can remain after its evidence expires, so authored product
+ # pages must not carry one. Generated historical feeds are exact upstream
+ # release records and stay outside this current-state contract.
+ for path in docs_dir.rglob("*.md"):
+ relative = path.relative_to(docs_dir).as_posix()
+ if relative in HISTORICAL_DOC_PATHS or relative.startswith("packages/"):
+ continue
+ match = STATIC_MATURITY_PATTERN.search(path.read_text())
+ if match:
+ issues.append(
+ f"Static public maturity label in {relative}: {match.group(0)}"
+ )
+
return issues
@@ -164,7 +192,9 @@ def run_mkdocs_build(strict=False):
if strict:
cmd.append("--strict")
cmd.extend(["--site-dir", str(ROOT / "_site_check")])
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT))
+ result = subprocess.run(
+ cmd, capture_output=True, text=True, cwd=str(ROOT), check=False
+ )
# Clean up
site_dir = ROOT / "_site_check"
if site_dir.exists():
diff --git a/tests/js/production_lifecycle.test.cjs b/tests/js/production_lifecycle.test.cjs
new file mode 100644
index 0000000..04d4d97
--- /dev/null
+++ b/tests/js/production_lifecycle.test.cjs
@@ -0,0 +1,333 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const { createHash } = require("node:crypto");
+const test = require("node:test");
+
+const lifecycle = require("../../docs/javascripts/production-lifecycle.js");
+
+const NOW = Date.parse("2026-08-20T12:00:00Z");
+const TARGET_CONFIG = {
+ agent: ["qualified_agent_bridge_release", "public_package"],
+ capture: ["qualified_native_recorder_release", "public_package"],
+ cloud: ["qualified_workflow_control_plane_deployment", "private_deployment"],
+ desktop: ["qualified_native_workflow_desktop_release", "public_package"],
+ docs: ["production_documentation_deployment", "public_deployment"],
+ flow: ["qualified_workflow_runtime_release", "public_package"],
+ openadapt: ["qualified_workflow_launcher_release", "public_package"],
+};
+
+function release(kind) {
+ if (kind === "public_package") return { kind, version: "1.2.3" };
+ if (kind === "public_deployment") return { kind, deployment_id: "docs-123" };
+ return { kind, deployment_release_id: "cloud-123" };
+}
+
+function admission(targetId, sequence = 1, overrides = {}) {
+ const [claimScope, releaseKind] = TARGET_CONFIG[targetId];
+ return {
+ admission_id: `production:${targetId}:${sequence}`,
+ target: targetId,
+ claim_scope: claimScope,
+ release_identity: {
+ schema_version: "openadapt.monotonic-production-release/v1",
+ channel: "production",
+ sequence,
+ previous_admission_sha256: null,
+ },
+ policy_revision: 1,
+ release: release(releaseKind),
+ acceptance_evidence: {
+ summary_url: `https://evidence.openadapt.ai/${targetId}.json`,
+ summary_sha256: `sha256:${"a".repeat(64)}`,
+ },
+ issued_at: "2026-08-19T12:00:00Z",
+ expires_at: "2026-09-18T12:00:00Z",
+ revoked_at: null,
+ ...overrides,
+ };
+}
+
+function source(admissionsDigest) {
+ const commit = "d".repeat(40);
+ const base = `https://raw.githubusercontent.com/OpenAdaptAI/.github/${commit}/`;
+ const paths = {
+ admissions: "production-lifecycle-admissions.json",
+ admissions_schema: "schemas/production-lifecycle-admissions.schema.json",
+ evidence_manifest_schema:
+ "schemas/production-lifecycle-evidence-manifest.schema.json",
+ evidence_summary_schema:
+ "schemas/production-lifecycle-evidence-summary.schema.json",
+ lifecycle: "repository-lifecycle.yml",
+ policy: "production-lifecycle-policy.json",
+ policy_schema: "schemas/production-lifecycle-policy.schema.json",
+ validator: "scripts/validate_production_lifecycle.py",
+ };
+ return {
+ schema_version: "openadapt.production-lifecycle-source/v1",
+ repository: "OpenAdaptAI/.github",
+ source_commit: commit,
+ files: Object.fromEntries(
+ Object.entries(paths).map(([key, path]) => [
+ key,
+ {
+ path,
+ url: `${base}${path}`,
+ sha256:
+ key === "admissions"
+ ? admissionsDigest
+ : `sha256:${(key === "policy" ? "b" : "c").repeat(64)}`,
+ },
+ ]),
+ ),
+ };
+}
+
+function makeFixture(histories = {}) {
+ const admissions = lifecycle.TARGET_IDS.flatMap(
+ (targetId) => histories[targetId] ?? [admission(targetId)],
+ );
+ const live = {
+ $schema: "schemas/production-lifecycle-admissions.schema.json",
+ schema_version: "openadapt.production-lifecycle-admissions/v1",
+ policy_sha256: `sha256:${"b".repeat(64)}`,
+ admissions,
+ };
+ const bytes = Buffer.from(JSON.stringify(live));
+ const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
+ const projection = {
+ $schema: "schemas/production-lifecycle-public.schema.json",
+ schema_version: "openadapt.public-production-lifecycle/v1",
+ source: source(digest),
+ policy_revision: 1,
+ maximum_admission_days: 30,
+ derivation: {
+ mode: "latest_signed_admission_at_read_time",
+ static_production_state: false,
+ expired_or_revoked_latest_behavior: "no_production",
+ fallback_to_older_release: false,
+ },
+ targets: lifecycle.TARGET_IDS.map((targetId) => {
+ const history = [...(histories[targetId] ?? [admission(targetId)])].sort(
+ (left, right) => left.release_identity.sequence - right.release_identity.sequence,
+ );
+ const [claimScope, releaseKind] = TARGET_CONFIG[targetId];
+ return {
+ id: targetId,
+ display_name: targetId,
+ lifecycle_scope: targetId === "docs" ? "public_surface" : "repository",
+ lifecycle_subject: targetId,
+ source_repository: `OpenAdaptAI/${targetId}`,
+ required_claim_scope: claimScope,
+ release_kind: releaseKind,
+ required_artifact_kinds: [],
+ package_index_project: null,
+ artifact_authority_by_kind: {},
+ admission_history: history,
+ latest_admission: history.at(-1) ?? null,
+ };
+ }),
+ };
+ return { bytes, live, projection };
+}
+
+function fetchFixture(fixture, options = {}) {
+ return async (url) => {
+ if (options.unavailable) return { ok: false };
+ if (url.startsWith(`${lifecycle.PROJECTION_URL}?openadapt_lifecycle_request=`)) {
+ return { ok: true, json: async () => structuredClone(fixture.projection) };
+ }
+ if (url.startsWith("https://pypi.org/pypi/")) {
+ const project = url.split("/")[4];
+ const version = options.pypiVersions?.[project] ?? "1.2.3";
+ const hasYankedValue = Object.prototype.hasOwnProperty.call(
+ options.pypiYankedValues ?? {},
+ project,
+ );
+ const yanked = hasYankedValue
+ ? options.pypiYankedValues[project]
+ : (options.pypiYanked?.includes(project) ?? false);
+ const artifact = (packagetype) =>
+ options.pypiMissingYanked?.includes(project)
+ ? { packagetype }
+ : { packagetype, yanked };
+ return {
+ ok: !options.pypiUnavailable,
+ json: async () => ({
+ info: { version },
+ releases: {
+ [version]: [
+ artifact("bdist_wheel"),
+ artifact("sdist"),
+ ],
+ },
+ }),
+ };
+ }
+ const bytes = options.bytes ?? fixture.bytes;
+ return {
+ ok: true,
+ arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
+ };
+ };
+}
+
+test("a current digest-bound admission renders every target as Production", async () => {
+ const fixture = makeFixture();
+ const state = await lifecycle.load(fetchFixture(fixture), NOW);
+
+ assert.ok(state.activeTargets instanceof Map);
+ assert.equal(state.defaultInstallVerified, true);
+ assert.deepEqual([...state.activeTargets.keys()].sort(), lifecycle.TARGET_IDS);
+ assert.equal(state.activeTargets.get("flow").releaseLabel, "release 1.2.3");
+ assert.equal(
+ state.activeTargets.get("cloud").releaseLabel,
+ "deployment release cloud-123",
+ );
+});
+
+test("an expired latest admission does not retain Production", async () => {
+ const expired = admission("flow", 1, { expires_at: "2026-08-20T11:59:59Z" });
+ const fixture = makeFixture({ flow: [expired] });
+ const state = await lifecycle.load(fetchFixture(fixture), NOW);
+
+ assert.ok(state.activeTargets instanceof Map);
+ assert.equal(state.activeTargets.has("flow"), false);
+ assert.equal(state.defaultInstallVerified, false);
+});
+
+test("a revoked latest admission never falls back to an older active release", async () => {
+ const old = admission("flow", 1);
+ const revoked = admission("flow", 2, { revoked_at: "2026-08-20T11:00:00Z" });
+ const fixture = makeFixture({ flow: [old, revoked] });
+ const state = await lifecycle.load(fetchFixture(fixture), NOW);
+
+ assert.ok(state.activeTargets instanceof Map);
+ assert.equal(state.activeTargets.has("flow"), false);
+});
+
+test("a live-record digest mismatch fails closed", async () => {
+ const fixture = makeFixture();
+ const changed = Buffer.from(`${fixture.bytes.toString()}\n`);
+
+ assert.equal(await lifecycle.load(fetchFixture(fixture, { bytes: changed }), NOW), null);
+ assert.equal(await lifecycle.load(fetchFixture(fixture, { unavailable: true }), NOW), null);
+});
+
+test("each validation requests uncached projection and live-record URLs", async () => {
+ const fixture = makeFixture();
+ const urls = [];
+ const fetchImpl = fetchFixture(fixture);
+ const recordingFetch = async (url, options) => {
+ urls.push(url);
+ assert.equal(options.cache, "no-store");
+ return fetchImpl(url, options);
+ };
+
+ await lifecycle.load(recordingFetch, NOW);
+ await lifecycle.load(recordingFetch, NOW);
+
+ assert.equal(urls.length, 14);
+ assert.notEqual(urls[0], urls[7]);
+ assert.notEqual(urls[1], urls[8]);
+ assert.match(urls[0], /^\/production-lifecycle\.json\?openadapt_lifecycle_request=/);
+ assert.match(
+ urls[1],
+ /^https:\/\/raw\.githubusercontent\.com\/OpenAdaptAI\/\.github\/main\/production-lifecycle-admissions\.json\?openadapt_lifecycle_request=/,
+ );
+});
+
+test("PyPI drift or a yanked default artifact blocks only product-wide Production", async () => {
+ const fixture = makeFixture();
+ const drifted = await lifecycle.load(
+ fetchFixture(fixture, { pypiVersions: { "openadapt-flow": "1.2.4" } }),
+ NOW,
+ );
+ assert.equal(drifted.activeTargets.size, 7);
+ assert.equal(drifted.defaultInstallVerified, false);
+
+ const yanked = await lifecycle.load(
+ fetchFixture(fixture, { pypiYanked: ["openadapt-agent"] }),
+ NOW,
+ );
+ assert.equal(yanked.activeTargets.size, 7);
+ assert.equal(yanked.defaultInstallVerified, false);
+
+ const missingYanked = await lifecycle.load(
+ fetchFixture(fixture, { pypiMissingYanked: ["openadapt-capture"] }),
+ NOW,
+ );
+ assert.equal(missingYanked.defaultInstallVerified, false);
+
+ const malformedYanked = await lifecycle.load(
+ fetchFixture(fixture, {
+ pypiYankedValues: { "openadapt-desktop": "false" },
+ }),
+ NOW,
+ );
+ assert.equal(malformedYanked.defaultInstallVerified, false);
+});
+
+test("an older delayed refresh cannot overwrite a newer negative state", async () => {
+ const product = { textContent: "" };
+ const document = {
+ querySelectorAll(selector) {
+ if (selector === "[data-openadapt-production-product]") return [product];
+ return [];
+ },
+ };
+ let resolveOlder;
+ let resolveNewer;
+ const olderState = new Promise((resolve) => {
+ resolveOlder = resolve;
+ });
+ const newerState = new Promise((resolve) => {
+ resolveNewer = resolve;
+ });
+ const activeTargets = new Map(lifecycle.TARGET_IDS.map((id) => [id, {}]));
+
+ const olderRefresh = lifecycle.refreshWithLoader(document, () => olderState);
+ const newerRefresh = lifecycle.refreshWithLoader(document, () => newerState);
+ resolveNewer(null);
+ assert.equal(await newerRefresh, true);
+ assert.equal(product.textContent, lifecycle.PRODUCT_REQUIREMENT);
+
+ resolveOlder({ activeTargets, defaultInstallVerified: true });
+ assert.equal(await olderRefresh, false);
+ assert.equal(product.textContent, lifecycle.PRODUCT_REQUIREMENT);
+});
+
+test("product-wide Production requires all seven active targets", () => {
+ const product = { textContent: "" };
+ const document = {
+ querySelectorAll(selector) {
+ if (selector === "[data-openadapt-production-product]") return [product];
+ return [];
+ },
+ };
+ const six = new Map(lifecycle.TARGET_IDS.slice(0, 6).map((id) => [id, {}]));
+ lifecycle.render(document, { activeTargets: six, defaultInstallVerified: true });
+ assert.equal(product.textContent, lifecycle.PRODUCT_REQUIREMENT);
+
+ const seven = new Map(lifecycle.TARGET_IDS.map((id) => [id, {}]));
+ lifecycle.render(document, {
+ activeTargets: seven,
+ defaultInstallVerified: false,
+ });
+ assert.equal(product.textContent, lifecycle.PRODUCT_REQUIREMENT);
+ lifecycle.render(document, {
+ activeTargets: seven,
+ defaultInstallVerified: true,
+ });
+ assert.equal(product.textContent, "Production");
+});
+
+test("the committed admission-free projection keeps every label neutral", async () => {
+ const committed = require("../../docs/production-lifecycle.json");
+ const targets = lifecycle.validateProjection(committed);
+
+ assert.ok(targets instanceof Map);
+ for (const target of targets.values()) {
+ assert.equal(lifecycle.deriveTarget(target, committed, NOW), null);
+ }
+});
diff --git a/tests/test_production_lifecycle_browser.py b/tests/test_production_lifecycle_browser.py
new file mode 100644
index 0000000..16a95ec
--- /dev/null
+++ b/tests/test_production_lifecycle_browser.py
@@ -0,0 +1,26 @@
+"""Run the browser lifecycle contract tests in the normal pytest gate."""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def test_browser_lifecycle_contract() -> None:
+ node = shutil.which("node")
+ if node is None:
+ pytest.fail("Node.js is required for the public lifecycle browser contract")
+ completed = subprocess.run(
+ [node, "--test", "tests/js/production_lifecycle.test.cjs"],
+ cwd=ROOT,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=60,
+ )
+ assert completed.returncode == 0, completed.stdout + completed.stderr
diff --git a/tests/test_published_version_claims.py b/tests/test_published_version_claims.py
index 5613dd6..5410b5c 100644
--- a/tests/test_published_version_claims.py
+++ b/tests/test_published_version_claims.py
@@ -20,7 +20,7 @@
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
-from check_published_version_claims import ( # noqa: E402
+from check_published_version_claims import (
Report,
check_changelog_structure,
check_claim_locations,
@@ -30,6 +30,7 @@
parse_changelog,
scan_for_unregistered_claims,
)
+from render_published_version_claims import render_version_claims
CHANGELOG = REPO_ROOT / "docs" / "changelog.md"
@@ -248,6 +249,196 @@ def test_missing_claim_file_fails(tmp_path):
assert any("does not exist" in error for error in report.errors)
+# --------------------------------------------------------------------------
+# One source renders every active managed-runtime version
+# --------------------------------------------------------------------------
+
+
+def _rendered_registry(version="1.31.0"):
+ return {
+ "claims": [
+ {
+ "id": "managed-runtime",
+ "kind": "pinned-deployment",
+ "package": "openadapt-flow",
+ "version": version,
+ "release_commit": "a" * 40,
+ "wheel_sha256": "b" * 64,
+ "sdist_sha256": "c" * 64,
+ "rendered_locations": [
+ {"file": "docs/a.md", "values": {"version": 1}},
+ {"file": "docs/b.md", "values": {"version": 2}},
+ ],
+ }
+ ]
+ }
+
+
+def _marked(
+ claim_id="managed-runtime",
+ field="version",
+ value="1.31.0",
+):
+ return (
+ f"{value}"
+ f""
+ )
+
+
+def test_one_registry_version_renders_every_registered_location(tmp_path):
+ registry = _rendered_registry(version="1.32.0")
+ _tree(
+ tmp_path,
+ {
+ "docs/a.md": f"Flow {_marked()} artifact\n",
+ "docs/b.md": f"runner {_marked()} and compiler {_marked()}\n",
+ },
+ )
+
+ errors, changed = render_version_claims(registry, root=tmp_path)
+
+ assert errors == []
+ assert {path.name for path in changed} == {"a.md", "b.md"}
+ assert "1.31.0" not in (tmp_path / "docs/a.md").read_text()
+ assert (tmp_path / "docs/a.md").read_text().count("1.32.0") == 1
+ assert (tmp_path / "docs/b.md").read_text().count("1.32.0") == 2
+
+
+def test_one_registry_claim_renders_the_complete_artifact_tuple(tmp_path):
+ registry = _rendered_registry(version="1.32.0")
+ claim = registry["claims"][0]
+ claim.update(
+ release_commit="d" * 40,
+ wheel_sha256="e" * 64,
+ sdist_sha256="f" * 64,
+ rendered_locations=[
+ {
+ "file": "docs/tuple.md",
+ "values": {
+ "version": 1,
+ "release_commit": 1,
+ "wheel_sha256": 1,
+ "sdist_sha256": 1,
+ },
+ }
+ ],
+ )
+ _tree(
+ tmp_path,
+ {
+ "docs/tuple.md": " ".join(
+ [
+ _marked(),
+ _marked(field="release_commit", value="a" * 40),
+ _marked(field="wheel_sha256", value="b" * 64),
+ _marked(field="sdist_sha256", value="c" * 64),
+ ]
+ )
+ },
+ )
+
+ errors, changed = render_version_claims(registry, root=tmp_path)
+
+ rendered = (tmp_path / "docs/tuple.md").read_text()
+ assert errors == []
+ assert [path.name for path in changed] == ["tuple.md"]
+ for expected in ("1.32.0", "d" * 40, "e" * 64, "f" * 64):
+ assert expected in rendered
+
+
+def test_render_check_fails_when_a_generated_value_is_stale(tmp_path):
+ registry = _rendered_registry(version="1.32.0")
+ _tree(
+ tmp_path,
+ {
+ "docs/a.md": f"Flow {_marked()} artifact\n",
+ "docs/b.md": f"runner {_marked()} and compiler {_marked()}\n",
+ },
+ )
+
+ errors, changed = render_version_claims(registry, root=tmp_path, check=True)
+
+ assert changed == []
+ assert any("rendered version claims are stale" in error for error in errors)
+ assert "1.31.0" in (tmp_path / "docs/a.md").read_text()
+
+
+def test_render_check_fails_for_missing_or_extra_marker(tmp_path):
+ registry = _rendered_registry()
+ _tree(
+ tmp_path,
+ {
+ "docs/a.md": f"Flow {_marked()} artifact\n",
+ "docs/b.md": f"runner {_marked()}\n",
+ "docs/unregistered.md": f"Flow {_marked('other')}\n",
+ },
+ )
+
+ errors, _ = render_version_claims(registry, root=tmp_path, check=True)
+
+ assert any("docs/b.md has 1 rendered marker" in error for error in errors)
+ assert any("claim 'other'" in error for error in errors)
+
+
+def test_render_refuses_malformed_marker_without_partial_writes(tmp_path):
+ registry = _rendered_registry(version="1.32.0")
+ malformed = "1.31.0"
+ _tree(
+ tmp_path,
+ {
+ "docs/a.md": f"Flow {_marked()} artifact\n",
+ "docs/b.md": f"runner {_marked()} and compiler {malformed}\n",
+ },
+ )
+
+ errors, changed = render_version_claims(registry, root=tmp_path)
+
+ assert changed == []
+ assert any("incomplete or malformed" in error for error in errors)
+ assert "1.31.0" in (tmp_path / "docs/a.md").read_text()
+
+
+def test_render_rejects_a_marker_with_an_invalid_identifier(tmp_path):
+ registry = _rendered_registry()
+ invalid = (
+ "1.31.0"
+ ""
+ )
+ _tree(
+ tmp_path,
+ {
+ "docs/a.md": f"Flow {_marked()} artifact\n",
+ "docs/b.md": f"runner {_marked()} and compiler {invalid}\n",
+ },
+ )
+
+ errors, changed = render_version_claims(registry, root=tmp_path)
+
+ assert changed == []
+ assert any("incomplete or malformed" in error for error in errors)
+
+
+def test_render_rejects_a_marker_with_a_missing_colon(tmp_path):
+ registry = _rendered_registry(version="1.32.0")
+ invalid = (
+ "1.31.0"
+ ""
+ )
+ _tree(
+ tmp_path,
+ {
+ "docs/a.md": f"Flow {_marked()} artifact\n",
+ "docs/b.md": f"runner {_marked()} and compiler {invalid}\n",
+ },
+ )
+
+ errors, changed = render_version_claims(registry, root=tmp_path)
+
+ assert changed == []
+ assert any("incomplete or malformed" in error for error in errors)
+ assert "1.31.0" in (tmp_path / "docs/a.md").read_text()
+
+
# --------------------------------------------------------------------------
# Changelog structure
# --------------------------------------------------------------------------
@@ -288,6 +479,8 @@ def test_committed_docs_pass_the_offline_checks(registry):
"""Whatever else changes, the committed tree must be self-consistent."""
report = Report()
check_claim_locations(registry, report)
+ render_errors, _ = render_version_claims(registry, check=True)
+ report.errors.extend(render_errors)
scan_for_unregistered_claims(registry, report)
check_changelog_structure(registry, report)
diff --git a/tests/test_sync_readmes.py b/tests/test_sync_readmes.py
index e9393dc..905740a 100644
--- a/tests/test_sync_readmes.py
+++ b/tests/test_sync_readmes.py
@@ -3,11 +3,9 @@
import pathlib
import sys
-import pytest
-
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "scripts"))
-from sync_readmes import sync, load_repos, fetch_readme
+from sync_readmes import fetch_readme, load_repos, sync
FIXTURES = pathlib.Path(__file__).parent / "fixtures"
ROOT = pathlib.Path(__file__).resolve().parent.parent
@@ -16,12 +14,8 @@
def test_load_repos():
repos = load_repos()
assert len(repos) > 0
- assert all("name" in r and "github" in r and "lifecycle" in r for r in repos)
- assert {r["lifecycle"] for r in repos} <= {
- "beta", "experimental", "research", "deprecated",
- }
- lifecycle_by_name = {r["name"]: r["lifecycle"] for r in repos}
- assert lifecycle_by_name["openadapt-agent"] == "experimental"
+ assert all("name" in r and "github" in r for r in repos)
+ assert all("lifecycle" not in r for r in repos)
def test_sync_renders_pages(tmp_path, mocker):
diff --git a/tests/test_validate_docs.py b/tests/test_validate_docs.py
index 91bebfc..a7df12e 100644
--- a/tests/test_validate_docs.py
+++ b/tests/test_validate_docs.py
@@ -8,14 +8,15 @@
from validate_docs import check_empty_pages, check_product_docs_contract
-def test_openadapt_agent_catalog_describes_active_v2_bridge():
+def test_product_catalog_binds_all_admitted_targets_to_live_state():
root = pathlib.Path(__file__).resolve().parent.parent
content = (root / "docs" / "ecosystem" / "index.md").read_text()
- assert "openadapt-agent](https://github.com/OpenAdaptAI/openadapt-agent)" in content
- assert "Active v2 bridge" in content
- assert "repository itself is active" in content
- assert "Superseded execution direction being folded into `openadapt-flow`" not in content
+ for target in ("agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"):
+ assert f'data-openadapt-production-target="{target}"' in content
+ assert "Production requires an active signed admission" in content
+ assert "**Beta**" not in content
+ assert "**Experimental**" not in content
def test_check_empty_pages_finds_issues(tmp_path):
@@ -89,6 +90,11 @@ def _write_contract_docs(root):
"`openadapt-flow >=1.22,<2` + `openadapt-capture >=1.1.0`\n\n"
"Production deployments should pin the exact versions."
),
+ "reference/production-lifecycle.md": (
+ "# Production admission\n\nA qualified workflow requires an active signed "
+ "admission and at least three trials per task per condition. Report "
+ "each silent-incorrect-success and over-halt count."
+ ),
"packages/openadapt.md": (
"---\nredirect_to: /ecosystem/\n---\n\n"
"# OpenAdapt package documentation moved\n\n"
@@ -149,6 +155,23 @@ def test_product_docs_contract_rejects_stale_prelaunch_copy(tmp_path):
assert any("Stale prelaunch copy" in issue for issue in issues)
+def test_product_docs_contract_rejects_static_maturity_copy(tmp_path):
+ docs_dir = tmp_path / "docs"
+ pages = _write_contract_docs(docs_dir)
+ hosted = docs_dir / "guides/hosted.md"
+ hosted.write_text(hosted.read_text() + "\nThe recorder is Experimental.\n")
+ mkdocs_file = tmp_path / "mkdocs.yml"
+ mkdocs_file.write_text(
+ "nav:\n - Reference:\n"
+ + "".join(f" - {path}\n" for path in pages)
+ + " - Package and repository lifecycle: ecosystem/index.md\n"
+ )
+
+ issues = check_product_docs_contract(docs_dir, mkdocs_file)
+
+ assert any("Static public maturity label" in issue for issue in issues)
+
+
def test_product_docs_contract_rejects_competing_install_identity(tmp_path):
docs_dir = tmp_path / "docs"
pages = _write_contract_docs(docs_dir)