Releases: automators-com/flowproof
Release list
v0.22.0
What's Changed
- docs: close the flowproof config / README-adopting.md alignment gap by @HappyDevs1 in #547
- chore(python): classify the package as alpha, not pre-alpha by @AminChirazi in #548
- feat(trace): first-class side_effect record and lane (#465 Phase A, 1/5) by @AminChirazi in #468
- feat(agent): record observed fs writes into the side-effect lane (#465 Phase A, 2/5) by @AminChirazi in #469
- docs: plan 007, autonomous flow repair loop by @HappyDevs1 in #549
- feat(agent,cli): implement plan 007's autonomous flow repair loop by @HappyDevs1 in #550
- feat(agent,cli): recognize load-timing races as a distinct repair category by @HappyDevs1 in #551
- feat(cli): give an engine-gap verdict one fresh retry before it's final by @HappyDevs1 in #552
- feat(cli): notify on every command when a newer flowproof is available by @HappyDevs1 in #554
- chore(release): bump version to 0.22.0 by @HappyDevs1 in #555
Full Changelog: v0.21.0...v0.22.0
v0.21.0
What's Changed
- ci: run the Linux workflows on the self-hosted runners by @AminChirazi in #525
- ci: use free hosted runners for public workflows by @AminChirazi in #526
- ci: move workflows from self-hosted runners to GitHub-hosted runners by @HappyDevs1 in #530
- ci: remove Operator PR package workflow by @AminChirazi in #533
- feat(cli): flowproof config sap/fiori — plan 1, designed and built by @HappyDevs1 in #531
- docs: add plan 3 — agent skill for flowproof config by @HappyDevs1 in #535
- feat(cli,adapters): flowproof doctor --sap / --fiori by @HappyDevs1 in #532
- Support shareable single-flow values files by @HappyDevs1 in #537
- Plan Fiori field commit hardening by @HappyDevs1 in #538
- feat: add AI authoring config by @HappyDevs1 in #544
- docs: v1 compatibility, support and deprecation contract by @HappyDevs1 in #510
- test(examples): verify a purchasing info record via OData directly by @HappyDevs1 in #517
- docs: publish a security reporting policy and threat model by @HappyDevs1 in #506
- docs: sync product docs with flowproof config, doctor, and AI authoring by @HappyDevs1 in #546
Full Changelog: v0.20.0...v0.21.0
v0.20.0
Added
-
A same-origin framed
ClickandPress … buttonnow dispatch a real
trusted click, not a refusal. A classic SAP GUI for HTML screen embeds
its whole UI in a same-origin iframe, and every tab strip, button, and
field on it lived behind that frame boundary. Clicking one was refused
outright: a pointer action inside a frame could only ever reach it as an
untrusted synthetic event, which an application checkingisTrustedis
free to ignore while the step still reports success - a real
release-without-effect false green, so the refusal was the honest answer
at the time. It no longer has to be. The driver now computes the target's
page-absolute point (the frame's own offset in the parent document, plus
the element's offset inside the frame) and dispatches the same
Input.dispatchMouseEventsequence the top-level document's click
already used - the click is real,isTrustedis true, and an application
driven this way cannot tell the difference from a person.Hover,
Double-click,Right-click, andUploadinside a frame stay refused;
none has this point-computation wired up yet. -
A framed
Typeinto a real<input>/<textarea>now types for real.
Framed value-writes previously set.valuethrough the native setter and
fired syntheticinput/changeevents - correct for anything listening
to those events, and wrong for anything that is not. A live SAP GUI for
HTML screen tracks its own field state off real keyboard events, the same
way its desktop ancestor always did, and never saw a synthetic write at
all: the field stayed visibly empty no matter what the trace said it
typed. The driver now clicks the field for real, selects any existing
value, and types the real keystrokes CDP would generate for an actual
person - the same trusted path the top-level document'sTypealready
used. Anything framed that is not a native input (a contenteditable, a
custom widget) keeps the synthetic write, since there is no native
keystroke target to land on. -
A framed element is now found by the label its OWN form control
resolves to, not by matching arbitrary text.the "Material" in the iframe "X"used to search the frame for any element whose raw
textContentread exactly"Material"- which, on a screen where the
visible label and the input it labels are separate DOM nodes (common
outside hand-rolled markup), matched the label itself, never the field.
Typing then landed on a<div>, silently no-op'd through the
now-corrected trusted-keystroke path, and the search it was meant to fill
in ran empty. Both the write and read paths now check a form control's
native.labelsassociation first - the same relationshipscene()
already uses to report a field's label back to authoring - before falling
back to the old plain-text search, so a label naming a real field finds
that field. -
flowproof recordshows the browser by default;--headed/--headless
make the choice a flag, not tribal knowledge. Watching awebrecording
has been possible sinceFLOWPROOF_HEADEDshipped, but only if you already
knew the variable existed - nothing in--helpsaid so, and a new user
evaluating flowproof against a tool like Tosca (where you always watch the
browser while recording) got a blank terminal by default. Recording is the
one human-in-the-loop step, so it now opens Chromium unless told not to;
flowproof run(replay) is unchanged and stays headless, because it is
meant to run unattended on CI runners that often have no display at all,
and a default flip there risks the false-green class of bug this project
exists to catch - headed Chromium sizes its window from the desktop, so a
visual baseline recorded one way and replayed the other reports a mismatch
that has nothing to do with the flow. Anything that scriptsflowproof recordwithout a person watching (CI, a container, a cassette-generation
pipeline) needs--headlessnow; everything else needs nothing at all. -
The SAP test simulator models more than one screen shape. Every
automated SAP check ran against the same flat VA01 window - onewnd[0],
every field hanging straight off it. A fixture like that cannot fail.
Window scoping could break, nesting could break, and nothing short of a
person sitting at the one real SAP machine the team has would notice.The simulator now serves two more shapes. A classic
GuiTableControl
carries its cells as CHILDREN, under SAP's real[column,row]ids, so the
tree walk has to recurse past depth two andFindByIdhas to survive an id
with brackets and a comma in it. Back opens awnd[1]modal that sits in
the session tree besidewnd[0]and leaves it again when dismissed - and
the two windows deliberately share no text, so which window a reader is
reading is a question with an answer.sap_sim_e2edrives both through the production COM engine.
Fixed
-
The Python SDK's dev/test lock resolved a known-vulnerable
pytestfor
Python 3.9.pytest < 9.0.3has insecure/tmp/pytest-of-{user}
handling on UNIX (GHSA-6w46-j5rx-g56g); the fix only ever shipped in the
9.x line, and 9.x dropped Python 3.9 support outright, so a plain floor
bump was unsatisfiable for the 3.9 branch of the lock. Thedev
dependency group now declares its ownrequires-python = ">=3.10"
([tool.uv.dependency-groups]), separate from the package's own
requires-python = ">=3.9"— running the test suite from source now needs
3.10+, same as CI already effectively runs, while nothing changes for
anyone installing the shippedflowproofpackage on 3.9. The lock now
resolves onepytestversion everywhere: 9.1.1, patched. -
The nightly SAP suite was failing on the same field, every night, for a
reason nobody had looked for. Every scheduledsap-e2erun since early
August had failed on the first field of every flow that types into one —
while the one flow that never touches a field kept passing. The heartbeat
script written to keep an idle SAP session alive (#453) had nothing to do
with it: it isn't invoked from the workflow at all, and even where it was
separately registered on the runner, its scheduled task had no
-Principal, so it silently stopped reaching the interactive desktop the
moment that session's RDP connection went from active to disconnected.The actual failure was inside the job itself.
sap-session-bootstrap.ps1
logs the session in once, at job start; the login is genuinely fine at
that point, confirmed bycargo test sap_e2epassing cleanly a few
minutes later. But two fullcargo builds follow with no shared
incremental cache — around seven minutes of pure compilation, untouched
by any SAP activity — and by the time the flow suite starts, the
application server has logged the session out for sitting idle. The
client raises its own "maximum user idle time exceeded" notice, a native
Win32 dialog outside SAP's scripting object model — invisible to
FindById, so nothing already in the driver could see or close it, and
every field lookup after it just timed out waiting for a screen that was
never going to render.sap-session-bootstrap.ps1now recognizes that dialog and dismisses it
(declining, not accepting — the default button opens a second window
that would need dismissing too), andsap-e2e.ymlruns bootstrap a
second time immediately before the flow suite, not only once at job
start, closing the actual idle window instead of one that had already
passed.
v0.19.0
Added
-
A refused value stays out of the trace. A recorded correction used to
memorialize its own fumble: type 1200, type 800 (appended into 1200800 by
the pre-0.18 typing), clear, type 500 - four steps where one is the truth,
and every replay re-performed the lot, driving the app through its error
state on purpose, for ever.The repo already had the principle, applied everywhere except here. An
occluded click is not recorded, because it records a success the page never
saw. A walk-away from a rejected form is not recorded, because it records a
success the page refused to give. But a VALUE the page rejected - same
verdict, from the same page - was recorded, and then replayed.When the rejected-form guard fires and the correction sticks, the writes
that produced the red fields are now dropped before the trace is minted and
the survivors renumbered. The trace reads as the one line a person would
have left: the value that stuck. The live run is unchanged; only the
fumble's memorial goes.Three guards keep the drop honest. The recording spans join BEFORE the
prune, so heal's before/after frames keep pointing at the moment they
filmed. An observer between fumble and correction - a capture, an assert, a
click - blocks the prune, because it read the page WITH the fumble in it
and a trace without it would replay a different observation than the one
recording verified. And positional selector paths are never pruned, since
the same string can name different elements across a DOM mutation.
Everything unprovable fails open to the journal.
v0.18.0
Changed
-
Targeted typing means fill, on every adapter. Typing into a NAMED
field now states what the field should read: whatever it held is replaced,
the way every mainstream tool works. Appending was never a designed
contract - it was an accident of the web keystroke path, and not even a
uniform one, since framed typing, the select branch and SAP fields always
replaced.What the accident cost was found the expensive way. A correction typed
800 - the right value - into a payload field still holding the refused
9000, and the page saw 9000800. The recording survived only because the
model noticed the tool's own behaviour in the scene, cleared the field and
retyped.Each adapter keeps the contract in its own idiom: web selects the content
and sends the same trusted keystrokes (a keydown-filtering app still sees
real keys), Windows UIA selects-all with its own chord and gains the
clear_textthe Replace grammar always erred on there, vision composes
the chord its clear already used, SAP was already fill. Focused typing -
Type <text>with no target - stays raw keystrokes and APPENDS: it is
the step for dropdown filters, pre-focused rename boxes, and adding to
what is there.A spec that relied on targeted typing appending would change meaning. No
committed flow does, and the docs never promised it - but that is the
reason this is 0.18.0 and not 0.17.1.
v0.17.0
Added
-
A refused value re-authors the rest of its step. A person handed "fill
out all the vehicle data" types a truck-plausible payload, watches the form
go red, reads "must be a number between 1 and 1000", types a smaller number
and carries on. flowproof could not, and a recording died on a form a person
would have finished.Two things stopped it, and the spec was not one of them.
The guard that catches a rejected form before the click that leaves keyed
its one-correction-per-problem rule on the FIELD, so a second attempt read
as a repeat of the first however different the value — one try, where a
person gets as many as it takes. The value was in the scene and unused. It
is in the key now: a different value earns another look, the same value
twice still reads as a decision.And nothing weighed the rule at the moment of choosing. A value outside a
plainly stated range is now CHALLENGED before a keystroke reaches the
browser, which is cheaper than a rejected form and leaves the guard's
correction for a problem it has not already been warned about.Challenged, not refused. The rule is scraped from copy written for a person:
"between 3 and 20 characters" is the same six words as a value range, a
wrapper can lend one field's message to its neighbour, and a step may MEAN
to enter something the page refuses to see what it says. A model that
repeats its value after being asked is taken at its word, and the
application decides. Being wrong costs a question, never a recording.Recorded against an unchanged spec, the trace now reads 5000, then 800,
then 500 — the sequence a person would have produced.
v0.16.1
Fixed
-
One rejected field was reported as three, two of them unfixable. A
validation framework sets its marker on the WRAPPER, not the control, so
the check for it walks up from the element. Asked of every scene entry,
that flagged the label and the error message inside the wrapper as well as
the field: one bad value named three times, two of them things nothing can
type into.A recording died on it. The page refused a payload, the guard caught it
before the click that leaves the screen and re-authored the step quoting
the rule back — and the correction it asked for listed#payloadbeside a
label.mainand aspan.errorcarrying the same sentence. A model gets
one correction before the same rejection is treated as a decision rather
than a hiccup, and that one can be spent on a span.Only a control can be rejected, and only a control can be corrected, so
only a control is asked. The rule the page states is read the same way: a
label repeating a constraint is not a field that has one.
v0.16.0
Recording could not finish a flow written the way a tester talks. This
release is the seven-defect chain that stopped it, found by recording one
insurance quote against a live application until it worked.
Every one of them was a fault in what the recorder could PERCEIVE, and
every one of them read, at first, as the authoring model being wrong. It
never was. The scene described a page that had moved on, or omitted a
control the page had drawn for itself, or reported a rejected form as a
clean one — and the model reasoned correctly from what it was handed.
The flow this was found on now records end to end and replays
deterministically, six consecutive runs, zero model calls.
Changed
-
A version tag now publishes. Both publish workflows were
workflow_dispatchonly, so pushingv0.15.0did nothing and the release
still had to be triggered by hand — a step easy to forget after tagging, and
easy to mistake for having released.They now also fire on a tag matching
v[0-9]+.[0-9]+.[0-9]+. The pattern is
exact rather thanv*so an experimental tag cannot ship a release, and a
new guard refuses a tag whose name disagrees with the version it would
build. That guard is not hypothetical: av0.14.0tag was created on the
commit that bumps to 0.15.0. While nothing consumed tags it was merely wrong
in the history; with tags publishing, the same slip would ship 0.15.0
labelled 0.14.0, and registry versions are immutable.
Fixed
-
The authoring scene could describe a page that no longer existed, and the
model took the blame. The inventory was read the instant the previous step
finished. A step that navigates leaves the next step's scene racing the new
page, and a server that renders every variant of a form and hides the
irrelevant ones in script briefly presents all of them as rendered.On the Tricentis sample app this was reliable rather than rare.
Open the truck insurance quotewas authored correctly, and the scene captured
immediately after was the union of two forms: a motorcycle#model—
actionable, with its own options list — beside the truck's#payload. The
model grounded faithfully onto#model, exactly as instructed, and the
recorder then refused the step because no such element existed by the time
it ran. The failure read as a model inventing a selector. It was the scene
that lied; the model had copied a target it was entitled to copy.The web scene is now read only once two readings agree on which targets
exist, with the document loaded. That narrows the window rather than
closing it — a navigation that has not begun still presents a settled old
page, and a single-page app never leavesreadyState completeat all — but
it removes the case this was losing recordings to.
Agreement before load completes does not count, because that is precisely
the window the pre-script union looks stable in. Only the set of targets has
to hold still — a clock, a character counter, or a field mid-edit changes
text on every reading without changing which elements exist, and waiting on
those would wait forever. A page that never goes quiet is captured anyway
after a bounded wait: recording a carousel is better than hanging on one. -
Recording refused a click replay would have waited for. The occlusion
probe's own comment said it used "the same predicate as replay's
actionability gate, so the two agree by construction". The predicate was the
same. The waiting was not: replay polls the gate to the step's deadline,
recording asked once and aborted. A toast on its way out, a modal backdrop
mid-fade, a carousel between slides — each reports "something else would
receive this click" the instant it is asked, and each is gone a moment
later. Recording refused pages it could have recorded, and the trace it
declined to write would have replayed.Recording now polls the same gate for the same budget, measured as wall
clock exactly as replay measures it — a count of polls would omit the probe
round-trips between them and let recording out-wait replay on a slow
transport. Not a new constant:
recording bakesstep_timeout_ms()into every targeted step's existence
precondition and replay waits exactly that recorded value, so both are
derived from the one number and follow itsFLOWPROOF_STEP_TIMEOUT_MS
override together. The direction is the part that matters — recording must
never accept a click replay would reject, so out-waiting replay is the one
error that cannot be tolerated: it mints a trace whose first replay fails.An occluder that outlasts the budget is still refused, with the message it
always had. This buys the transient cases only, which is all a wait can buy. -
The two E2E jobs had been red on
mainfor a day, and nothing on a pull
request could see it.web E2Eandwindows build + E2Eare off the PR
path, so they run only on push to main — every PR involved went green, and
main went red on merge, repeatedly, for two unrelated reasons.notepad_author_e2ebuilds aFlowSpecliterally and the struct had grown
three fields it did not list (apps,exports,login). The file is
#![cfg(windows)], so no amount ofcargo clippy --all-targetson Linux or
macOS ever compiled it.web_e2easserted the whole-run GIF renders, but GIF assembly became opt-in
when--videolanded and the test kept running with defaults — it expected
an artifact nothing had been told to produce. It now asks for one, so the
assertion tests what it claims to.
v0.15.0
What's Changed
- fix(agent): leave room for Anthropic reasoning by @AminChirazi in #395
- Fix SAP Logon startup, login, and transaction navigation by @AminChirazi in #396
- docs: add Tricentis insurance example by @AminChirazi in #398
- Add visible browser inspection mode by @AminChirazi in #397
- Make visual recording configurable and video opt-in by @AminChirazi in #399
- docs: record the visual-recording default change by @AminChirazi in #400
- feat(python): expose the recording controls on the SDK and MCP tools by @AminChirazi in #402
- chore(release): prepare 0.13.0 by @AminChirazi in #401
- docs: the recording page opened with the design, not the controls by @AminChirazi in #404
- ci: a deploy that never happened reported success by @AminChirazi in #405
- docs: three flags shipped that no page mentioned by @AminChirazi in #403
- fix(gate): protection.json would have removed the constitution checks by @AminChirazi in #406
- feat(agent): flows declare exports: — values handed to the flows after them by @AminChirazi in #407
- fix(agent): a step naming a whole form authored one field by @AminChirazi in #409
- feat(cli): a suite run threads a passing flow's exports to the flows after it by @AminChirazi in #410
- perf(adapters): answer existence and actionability probes in one CDP round trip by @AminChirazi in #412
- fix(replay): render each recording frame under exactly one step by @AminChirazi in #411
- feat(agent): the multi-surface vocabulary — apps: and in: blocks parse and validate by @AminChirazi in #413
- feat(agent): surface semantics — which kinds may be a surface, and where config sits by @AminChirazi in #415
- feat(trace): a step can say which surface ran it — apps map + surface attribution by @AminChirazi in #414
- feat(driver): a SurfaceRegistry holds several surfaces and drives exactly one by @AminChirazi in #417
- feat(agent): a multi-surface flow records — surfaces activate, captures cross blocks by @AminChirazi in #418
- feat(replay): a multi-surface trace replays — each step on its recorded surface by @AminChirazi in #419
- feat(trace): a web surface carries its own browser: config by @AminChirazi in #420
- feat(agent): a desktop surface carries its own window: — and vision becomes a surface by @AminChirazi in #421
- feat(agent): a screenshot baseline names its surface — assert_screenshot joins multi-surface flows by @AminChirazi in #426
- feat(agent): heal works on multi-surface flows — Phase 2's list is empty by @AminChirazi in #429
- docs: Phase 3 design — the in-flow agent seam by @AminChirazi in #430
- chore(release): prepare 0.14.0 by @AminChirazi in #416
- fix(python): upgrade PyO3 to 0.29, clearing two RustSec advisories by @AminChirazi in #431
- fix(sdk): upgrade cryptography past GHSA-g6cj-pr64-35w5 by @AminChirazi in #434
- test(agent): read the docs, so a refused step cannot be documented again by @AminChirazi in #435
- A whole-form step could author nothing, and a keystroke cost 213ms by @AminChirazi in #437
- chore(release): prepare 0.15.0 by @AminChirazi in #439
Full Changelog: v0.12.2...v0.15.0
FlowProof 0.12.2
FlowProof 0.12.2
This release makes natural-language authoring substantially more capable while keeping recorded replay deterministic and model-free.
Highlights
- Author ordinary human instructions for clicking, typing, scrolling, keyboard input, drag-and-drop, single and multi-selects, counting, captured-value reuse, tables, and same-origin iframe content.
- Ground model output against the live screen inventory, including scoped and opaque scene tokens, instead of requiring users to write CSS or element IDs.
- Close headed Chrome sessions cleanly after execution, avoiding orphan windows, empty tabs, and accumulating browser processes.
- Improve Windows CI coverage for containment, native UI Automation authoring/replay, and Windows Server 2025 runner behavior.
Documentation
The release assets include refreshed training and solutions PDFs written with human-language FlowProof steps for version 0.12.2.
See CHANGELOG.md for the detailed change list.