Skip to content

Self-repair's new clicks on native and pixel-only backends, by element identity and the record check - #1709

Merged
abrichr merged 4 commits into
mainfrom
claude/agnostic-repair-bdfb3c
Nov 2, 2026
Merged

abrichr merged 4 commits into
mainfrom
claude/agnostic-repair-bdfb3c

Conversation

@abrichr

@abrichr abrichr commented Nov 2, 2026

Copy link
Copy Markdown
Member

Self-repair could add a new click (dismiss an update notice, close a new dialog) only in a browser, because each new click needed a DOM record identity. This change makes the rule backend-neutral. Design: packages/flow/docs/REPAIR_ON_EVERY_BACKEND.md.

What changes

A new bridge click is admitted as follows (openadapt_flow/repair/element_identity.py):

Backend kind Admitted when
DOM (browser) A record the recording verified sits under the click. Unchanged.
Accessibility tree (macOS AX now; Windows UIA when its hook lands) The same record identity, or: the element identity (role, name, ancestry, window) is readable, has a name or id, ends at the pinned window, and is the only match for its locator; and a record check covers the workflow.
Pixels only (RDP, Citrix) A record check covers the workflow.

"A record check covers the workflow" (record_check_cover): the run has a system-of-record verifier, the profile is standard or regulated, there is at least one save, and every save declares confirmed effects on each path and has an armed identity check of its own. Anything else refuses the click, and the run stays stopped for a person.

A backend that has accessibility reads but no element_identity_at (Windows and Linux today) is refused with that reason, not treated as pixels only.

A click admitted on the record check:

  • carries the dialog around it as its identity band (_dialog_band): the box from the AX ancestry or the drawn box on pixels, the text hashed with the bundle's salt. It's a screen pre-check, and it keeps the clinical-write armed-click rule intact. No dialog, or no readable text, refuses the click.
  • makes the fix need a person: a signed standard change never applies it (_Patch.needs_person).
  • is named in the evidence (step_x admitted by element_and_record_check (button 'Later')) and in what the approver sees ("checked by the record check").

Also: MacOSBackend.element_identity_at and QuartzMacAXClient.ancestry; the repair hold answers element_identity; agent teach takes the dialog box from the identity's ancestry when there is one.

Tests

  • tests/test_repair_element_identity.py (46): parsing and fail-closed validation, backend kind (including the real MacOSBackend, FreeRDPBackend and RemoteDisplayBackend classes), uniqueness, cover, admission per kind, the dialog band, needs_person, the hold op and agent teach.
  • The existing repair, self-repair, auto-repair, agent-teach, hold, standard-change and macOS suites: 262 passed, 12 skipped.

Real-app qualification (macOS)

benchmark/repair_every_backend/qualify_macos.py records a note workflow on a native Cocoa app (tests/native_apps/Chart.swift, SQLite) and repairs its new "Update available" notice. By CAPABILITIES' definitions this is a synthetic app (OpenAdapt's own), driven through live macOS AX, screen capture, input and database. Scripted repair agent and approver.

Through MacOSBackend, live: the run stops at the notice before any save; the "Later" click's AX identity is read (button "Later" inside AXSheet "Update available", in window Chart), found unique and admitted on element_and_record_check; the patched bundle passes clinical-write; under demo the click is refused and nobody is asked; with no verifier the run gate refuses. No run wrote a wrong row.

Not passing end to end yet, for reasons outside the repair code (details in the benchmark README):

  1. A durable run on MacOSBackend refuses its first structural click: remote delivery receipt does not bind the resolved target (action_evidence._delivery_receipt_error expects the structural fingerprint; click_guarded binds frame and point). The unrepaired bundle fails the same way, so no standard-profile run on native macOS completes today.
  2. MacOSBackend refuses input to a real NSAlert sheet (a separate CG window); the test app draws its notice in the window.
  3. Pixels only: a click whose next screen differs ends "delivery uncertain" / RECONCILIATION_REQUIRED, so it isn't repairable; plus a focus race after launch and shifted typing on a macOS host.

Not covered: Windows UIA and RDP (waiting on WindowsBackend.element_identity_at and the lab), and the CLI (run --self-repair still refuses non-web).

Review

Changes repair and identity: needs a recorded independent adversarial review before merge. Over 1,000 code lines, so the coordinator merges it by hand.

…by element identity and the record check

A bridge click with no record identity under it is admitted outside a browser
only when a record check covers every save: on an accessibility backend its
element (role, name, ancestry, window) must also be unique in the pinned window;
on pixels the record check alone admits it. The new step carries the dialog
around it as a screen pre-check, and only a person approves such a fix. The
browser rule is unchanged.

Adds repair/element_identity.py, MacOSBackend.element_identity_at, the repair
hold's element_identity op, the design note docs/REPAIR_ON_EVERY_BACKEND.md and
a macOS real-app qualification script with its first results.
@abrichr

abrichr commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

Independent adversarial review

Verdict: APPROVE. No blockers.

Reviewer. An independent subagent. It was given the diff and the product's safety rules, not the author's conclusions. It worked read-only and drove no screen.

