flowproof 0.23.30__tar.gz → 0.23.32__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. {flowproof-0.23.30 → flowproof-0.23.32}/Cargo.lock +7 -7
  2. {flowproof-0.23.30 → flowproof-0.23.32}/Cargo.toml +1 -1
  3. {flowproof-0.23.30 → flowproof-0.23.32}/PKG-INFO +1 -1
  4. flowproof-0.23.32/crates/flowproof-adapters/src/kept_browser.rs +388 -0
  5. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/lib.rs +3 -0
  6. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/web.rs +110 -5
  7. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/lib.rs +1 -0
  8. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/recorder.rs +59 -6
  9. flowproof-0.23.32/crates/flowproof-agent/src/selection.rs +312 -0
  10. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/spec.rs +14 -0
  11. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/lib.rs +163 -8
  12. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-replay/src/lib.rs +358 -142
  13. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-replay/src/report.rs +111 -2
  14. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-replay/src/runrecord.rs +3 -0
  15. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-replay/tests/replay_calc.rs +20 -0
  16. flowproof-0.23.32/crates/flowproof-replay/tests/spec_step.rs +169 -0
  17. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/schema/trace-v1.schema.json +5 -0
  18. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/captures.rs +107 -0
  19. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/format.rs +7 -0
  20. {flowproof-0.23.30 → flowproof-0.23.32}/flowproof/__init__.py +1 -1
  21. {flowproof-0.23.30 → flowproof-0.23.32}/flowproof/flow.py +7 -0
  22. {flowproof-0.23.30 → flowproof-0.23.32}/pyproject.toml +1 -1
  23. {flowproof-0.23.30 → flowproof-0.23.32}/README.md +0 -0
  24. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/Cargo.toml +0 -0
  25. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/examples/vision_bench.rs +0 -0
  26. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/examples/vision_do.rs +0 -0
  27. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
  28. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
  29. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress.rs +0 -0
  30. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
  31. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
  32. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
  33. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/env_block.rs +0 -0
  34. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
  35. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
  36. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
  37. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
  38. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
  39. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
  40. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
  41. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
  42. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/fiori.rs +0 -0
  43. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
  44. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
  45. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
  46. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
  47. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/sap_com.rs +0 -0
  48. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/vision/start.rs +0 -0
  49. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/vision.rs +0 -0
  50. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/vision_bench.rs +0 -0
  51. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/vision_elements.rs +0 -0
  52. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/src/vision_winocr.rs +0 -0
  53. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/a11y_capture.rs +0 -0
  54. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
  55. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/fingerprint_capture.rs +0 -0
  56. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/fixtures/windows-start.png +0 -0
  57. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
  58. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/grid_scene_e2e.rs +0 -0
  59. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/list_scene_e2e.rs +0 -0
  60. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/network_idle_e2e.rs +0 -0
  61. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/password_recording_e2e.rs +0 -0
  62. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/shared_browser_shutdown_e2e.rs +0 -0
  63. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/side_effect_http_e2e.rs +0 -0
  64. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/static_area_e2e.rs +0 -0
  65. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/visibility_detach_e2e.rs +0 -0
  66. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +0 -0
  67. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/Cargo.toml +0 -0
  68. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/agent_steps.rs +0 -0
  69. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/author.rs +0 -0
  70. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/clarify.rs +0 -0
  71. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/code_author.rs +0 -0
  72. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/doc_author.rs +0 -0
  73. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/doc_formats/hp_alm.rs +0 -0
  74. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/doc_formats.rs +0 -0
  75. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/draft_assembly.rs +0 -0
  76. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/heal.rs +0 -0
  77. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/llm.rs +0 -0
  78. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/repair.rs +0 -0
  79. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/src/rules.rs +0 -0
  80. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
  81. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/Cargo.toml +0 -0
  82. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/skills/flowproof-config/SKILL.md +0 -0
  83. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/agent_flow.rs +0 -0
  84. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/agent_surface.rs +0 -0
  85. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/capture.rs +0 -0
  86. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/config.rs +0 -0
  87. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/doctor.rs +0 -0
  88. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/main.rs +0 -0
  89. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/suite.rs +0 -0
  90. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/suite_checkpoint.rs +0 -0
  91. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/suite_inputs.rs +0 -0
  92. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/suite_journal.rs +0 -0
  93. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/src/update_check.rs +0 -0
  94. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/a11y_selector_e2e.rs +0 -0
  95. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/agent_conversation_anthropic_e2e.rs +0 -0
  96. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/agent_conversation_streaming_e2e.rs +0 -0
  97. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
  98. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
  99. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
  100. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
  101. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/author_from_code_e2e.rs +0 -0
  102. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  103. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/citrix_start.rs +0 -0
  104. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
  105. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/config_cli_e2e.rs +0 -0
  106. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/config_seed_e2e.rs +0 -0
  107. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/config_skill_e2e.rs +0 -0
  108. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
  109. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/doctor_ai_e2e.rs +0 -0
  110. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
  111. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/documented_flags.rs +0 -0
  112. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
  113. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
  114. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/export_e2e.rs +0 -0
  115. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
  116. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
  117. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
  118. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
  119. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
  120. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
  121. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
  122. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
  123. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/falsifiability_side_effect.rs +0 -0
  124. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/fiori_field_commit_e2e.rs +0 -0
  125. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
  126. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/heal_apply_proposal.rs +0 -0
  127. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/heal_vars_e2e.rs +0 -0
  128. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
  129. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
  130. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
  131. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
  132. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
  133. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  134. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
  135. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
  136. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/record_progress.rs +0 -0
  137. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
  138. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
  139. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
  140. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
  141. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
  142. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/sigterm_shared_browser_e2e.rs +0 -0
  143. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
  144. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/suite_checkpoint.rs +0 -0
  145. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
  146. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
  147. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
  148. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/suite_policy.rs +0 -0
  149. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
  150. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
  151. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  152. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/vision_start_replay.rs +0 -0
  153. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
  154. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/Cargo.toml +0 -0
  155. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/app.rs +0 -0
  156. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/backend.rs +0 -0
  157. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/gdi.rs +0 -0
  158. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/lib.rs +0 -0
  159. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/mock.rs +0 -0
  160. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/oob.rs +0 -0
  161. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/recording.rs +0 -0
  162. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/redact.rs +0 -0
  163. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/surface.rs +0 -0
  164. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/uia_scan.rs +0 -0
  165. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/visual.rs +0 -0
  166. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-driver/src/window.rs +0 -0
  167. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-python/Cargo.toml +0 -0
  168. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-python/src/lib.rs +0 -0
  169. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-replay/Cargo.toml +0 -0
  170. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/Cargo.toml +0 -0
  171. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/schema/cassette-v1.schema.json +0 -0
  172. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/schema/side-effect-v1.schema.json +0 -0
  173. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/cassette.rs +0 -0
  174. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
  175. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/egress.rs +0 -0
  176. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/lib.rs +0 -0
  177. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/secret.rs +0 -0
  178. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/secret_scan.rs +0 -0
  179. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/side_effect.rs +0 -0
  180. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/substitution.rs +0 -0
  181. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/toolcalls.rs +0 -0
  182. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/src/tosca.rs +0 -0
  183. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/cassette_conformance.rs +0 -0
  184. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/fixtures/guarded.trace.jsonl +0 -0
  185. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/fixtures/multi.trace.jsonl +0 -0
  186. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/fixtures/repeat.trace.jsonl +0 -0
  187. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
  188. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
  189. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/side_effect_conformance.rs +0 -0
  190. {flowproof-0.23.30 → flowproof-0.23.32}/crates/flowproof-trace/tests/tosca_export.rs +0 -0
  191. {flowproof-0.23.30 → flowproof-0.23.32}/flowproof/cli.py +0 -0
  192. {flowproof-0.23.30 → flowproof-0.23.32}/flowproof/mcp_server.py +0 -0
  193. {flowproof-0.23.30 → flowproof-0.23.32}/flowproof/py.typed +0 -0
