flowproof 0.23.8__tar.gz → 0.23.9__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 (169) hide show
  1. {flowproof-0.23.8 → flowproof-0.23.9}/Cargo.lock +8 -7
  2. {flowproof-0.23.8 → flowproof-0.23.9}/Cargo.toml +1 -1
  3. {flowproof-0.23.8 → flowproof-0.23.9}/PKG-INFO +3 -3
  4. {flowproof-0.23.8 → flowproof-0.23.9}/README.md +1 -1
  5. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/web.rs +9 -4
  6. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/password_recording_e2e.rs +36 -7
  7. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/recorder.rs +46 -48
  8. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/spec.rs +84 -22
  9. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/Cargo.toml +1 -0
  10. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/agent_flow.rs +85 -0
  11. flowproof-0.23.9/crates/flowproof-cli/src/agent_surface.rs +108 -0
  12. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/lib.rs +78 -9
  13. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/sap_pipeline.rs +114 -0
  14. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/app.rs +40 -0
  15. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/surface.rs +34 -1
  16. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-replay/src/lib.rs +7 -0
  17. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/schema/trace-v1.schema.json +35 -3
  18. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/format.rs +11 -0
  19. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/tests/schema_conformance.rs +17 -0
  20. {flowproof-0.23.8 → flowproof-0.23.9}/flowproof/__init__.py +1 -1
  21. {flowproof-0.23.8 → flowproof-0.23.9}/pyproject.toml +2 -2
  22. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/Cargo.toml +0 -0
  23. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
  24. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
  25. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress.rs +0 -0
  26. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
  27. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
  28. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
  29. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/env_block.rs +0 -0
  30. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
  31. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
  32. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
  33. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
  34. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
  35. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
  36. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
  37. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
  38. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/fiori.rs +0 -0
  39. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
  40. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/lib.rs +0 -0
  41. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
  42. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
  43. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
  44. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/sap_com.rs +0 -0
  45. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/src/vision.rs +0 -0
  46. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/a11y_capture.rs +0 -0
  47. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
  48. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
  49. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/grid_scene_e2e.rs +0 -0
  50. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/list_scene_e2e.rs +0 -0
  51. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/network_idle_e2e.rs +0 -0
  52. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/shared_browser_shutdown_e2e.rs +0 -0
  53. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/side_effect_http_e2e.rs +0 -0
  54. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/static_area_e2e.rs +0 -0
  55. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/visibility_detach_e2e.rs +0 -0
  56. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +0 -0
  57. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/Cargo.toml +0 -0
  58. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/agent_steps.rs +0 -0
  59. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/author.rs +0 -0
  60. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/clarify.rs +0 -0
  61. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/doc_author.rs +0 -0
  62. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/doc_formats/hp_alm.rs +0 -0
  63. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/doc_formats.rs +0 -0
  64. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/draft_assembly.rs +0 -0
  65. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/heal.rs +0 -0
  66. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/lib.rs +0 -0
  67. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/llm.rs +0 -0
  68. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/repair.rs +0 -0
  69. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/src/rules.rs +0 -0
  70. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
  71. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/skills/flowproof-config/SKILL.md +0 -0
  72. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/capture.rs +0 -0
  73. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/config.rs +0 -0
  74. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/doctor.rs +0 -0
  75. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/main.rs +0 -0
  76. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/suite.rs +0 -0
  77. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/suite_checkpoint.rs +0 -0
  78. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/suite_inputs.rs +0 -0
  79. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/suite_journal.rs +0 -0
  80. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/src/update_check.rs +0 -0
  81. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/a11y_selector_e2e.rs +0 -0
  82. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/agent_conversation_anthropic_e2e.rs +0 -0
  83. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/agent_conversation_streaming_e2e.rs +0 -0
  84. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
  85. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
  86. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
  87. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
  88. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  89. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
  90. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/config_cli_e2e.rs +0 -0
  91. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/config_seed_e2e.rs +0 -0
  92. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/config_skill_e2e.rs +0 -0
  93. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
  94. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/doctor_ai_e2e.rs +0 -0
  95. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
  96. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/documented_flags.rs +0 -0
  97. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
  98. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
  99. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
  100. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
  101. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
  102. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
  103. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
  104. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
  105. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
  106. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
  107. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/falsifiability_side_effect.rs +0 -0
  108. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/fiori_field_commit_e2e.rs +0 -0
  109. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
  110. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
  111. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
  112. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
  113. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
  114. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
  115. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  116. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
  117. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
  118. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
  119. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
  120. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
  121. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
  122. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/sigterm_shared_browser_e2e.rs +0 -0
  123. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
  124. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/suite_checkpoint.rs +0 -0
  125. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
  126. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
  127. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
  128. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/suite_policy.rs +0 -0
  129. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
  130. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
  131. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  132. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
  133. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/Cargo.toml +0 -0
  134. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/backend.rs +0 -0
  135. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/gdi.rs +0 -0
  136. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/lib.rs +0 -0
  137. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/mock.rs +0 -0
  138. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/oob.rs +0 -0
  139. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/recording.rs +0 -0
  140. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/redact.rs +0 -0
  141. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/visual.rs +0 -0
  142. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-driver/src/window.rs +0 -0
  143. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-python/Cargo.toml +0 -0
  144. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-python/src/lib.rs +0 -0
  145. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-replay/Cargo.toml +0 -0
  146. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-replay/src/report.rs +0 -0
  147. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-replay/src/runrecord.rs +0 -0
  148. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
  149. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/Cargo.toml +0 -0
  150. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/schema/cassette-v1.schema.json +0 -0
  151. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/schema/side-effect-v1.schema.json +0 -0
  152. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/captures.rs +0 -0
  153. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/cassette.rs +0 -0
  154. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
  155. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/egress.rs +0 -0
  156. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/lib.rs +0 -0
  157. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/secret.rs +0 -0
  158. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/secret_scan.rs +0 -0
  159. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/side_effect.rs +0 -0
  160. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/substitution.rs +0 -0
  161. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/src/toolcalls.rs +0 -0
  162. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/tests/cassette_conformance.rs +0 -0
  163. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/tests/fixtures/multi.trace.jsonl +0 -0
  164. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
  165. {flowproof-0.23.8 → flowproof-0.23.9}/crates/flowproof-trace/tests/side_effect_conformance.rs +0 -0
  166. {flowproof-0.23.8 → flowproof-0.23.9}/flowproof/cli.py +0 -0
  167. {flowproof-0.23.8 → flowproof-0.23.9}/flowproof/flow.py +0 -0
  168. {flowproof-0.23.8 → flowproof-0.23.9}/flowproof/mcp_server.py +0 -0
  169. {flowproof-0.23.8 → flowproof-0.23.9}/flowproof/py.typed +0 -0
