Skip to content

openadapt-flow 2.5.0

Choose a tag to compare

@openadapt-release openadapt-release released this 03 Nov 00:17
· 46 commits to main since this release
Immutable release. Only release title and notes can be modified.
5a3bf49

Shadow mode checks the writes of an agent you already run against the system of record without blocking any of them, the whole-database frame works on PostgreSQL, a write that lands after the verdict can be caught by a recheck, and several verifier checks that could be unreadable or pass on the wrong value now read and compare exactly.

Features

  • Self-repair's new clicks no longer need a browser. On a backend with an accessibility tree, a click with no record identity under it is admitted when its element (role, name, ancestry, window) is the only one with that locator in the pinned window and a record check covers every save: a system-of-record verifier, a standard or regulated profile, and confirmed effects and an armed identity check on each save. On a pixel-only backend the record check alone admits it. The new step carries the dialog around it as its identity band, a screen pre-check, and only a person approves such a fix; a signed standard change can't. The browser rule is unchanged. MacOSBackend.element_identity_at reads the AX element, the repair hold answers element_identity, and agent teach takes the dialog box from it. Design: docs/REPAIR_ON_EVERY_BACKEND.md (#1709).

  • Shadow mode: openadapt shadow runs beside an agent you already have, as a loopback pass-through proxy, and checks each write it makes against the system of record (CONFIRMED, REFUTED, UNCERTAIN). It never blocks, changes or resends a write; the only delay is the read before a write, at most hold_ms, measured per write. FHIR writes are checked by ensure_record.verify_ensure and SQL writes by verifier.begin_write/verify_write with the whole-database frame. Each CONFIRMED is checked again later (recheck_s, or the contract's recheck_after_s), and a late REFUTED gets its own line in report.html and report.json. verify_ensure takes declared: resources of other writes the caller saw, judged on their own (#1678).

  • The whole-database frame (frame.database) and its recheck now work on PostgreSQL (dialect: postgresql). The snapshot is one read-only repeatable-read transaction over every schema but the system ones. Every column type is read as text by a declared rule, in a session pinned to UTC and exact float output: numeric at full precision (with its stored scale when it has no declared one), floats exactly, bytea as hex, json as stored and jsonb canonical, arrays, ranges, enums, domains, uuid, network, time, interval, money, bit, xml, text-search and geometric types. A table without a primary key is keyed by the whole row, with duplicates counted. A column type without a rule, a table the account can't read in full (no SELECT, row-level security), a foreign table and an unreadable sequence are refused unless the allow-list skips them with a reason. A sequence a column owns is that table's counter, and a counter set back counts as a change; any other sequence is compared as a one-row table. Large objects are read by the SHA-256 of their content. frame.database.schemas means the same as on MySQL (each must be visible, and every visible schema is read); a schema is left out only by an allow_tables entry <schema>.* with mode: skip and a reason.

  • A lost update now stops the run. frame.versions names a read of the target's versions (HAPI's hfj_res_ver, a version column, or an app's action-history table) and, where the app records one, the author of each version. The verifier reads them before and after the write; a version of a pre-existing target by anyone other than the agent, or more than one version step when no author is read, refutes the frame. A versions read that fails, a target with no version, or a version with no author leaves the frame not checked. The receipt holds counts and a problem code (evidence.frame.versions). Contracts without frame.versions keep their digests.

  • A write that lands after the verdict can now be caught. With frame.database.recheck_after_s, a verified write's receipt says when a recheck is due (recheck_due_at), and openadapt_flow.verifier.recheck reads the whole database again at or after that time. It judges every change since the post-save read with the frame's rules: the app profile's bookkeeping, other users' writes the audit log attributes, and changes a later write by the same actor under the same contract saw in its own window (excused_by, any outcome). Anything else turns the earlier result into REFUTED (late). A change that may be what refuted a later write, a database that can't be read, or a saved state that wasn't issued here or doesn't match its receipt gives UNCERTAIN. The saved state holds keyed hashes, not values. frame.database.settle_attempts lets the reads after a save, and at a recheck, retry within a stated budget until two in a row match; rows added to append-only tables between them don't count as movement. Without these settings, nothing changes (#1639).

  • A CONFIRMED write with a recheck due now says it is provisional (WriteResult.provisional, provisional in to_dict()). MCP clients of openadapt verify serve can run the recheck with the new verify_recheck tool. A recheck that gives REFUTED or UNCERTAIN now resolves the write's ledger attempt again for review, so a retry under the same key is held for a person instead of being handed the earlier CONFIRMED. A recheck from a saved recheck.json finds the attempt by the receipt's correlation_id; the result carries its ledger (WriteResult.ledger); when no attempt is re-resolved, the recheck's reasons say so. Over MCP, verify_recheck excuses the agent's own later writes under the same contract.

Fixes

  • A composite primary key in the whole-database frame is now kept as a JSON array of its values. Before, the values were joined with |, so the keys (x|y, z) and (x, y|z) were the same, one row hid the other, and deleting it went unseen. This applies to every dialect. A snapshot or recheck state saved by an earlier version keys rows differently, so recheck a write with the version that checked it.

  • The read-only SQL guard now reads the query's tokens instead of matching substrings. REPLACE(phone, '-', ''), a ; inside a string literal, and a table quoted as `call` no longer refuse the query. Every write and multi-statement form it refused before is still refused. It now also refuses a backslash, a dollar-quoted string, an unterminated literal, and a call to query_to_xml or eval, which run a query passed as text. When the query has parameters, it also refuses a % placeholder inside a literal or quoted name (the driver splices the value in as SQL; %% is fine). It refuses a dollar quote right after a number, row locks (FOR SHARE, FOR KEY SHARE, LOCK IN SHARE MODE), and get_lock, pg_advisory_*, pg_sleep_for, pg_sleep_until, master_pos_wait, source_pos_wait, pg_terminate_backend, pg_cancel_backend, load_file, pg_read_file, pg_ls_dir and pg_notify.

  • The whole-database snapshot reads a SQLite REAL with 17 digits, so 0.3 and 0.1+0.2 no longer read the same, and the text "\x00null" no longer reads as NULL. A number's plain value, which selectors compare with, is the shortest text that reads back as the stored value (1.1 for a MySQL FLOAT, 1.5 for a DECIMAL(24,8)), so a correct write is no longer stopped because a selector stopped matching.

  • The whole-database frame (frame.database) now reads MySQL and MariaDB schemas with every column type. A POINT column (Dolibarr 21) made the read raise TypeError; a BIT column did the same; FLOAT values compared by six printed digits; decimals lost digits past 28. Binary strings, BIT, geometries and vectors are read as hex, FLOAT is widened to DOUBLE, JSON is read as its stored text, and numbers parse as Decimal. A value that still can't be read exactly makes the database unreadable, never a column dropped, and begin_write now says why the frame could not be read.

  • A parameter set to None no longer makes the record check unreadable. Before, verify_write returned UNCERTAIN ("could not be read back") whatever was written, because the null could not be turned into text. Now a None in an effect's match, new_records or value matches only a field that is present and null. A missing field matches only where the contract declares absent_is_null: true on it. A null never matches "" or any other value, and a stored null no longer matches the text "None". A None where a null can't be bound (a query parameter, a key, the frame, a screen read) is refused at begin_write. The write ledger keys None and "" as different writes; a null write is held while any attempt stands under its old key (None keyed as ""), and that attempt's result is never handed back. A read that loses information (a FHIR path that never resolves, an outer join that filters rows) can confirm a null, so match a null only on a base column (#1660).

  • Behavior change: field_equals with an expected "" no longer passes on a stored NULL. A contract that clears a field on an app that stores a cleared field as NULL should expect None.

  • A whole-number float parameter now matches its stored value. Before, hours: 2.0 reached the check as the text "2" and a SQL float8 column read back as 2.0, so the two never matched and a correct write was refuted. Now a value the store typed as a number (a SQL numeric column; a JSON number) compares with an expected number by exact decimal value. Text stays byte-exact, and there is no tolerance.