@@ -911,7 +911,7 @@ dependencies = [
911
911
 
912
912
  [[package]]
913
913
  name = "flowproof-adapters"
914
- version = "0.23.30"
914
+ version = "0.23.32"
915
915
  dependencies = [
916
916
  "anyhow",
917
917
  "flowproof-driver",
@@ -930,7 +930,7 @@ dependencies = [
930
930
 
931
931
  [[package]]
932
932
  name = "flowproof-agent"
933
- version = "0.23.30"
933
+ version = "0.23.32"
934
934
  dependencies = [
935
935
  "base64",
936
936
  "chrono",
@@ -949,7 +949,7 @@ dependencies = [
949
949
 
950
950
  [[package]]
951
951
  name = "flowproof-cli"
952
- version = "0.23.30"
952
+ version = "0.23.32"
953
953
  dependencies = [
954
954
  "ab_glyph",
955
955
  "chrono",
@@ -975,7 +975,7 @@ dependencies = [
975
975
 
976
976
  [[package]]
977
977
  name = "flowproof-driver"
978
- version = "0.23.30"
978
+ version = "0.23.32"
979
979
  dependencies = [
980
980
  "calamine",
981
981
  "flowproof-trace",
@@ -991,7 +991,7 @@ dependencies = [
991
991
 
992
992
  [[package]]
993
993
  name = "flowproof-python"
994
- version = "0.23.30"
994
+ version = "0.23.32"
995
995
  dependencies = [
996
996
  "flowproof-agent",
997
997
  "flowproof-cli",
@@ -1003,7 +1003,7 @@ dependencies = [
1003
1003
 
1004
1004
  [[package]]
1005
1005
  name = "flowproof-replay"
1006
- version = "0.23.30"
1006
+ version = "0.23.32"
1007
1007
  dependencies = [
1008
1008
  "chrono",
1009
1009
  "flowproof-agent",
@@ -1017,7 +1017,7 @@ dependencies = [
1017
1017
 
1018
1018
  [[package]]
1019
1019
  name = "flowproof-trace"
1020
- version = "0.23.30"
1020
+ version = "0.23.32"
1021
1021
  dependencies = [
1022
1022
  "jsonschema",
1023
1023
  "regex",
@@ -22,7 +22,7 @@ members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproo
22
22
  exclude = ["spike/windows-containment"]
23
23
 
24
24
  [workspace.package]
25
- version = "0.23.30"
25
+ version = "0.23.32"
26
26
  edition = "2021"
27
27
  license = "Apache-2.0"
28
28
  repository = "https://github.com/automators-com/flowproof"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowproof
3
- Version: 0.23.30
3
+ Version: 0.23.32
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,388 @@
1
+ //! A browser left open after a run (`run --leave-open <file>`), so a later
2
+ //! run can carry on in it (`run --attach <file>`), and closed on request
3
+ //! (`flowproof session close <file>`).
4
+ //!
5
+ //! A browser flowproof launches normally dies with the run: the launcher
6
+ //! kills it and deletes its profile when its handle drops. A kept browser is
7
+ //! started differently - detached, in its own process group, with a profile
8
+ //! directory of its own - and the run CONNECTS to it, so the run ends and the
9
+ //! browser stays. The session file is how the next run, and whoever closes
10
+ //! it, finds it again: the DevTools address, the process, the profile.
11
+ //!
12
+ //! The DevTools address is full control of a logged-in browser, so it is
13
+ //! bound to 127.0.0.1 and the session file is created private (0600 on unix)
14
+ //! and refused when it is not.
15
+
16
+ use std::path::{Path, PathBuf};
17
+ use std::time::{Duration, Instant};
18
+
19
+ use serde::{Deserialize, Serialize};
20
+
21
+ /// Set by the CLI for `run --leave-open <file>`: launch a kept browser and
22
+ /// record it in `<file>`.
23
+ pub const LEAVE_OPEN_ENV: &str = "FLOWPROOF_LEAVE_BROWSER_OPEN";
24
+ /// Set by the CLI for `run --attach <file>`: carry on in the kept browser
25
+ /// `<file>` records instead of launching one.
26
+ pub const ATTACH_ENV: &str = "FLOWPROOF_ATTACH_BROWSER";
27
+
28
+ const SCHEMA: u32 = 1;
29
+ /// Every kept profile lives directly under the temp directory with this
30
+ /// prefix; `close` deletes nothing else, whatever a session file says.
31
+ const PROFILE_PREFIX: &str = "flowproof-kept-";
32
+ const STARTUP_TIMEOUT: Duration = Duration::from_secs(20);
33
+
34
+ /// What a session file records about one kept browser.
35
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36
+ #[serde(deny_unknown_fields)]
37
+ pub struct KeptSession {
38
+ pub schema: u32,
39
+ pub pid: u32,
40
+ /// `ws://127.0.0.1:<port>/devtools/browser/<id>`.
41
+ pub ws_url: String,
42
+ pub profile_dir: PathBuf,
43
+ /// Unix seconds.
44
+ pub created: u64,
45
+ }
46
+
47
+ impl KeptSession {
48
+ fn port(&self) -> Option<u16> {
49
+ self.ws_url
50
+ .strip_prefix("ws://127.0.0.1:")?
51
+ .split('/')
52
+ .next()?
53
+ .parse()
54
+ .ok()
55
+ }
56
+
57
+ /// Whether the browser is still there: its DevTools port still answers.
58
+ pub fn is_alive(&self) -> bool {
59
+ self.port().is_some_and(|port| {
60
+ std::net::TcpStream::connect_timeout(
61
+ &std::net::SocketAddr::from(([127, 0, 0, 1], port)),
62
+ Duration::from_millis(500),
63
+ )
64
+ .is_ok()
65
+ })
66
+ }
67
+ }
68
+
69
+ /// Start a visible Chromium that outlives this process and wait until its
70
+ /// DevTools endpoint is up.
71
+ pub fn spawn(chrome: &Path) -> Result<KeptSession, String> {
72
+ let now = std::time::SystemTime::now()
73
+ .duration_since(std::time::UNIX_EPOCH)
74
+ .unwrap_or_default();
75
+ let nanos = now.as_nanos();
76
+ let profile_dir =
77
+ std::env::temp_dir().join(format!("{PROFILE_PREFIX}{}-{nanos}", std::process::id()));
78
+ private_dir(&profile_dir)?;
79
+
80
+ let mut command = std::process::Command::new(chrome);
81
+ command
82
+ .args(headless_chrome::browser::DEFAULT_ARGS)
83
+ .arg("--no-sandbox")
84
+ .arg("--no-default-browser-check")
85
+ .arg("--remote-debugging-address=127.0.0.1")
86
+ .arg("--remote-debugging-port=0")
87
+ .arg(format!("--user-data-dir={}", profile_dir.display()))
88
+ .arg("about:blank")
89
+ .stdin(std::process::Stdio::null())
90
+ .stdout(std::process::Stdio::null())
91
+ .stderr(std::process::Stdio::null());
92
+ // Its own process group, so stopping the run that started it (a signal
93
+ // to the run's group) leaves it alone, and `close` can stop it whole.
94
+ #[cfg(unix)]
95
+ {
96
+ use std::os::unix::process::CommandExt;
97
+ command.process_group(0);
98
+ }
99
+ #[cfg(windows)]
100
+ {
101
+ use std::os::windows::process::CommandExt;
102
+ const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
103
+ command.creation_flags(CREATE_NEW_PROCESS_GROUP);
104
+ }
105
+ #[cfg(all(windows, feature = "sap-com"))]
106
+ let spawned = crate::sap_com::spawn_detached(&mut command);
107
+ #[cfg(not(all(windows, feature = "sap-com")))]
108
+ let spawned = command.spawn();
109
+ let mut child = spawned.map_err(|e| {
110
+ let _ = std::fs::remove_dir_all(&profile_dir);
111
+ format!("starting {}: {e}", chrome.display())
112
+ })?;
113
+
114
+ // Chromium writes the port it picked (and the browser target's path)
115
+ // into the profile once DevTools is listening.
116
+ let active_port = profile_dir.join("DevToolsActivePort");
117
+ let started = Instant::now();
118
+ let ws_url = loop {
119
+ if let Ok(text) = std::fs::read_to_string(&active_port) {
120
+ let mut lines = text.lines();
121
+ if let (Some(port), Some(path)) = (lines.next(), lines.next()) {
122
+ if let Ok(port) = port.trim().parse::<u16>() {
123
+ break format!("ws://127.0.0.1:{port}{}", path.trim());
124
+ }
125
+ }
126
+ }
127
+ if let Ok(Some(status)) = child.try_wait() {
128
+ let _ = std::fs::remove_dir_all(&profile_dir);
129
+ return Err(format!(
130
+ "the browser exited during startup ({status}); a host with no desktop \
131
+ session cannot show one"
132
+ ));
133
+ }
134
+ if started.elapsed() > STARTUP_TIMEOUT {
135
+ stop(child.id());
136
+ let _ = std::fs::remove_dir_all(&profile_dir);
137
+ return Err("the browser did not open its DevTools port within 20s".into());
138
+ }
139
+ std::thread::sleep(Duration::from_millis(100));
140
+ };
141
+ Ok(KeptSession {
142
+ schema: SCHEMA,
143
+ pid: child.id(),
144
+ ws_url,
145
+ profile_dir,
146
+ created: now.as_secs(),
147
+ })
148
+ }
149
+
150
+ /// Write `session` to `path`, private and whole: a temp file in the same
151
+ /// directory, then a rename.
152
+ pub fn write(path: &Path, session: &KeptSession) -> Result<(), String> {
153
+ let text = serde_json::to_string_pretty(session).map_err(|e| e.to_string())?;
154
+ if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
155
+ std::fs::create_dir_all(parent)
156
+ .map_err(|e| format!("creating {}: {e}", parent.display()))?;
157
+ }
158
+ let tmp = path.with_extension("tmp");
159
+ let _ = std::fs::remove_file(&tmp);
160
+ let mut options = std::fs::OpenOptions::new();
161
+ options.write(true).create_new(true);
162
+ #[cfg(unix)]
163
+ {
164
+ use std::os::unix::fs::OpenOptionsExt;
165
+ options.mode(0o600);
166
+ }
167
+ let written = options.open(&tmp).and_then(|mut file| {
168
+ use std::io::Write;
169
+ file.write_all(text.as_bytes())?;
170
+ file.sync_all()
171
+ });
172
+ written
173
+ .and_then(|()| std::fs::rename(&tmp, path))
174
+ .map_err(|e| {
175
+ let _ = std::fs::remove_file(&tmp);
176
+ format!("writing {}: {e}", path.display())
177
+ })
178
+ }
179
+
180
+ /// Read and check a session file: a regular, private file naming a
181
+ /// loopback DevTools address and a profile flowproof made.
182
+ pub fn read(path: &Path) -> Result<KeptSession, String> {
183
+ let meta = std::fs::symlink_metadata(path)
184
+ .map_err(|e| format!("no kept browser at {}: {e}", path.display()))?;
185
+ if !meta.is_file() {
186
+ return Err(format!("{} is not a regular file", path.display()));
187
+ }
188
+ #[cfg(unix)]
189
+ {
190
+ use std::os::unix::fs::PermissionsExt;
191
+ if meta.permissions().mode() & 0o077 != 0 {
192
+ return Err(format!(
193
+ "{} must have private permissions (chmod 600)",
194
+ path.display()
195
+ ));
196
+ }
197
+ }
198
+ let text =
199
+ std::fs::read_to_string(path).map_err(|e| format!("reading {}: {e}", path.display()))?;
200
+ let session: KeptSession = serde_json::from_str(&text)
201
+ .map_err(|e| format!("{} is not a kept-browser session: {e}", path.display()))?;
202
+ if session.schema != SCHEMA {
203
+ return Err(format!(
204
+ "{} was written by a different flowproof (schema {})",
205
+ path.display(),
206
+ session.schema
207
+ ));
208
+ }
209
+ if session.port().is_none() {
210
+ return Err(format!(
211
+ "{} names a DevTools address that is not on 127.0.0.1",
212
+ path.display()
213
+ ));
214
+ }
215
+ if !is_kept_profile(&session.profile_dir) {
216
+ return Err(format!(
217
+ "{} names a profile directory flowproof did not make",
218
+ path.display()
219
+ ));
220
+ }
221
+ Ok(session)
222
+ }
223
+
224
+ /// Close the kept browser `path` records and remove its profile and the
225
+ /// file. Nothing there, or a browser already gone, is not an error.
226
+ pub fn close(path: &Path) -> Result<(), String> {
227
+ if std::fs::symlink_metadata(path).is_err() {
228
+ return Ok(());
229
+ }
230
+ let session = read(path)?;
231
+ discard(&session);
232
+ std::fs::remove_file(path).map_err(|e| format!("removing {}: {e}", path.display()))
233
+ }
234
+
235
+ /// Stop a kept browser and remove its profile.
236
+ pub fn discard(session: &KeptSession) {
237
+ // Only a browser still answering on its port is signalled: a pid alone
238
+ // could by now belong to anything.
239
+ if session.is_alive() {
240
+ stop(session.pid);
241
+ let started = Instant::now();
242
+ while session.is_alive() && started.elapsed() < Duration::from_secs(3) {
243
+ std::thread::sleep(Duration::from_millis(100));
244
+ }
245
+ if session.is_alive() {
246
+ kill(session.pid);
247
+ }
248
+ }
249
+ if is_kept_profile(&session.profile_dir) {
250
+ let _ = std::fs::remove_dir_all(&session.profile_dir);
251
+ }
252
+ }
253
+
254
+ fn is_kept_profile(dir: &Path) -> bool {
255
+ let temp = std::env::temp_dir();
256
+ dir.parent() == Some(temp.as_path())
257
+ && dir
258
+ .file_name()
259
+ .and_then(|n| n.to_str())
260
+ .is_some_and(|n| n.starts_with(PROFILE_PREFIX) && !n.contains(".."))
261
+ }
262
+
263
+ fn private_dir(dir: &Path) -> Result<(), String> {
264
+ let mut builder = std::fs::DirBuilder::new();
265
+ #[cfg(unix)]
266
+ {
267
+ use std::os::unix::fs::DirBuilderExt;
268
+ builder.mode(0o700);
269
+ }
270
+ builder
271
+ .create(dir)
272
+ .map_err(|e| format!("creating {}: {e}", dir.display()))
273
+ }
274
+
275
+ /// Ask the browser's whole process group to quit.
276
+ fn stop(pid: u32) {
277
+ #[cfg(unix)]
278
+ signal_group(pid, "TERM");
279
+ #[cfg(windows)]
280
+ kill(pid);
281
+ }
282
+
283
+ fn kill(pid: u32) {
284
+ #[cfg(unix)]
285
+ signal_group(pid, "KILL");
286
+ #[cfg(windows)]
287
+ {
288
+ let _ = std::process::Command::new("taskkill")
289
+ .args(["/T", "/F", "/PID", &pid.to_string()])
290
+ .stdout(std::process::Stdio::null())
291
+ .stderr(std::process::Stdio::null())
292
+ .status();
293
+ }
294
+ }
295
+
296
+ #[cfg(unix)]
297
+ fn signal_group(pid: u32, signal: &str) {
298
+ let _ = std::process::Command::new("kill")
299
+ .args(["-s", signal, "--", &format!("-{pid}")])
300
+ .stdout(std::process::Stdio::null())
301
+ .stderr(std::process::Stdio::null())
302
+ .status();
303
+ }
304
+
305
+ #[cfg(test)]
306
+ mod tests {
307
+ use super::*;
308
+
309
+ fn session() -> KeptSession {
310
+ KeptSession {
311
+ schema: SCHEMA,
312
+ pid: 4242,
313
+ ws_url: "ws://127.0.0.1:9333/devtools/browser/abc".into(),
314
+ profile_dir: std::env::temp_dir().join(format!("{PROFILE_PREFIX}test")),
315
+ created: 1_793_836_800,
316
+ }
317
+ }
318
+
319
+ fn temp_file(name: &str) -> PathBuf {
320
+ let dir = std::env::temp_dir().join(format!("flowproof-kept-tests-{name}"));
321
+ let _ = std::fs::remove_dir_all(&dir);
322
+ std::fs::create_dir_all(&dir).expect("temp dir");
323
+ dir.join("session.json")
324
+ }
325
+
326
+ #[test]
327
+ fn a_session_round_trips_through_its_file() {
328
+ let path = temp_file("round-trip");
329
+ write(&path, &session()).expect("writes");
330
+ assert_eq!(read(&path).expect("reads"), session());
331
+ #[cfg(unix)]
332
+ {
333
+ use std::os::unix::fs::PermissionsExt;
334
+ let mode = std::fs::metadata(&path)
335
+ .expect("metadata")
336
+ .permissions()
337
+ .mode();
338
+ assert_eq!(mode & 0o777, 0o600);
339
+ }
340
+ // Writing again replaces it.
341
+ write(&path, &session()).expect("rewrites");
342
+ }
343
+
344
+ #[cfg(unix)]
345
+ #[test]
346
+ fn a_session_file_others_can_read_is_refused() {
347
+ use std::os::unix::fs::PermissionsExt;
348
+ let path = temp_file("perms");
349
+ write(&path, &session()).expect("writes");
350
+ std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).expect("chmod");
351
+ assert!(read(&path).expect_err("refused").contains("chmod 600"));
352
+ }
353
+
354
+ #[test]
355
+ fn a_session_pointing_anywhere_but_loopback_or_its_own_profile_is_refused() {
356
+ let path = temp_file("tamper");
357
+ let mut remote = session();
358
+ remote.ws_url = "ws://10.0.0.5:9333/devtools/browser/abc".into();
359
+ write(&path, &remote).expect("writes");
360
+ assert!(read(&path).expect_err("refused").contains("127.0.0.1"));
361
+
362
+ let mut elsewhere = session();
363
+ elsewhere.profile_dir = PathBuf::from("/home/someone");
364
+ write(&path, &elsewhere).expect("writes");
365
+ assert!(read(&path).expect_err("refused").contains("profile"));
366
+
367
+ std::fs::write(&path, "{\"schema\":1,\"extra\":true}").expect("writes");
368
+ #[cfg(unix)]
369
+ {
370
+ use std::os::unix::fs::PermissionsExt;
371
+ std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).expect("chmod");
372
+ }
373
+ assert!(read(&path).is_err());
374
+ }
375
+
376
+ #[test]
377
+ fn closing_a_session_whose_browser_is_gone_just_cleans_up() {
378
+ let path = temp_file("gone");
379
+ let mut gone = session();
380
+ // Port 1 does not answer, so nothing is signalled.
381
+ gone.ws_url = "ws://127.0.0.1:1/devtools/browser/abc".into();
382
+ write(&path, &gone).expect("writes");
383
+ close(&path).expect("closes");
384
+ assert!(!path.exists());
385
+ // Idempotent.
386
+ close(&path).expect("nothing to close");
387
+ }
388
+ }
@@ -55,6 +55,9 @@ pub mod mcp_http;
55
55
  #[cfg(feature = "agent")]
56
56
  pub mod mcp_stdio;
57
57
 
58
+ #[cfg(feature = "web")]
59
+ pub mod kept_browser;
60
+
58
61
  #[cfg(feature = "sap-com")]
59
62
  pub mod sap_com;
60
63
 
@@ -1487,6 +1487,13 @@ pub struct WebAppDriver {
1487
1487
  /// request ID and must not leave a phantom in-flight request behind.
1488
1488
  network_inflight: Arc<NetworkActivity>,
1489
1489
  observed_collections: Vec<serde_json::Value>,
1490
+ /// A kept browser (`run --leave-open` / `--attach`): connected to, not
1491
+ /// launched, so it outlives this driver. See [`crate::kept_browser`].
1492
+ kept: bool,
1493
+ /// Carrying on in a kept browser an earlier run left open: its page is
1494
+ /// already where that run stopped, so `launch` neither navigates nor
1495
+ /// stages a session.
1496
+ attached: bool,
1490
1497
  }
1491
1498
 
1492
1499
  /// Shared state between the driver and the flow-wide dialog listener.
@@ -1671,6 +1678,12 @@ impl WebAppDriver {
1671
1678
  /// headed. Headed ownership prevents the keep-alive window from surviving
1672
1679
  /// after the visible flow window closes.
1673
1680
  pub fn new() -> Result<Self, AdapterError> {
1681
+ if let Some(path) = std::env::var_os(crate::kept_browser::ATTACH_ENV) {
1682
+ return Self::on_kept_browser(std::path::Path::new(&path), true);
1683
+ }
1684
+ if let Some(path) = std::env::var_os(crate::kept_browser::LEAVE_OPEN_ENV) {
1685
+ return Self::on_kept_browser(std::path::Path::new(&path), false);
1686
+ }
1674
1687
  if !should_share_browser(
1675
1688
  headed_requested(),
1676
1689
  std::env::var_os("FLOWPROOF_NO_SHARED_BROWSER").is_some(),
@@ -1689,6 +1702,8 @@ impl WebAppDriver {
1689
1702
  claimed_downloads: Default::default(),
1690
1703
  network_inflight: Default::default(),
1691
1704
  observed_collections: Vec::new(),
1705
+ kept: false,
1706
+ attached: false,
1692
1707
  });
1693
1708
  }
1694
1709
  let browser = shared_browser()?;
@@ -1710,9 +1725,87 @@ impl WebAppDriver {
1710
1725
  claimed_downloads: Default::default(),
1711
1726
  network_inflight: Default::default(),
1712
1727
  observed_collections: Vec::new(),
1728
+ kept: false,
1729
+ attached: false,
1713
1730
  })
1714
1731
  }
1715
1732
 
1733
+ /// A driver on a kept browser: a new one recorded in `path`
1734
+ /// (`run --leave-open`), or the one `path` already records (`--attach`).
1735
+ fn on_kept_browser(path: &std::path::Path, attach: bool) -> Result<Self, AdapterError> {
1736
+ use crate::kept_browser;
1737
+ let session = if attach {
1738
+ let session = kept_browser::read(path).map_err(AdapterError::Web)?;
1739
+ if !session.is_alive() {
1740
+ return Err(AdapterError::Web(
1741
+ "the browser an earlier run left open is gone (closed, or the computer \
1742
+ restarted); run the flow from the start"
1743
+ .into(),
1744
+ ));
1745
+ }
1746
+ session
1747
+ } else {
1748
+ if std::fs::symlink_metadata(path).is_ok() {
1749
+ return Err(AdapterError::Web(format!(
1750
+ "{} already records a kept browser; close it first with \
1751
+ `flowproof session close {}`",
1752
+ path.display(),
1753
+ path.display()
1754
+ )));
1755
+ }
1756
+ let session = kept_browser::spawn(&find_browser()?).map_err(AdapterError::Web)?;
1757
+ if let Err(e) = kept_browser::write(path, &session) {
1758
+ kept_browser::discard(&session);
1759
+ return Err(AdapterError::Web(e));
1760
+ }
1761
+ session
1762
+ };
1763
+ let browser =
1764
+ Browser::connect_with_timeout(session.ws_url.clone(), BROWSER_IDLE_TIMEOUT)
1765
+ .map_err(|e| AdapterError::Web(format!("connecting to the kept browser: {e}")))?;
1766
+ Ok(Self {
1767
+ browser,
1768
+ context_id: None,
1769
+ tab: None,
1770
+ staged_session: None,
1771
+ seed_script: None,
1772
+ console: Default::default(),
1773
+ staged_mocks: Vec::new(),
1774
+ staged_browser: None,
1775
+ dialogs: Default::default(),
1776
+ downloads_dir: None,
1777
+ claimed_downloads: Default::default(),
1778
+ network_inflight: Default::default(),
1779
+ observed_collections: Vec::new(),
1780
+ kept: true,
1781
+ attached: attach,
1782
+ })
1783
+ }
1784
+
1785
+ /// The page an attached run carries on in: the one the earlier run left.
1786
+ fn kept_page(&self) -> anyhow::Result<Arc<Tab>> {
1787
+ // A fresh connection learns the browser's existing pages from its
1788
+ // target discovery events, which arrive just after connecting.
1789
+ let deadline = Instant::now() + Duration::from_secs(5);
1790
+ loop {
1791
+ let page = self
1792
+ .browser
1793
+ .get_tabs()
1794
+ .lock()
1795
+ .unwrap_or_else(|e| e.into_inner())
1796
+ .iter()
1797
+ .find(|tab| !tab.get_url().starts_with("devtools://"))
1798
+ .cloned();
1799
+ if let Some(page) = page {
1800
+ return Ok(page);
1801
+ }
1802
+ if Instant::now() > deadline {
1803
+ anyhow::bail!("the kept browser has no page open");
1804
+ }
1805
+ std::thread::sleep(Duration::from_millis(100));
1806
+ }
1807
+ }
1808
+
1716
1809
  /// Apply staged session state to a fresh tab BEFORE navigation: cookies
1717
1810
  /// via CDP, localStorage via an on-new-document script (Playwright's
1718
1811
  /// addInitScript pattern — it runs before any page script on every
@@ -3217,6 +3310,13 @@ impl AppDriver for WebAppDriver {
3217
3310
  // isolated), paying its cold start instead of sharing.
3218
3311
  if let Some(config) = &staged_browser {
3219
3312
  if !config.args.is_empty() {
3313
+ if self.kept {
3314
+ return Err(DriverError::Browser(
3315
+ "a flow with `browser.args` cannot keep its browser open: the flags \
3316
+ only apply to a browser flowproof launches itself"
3317
+ .into(),
3318
+ ));
3319
+ }
3220
3320
  self.browser = launch_browser(&config.args)
3221
3321
  .map_err(|e| DriverError::Browser(e.to_string()))?;
3222
3322
  self.context_id = None;
@@ -3239,6 +3339,7 @@ impl AppDriver for WebAppDriver {
3239
3339
  for_tab: None,
3240
3340
  hidden: None,
3241
3341
  }),
3342
+ None if self.attached => self.kept_page(),
3242
3343
  None => private_flow_tab(&self.browser),
3243
3344
  }
3244
3345
  .map_err(|e| web_err("opening tab", e))?;
@@ -3317,7 +3418,8 @@ impl AppDriver for WebAppDriver {
3317
3418
  .map_err(|e| web_err("pinning randomness", e))?;
3318
3419
  }
3319
3420
  }
3320
- if let Some(session) = self.staged_session.take() {
3421
+ // An attached browser is already signed in to wherever it was left.
3422
+ if let Some(session) = self.staged_session.take().filter(|_| !self.attached) {
3321
3423
  self.seed_script = Self::apply_session(&tab, &session, command)?;
3322
3424
  }
3323
3425
  // Network mocks: install interception BEFORE navigation so even the
@@ -3566,10 +3668,13 @@ impl AppDriver for WebAppDriver {
3566
3668
  .into_iter()
3567
3669
  .collect();
3568
3670
  self.downloads_dir = Some(downloads_dir);
3569
- tab.navigate_to(command)
3570
- .map_err(|e| web_err(&format!("navigating to {command}"), e))?;
3571
- tab.wait_until_navigated()
3572
- .map_err(|e| web_err("waiting for page load", e))?;
3671
+ // Attached: carry on from the page the earlier run left, not `url:`.
3672
+ if !self.attached {
3673
+ tab.navigate_to(command)
3674
+ .map_err(|e| web_err(&format!("navigating to {command}"), e))?;
3675
+ tab.wait_until_navigated()
3676
+ .map_err(|e| web_err("waiting for page load", e))?;
3677
+ }
3573
3678
  self.tab = Some(tab);
3574
3679
  // The first document has now run the seed. Drop the script so no
3575
3680
  // later navigation re-seeds over what the flow has since changed.
@@ -16,6 +16,7 @@ pub mod llm;
16
16
  pub mod recorder;
17
17
  pub mod repair;
18
18
  pub mod rules;
19
+ pub mod selection;
19
20
  pub mod spec;
20
21
 
21
22
  pub use clarify::{Clarification, ClarifyStage};