@@ -930,7 +930,7 @@ dependencies = [
930
930
 
931
931
  [[package]]
932
932
  name = "flowproof-adapters"
933
- version = "0.23.8"
933
+ version = "0.23.9"
934
934
  dependencies = [
935
935
  "anyhow",
936
936
  "flowproof-driver",
@@ -949,7 +949,7 @@ dependencies = [
949
949
 
950
950
  [[package]]
951
951
  name = "flowproof-agent"
952
- version = "0.23.8"
952
+ version = "0.23.9"
953
953
  dependencies = [
954
954
  "chrono",
955
955
  "flowproof-driver",
@@ -967,7 +967,7 @@ dependencies = [
967
967
 
968
968
  [[package]]
969
969
  name = "flowproof-cli"
970
- version = "0.23.8"
970
+ version = "0.23.9"
971
971
  dependencies = [
972
972
  "ab_glyph",
973
973
  "chrono",
@@ -988,11 +988,12 @@ dependencies = [
988
988
  "sha2 0.10.9",
989
989
  "tiny_http",
990
990
  "ureq",
991
+ "uuid",
991
992
  ]
992
993
 
993
994
  [[package]]
994
995
  name = "flowproof-driver"
995
- version = "0.23.8"
996
+ version = "0.23.9"
996
997
  dependencies = [
997
998
  "calamine",
998
999
  "flowproof-trace",
@@ -1008,7 +1009,7 @@ dependencies = [
1008
1009
 
1009
1010
  [[package]]
1010
1011
  name = "flowproof-python"
1011
- version = "0.23.8"
1012
+ version = "0.23.9"
1012
1013
  dependencies = [
1013
1014
  "flowproof-agent",
1014
1015
  "flowproof-cli",
@@ -1020,7 +1021,7 @@ dependencies = [
1020
1021
 
1021
1022
  [[package]]
1022
1023
  name = "flowproof-replay"
1023
- version = "0.23.8"
1024
+ version = "0.23.9"
1024
1025
  dependencies = [
1025
1026
  "chrono",
1026
1027
  "flowproof-agent",
@@ -1034,7 +1035,7 @@ dependencies = [
1034
1035
 
1035
1036
  [[package]]
1036
1037
  name = "flowproof-trace"
1037
- version = "0.23.8"
1038
+ version = "0.23.9"
1038
1039
  dependencies = [
1039
1040
  "jsonschema",
1040
1041
  "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.8"
25
+ version = "0.23.9"
26
26
  edition = "2021"
27
27
  license = "Apache-2.0"
28
28
  repository = "https://github.com/automators-com/flowproof"
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowproof
3
- Version: 0.23.8
4
- Classifier: Development Status :: 3 - Alpha
3
+ Version: 0.23.9
4
+ Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
7
7
  Classifier: Programming Language :: Rust
@@ -61,7 +61,7 @@ The wheel bundles the Rust engine; no separate install. Targets: `web`
61
61
  (SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
62
62
  (no UI — HTTP/SQL assertion flows, any OS).
63
63
 
64
- **Status: alpha, in active development.** Record→replay,
64
+ **Status: beta, in active development.** Record→replay,
65
65
  model-grounded authoring, healing with reviewable diffs, suites, run
66
66
  recordings, and the MCP server all work and are tested in CI.
67
67
 
@@ -43,7 +43,7 @@ The wheel bundles the Rust engine; no separate install. Targets: `web`
43
43
  (SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
44
44
  (no UI — HTTP/SQL assertion flows, any OS).
45
45
 
46
- **Status: alpha, in active development.** Record→replay,
46
+ **Status: beta, in active development.** Record→replay,
47
47
  model-grounded authoring, healing with reviewable diffs, suites, run
48
48
  recordings, and the MCP server all work and are tested in CI.
49
49
 
@@ -12,7 +12,7 @@ use std::time::Duration;
12
12
  use flowproof_driver::{
13
13
  AppDriver, DriverError, KeyMod, PixelRect, ScrollTo, UiaSelector, WebSession,
14
14
  };
15
- use headless_chrome::browser::tab::{ModifierKey, Tab};
15
+ use headless_chrome::browser::tab::{ModifierKey, NoElementFound, Tab};
16
16
  use headless_chrome::protocol::cdp::Target::CreateTarget;
17
17
  use headless_chrome::protocol::cdp::{Accessibility, Emulation, Input, Network, Page, DOM};
18
18
  use headless_chrome::types::Bounds;
@@ -5522,9 +5522,14 @@ impl AppDriver for WebAppDriver {
5522
5522
 
5523
5523
  fn password_rects(&mut self) -> Result<Vec<PixelRect>, DriverError> {
5524
5524
  let tab = self.tab()?;
5525
- let fields = tab
5526
- .find_elements("input[type=password]")
5527
- .map_err(|e| web_err("finding password fields", e))?;
5525
+ // A page with no password input is the ordinary case and has nothing
5526
+ // to mask; headless_chrome's find_elements reports an empty match as
5527
+ // an error, so only genuine CDP failures may abort the resolution.
5528
+ let fields = match tab.find_elements("input[type=password]") {
5529
+ Ok(fields) => fields,
5530
+ Err(e) if e.downcast_ref::<NoElementFound>().is_some() => Vec::new(),
5531
+ Err(e) => return Err(web_err("finding password fields", e)),
5532
+ };
5528
5533
  let mut rects = Vec::new();
5529
5534
  for field in fields {
5530
5535
  // Hidden wizard pages still contain password inputs, but have no
@@ -1,12 +1,8 @@
1
1
  //! Hidden password fields must not discard otherwise safe screenshots.
2
2
  use flowproof_driver::{AppDriver, UiaSelector};
3
- #[test]
4
- fn hidden_password_inputs_do_not_drop_frames_and_visible_passwords_stay_masked() {
5
- if std::env::var("FLOWPROOF_E2E").as_deref() != Ok("1") {
6
- return;
7
- }
8
- let _guard = flowproof_adapters::SharedBrowserGuard::new();
9
- let html = r#"<html><body><input type='password' value='visible-secret' style='width:200px;height:40px'><section id='later' style='display:none'><input type='password' value='hidden-secret'></section><button id='reveal' onclick="document.getElementById('later').style.display='block'">Reveal</button></body></html>"#;
3
+
4
+ /// Serve a static page on its own loopback port and return the port.
5
+ fn serve(html: &'static str) -> u16 {
10
6
  let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener");
11
7
  let port = listener.local_addr().expect("fixture address").port();
12
8
  std::thread::spawn(move || {
@@ -17,6 +13,10 @@ fn hidden_password_inputs_do_not_drop_frames_and_visible_passwords_stay_masked()
17
13
  let _ = write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", html.len(), html);
18
14
  }
19
15
  });
16
+ port
17
+ }
18
+
19
+ fn launch(port: u16) -> flowproof_adapters::WebAppDriver {
20
20
  let mut driver = flowproof_adapters::WebAppDriver::new().expect("Chrome launches");
21
21
  driver
22
22
  .launch(
@@ -25,6 +25,18 @@ fn hidden_password_inputs_do_not_drop_frames_and_visible_passwords_stay_masked()
25
25
  std::time::Duration::from_secs(30),
26
26
  )
27
27
  .expect("fixture loads");
28
+ driver
29
+ }
30
+
31
+ #[test]
32
+ fn hidden_password_inputs_do_not_drop_frames_and_visible_passwords_stay_masked() {
33
+ if std::env::var("FLOWPROOF_E2E").as_deref() != Ok("1") {
34
+ return;
35
+ }
36
+ let _guard = flowproof_adapters::SharedBrowserGuard::new();
37
+ let html = r#"<html><body><input type='password' value='visible-secret' style='width:200px;height:40px'><section id='later' style='display:none'><input type='password' value='hidden-secret'></section><button id='reveal' onclick="document.getElementById('later').style.display='block'">Reveal</button></body></html>"#;
38
+ let port = serve(html);
39
+ let mut driver = launch(port);
28
40
 
29
41
  let rects = flowproof_driver::redact::resolve_rects(&mut driver, &[])
30
42
  .expect("hidden inputs are safe to skip");
@@ -51,3 +63,20 @@ fn hidden_password_inputs_do_not_drop_frames_and_visible_passwords_stay_masked()
51
63
  2
52
64
  );
53
65
  }
66
+
67
+ #[test]
68
+ fn pages_without_password_inputs_resolve_to_no_masks() {
69
+ if std::env::var("FLOWPROOF_E2E").as_deref() != Ok("1") {
70
+ return;
71
+ }
72
+ let _guard = flowproof_adapters::SharedBrowserGuard::new();
73
+ // No password input anywhere: the common case must resolve to no masks,
74
+ // not fail the resolve (which would drop every recorded frame).
75
+ let port = serve(r#"<html><body><div>nothing sensitive</div></body></html>"#);
76
+ let mut driver = launch(port);
77
+ assert_eq!(
78
+ flowproof_driver::redact::resolve_rects(&mut driver, &[])
79
+ .expect("password-less pages resolve their masks"),
80
+ Vec::new()
81
+ );
82
+ }
@@ -112,19 +112,6 @@ pub enum RecordError {
112
112
  MissingUrl,
113
113
  #[error("app 'vision' requires a `window:` field in the spec (title of the window to drive)")]
114
114
  MissingWindow,
115
- /// A surface named an identity nothing can act as yet. `login:` parses
116
- /// and validates on a surface entry — it is the shape a same-system
117
- /// two-user case wants — but no surface stages its credentials, so
118
- /// recording it would drive whatever session was already open while the
119
- /// spec says otherwise. That is the exact confusion `login:` exists to
120
- /// prevent, so it is refused by name rather than run as somebody else.
121
- #[error(
122
- "surface `{0}` declares `login:`, but no surface logs itself in yet — recording it \
123
- would drive whatever SAP session is already open, as whoever opened it. Until the \
124
- engine lands, put each identity on its own single-surface `app: sap` flow with a \
125
- `login:` block and chain the case with `exports:`"
126
- )]
127
- SurfaceLogin(String),
128
115
  #[error("element for step '{intent}' not found: [{selector}]")]
129
116
  ElementNotFound { intent: String, selector: String },
130
117
  /// `expected` and `actual` are complete phrases, not bare values: the
@@ -417,6 +404,8 @@ fn selectors_for(app: &str, target: &Target, label: Option<&str>) -> Vec<Selecto
417
404
  vec![Selector {
418
405
  tier: SelectorTier::TextAnchor,
419
406
  provenance: match app {
407
+ "api" => flowproof_trace::format::Adapter::Api,
408
+ "agent" => flowproof_trace::format::Adapter::Agent,
420
409
  "web" => flowproof_trace::format::Adapter::Web,
421
410
  "sap" => flowproof_trace::format::Adapter::SapCom,
422
411
  "vision" => flowproof_trace::format::Adapter::Vision,
@@ -1262,12 +1251,7 @@ fn surface_app_info(surface: &crate::spec::SurfaceSpec) -> AppInfo {
1262
1251
  // A web surface's launch/emulation shape travels with the surface,
1263
1252
  // so record and every replay stage it before THIS surface launches.
1264
1253
  browser: surface.browser.clone(),
1265
- // Deliberately not `surface.login`: no surface logs itself in yet,
1266
- // so a recording never acted as that user, and a header that named
1267
- // one would be claiming evidence the run does not have. Record
1268
- // refuses such a flow by name (`RecordError::SurfaceLogin`), so
1269
- // this arm is only ever reached by a surface with no `login:`.
1270
- login_user: None,
1254
+ login_user: surface.login.as_ref().map(|login| login.user.clone()),
1271
1255
  version: None,
1272
1256
  }
1273
1257
  }
@@ -1282,14 +1266,11 @@ pub fn surface_targets(
1282
1266
  spec.apps
1283
1267
  .iter()
1284
1268
  .map(|(name, surface)| {
1285
- // The one surface shape that parses but cannot be driven: an
1286
- // identity nothing stages. Refused here, at the launch seam
1287
- // both `record` and `heal` pass through, so no trace is ever
1288
- // written claiming a user the run never was.
1289
- if surface.login.is_some() {
1290
- return Err(RecordError::SurfaceLogin(name.clone()));
1291
- }
1292
1269
  let target = match surface.app.id() {
1270
+ "api" | "agent" => flowproof_driver::AppTarget {
1271
+ command: String::new(),
1272
+ window_name: String::new(),
1273
+ },
1293
1274
  "web" => {
1294
1275
  let url = surface.url.as_deref().ok_or(RecordError::MissingUrl)?;
1295
1276
  let url = flowproof_trace::secret::resolve_refs(url)?;
@@ -2747,6 +2728,11 @@ pub fn record_with_reuse_and_options<D: AppDriver, C: ModelClient>(
2747
2728
  if let Some(login) = &spec.login {
2748
2729
  driver.stage_credentials(login.resolved()?)?;
2749
2730
  }
2731
+ for (name, surface) in &spec.apps {
2732
+ if let Some(login) = &surface.login {
2733
+ driver.stage_surface_credentials(name, login.resolved()?)?;
2734
+ }
2735
+ }
2750
2736
  if !spec.mock.is_empty() {
2751
2737
  driver.stage_mocks(spec.mock.iter().map(web_mock_from_rule).collect())?;
2752
2738
  }
@@ -2923,6 +2909,28 @@ pub fn record_with_reuse_and_options<D: AppDriver, C: ModelClient>(
2923
2909
  }
2924
2910
  }
2925
2911
  current_surface = Some(block.surface.clone());
2912
+ if spec.apps[&block.surface].app.id() == "agent" {
2913
+ let raw_steps = serde_json::to_value(&block.steps).map_err(|e| {
2914
+ RecordError::Driver(flowproof_driver::DriverError::Uia(e.to_string()))
2915
+ })?;
2916
+ let cassette = driver.agent_segment(&raw_steps, None, &captures)?;
2917
+ steps.push(flowproof_trace::Step {
2918
+ id: format!("s{:04}", steps.len() + 1),
2919
+ intent: format!("Run agent {}", block.surface),
2920
+ surface: current_surface.clone(),
2921
+ action: Action::AgentRun(flowproof_trace::format::AgentRunParams {
2922
+ steps: raw_steps,
2923
+ cassette,
2924
+ }),
2925
+ selectors: Vec::new(),
2926
+ sync: flowproof_trace::format::Sync {
2927
+ pre: Vec::new(),
2928
+ post: Vec::new(),
2929
+ },
2930
+ artifacts: Default::default(),
2931
+ });
2932
+ continue;
2933
+ }
2926
2934
  for (i, inner) in block.steps.iter().enumerate() {
2927
2935
  queue.insert(i, (inner.clone(), 0));
2928
2936
  }
@@ -6698,30 +6706,20 @@ steps:
6698
6706
  );
6699
6707
  }
6700
6708
 
6701
- /// `login:` on a surface parses and validates — it is the shape a
6702
- /// same-system two-user case wants — but nothing stages a surface's
6703
- /// credentials yet. Launching anyway would drive whatever session was
6704
- /// already open while the spec names somebody else, so the launch seam
6705
- /// refuses by name and no trace is written claiming that identity.
6706
6709
  #[test]
6707
- fn a_surface_that_names_a_login_refuses_to_launch() {
6708
- let spec = FlowSpec::parse(
6709
- "name: Two users\napps:\n \
6710
- clerk: {app: sap, connection: TS3, login: {user: obeva, password: pw}}\n \
6711
- portal: {app: web, url: 'http://x'}\nsteps:\n - in: clerk\n \
6712
- steps: [Go to /nVA01]\n - in: portal\n steps: [Click \"Search\"]\n",
6713
- )
6714
- .expect("spec parses — the refusal is at launch, not at parse");
6715
- let err = surface_targets(&spec).expect_err("a surface login cannot launch");
6716
- let message = err.to_string();
6717
- assert!(
6718
- message.contains("clerk") && message.contains("logs itself in"),
6719
- "the refusal names the surface and the gap: {message}"
6720
- );
6721
- assert!(
6722
- message.contains("exports:"),
6723
- "and names the shape that does work today: {message}"
6710
+ fn surface_login_targets_and_header_keep_identity_without_password() {
6711
+ let spec = FlowSpec::parse("name: users\napps:\n clerk: {app: sap, connection: TS3, login: {user: clerk, password: secret}}\nsteps:\n - in: clerk\n steps: [Go to /nVA01]\n").expect("valid login test fixture");
6712
+ assert_eq!(
6713
+ surface_targets(&spec).expect("valid login test fixture")[0]
6714
+ .1
6715
+ .command,
6716
+ "TS3"
6724
6717
  );
6718
+ let info = surface_app_info(&spec.apps["clerk"]);
6719
+ assert_eq!(info.login_user.as_deref(), Some("clerk"));
6720
+ assert!(!serde_json::to_string(&info)
6721
+ .expect("valid login test fixture")
6722
+ .contains("secret"));
6725
6723
  }
6726
6724
 
6727
6725
  /// The counterpart: surfaces without `login:` still launch, so the
@@ -119,10 +119,7 @@ pub struct SurfaceSpec {
119
119
  /// For sap surfaces: who this surface logs in as, exactly as on a
120
120
  /// single-surface flow. This is the shape a same-system two-user case
121
121
  /// wants — one flow, `clerk` and `approver` surfaces, one identity each.
122
- /// It parses and validates today, but the multi-surface driver does not
123
- /// log a surface in yet (docs/multi-surface.md), so the same case is a
124
- /// suite of single-surface flows chained with `exports:`, one `login:`
125
- /// each, until it does.
122
+ /// Staged separately for each surface before its first activation.
126
123
  #[serde(default, skip_serializing_if = "Option::is_none")]
127
124
  pub login: Option<LoginSpec>,
128
125
  /// For web surfaces: launch/emulation config (viewport, user-agent,
@@ -138,6 +135,14 @@ pub struct SurfaceSpec {
138
135
  /// sized with `browser: viewport`, not a window.
139
136
  #[serde(default, skip_serializing_if = "Option::is_none")]
140
137
  pub window: Option<WindowSpec>,
138
+ #[serde(default, skip_serializing_if = "Option::is_none")]
139
+ pub agent: Option<AgentSpec>,
140
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
141
+ pub tools: Vec<ToolMock>,
142
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
143
+ pub mcp: Vec<McpServerSpec>,
144
+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
145
+ pub strict: bool,
141
146
  }
142
147
 
143
148
  /// Who a SAP flow logs in as. Every value may carry `${VAR}` references,
@@ -1090,6 +1095,29 @@ impl FlowSpec {
1090
1095
  /// it goes. (`assert_screenshot` needs no rule here: the recorder
1091
1096
  /// qualifies each baseline with its surface, so a `gui` baseline can
1092
1097
  /// never be compared against a `portal` frame.)
1098
+ /// Agent execution uses the existing single-agent contract per block.
1099
+ pub fn surface_flow(&self, name: &str, steps: Vec<SpecStep>) -> Result<Self, SpecError> {
1100
+ let surface = self
1101
+ .apps
1102
+ .get(name)
1103
+ .ok_or_else(|| SpecError::Surfaces(format!("Unknown surface {name}")))?;
1104
+ let mut flow = self.clone();
1105
+ flow.apps.clear();
1106
+ flow.app = surface.app.clone();
1107
+ flow.url = surface.url.clone();
1108
+ flow.connection = surface.connection.clone();
1109
+ flow.browser = surface.browser.clone();
1110
+ flow.window = surface.window.clone();
1111
+ flow.login = surface.login.clone();
1112
+ flow.agent = surface.agent.clone();
1113
+ flow.tools = surface.tools.clone();
1114
+ flow.mcp = surface.mcp.clone();
1115
+ flow.strict = surface.strict;
1116
+ flow.steps = steps;
1117
+ flow.exports.clear();
1118
+ Ok(flow)
1119
+ }
1120
+
1093
1121
  fn validate_surfaces(&self) -> Result<(), SpecError> {
1094
1122
  let bad = |m: String| Err(SpecError::Surfaces(m));
1095
1123
  let multi = !self.apps.is_empty();
@@ -1130,21 +1158,19 @@ impl FlowSpec {
1130
1158
  return bad(format!("surface `{name}` names no app"));
1131
1159
  }
1132
1160
  let id = surface.app.id();
1133
- match id {
1134
- "agent" => {
1135
- return bad(format!(
1136
- "surface `{name}`: `agent` is a model boundary, not a UI surface — \
1137
- chain an `app: agent` flow in the suite instead (docs/multi-surface.md \
1138
- defers the in-flow agent seam deliberately)"
1139
- ))
1140
- }
1141
- "api" => {
1142
- return bad(format!(
1143
- "surface `{name}`: an `api` surface has nothing to drive — \
1144
- `assert_api` runs fine inside any `in:` block"
1145
- ))
1146
- }
1147
- _ => {}
1161
+ self.surface_flow(
1162
+ name,
1163
+ if id == "agent" {
1164
+ vec![SpecStep::Prompt {
1165
+ prompt: "validate configuration".into(),
1166
+ }]
1167
+ } else {
1168
+ Vec::new()
1169
+ },
1170
+ )?
1171
+ .validate_agent()?;
1172
+ if matches!(id, "agent" | "api") && surface.window.is_some() {
1173
+ return bad(format!("surface `{name}` has no window"));
1148
1174
  }
1149
1175
  if id == "web" && surface.url.is_none() {
1150
1176
  return bad(format!("web surface `{name}` needs `url:`"));
@@ -1227,6 +1253,39 @@ impl FlowSpec {
1227
1253
  declared()
1228
1254
  ));
1229
1255
  }
1256
+ self.surface_flow(&block.surface, block.steps.clone())?
1257
+ .validate_agent()?;
1258
+ if self.apps[&block.surface].app.id() == "agent"
1259
+ && block.steps.iter().any(|s| {
1260
+ !matches!(
1261
+ s,
1262
+ SpecStep::Prompt { .. }
1263
+ | SpecStep::Conversation { .. }
1264
+ | SpecStep::Assert { .. }
1265
+ | SpecStep::AssertToolCall { .. }
1266
+ | SpecStep::AssertNoToolCall { .. }
1267
+ | SpecStep::AssertNoEgress
1268
+ | SpecStep::AssertNoSideEffect { .. }
1269
+ | SpecStep::AssertNoSecretLeak { .. }
1270
+ )
1271
+ })
1272
+ {
1273
+ return bad(format!(
1274
+ "Agent surface {} accepts prompts, scripted conversations and agent assertions",
1275
+ block.surface
1276
+ ));
1277
+ }
1278
+ if self.apps[&block.surface].app.id() == "api"
1279
+ && block
1280
+ .steps
1281
+ .iter()
1282
+ .any(|s| !matches!(s, SpecStep::AssertApi { .. } | SpecStep::AssertSql { .. }))
1283
+ {
1284
+ return bad(format!(
1285
+ "API surface {} accepts assert_api and assert_sql steps",
1286
+ block.surface
1287
+ ));
1288
+ }
1230
1289
  if block.steps.is_empty() {
1231
1290
  return bad(format!("`in: {}` has no steps", block.surface));
1232
1291
  }
@@ -1280,7 +1339,7 @@ impl FlowSpec {
1280
1339
  if self.agent.is_some() || !self.tools.is_empty() || !self.mcp.is_empty() || self.strict {
1281
1340
  return bad(
1282
1341
  "`agent:`, `tools:`, `mcp:` and `strict:` belong to an `app: agent` flow \
1283
- — an agent is a model boundary, not a surface"
1342
+ — put them inside its `apps:` entry"
1284
1343
  .into(),
1285
1344
  );
1286
1345
  }
@@ -4360,8 +4419,11 @@ steps:
4360
4419
  /// Each refused surface kind names its reason and its alternative —
4361
4420
  /// a vocabulary that parses but cannot run would be a trap.
4362
4421
  #[test]
4363
- fn non_ui_surface_kinds_are_refused_by_name() {
4364
- for (kind, expect) in [("agent", "model boundary"), ("api", "nothing to drive")] {
4422
+ fn invalid_non_ui_surface_configuration_is_refused_by_name() {
4423
+ for (kind, expect) in [
4424
+ ("agent", "needs an `agent:` block"),
4425
+ ("api", "accepts assert_api"),
4426
+ ] {
4365
4427
  let err = spec(&format!(
4366
4428
  "name: n\napps:\n s: {{app: {kind}}}\nsteps:\n - in: s\n steps: [Press Enter]\n"
4367
4429
  ))
@@ -16,6 +16,7 @@ name = "flowproof"
16
16
  path = "src/main.rs"
17
17
 
18
18
  [dependencies]
19
+ uuid = { workspace = true, features = ["v4"] }
19
20
  chrono = { workspace = true }
20
21
  clap = { workspace = true }
21
22
  # Cross-platform Ctrl-C handling for the long-lived `capture` listener, so a
@@ -4201,6 +4201,91 @@ mod tests {
4201
4201
  assert!(!out.exists());
4202
4202
  }
4203
4203
 
4204
+ #[test]
4205
+ fn mixed_api_agent_pipeline_records_and_replays_in_order() {
4206
+ let _guard = INTERACTIVE_ENV_LOCK
4207
+ .lock()
4208
+ .unwrap_or_else(|e| e.into_inner());
4209
+ let dir = ConversationTestDir::new();
4210
+ let agent = write_fake_interactive_agent();
4211
+ let (url, handle) = spawn_fake_upstream(vec!["hello"]);
4212
+ let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("mixed pipeline fixture");
4213
+ let api_url = format!(
4214
+ "http://{}",
4215
+ listener.local_addr().expect("mixed pipeline fixture")
4216
+ );
4217
+ let server = std::thread::spawn(move || {
4218
+ use std::io::{Read, Write};
4219
+ for _ in 0..4 {
4220
+ let (mut socket, _) = listener.accept().expect("mixed pipeline fixture");
4221
+ let mut request = [0; 8192];
4222
+ let read = socket.read(&mut request).expect("read request");
4223
+ assert!(read > 0);
4224
+ socket
4225
+ .write_all(
4226
+ b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK",
4227
+ )
4228
+ .expect("mixed pipeline fixture");
4229
+ }
4230
+ });
4231
+ std::env::set_var("FLOWPROOF_AGENT_UPSTREAM", url);
4232
+ let spec = FlowSpec::parse(&format!("name: mixed\napps:\n api:\n app: api\n assistant:\n app: agent\n agent:\n command: python3 \"{}\"\nsteps:\n - in: api\n steps:\n - assert_api:\n request: GET {api_url}\n - in: assistant\n steps:\n - prompt: hi\n - assert: reply contains hello\n - in: api\n steps:\n - assert_api:\n request: GET {api_url}\n", agent.display())).expect("mixed pipeline fixture");
4233
+ let path = dir.path().join("mixed.trace.jsonl");
4234
+ let mut driver = crate::record_driver(&spec).expect("mixed pipeline fixture");
4235
+ let result = flowproof_agent::recorder::record_with_author(
4236
+ &spec,
4237
+ &mut driver,
4238
+ &path,
4239
+ flowproof_agent::recorder::Author::Rules,
4240
+ );
4241
+ std::env::remove_var("FLOWPROOF_AGENT_UPSTREAM");
4242
+ result.expect("record mixed flow");
4243
+ handle.join().expect("mixed pipeline fixture");
4244
+ let (header, steps) = flowproof_replay::load_trace(&path).expect("mixed pipeline fixture");
4245
+ assert_eq!(
4246
+ steps
4247
+ .iter()
4248
+ .map(|s| s.surface.as_deref())
4249
+ .collect::<Vec<_>>(),
4250
+ vec![Some("api"), Some("assistant"), Some("api")]
4251
+ );
4252
+ assert!(matches!(
4253
+ steps[1].action,
4254
+ flowproof_trace::format::Action::AgentRun(_)
4255
+ ));
4256
+ let mut driver = crate::replay_driver(&header, &spec).expect("mixed pipeline fixture");
4257
+ let (report, _) =
4258
+ flowproof_replay::run_trace(&path, &mut driver).expect("mixed pipeline fixture");
4259
+ assert!(report.passed, "{report:?}");
4260
+ server.join().expect("mixed pipeline fixture");
4261
+ }
4262
+
4263
+ #[test]
4264
+ fn agent_surface_records_and_replays_without_upstream() {
4265
+ use flowproof_driver::AppDriver;
4266
+ let _guard = INTERACTIVE_ENV_LOCK
4267
+ .lock()
4268
+ .unwrap_or_else(|e| e.into_inner());
4269
+ let agent = write_fake_interactive_agent();
4270
+ let (url, handle) = spawn_fake_upstream(vec!["hello"]);
4271
+ std::env::set_var("FLOWPROOF_AGENT_UPSTREAM", url);
4272
+ let spec = FlowSpec::parse(&format!("name: mixed\napps:\n assistant:\n app: agent\n agent:\n command: python3 \"{}\"\n api:\n app: api\nsteps:\n - in: assistant\n steps:\n - prompt: hi\n - assert: reply contains hello\n - in: api\n steps:\n - assert_api:\n request: GET http://localhost/health\n", agent.display())).expect("mixed spec");
4273
+ let captures = [("greeting".into(), "hi".into())].into_iter().collect();
4274
+ let steps = serde_json::json!([{"prompt":"${captured.greeting}"}, {"assert":"reply contains hello"}]);
4275
+ let mut driver = crate::agent_surface::AgentSurface::new(spec, "assistant".into());
4276
+ let cassette = driver.agent_segment(&steps, None, &captures);
4277
+ std::env::remove_var("FLOWPROOF_AGENT_UPSTREAM");
4278
+ handle.join().expect("upstream completed");
4279
+ let cassette = cassette.expect("record agent segment");
4280
+ driver
4281
+ .agent_segment(&steps, Some(&cassette), &captures)
4282
+ .expect("offline replay");
4283
+ let bad = serde_json::json!([{"prompt":"hi"}, {"assert":"reply contains WRONG"}]);
4284
+ assert!(driver
4285
+ .agent_segment(&bad, Some(&cassette), &captures)
4286
+ .is_err());
4287
+ }
4288
+
4204
4289
  #[test]
4205
4290
  fn scripted_conversation_records_delivery_metadata_and_replays() {
4206
4291
  let _guard = INTERACTIVE_ENV_LOCK