Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ improvising or retrying when something is off.
| Compile one or more demonstrations into a deterministic program | Released | Real app (OpenEMR referral, 62 timed runs) | `openadapt-flow record`, `compile`; [packages/flow](packages/flow/README.md) |
| Classify common local edits without a person (select-all, Enter in a field or dropdown, an in-form tab, an informational dialog's dismiss), only when the recording's network capture shows no write and a runtime check confirms only the expected change | On main ([#1366](https://github.com/OpenAdaptAI/OpenAdapt/pull/1366)) | Synthetic; OpenEMR referral recording re-admitted offline (violations 12 to 1 under `permissive`) | [LOCAL_EDIT_CLASSIFICATION.md](packages/flow/docs/design/LOCAL_EDIT_CLASSIFICATION.md), `compiler/local_edit.py` |
| Keep the empty search result a person confirmed through (a duplicate check's "No matches were found.") as an exact check after the click that opens the dialog and again right before Confirm, so a run whose dialog lists a match stops before the save; only fixed-vocabulary search-result statements shown once, never text with a name or search term, and a confirmation dialog without one is noted in `compile_notes.json` | On main | Real app frames (an OpenEMR referral recording, compiled offline) | `compiler/empty_result_guard.py` |
| Mine that check in any language: induction keeps the one line a named dialog adds (not its title or buttons, not a question, no typed value or number) when two or more demonstrations whose typed values all differ show the same line, refuses when the demonstrations disagree on a mined check instead of copying the first one's, and qualification then needs a business-rule run that halts at it | On main | Synthetic (a search dialog recorded in Chromium by the real recorder, German statement, real OCR) | `compiler/empty_result_guard.py`, `compiler/induction.py` |
| Qualify against a rule the app itself enforces: verify.yaml `business_rules` seed the conflicting record (an existing chart for the same person, checked in the read-back), and the certified bundle must stop at the rule's check before the save with nothing written on every trial, or certification is withdrawn | On main | Unit tests; the OpenEMR fixture's verify.yaml declares the existing-duplicate rule | [QUALIFY_A_WORKFLOW.md](packages/flow/docs/QUALIFY_A_WORKFLOW.md) |
| Right-record check before any write (structure, then OCR, then pixels) | Released | Real app and synthetic | [RESOLUTION_LADDER.md](packages/flow/docs/RESOLUTION_LADDER.md), `runtime/identity.py` |
| Read the saved record back from the system of record (API, database, FHIR, separate session) | Released | Real app (OpenEMR 12 of 12 fields) | [VERIFICATION.md](packages/flow/docs/VERIFICATION.md), `runtime/effects/` |
Expand Down
3 changes: 2 additions & 1 deletion llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ improvising or retrying when something is off.
| Compile one or more demonstrations into a deterministic program | Released | Real app (OpenEMR referral, 62 timed runs) | `openadapt-flow record`, `compile`; [packages/flow](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/README.md) |
| Classify common local edits without a person (select-all, Enter in a field or dropdown, an in-form tab, an informational dialog's dismiss), only when the recording's network capture shows no write and a runtime check confirms only the expected change | On main ([#1366](https://github.com/OpenAdaptAI/OpenAdapt/pull/1366)) | Synthetic; OpenEMR referral recording re-admitted offline (violations 12 to 1 under `permissive`) | [LOCAL_EDIT_CLASSIFICATION.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/design/LOCAL_EDIT_CLASSIFICATION.md), `compiler/local_edit.py` |
| Keep the empty search result a person confirmed through (a duplicate check's "No matches were found.") as an exact check after the click that opens the dialog and again right before Confirm, so a run whose dialog lists a match stops before the save; only fixed-vocabulary search-result statements shown once, never text with a name or search term, and a confirmation dialog without one is noted in `compile_notes.json` | On main | Real app frames (an OpenEMR referral recording, compiled offline) | `compiler/empty_result_guard.py` |
| Mine that check in any language: induction keeps the one line a named dialog adds (not its title or buttons, not a question, no typed value or number) when two or more demonstrations whose typed values all differ show the same line, refuses when the demonstrations disagree on a mined check instead of copying the first one's, and qualification then needs a business-rule run that halts at it | On main | Synthetic (a search dialog recorded in Chromium by the real recorder, German statement, real OCR) | `compiler/empty_result_guard.py`, `compiler/induction.py` |
| Qualify against a rule the app itself enforces: verify.yaml `business_rules` seed the conflicting record (an existing chart for the same person, checked in the read-back), and the certified bundle must stop at the rule's check before the save with nothing written on every trial, or certification is withdrawn | On main | Unit tests; the OpenEMR fixture's verify.yaml declares the existing-duplicate rule | [QUALIFY_A_WORKFLOW.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/QUALIFY_A_WORKFLOW.md) |
| Right-record check before any write (structure, then OCR, then pixels) | Released | Real app and synthetic | [RESOLUTION_LADDER.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/RESOLUTION_LADDER.md), `runtime/identity.py` |
| Read the saved record back from the system of record (API, database, FHIR, separate session) | Released | Real app (OpenEMR 12 of 12 fields) | [VERIFICATION.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFICATION.md), `runtime/effects/` |
Expand Down Expand Up @@ -1251,7 +1252,7 @@ signed; the Python packages are published from the reviewed release workflow.
| Deterministic re-resolution | **Required CI** | Bounded visual or structural drift can be resolved through non-model evidence and recorded as a reviewable change. | It is not general adaptation to a redesigned workflow, changed business rules, or missing evidence. |
| AI-assisted repair | **Required CI contracts; measured on synthetic apps; deployment evidence required** | An explicitly enabled model can propose a target or interpret a changed screen. Existing runtime checks still apply. Once a deployment names a repair agent, every stop that wrote nothing goes to it by default, and a person approves each fix unless a signed policy pre-approves its class. On MockMed's 22 drift modes, 6 of 21 stops that reached repair resumed and verified, with 0 wrong saves in 66 trials ([benchmark/auto_repair](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/../benchmark/auto_repair/README.md)). | A model proposal is not authorization, proof of identity, or proof that a business transaction succeeded. A stop after a possible write is never repaired automatically. The measured rate is for one agent on one synthetic app; it is not a field repair rate. |
| Human teaching and resume | **Required CI contracts; field evidence required** | A halt can produce evidence for an operator correction, guarded promotion, and durable resume. | Field recovery time, broad authoring UX, enterprise identity integration, and non-repudiation are not established. |
| Windows UI Automation | **Counted task acceptance** | The native UIA backend completed 3/3 exact effects in an in-tree WinForms app in a Windows 11 VM, and 3/3 refusals for both stale and ambiguous targets. | Acceptance is bound to the tested workflow and Windows environment; each application retains its own identity, effect, and version qualification. |
| Windows UI Automation | **Counted task acceptance** | The native UIA backend completed 3/3 exact effects in an in-tree WinForms app in a Windows 11 VM, and 3/3 refusals for both stale and ambiguous targets. | Acceptance is bound to the tested workflow and Windows environment; each application retains its own identity, effect, and version qualification. A qualification case pins the foreground executable's file digest, the machine and its exact OS build, so an OS update or a new executable needs a new qualification. For an app that a script interpreter, browser or shared runtime hosts, that digest pins the host executable, so the app's own release is pinned by requalifying it. |
| Native macOS automation | **Counted task acceptance** | The AX/AppKit backend, driven directly, completed 3/3 exact TextEdit file effects and refused an ambiguous two-window target; structural record/locate/refuse behavior is covered separately. Known AX-only ` - Google Chrome` and ` - Google Chrome for Testing` window-title suffixes are accepted only for the corresponding exact CoreGraphics application owner; PID, unique-candidate, focused/main, and topmost-window checks remain mandatory. | Acceptance is bound to the tested host and application. Accessibility exposure and supported native actions vary by application. |
| Native Linux automation | **Unit tests plus CI recording** | Root CI records under Xvfb and reads a GTK3 button over AT-SPI. Replay and the stops on ambiguous and stale targets are unit-tested with a fake AT-SPI client. | The built-in path is X11. Wayland needs an operator-approved portal session, and each application retains its own qualification. |
| RDP automation | **Counted task acceptance** | An Aardwolf batch into a Windows 11 VM passed 3/3 (transport and input) with an independent guest-tools oracle; a separate real-FreeRDP batch covers record, compile, governed replay, and drift refusal. | The records do not cover arbitrary applications, WAN conditions, or Citrix ICA/HDX. A real AVC444 session (same virtual network, software encoding) stops on 12 of 20 healthy OpenEMR runs; the causes are ranked in benchmark/rdp_avc_real. |
Expand Down
2 changes: 2 additions & 0 deletions packages/flow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

**Breaking:** admission of every JIT draft now needs one (`teach_auth_not_recorded` otherwise), in every admission mode; so does a self-repair of an agent-taught program and the approval of a calling agent's fix. Drafts from the airbag handoff are refused, and browser teach (`serve --teach-browser`) no longer teaches or runs write tools.

- A dialog's empty-result check in any language. Induction mines the one line a named confirmation dialog adds when every demonstration shows the same line and every value typed before the dialog differs between them. Questions, numbers, typed values and lines from one demonstration only are not mined. When the demonstrations disagree on a mined check, induction refuses (`ambiguous_guard`) instead of copying the first recording's. The opener and its Confirm click no longer read as a two-row loop. Qualification asks for a business-rule case that halts at such a check. See `docs/DUPLICATE_CHECK.md`.

- `run --self-repair` (and repair by default) now works outside a browser. `repair.session` in `deploy.yaml` says how to start the app for each probe, fault probe and resumed run (an argv, no shell, kept running while the app is up; `fault_commands`, `close_command`, `ready_s`). The run's configured backend must bind to a window owned by that command's process group, so a session never binds to the halted session or a copy already open; Flow ends the group afterwards, and closes the halted session before each probe. This works where the bound window is on Flow's machine (macOS, and the local RDP or Citrix client window); Windows over its agent and FreeRDP are refused for now. Without `repair.session`, a desktop run refuses repair with that reason, as before.

- Signed write receipts. A verifier verdict, with its scope (stores read, time window, strength) and a plain list of what it did not check, is signed as a COSE_Sign1 in the IETF SCITT shape and appended to a local transparency log. `openadapt-flow receipt verify FILE --issuer-key --log-key` checks one offline and prints the not-checked list every time; a receipt that claims more than its scope is neither issued nor accepted. `verify_write(..., write_receipt=True)`, `issue_write_receipt(result)` and `openadapt-flow shadow --receipts` issue them; `receipt anchor` puts the digest in Rekor. Extra: `openadapt-flow[receipts]`. Design: `docs/WRITE_RECEIPTS.md`.
Expand Down
39 changes: 39 additions & 0 deletions packages/flow/docs/DUPLICATE_CHECK.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,45 @@ compiler mines that check from the recording: the dialog's empty result is an
exact check on the click that opens the dialog and again before Confirm
(`compiler/empty_result_guard.py`).

The compiler knows an English vocabulary for that statement. A statement in
other words ("Keine Treffer gefunden.") is mined from two or more
demonstrations instead. The dialog must be named, and it must add exactly one
line besides its title and button row. That line must read the same in every
demonstration although every value typed before the dialog differs. Then
induction checks it the same way. The line is not mined when:

- it changes with the search;
- it carries a typed value, a number other than 0, or an email address;
- it is a question ("Wirklich speichern?");
- it comes from one demonstration only.

The PHI check on the line runs only when the optional privacy scrubber is
installed. The compile notes of each recording, and of a bundle `admit`
induces, say why a line was not mined.
When the demonstrations disagree on a mined check, induction refuses
(`ambiguous_guard`). It never copies the first recording's check, and it
never drops the check quietly.

A check mined this way was chosen because the demonstrations agree, not
because of what its words say. So qualification needs a `business_rules` case
that halts at it.

Limits:

- The dialog's title comes from the browser recorder (its accessible name),
so desktop recordings use the vocabulary only.
- Agreement alone can't tell the statement from other fixed text that is the
dialog's only line, such as an empty table's single header or an
instruction. That needs a recording where the search found rows, and there
is no such contrast check yet. The business-rule run is what catches it,
and the qualification campaign reads linear bundles only, so an induced
bundle that carries such a check can't be qualified yet.
- Only typed and selected values count as the search. A record chosen by
clicking a list row is not seen: the same row clicked in every
demonstration doesn't stop the line from being mined.
- The confirmation pair itself is still found from English button labels
("Confirm", "Yes").

OpenEMR lists a chart that shares only a first name with the new patient. On
the lifecycle soak's synthetic worklist, most of those stops were false: the
listed chart was clearly someone else, and a person entered the item by hand.
Expand Down
6 changes: 5 additions & 1 deletion packages/flow/openadapt_flow/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2176,18 +2176,22 @@ def _cmd_induce(args: argparse.Namespace) -> int:
# bundle case CLI-side keeps induce usable on artifacts the operator
# already has, without touching the library API.
from openadapt_flow import sealed_set
from openadapt_flow.compiler.fixup_passes import read_compile_notes
from openadapt_flow.compiler.induction import induce_program, validate_held_out
from openadapt_flow.ir import Workflow

traces: list = []
notes: list = []
sealed_inputs = False
for d in args.recording:
path = Path(d)
sealed_inputs = sealed_inputs or sealed_set.is_sealed(path)
if (path / "workflow.json").is_file() or (path / "workflow.json.enc").is_file():
traces.append(Workflow.load(path))
notes.append(read_compile_notes(path))
else:
traces.append(path)
notes.append(None)
if sealed_inputs and (args.report or args.probe_on):
# Both write the induced program (or probe bundles) outside any
# sealed set; refuse rather than write plaintext.
Expand All @@ -2206,7 +2210,7 @@ def _cmd_induce(args: argparse.Namespace) -> int:
return probed
result = probed
else:
result = induce_program(traces, **spoken)
result = induce_program(traces, compile_notes=notes, **spoken)
print(result.render())
if args.report:
report_path = Path(args.report)
Expand Down
14 changes: 13 additions & 1 deletion packages/flow/openadapt_flow/admit_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ def prepare_admission(
"""

from openadapt_flow.compiler import compile_recording
from openadapt_flow.compiler.fixup_passes import (
read_compile_notes,
write_compile_notes,
)
from openadapt_flow.compiler.induction import induce_program
from openadapt_flow.ir import Workflow
from openadapt_flow.qualification_proposal import (
Expand Down Expand Up @@ -366,10 +370,14 @@ def stop(status: PendingStatus, reason: str, **extra: Any) -> PendingAdmission:

bundle_dir = target / BUNDLE_DIR
induced = False
guard_notes: list[dict[str, str]] = []
if len(compiled) == 1:
workflow = Workflow.load(compiled[0])
else:
result = induce_program([Workflow.load(p) for p in compiled])
result = induce_program(
[Workflow.load(p) for p in compiled],
compile_notes=[read_compile_notes(p) for p in compiled],
)
if not result.certified or result.workflow is None:
return stop(
"halted",
Expand All @@ -381,6 +389,7 @@ def stop(status: PendingStatus, reason: str, **extra: Any) -> PendingAdmission:
)
workflow = result.workflow
induced = True
guard_notes = list(result.guard_notes)
workflow = workflow.model_copy(update={"name": name})
from openadapt_flow import sealed_set

Expand All @@ -398,9 +407,12 @@ def stop(status: PendingStatus, reason: str, **extra: Any) -> PendingAdmission:
templates.setdefault(rel, sealed_set.read_bytes(item))
workflow._decrypted_templates = templates
with sealed_set.writing(bundle_dir, "bundle", passphrase):
write_compile_notes(bundle_dir, guard_notes)
workflow.save(bundle_dir)
else:
_copy_templates(compiled, bundle_dir)
# Induction's dialog-check decisions, as a compile's own notes.
write_compile_notes(bundle_dir, guard_notes)
workflow.save(bundle_dir)
workflow = Workflow.load(bundle_dir)

Expand Down
Loading
Loading