Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0055d2a
feat(browser): attach recorder to existing sessions
abrichr Aug 18, 2026
67f38b5
docs: align generated verification timestamp
abrichr Aug 18, 2026
64285e7
fix(browser): harden attached recording boundaries
abrichr Aug 18, 2026
b5621bd
fix(browser): harden attached recording finalization
abrichr Aug 18, 2026
66dad30
fix(browser): close attach evidence gaps
abrichr Aug 18, 2026
b2931e8
fix(browser): latch attached recording races
abrichr Aug 18, 2026
63bfc99
fix(browser): close attached finalization gaps
abrichr Aug 18, 2026
ca7c61a
fix(browser): bind same-task secret mutations
abrichr Aug 18, 2026
f433eba
fix(browser): retain masks through final capture
abrichr Aug 18, 2026
bc56410
docs: keep generated verification report in sync
abrichr Aug 18, 2026
a274c4c
Harden browser attach secret boundaries
abrichr Aug 18, 2026
ee99b9d
Refresh browser attach verification artifacts
abrichr Aug 18, 2026
9cf2a70
Use deterministic claims report timestamp
abrichr Aug 18, 2026
8ecd6cf
fix(browser): make secret classification sticky across DOM replacement
abrichr Aug 18, 2026
57f53e1
chore(claims): regenerate verification report and artifact inventory …
abrichr Aug 18, 2026
4c0229c
fix(browser): bind secret redaction to the element, never to keystrok…
abrichr Aug 19, 2026
58544e4
test(browser): assert the refused DOM identity states its reason
abrichr Aug 19, 2026
6e0607c
fix(browser): recognise a keystroke prefix per declared field, not gl…
abrichr Aug 19, 2026
aa20d5a
docs(browser): correct the commit-point comment
abrichr Aug 19, 2026
a895bc8
fix(browser): never drop a value the field holds or a commit point re…
abrichr Aug 19, 2026
81853fc
docs(browser): state the retention rule and every withheld identity
abrichr Aug 19, 2026
c8fa035
fix(browser): replace secret value retention with capture-time withho…
abrichr Aug 19, 2026
30657a1
chore(browser): re-pin the public artifact inventory after the claims…
abrichr Aug 19, 2026
948610e
fix(browser): prefer the definite withhold reason over the ambiguous one
abrichr Aug 19, 2026
9c279ff
docs(browser): correct comments that still described the removed scru…
abrichr Aug 19, 2026
3a303c1
fix(browser): close three round-4 blockers and redact URLs by structure
abrichr Aug 19, 2026
1ff222c
docs(browser): state the structured URL rule and what it still costs
abrichr Aug 19, 2026
1c281d8
fix(browser): apply the same proof to a bare URL fragment
abrichr Aug 19, 2026
8c555fa
fix(browser): withhold a later document's URL; the path is not struct…
abrichr Aug 19, 2026
d7eabd1
fix(browser): treat a page that consumes its own field as having held…
abrichr Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ build/
.DS_Store
/runs/
/recordings/
.openadapt-chrome-profile/
.openadapt-recording-partial-*/
dist/
benchmark/openemr/finals/
benchmark/openemr/rows.jsonl
Expand Down
65 changes: 60 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ openadapt-flow record --backend web --url https://your.app --out rec
openadapt-flow compile rec --out bundle --name my-task
openadapt-flow replay bundle --backend web --url https://your.app

# Browser with an existing local SSO/2FA session: attach one open tab.
openadapt-flow record --backend web --url https://your.app \
--browser-cdp-endpoint http://127.0.0.1:9222 --out rec

# Native Windows: Capture records the local target window. WAA drives replay.
openadapt-flow record --backend windows --window "Target App" \
--task "add a patient note" --out rec
Expand Down Expand Up @@ -241,6 +245,17 @@ ignoring them; pass them to `replay` or `run`. Drive a real deployment with
effects, actuation, durable, and policy sections from one config. Recorded
parameter values are the defaults, and `--param` overrides them at replay.

The browser recorder can launch a clean Playwright browser or attach to one
existing local Chromium tab. Attach mode preserves a browser profile that has
already completed sign-in, SSO, or 2FA. It refuses remote CDP endpoints and
ambiguous same-origin tabs. It does not navigate or close the attached browser.
You can resize the tab or move its window between monitors. Flow waits for a
stable CSS-pixel frame and binds the next event to the new viewport. It refuses
an action only if that action overlaps the coordinate-space transition.
See the [browser recording guide](docs/BROWSER_RECORDING.md) for setup, exact
tab selection, secret handling, and the boundary with the Capture Chrome
extension prototype.