Commits reviewed. origin/main...ea0c863df: 534abd7, 70ddadf (docs links only) and ea0c863. The only later commit, e731f07, adds the two result JSONs to public-artifacts.json and changes no code.

Tests the reviewer ran. tests/test_repair_element_identity.py, tests/test_self_repair_bridge.py and tests/test_standard_change.py: 72 passed.

What was probed, and held

  • The browser rule is unchanged. admit_new_click tries _recorded_identity first. A dom backend then refuses with the old reason. For the record-identity basis, structural is the same as before. The only difference: an unexpected exception from _recorded_identity now becomes a refusal instead of propagating, which still fails closed.
  • The person gate covers the only path that applies a fix automatically. StandardChanges.apply is skipped when patch.needs_person, and it is the only caller of RepairStore.approve_standard_change. _ask_person counts only a human decision with a person identity. cli_self_repair builds no standard_changes. Teach-path candidates fail authorize, because their repaired_by is not self_repair.
  • A missing profile or verifier fails closed. CliTarget has no profile, so record_check_cover refuses it. ReplayTarget carries both a profile and a verifier.
  • Data from the hold socket is validated. It goes through ElementIdentity.from_mapping on both ends: types and lengths are checked, regions must be positive integers, and ancestry depth is capped. dialog_region feeds only the screen pre-check band.
  • Windows and Linux are refused, not treated as pixel-only. Both have locate_structural but no element_identity_at, so backend_kind refuses them.
  • macOS uniqueness holds. locate_structural raises on an ambiguous or truncated search and checks each candidate's app and window. ancestry() requires the AXWindow title to equal the element's window title. An app's own AX labels can only make a click fail; they can't widen what is admitted.
  • Identifier crops are encrypted in encrypted bundles. They sit under templates/identifiers/, and Workflow.save encrypts everything under templates/.
  • No regressions in existing callers. The new 5-tuple from _new_click is consumed at its single call site.

Findings (all non-blockers; left for a follow-up PR under the post-approval freeze)

  1. backend_kind fails open for unknown backends. A backend without page, element_identity_at or a tree read is classed as pixel-only. A wrapper such as RenderScaledBackend, which hides those members, would get the weaker rule. This is not reachable today, because target.open returns raw backends. Fix: class a backend as pixel-only only on a positive marker, unwrap wrappers first, and refuse anything else.
  2. A bridge click that itself writes is guarded only by its label's write shape. Fix: also check element.name and automation_id, and require the whole-database recheck for record-check bases where it is available.
  3. needs_person is held only in memory. Fix: persist the admission basis on the candidate, and refuse it in standard_change.authorize.
  4. ea0c863 scopes uniqueness to the backend locator's window. Fix: require locator.window_name to be the identity's window or a name in its ancestry, and refuse a handle with no region.
  5. The approval request's task_scope says the identity evidence is the recorded one. For record-check fixes it should name the admission basis instead.
  6. Element names and window titles are written in plaintext to the teach recording's events.jsonl and to the patch evidence. Fix: hash or scrub them.

@abrichr
abrichr merged commit 5a3bf49 into main Nov 2, 2026
45 checks passed
@abrichr
abrichr deleted the claude/agnostic-repair-bdfb3c branch November 2, 2026 23:42
abrichr added a commit that referenced this pull request Nov 3, 2026
Self-repair and repair by default run outside a browser with
repair.session, qualified end to end on macOS AX and not yet on Windows
UI Automation or RDP. Teaching the fix (--hold-on-halt) and rollback
stay web-only.
abrichr added a commit that referenced this pull request Nov 3, 2026
…1706)

* Backend parity: capability by surface, with evidence for every cell

Rewrites docs/BACKEND_PARITY.md as a capability x surface matrix (web,
Windows, macOS, Linux, RDP, Citrix), each cell QUALIFIED, TESTED,
CODE ONLY or MISSING with its evidence path, and links the twelve
backend-parity issues.

Corrects claims the evidence doesn't support: Citrix in-session has no
run inside a Citrix session, the Linux GTK3 job doesn't run in the
monorepo, the macOS trials drove the backend directly, repair and
rollback run on web apps only, and the RDP numbers name their apps and
scripted harness.

* Backend parity: apply the adversarial review

Split rollback (refused off the web) from undo (code only off the web);
add the surface-neutral guarded save; one rule for scripted recordings;
Windows compile and record were run on a real VM, with the full 4 of 10
run tally; shadow mode gets the same verdict on every surface; web
approvals drop to tested (scripted person, local Cloud stand-in); live
repair vs offline teach; more refusals; the Aardwolf RDP batch was a
Windows 11 VM; flow's linux-atspi-x11 job, not capture's AT-SPI read,
is the one that never runs.

* Backend parity: non-web self-repair exists (#1709, #1715, #1717)

Self-repair and repair by default run outside a browser with
repair.session, qualified end to end on macOS AX and not yet on Windows
UI Automation or RDP. Teaching the fix (--hold-on-halt) and rollback
stay web-only.

* Backend parity: describe qualified surfaces with evidence
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant