From d07a0d12a490bdb3a05ac87836c6d971c28b5b8e Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 11:42:11 -0400 Subject: [PATCH 1/9] docs: document existing-session browser recording --- docs/concepts/backends.md | 17 +++++++-- docs/ecosystem/index.md | 2 +- docs/get-started/first-workflow.md | 6 ++++ docs/get-started/what-works-today.md | 2 +- docs/guides/record-your-app.md | 54 ++++++++++++++++++++++++++-- docs/reference/cli.md | 4 ++- docs/reference/configuration.md | 7 ++++ 7 files changed, 84 insertions(+), 8 deletions(-) diff --git a/docs/concepts/backends.md b/docs/concepts/backends.md index c3acf92..52d2350 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,21 @@ 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. It refuses only an action that overlaps an + unverified transition. It shares the same bundle, resolution ladder, and identity gate as every other substrate; nothing about the safety model is specific to it. +The custom Chrome extension in `openadapt-capture` remains a development +prototype. Browser acquisition stays Playwright-native because source-time +secret exclusion, DOM identity, field geometry, and exact event/frame binding +are part of the supported compiler contract. + ### Desktop: Windows (UIA) The public `WindowsBackend` now narrows the in-session boundary to typed diff --git a/docs/ecosystem/index.md b/docs/ecosystem/index.md index 663bf14..d6727d0 100644 --- a/docs/ecosystem/index.md +++ b/docs/ecosystem/index.md @@ -35,7 +35,7 @@ module inside it: | [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-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.2 retains Windows UIA evidence at action time; remote sessions remain externally black-box. Browser recording remains inside Flow's Playwright recorder, which can launch Chromium or attach to one existing local tab. The custom Capture extension remains a prototype. | | [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. | 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..39ebd64 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. The custom Capture extension remains a prototype. | | 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. | diff --git a/docs/guides/record-your-app.md b/docs/guides/record-your-app.md index ea578bf..471b6ee 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,54 @@ 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. For example, on macOS: + +```bash +"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + --remote-debugging-address=127.0.0.1 \ + --remote-debugging-port=9222 \ + --user-data-dir="./.openadapt-chrome-profile" +``` + +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. The attached +screenshots use the tab's actual CSS viewport, so retained frames and DOM input +coordinates stay aligned on high-density displays. Password fields and fields +declared with `--secret` keep the same source-time exclusion and frame-redaction +contract as launch mode. + +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 or an event from an iframe. 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. Stop interacting for a moment after a resize. Recording then +continues automatically. + +The custom Chrome extension in `openadapt-capture` is a development prototype. +It is not this supported path and it is not a governed replay mechanism. + ## Compile and replay ```bash 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). | From 07ec05d44cba24ece5a66221d27bb1580d031657 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:11:14 -0400 Subject: [PATCH 2/9] docs: align Agent lifecycle with Beta --- docs/ecosystem/index.md | 2 +- repos.yml | 2 +- tests/test_sync_readmes.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ecosystem/index.md b/docs/ecosystem/index.md index d6727d0..dd4bdb7 100644 --- a/docs/ecosystem/index.md +++ b/docs/ecosystem/index.md @@ -34,7 +34,7 @@ module inside it: | [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-agent](https://github.com/OpenAdaptAI/openadapt-agent) | **Beta** | 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.2 retains Windows UIA evidence at action time; remote sessions remain externally black-box. Browser recording remains inside Flow's Playwright recorder, which can launch Chromium or attach to one existing local tab. The custom Capture extension remains a prototype. | | [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. | diff --git a/repos.yml b/repos.yml index 0bb16b1..d95fb6c 100644 --- a/repos.yml +++ b/repos.yml @@ -46,7 +46,7 @@ repos: - name: openadapt-agent github: OpenAdaptAI/openadapt-agent category: core - lifecycle: experimental + lifecycle: beta changelog: true - name: openadapt-ml diff --git a/tests/test_sync_readmes.py b/tests/test_sync_readmes.py index e9393dc..006d274 100644 --- a/tests/test_sync_readmes.py +++ b/tests/test_sync_readmes.py @@ -21,7 +21,7 @@ def test_load_repos(): "beta", "experimental", "research", "deprecated", } lifecycle_by_name = {r["name"]: r["lifecycle"] for r in repos} - assert lifecycle_by_name["openadapt-agent"] == "experimental" + assert lifecycle_by_name["openadapt-agent"] == "beta" def test_sync_renders_pages(tmp_path, mocker): From 431f9f0901395243dc9bf5a4869d89c6d853cffa Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:14:48 -0400 Subject: [PATCH 3/9] docs: remove temporary browser reference framing --- docs/index.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 6c30835..ab478ac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -218,10 +218,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 From d0ddfcc3efc592df88518e5b4103fbb1b5e439d1 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:22:30 -0400 Subject: [PATCH 4/9] docs: render managed runtime version from one source --- .github/workflows/sync.yml | 3 + README.md | 12 ++ docs/concepts/deployment-matrix.md | 2 +- docs/get-started/what-works-today.md | 2 +- docs/guides/hosted.md | 4 +- docs/guides/record-your-app.md | 5 +- docs/guides/security-review.md | 2 +- docs/published-version-claims.json | 19 +-- scripts/check_published_version_claims.py | 19 ++- scripts/render_published_version_claims.py | 190 +++++++++++++++++++++ tests/test_published_version_claims.py | 85 +++++++++ 11 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 scripts/render_published_version_claims.py 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..44bb909 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,18 @@ uv run mkdocs build --strict uv run python scripts/validate_docs.py ``` +The managed-runtime version has one editable source: +`docs/published-version-claims.json`. Change the claim's `version`, `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/deployment-matrix.md b/docs/concepts/deployment-matrix.md index 4761f4b..e6a0777 100644 --- a/docs/concepts/deployment-matrix.md +++ b/docs/concepts/deployment-matrix.md @@ -108,7 +108,7 @@ 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 +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 diff --git a/docs/get-started/what-works-today.md b/docs/get-started/what-works-today.md index 39ebd64..067c0e0 100644 --- a/docs/get-started/what-works-today.md +++ b/docs/get-started/what-works-today.md @@ -72,7 +72,7 @@ customer-controlled runtime connected to the same governance model. | 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..a28712e 100644 --- a/docs/guides/hosted.md +++ b/docs/guides/hosted.md @@ -38,7 +38,7 @@ switching, and sign-out. | Surface | Launch status | 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. | +| 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. | @@ -64,7 +64,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 471b6ee..89b3258 100644 --- a/docs/guides/record-your-app.md +++ b/docs/guides/record-your-app.md @@ -65,8 +65,9 @@ the viewport history and the exact before and after viewport for each event. Flow refuses a cross-origin navigation or an event from an iframe. 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. Stop interacting for a moment after a resize. Recording then -continues automatically. +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 custom Chrome extension in `openadapt-capture` is a development prototype. It is not this supported path and it is not a governed replay mechanism. diff --git a/docs/guides/security-review.md b/docs/guides/security-review.md index 6974b3a..aedab36 100644 --- a/docs/guides/security-review.md +++ b/docs/guides/security-review.md @@ -132,7 +132,7 @@ 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, diff --git a/docs/published-version-claims.json b/docs/published-version-claims.json index d6162c2..9002077 100644 --- a/docs/published-version-claims.json +++ b/docs/published-version-claims.json @@ -21,6 +21,9 @@ " be reworded into one. Bump it only with evidence.", " historical A version some retained measurement was taken on.", " Frozen forever; bumping it would falsify the record.", + " rendered_locations Pages whose marked version is generated from the", + " claim's one version field. Change that field once,", + " then run scripts/render_published_version_claims.py.", "", "Adding a 'published X.Y.Z' style sentence to docs/ without a matching", "pypi-latest entry here fails the check." @@ -43,28 +46,24 @@ "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.", + "evidence": "openadapt-cloud origin/main 4e0257a1299cc0869af3e3f664a6ebfa0b59db1 runner/runtime-version.json pins the openadapt_flow artifact at release commit 2d225dea9a0ad29ca84ce1b037cc0ac671367e28 (wheel sha256 81133db1528ad1bb1f26e3fcb6aea61b0651db6d905cf2e4943e8383c1f3d29c; sdist sha256 cf1fc356d14d267df82be188de3e9a3575734f18f46ef91ac8075438cc731540). The package version is the claim's version field. 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" + "count": 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" + "count": 1 }, { "file": "docs/guides/hosted.md", - "context": "managed-runtime manifest pins the Flow 1.31.0 artifact identity" + "count": 2 }, { "file": "docs/get-started/what-works-today.md", - "context": "managed-runtime manifest pins the Flow 1.31.0 runner/compiler artifact identity" + "count": 1 } ] }, diff --git a/scripts/check_published_version_claims.py b/scripts/check_published_version_claims.py index 35c2ec7..d8b817e 100644 --- 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 version marker matches the claim registry's one editable + version field. A release changes that field once 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 100644 index 0000000..a8657ae --- /dev/null +++ b/scripts/render_published_version_claims.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Render shared documentation versions 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" + +MARKER = re.compile( + r"" + r"(?P[^<\r\n]+?)" + r"" +) +OPEN_MARKER = re.compile(r"") +CLOSE_MARKER = re.compile(r"") +SEMVER = re.compile(r"\d+\.\d+\.\d+") + + +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]] = Counter() + + for claim in registry.get("claims", []): + claim_id = claim.get("id") + if not isinstance(claim_id, str) or not 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 [] + if rendered_locations and not SEMVER.fullmatch(str(claim.get("version", ""))): + errors.append( + f"claim {claim_id!r} has rendered locations but version " + f"{claim.get('version')!r} is not X.Y.Z" + ) + for location in rendered_locations: + file_name = location.get("file") + count = location.get("count", 1) + 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 + 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}" + ) + continue + expected[(file_name, claim_id)] += count + + docs_dir = root / "docs" + actual: Counter[tuple[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(matches) != len(OPEN_MARKER.findall(text)) + or len(matches) != len(CLOSE_MARKER.findall(text)) + ): + 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"))] += 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 = key + errors.append( + f"claim {claim_id!r}: {file_name} has {actual_count} rendered " + f"marker(s); the registry requires {expected_count}" + ) + + if errors: + return errors, [] + + rendered_by_path: dict[Path, str] = {} + for path, text in source_by_path.items(): + if not OPEN_MARKER.search(text): + continue + + def replace(match: re.Match[str]) -> str: + claim_id = match.group("id") + version = str(claims[claim_id]["version"]) + return ( + f"{version}" + 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/tests/test_published_version_claims.py b/tests/test_published_version_claims.py index 5613dd6..8da181e 100644 --- a/tests/test_published_version_claims.py +++ b/tests/test_published_version_claims.py @@ -30,6 +30,7 @@ parse_changelog, scan_for_unregistered_claims, ) +from render_published_version_claims import render_version_claims # noqa: E402 CHANGELOG = REPO_ROOT / "docs" / "changelog.md" @@ -248,6 +249,88 @@ 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, + "rendered_locations": [ + {"file": "docs/a.md", "count": 1}, + {"file": "docs/b.md", "count": 2}, + ], + } + ] + } + + +def _marked(claim_id="managed-runtime", version="1.31.0"): + return ( + f"{version}" + 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_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) + + # -------------------------------------------------------------------------- # Changelog structure # -------------------------------------------------------------------------- @@ -288,6 +371,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) From d89481dc5f748c65a079bf20f5045d2f585235b4 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:23:39 -0400 Subject: [PATCH 5/9] docs: state resize abort boundary explicitly --- docs/concepts/backends.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/concepts/backends.md b/docs/concepts/backends.md index 52d2350..d242492 100644 --- a/docs/concepts/backends.md +++ b/docs/concepts/backends.md @@ -43,8 +43,9 @@ browser exposes a full structured layer: 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. It refuses only an action that overlaps an - unverified transition. + 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. From 256f28d49094a1442dc46b93905c18b14ea2eee2 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:26:07 -0400 Subject: [PATCH 6/9] docs: clarify rendered location schema --- docs/published-version-claims.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/published-version-claims.json b/docs/published-version-claims.json index 9002077..e12f5e0 100644 --- a/docs/published-version-claims.json +++ b/docs/published-version-claims.json @@ -21,9 +21,10 @@ " be reworded into one. Bump it only with evidence.", " historical A version some retained measurement was taken on.", " Frozen forever; bumping it would falsify the record.", - " rendered_locations Pages whose marked version is generated from the", - " claim's one version field. Change that field once,", - " then run scripts/render_published_version_claims.py.", + "", + "rendered_locations:", + " Pages whose marked version is generated from the claim's one version", + " field. Change that field 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." From fe50b33210ca16c765a6d576ccaf7e51bfeebd46 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:27:27 -0400 Subject: [PATCH 7/9] test: refuse malformed version renders --- tests/test_published_version_claims.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_published_version_claims.py b/tests/test_published_version_claims.py index 8da181e..f2126ff 100644 --- a/tests/test_published_version_claims.py +++ b/tests/test_published_version_claims.py @@ -331,6 +331,24 @@ def test_render_check_fails_for_missing_or_extra_marker(tmp_path): 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() + + # -------------------------------------------------------------------------- # Changelog structure # -------------------------------------------------------------------------- From 05ce0aed54e2b5dd2b1c51005c0bf52090e402b3 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:37:14 -0400 Subject: [PATCH 8/9] docs: render complete runtime artifact tuple --- README.md | 7 +- docs/concepts/deployment-matrix.md | 8 +- docs/get-started/what-works-today.md | 2 +- docs/guides/hosted.md | 4 +- docs/guides/security-review.md | 2 +- docs/published-version-claims.json | 31 ++++++-- scripts/check_published_version_claims.py | 4 +- scripts/render_published_version_claims.py | 90 ++++++++++++++-------- tests/test_published_version_claims.py | 81 +++++++++++++++++-- 9 files changed, 172 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 44bb909..a49216f 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,10 @@ uv run mkdocs build --strict uv run python scripts/validate_docs.py ``` -The managed-runtime version has one editable source: -`docs/published-version-claims.json`. Change the claim's `version`, `evidence`, -and `verified_on` fields once, then render all marked pages: +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 diff --git a/docs/concepts/deployment-matrix.md b/docs/concepts/deployment-matrix.md index e6a0777..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/get-started/what-works-today.md b/docs/get-started/what-works-today.md index 067c0e0..f3a8baa 100644 --- a/docs/get-started/what-works-today.md +++ b/docs/get-started/what-works-today.md @@ -72,7 +72,7 @@ customer-controlled runtime connected to the same governance model. | 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 a28712e..9862cad 100644 --- a/docs/guides/hosted.md +++ b/docs/guides/hosted.md @@ -38,7 +38,7 @@ switching, and sign-out. | Surface | Launch status | 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. | +| 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. | @@ -64,7 +64,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/security-review.md b/docs/guides/security-review.md index aedab36..6e54d8c 100644 --- a/docs/guides/security-review.md +++ b/docs/guides/security-review.md @@ -132,7 +132,7 @@ 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, diff --git a/docs/published-version-claims.json b/docs/published-version-claims.json index e12f5e0..ba0919b 100644 --- a/docs/published-version-claims.json +++ b/docs/published-version-claims.json @@ -23,8 +23,8 @@ " Frozen forever; bumping it would falsify the record.", "", "rendered_locations:", - " Pages whose marked version is generated from the claim's one version", - " field. Change that field once, then run the deterministic renderer.", + " 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." @@ -47,24 +47,41 @@ "kind": "pinned-deployment", "package": "openadapt-flow", "version": "1.31.0", - "evidence": "openadapt-cloud origin/main 4e0257a1299cc0869af3e3f664a6ebfa0b59db1 runner/runtime-version.json pins the openadapt_flow artifact at release commit 2d225dea9a0ad29ca84ce1b037cc0ac671367e28 (wheel sha256 81133db1528ad1bb1f26e3fcb6aea61b0651db6d905cf2e4943e8383c1f3d29c; sdist sha256 cf1fc356d14d267df82be188de3e9a3575734f18f46ef91ac8075438cc731540). The package version is the claim's version field. 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", "rendered_locations": [ { "file": "docs/concepts/deployment-matrix.md", - "count": 1 + "values": { + "version": 1, + "release_commit": 1, + "wheel_sha256": 1, + "sdist_sha256": 1 + } }, { "file": "docs/guides/security-review.md", - "count": 1 + "values": { + "version": 1 + } }, { "file": "docs/guides/hosted.md", - "count": 2 + "values": { + "version": 2 + } }, { "file": "docs/get-started/what-works-today.md", - "count": 1 + "values": { + "version": 1 + } } ] }, diff --git a/scripts/check_published_version_claims.py b/scripts/check_published_version_claims.py index d8b817e..ec6d3e4 100644 --- a/scripts/check_published_version_claims.py +++ b/scripts/check_published_version_claims.py @@ -16,8 +16,8 @@ 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. Every generated version marker matches the claim registry's one editable - version field. A release changes that field once and runs the deterministic +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 diff --git a/scripts/render_published_version_claims.py b/scripts/render_published_version_claims.py index a8657ae..0611f42 100644 --- a/scripts/render_published_version_claims.py +++ b/scripts/render_published_version_claims.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Render shared documentation versions from the claim registry. +"""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 @@ -22,14 +22,23 @@ 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"" r"(?P[^<\r\n]+?)" - r"" + r"" ) -OPEN_MARKER = re.compile(r"") -CLOSE_MARKER = re.compile(r"") -SEMVER = re.compile(r"\d+\.\d+\.\d+") +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: @@ -51,11 +60,11 @@ def render_version_claims( errors: list[str] = [] claims: dict[str, dict] = {} - expected: Counter[tuple[str, str]] = Counter() + 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 claim_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: @@ -64,54 +73,72 @@ def render_version_claims( claims[claim_id] = claim rendered_locations = claim.get("rendered_locations") or [] - if rendered_locations and not SEMVER.fullmatch(str(claim.get("version", ""))): - errors.append( - f"claim {claim_id!r} has rendered locations but version " - f"{claim.get('version')!r} is not X.Y.Z" - ) for location in rendered_locations: file_name = location.get("file") - count = location.get("count", 1) 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 - if not isinstance(count, int) or isinstance(count, bool) or count < 1: + values = location.get("values") + if not isinstance(values, dict) or not values: errors.append( - f"claim {claim_id!r} has invalid marker count {count!r} " - f"for {file_name}" + f"claim {claim_id!r} has no rendered values for {file_name}" ) continue - expected[(file_name, claim_id)] += count + 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]] = Counter() + 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(matches) != len(OPEN_MARKER.findall(text)) - or len(matches) != len(CLOSE_MARKER.findall(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"))] += 1 + 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 = key + file_name, claim_id, field = key errors.append( - f"claim {claim_id!r}: {file_name} has {actual_count} rendered " - f"marker(s); the registry requires {expected_count}" + 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: @@ -119,15 +146,16 @@ def render_version_claims( rendered_by_path: dict[Path, str] = {} for path, text in source_by_path.items(): - if not OPEN_MARKER.search(text): + if not CLAIM_COMMENT.search(text): continue def replace(match: re.Match[str]) -> str: claim_id = match.group("id") - version = str(claims[claim_id]["version"]) + field = match.group("field") + value = str(claims[claim_id][field]) return ( - f"{version}" - f"" + f"{value}" + f"" ) rendered_by_path[path] = MARKER.sub(replace, text) diff --git a/tests/test_published_version_claims.py b/tests/test_published_version_claims.py index f2126ff..f370927 100644 --- a/tests/test_published_version_claims.py +++ b/tests/test_published_version_claims.py @@ -262,19 +262,26 @@ def _rendered_registry(version="1.31.0"): "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", "count": 1}, - {"file": "docs/b.md", "count": 2}, + {"file": "docs/a.md", "values": {"version": 1}}, + {"file": "docs/b.md", "values": {"version": 2}}, ], } ] } -def _marked(claim_id="managed-runtime", version="1.31.0"): +def _marked( + claim_id="managed-runtime", + field="version", + value="1.31.0", +): return ( - f"{version}" - f"" + f"{value}" + f"" ) @@ -297,6 +304,48 @@ def test_one_registry_version_renders_every_registered_location(tmp_path): 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( @@ -333,7 +382,7 @@ def test_render_check_fails_for_missing_or_extra_marker(tmp_path): def test_render_refuses_malformed_marker_without_partial_writes(tmp_path): registry = _rendered_registry(version="1.32.0") - malformed = "1.31.0" + malformed = "1.31.0" _tree( tmp_path, { @@ -349,6 +398,26 @@ def test_render_refuses_malformed_marker_without_partial_writes(tmp_path): 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) + + # -------------------------------------------------------------------------- # Changelog structure # -------------------------------------------------------------------------- From 7eb9c94075e0051732eae1e8597ef5afae6bfeef Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 18 Aug 2026 14:39:59 -0400 Subject: [PATCH 9/9] Fail closed on malformed version claim markers --- scripts/render_published_version_claims.py | 2 +- tests/test_published_version_claims.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/scripts/render_published_version_claims.py b/scripts/render_published_version_claims.py index 0611f42..066de7d 100644 --- a/scripts/render_published_version_claims.py +++ b/scripts/render_published_version_claims.py @@ -30,7 +30,7 @@ r"" ) CLAIM_COMMENT = re.compile( - r").)*version-claim:(?:(?!-->).)*-->", + r").)*version-claim(?:(?!-->).)*-->", re.DOTALL | re.IGNORECASE, ) FIELD_PATTERNS = { diff --git a/tests/test_published_version_claims.py b/tests/test_published_version_claims.py index f370927..d8dd8a0 100644 --- a/tests/test_published_version_claims.py +++ b/tests/test_published_version_claims.py @@ -418,6 +418,27 @@ def test_render_rejects_a_marker_with_an_invalid_identifier(tmp_path): 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 # --------------------------------------------------------------------------