**You don't have to name parameters up front.** The recorder passively
captures each typed field's label (DOM/accessibility, or nearby OCR on pixel
paths), and `compile` proposes a parameter named from it (`"Insurance No."`
Expand Down Expand Up @@ -295,18 +310,58 @@ Route a production receipt through
`sanitize` / `review-sanitized` / `approve-sanitized` before it crosses a trust
boundary.

**Secrets never get recorded.** An `input[type=password]` field (or any field
named with `--secret <name>`) is a secret parameter: its value is never written
to the recording, the events log, the compiled bundle, or the saved frames (its
region is redacted). At replay it is injected from the environment and a missing
one fails fast:
**Secret input values stay page-local.** An `input[type=password]` field (or a
field named with `--secret <name>`) becomes a secret parameter. Flow does not
send its literal to Python. It masks the bound field region in saved frames.
For every other piece of page text, **Flow reports it exactly or withholds it
and says why. Flow never rewrites captured text.** Matching uses only the value
a bound element holds at that moment, read live from the DOM; no value is kept
after the field stops holding it. A shadow field whose identity can change must
use a host with the same declared name or ID; Flow masks the complete host. It
refuses an unbound shadow input before it accepts a value. At replay, Flow
injects the secret from the environment and fails fast when it is absent:

```bash
openadapt-flow record --backend web --url https://your.app --out rec --secret password
export OPENADAPT_FLOW_SECRET_PASSWORD='…' # supplied at replay
openadapt-flow replay bundle --backend web --url https://your.app
```

Evidence splits in two. **Identity evidence** — the DOM selector, the control
role, the accessible name, the clicked row's identity characters, and the
receiving field's name — is exact or withheld with a stated reason, because
replay compares it against the live page and a rewritten copy would compare
against text the page never showed. **Reflected evidence** — the page URL and
the title — is sampled from Python once the page has settled, never inside the
capture-phase listener, which runs before the page's own handlers and so reads
the previous action's text.

Within a document, a URL is reduced by **structure**: Flow reports the origin
and the path, keeps every parameter name, and drops the value of any parameter
named after a declared secret field — deterministically, whatever the value is.
A dropped value becomes empty; Flow removes characters from a URL and never
adds characters the page did not show. A single-page application that routes
with `history.pushState` therefore keeps its URL evidence. If the URL Flow is
about to report still holds a value Flow can see, it withholds the whole URL
and warns you that the application put a secret in its own URL — a defect that
exposes it through browser history, logs, proxies and `Referer` headers with or
without Flow.

That reduction does **not** make a later document safe. A path segment has no
parameter name to identify it, so a server that answers a form submit with a
redirect to `/results/<value>` puts the value where structure cannot reach, and
the new document holds nothing to match it against. Flow therefore withholds
the URL and the title of every document after the one that first held a
declared value. A title has no structure to reduce and follows the same rule
within a document. `meta.json` records everything dropped and everything
withheld, and the CLI prints it.

This source-time contract does not track an application-defined transform of a
secret or an application copy into an unrelated visible element, and it starts
at the moment a bound field holds the value: text and pixels captured before
then are ordinary recording evidence. Keep every raw recording inside its
approved local boundary.

**Compiled is not the same as certified safe.** `lint` reports a bundle's
coverage gaps (clicks that act with no identity check, steps that assert
nothing, write steps left mis-classified) with a severity each; `certify`
Expand Down
137 changes: 137 additions & 0 deletions claims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,138 @@ claims:
proves: >-
The deterministic replayer resolves steps, substitutes parameters,
enforces postconditions and the risk gate — no model in the loop.
- path: tests/test_browser_attach.py
node: test_live_cdp_attach_records_compiles_and_leaves_browser_running_three_trials
proves: >-
Three real Chromium CDP-attach trials record and compile the same
synthetic workflow, exclude password values before persistence,
preserve CSS-pixel frame/coordinate alignment, and detach without
closing the external browser. The same live campaign covers same-task
replacement, declared open and closed shadow hosts, contenteditable
click metadata, and same-document URL/title reflection through
history.replaceState. A live case records and
compiles actions across viewport and monitor-scale changes. It binds
each event to its exact frame dimensions. A separate live case
refuses an action that overlaps the transition. Unit cases refuse
remote endpoints, cross-origin selectors and navigation, iframe
events, invalid viewport evidence, and ambiguous same-origin tabs.
- path: tests/test_browser_attach.py
node: test_launched_recording_withholds_a_later_document_url_after_a_get_submit
proves: >-
A live Chromium recording submits a same-origin GET form. The
document that submit reaches is a fresh closure that holds no value
to match against, so Flow withholds its URL and its title and says
so, and the recorded surface is stamped before publish.
- path: tests/test_browser_attach.py
node: test_launched_recording_withholds_a_redirect_that_puts_the_value_in_a_path
proves: >-
A server that answers a GET submit with a 302 to `/results/<value>`
puts a declared secret in a path segment, where no parameter name
identifies it. Flow withholds the whole URL for every document after
the one that first held the value, so the literal never reaches
events.jsonl.
- path: tests/test_browser_attach.py
node: test_page_closure_keeps_url_and_identity_evidence_for_a_lowercase_secret
proves: >-
A lowercase secret typed one character at a time into a page whose
URL, title, and button ID share those characters leaves the URL, the
title, the DOM selector, and the accessible name exact.
- path: tests/test_browser_attach.py
node: test_page_closure_keeps_all_evidence_for_a_password_starting_with_a_word
proves: >-
A password that begins with a common English word leaves that word
exact in the page URL, the page title, the clicked row's identity
characters, the accessible name, and an unrelated button ID. Matching
uses only the value the field holds at that moment, so an
intermediate keystroke prefix never becomes a matching value.
- path: tests/test_browser_attach.py
node: test_page_closure_withholds_identity_that_holds_a_declared_value
proves: >-
Identity evidence that holds a declared value is WITHHELD with a
stated reason, never rewritten. Replay compares identity evidence
against the live page, so a rewritten copy would compare against
characters the page never showed, invisibly. No placeholder string
appears anywhere in the recorded evidence.
- path: tests/test_browser_attach.py
node: test_page_closure_withholds_a_stale_reflection_from_a_swapping_input
proves: >-
A page that replaces its input element on every keystroke and writes
the value into its URL and title can end up showing a version the
field no longer holds. Flow withholds that reflected text whole --
an origin-only URL and an empty title -- instead of matching it
against the current value, and keeps the clicked element's identity
evidence exact.
- path: tests/test_browser_attach.py
node: test_page_closure_still_reports_a_same_document_route_after_the_fix
proves: >-
A single-page application route change through history.pushState does
not build a new document, so the closure that held the declared value
is the one being sampled and its URL is still reported exactly. The
cross-document rule that withholds a later document's URL therefore
does not cost this evidence.
- path: tests/test_browser_attach.py
node: test_page_closure_withholds_a_title_a_consumed_field_produced
proves: >-
A scanner input that writes the badge into the page URL and title and
then clears its own field holds nothing at any moment Flow samples.
Flow arms the document's secret boundary from the input event itself,
so both channels are checked and both are withheld.
- path: tests/test_browser_attach.py
node: test_page_closure_keeps_a_consumed_value_across_a_second_entry
proves: >-
A second scan into the same cleared field does not displace the first
badge while the first badge is still shown in the URL. A value the
next one does not continue was taken by the page, not edited away by
the operator, so it is promoted into the withhold-only set.
- path: tests/test_browser_attach.py
node: test_page_closure_keeps_a_consumed_value_while_another_field_is_live
proves: >-
A second declared field holding a value does not re-expose the first
field's consumed value. The last-value test is per element, so the
same URL is withheld before and after the second field is filled.
- path: tests/test_browser_attach.py
node: test_page_closure_withholds_a_selector_built_from_an_inbound_value
proves: >-
A DOM selector is identity evidence and uses the same value set as
the accessible name and the clicked-row identity. An element id built
from a value carried in an inbound declared parameter is withheld with
a reason, not emitted verbatim.
- path: tests/test_browser_attach.py
node: test_page_closure_drops_only_the_unproven_parameter_value
proves: >-
A query parameter whose value Flow cannot prove predates the moment
the document first held a declared value loses only ITS value. Every
parameter name survives, the path stays exact, and the drop is
recorded with its reason.
- path: tests/test_browser_attach.py
node: test_page_closure_withholds_a_url_that_holds_the_value_in_its_path
proves: >-
A value that no parameter name identifies -- here written into a path
segment -- is caught by detection rather than structure: Flow
withholds the whole URL, marks it, and warns the operator that the
application placed a declared secret into its own URL.
- path: tests/test_browser_attach.py
node: test_page_closure_withholds_identity_after_the_field_is_removed
proves: >-
A single-page wizard that removes its declared field and renders the
value into a summary row cannot leak it into the clicked row's
identity: the value the field held at a commit point is retained for
the single purpose of WITHHOLDING identity text, never to rewrite it
and never for the URL or the title.
- path: tests/test_browser_attach.py
node: test_page_closure_marks_a_withheld_secret_field_name
proves: >-
Flow never reads the visible text of a bound secret field, because
that text is the value. The resulting missing accessible name is
reported as WITHHELD with a reason and counted, never left as a
silent null, while a control field beside it returns its name.
- path: tests/test_browser_attach.py
node: test_page_closure_emits_no_reflected_text_from_the_capture_phase
proves: >-
No browser event carries a URL or a title. The in-page listeners run
in the capture phase, before the page's own handlers, so anything
they read describes the state before the action. Flow samples
reflected evidence from Python at the settled boundary instead.
caveats:
- >-
"Supported" is scoped to the reference headless-browser backend in this
Expand All @@ -73,6 +205,11 @@ claims:
- >-
The full record->compile->replay browser suite runs in the required
e2e-browser PR gate and repeats in the weekly compatibility matrix.
- >-
Existing-session attachment is Chromium-only and loopback-only. It
requires a dedicated browser process started with remote debugging.
It does not claim support for the Capture Chrome extension prototype
or direct extension replay.

# -------------------------------------------------- deterministic $0 replay
- id: deterministic-zero-model-replay
Expand Down
Loading