flowproof 0.12.0__tar.gz → 0.12.2__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 (127) hide show
  1. {flowproof-0.12.0 → flowproof-0.12.2}/Cargo.lock +7 -7
  2. {flowproof-0.12.0 → flowproof-0.12.2}/Cargo.toml +1 -1
  3. {flowproof-0.12.0 → flowproof-0.12.2}/PKG-INFO +2 -2
  4. {flowproof-0.12.0 → flowproof-0.12.2}/README.md +1 -1
  5. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs +17 -2
  6. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/web.rs +282 -17
  7. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +90 -31
  8. flowproof-0.12.2/crates/flowproof-agent/src/author.rs +1571 -0
  9. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/clarify.rs +10 -0
  10. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/heal.rs +6 -2
  11. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/llm.rs +8 -2
  12. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/recorder.rs +526 -126
  13. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/rules.rs +26 -0
  14. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/spec.rs +103 -3
  15. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/lib.rs +160 -55
  16. flowproof-0.12.2/crates/flowproof-cli/tests/llm_author_e2e.rs +556 -0
  17. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_author_e2e.rs +5 -5
  18. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/app.rs +5 -3
  19. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-python/src/lib.rs +41 -7
  20. {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/__init__.py +1 -1
  21. {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/flow.py +38 -9
  22. {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/mcp_server.py +17 -9
  23. {flowproof-0.12.0 → flowproof-0.12.2}/pyproject.toml +1 -1
  24. flowproof-0.12.0/crates/flowproof-agent/src/author.rs +0 -397
  25. flowproof-0.12.0/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -179
  26. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/Cargo.toml +0 -0
  27. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
  28. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
  29. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress.rs +0 -0
  30. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
  31. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
  32. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
  33. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
  34. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
  35. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
  36. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
  37. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
  38. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
  39. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
  40. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
  41. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
  42. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/lib.rs +0 -0
  43. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
  44. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
  45. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
  46. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/sap_com.rs +0 -0
  47. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/vision.rs +0 -0
  48. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
  49. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
  50. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/Cargo.toml +0 -0
  51. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/agent_steps.rs +0 -0
  52. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/lib.rs +0 -0
  53. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
  54. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/Cargo.toml +0 -0
  55. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/agent_flow.rs +0 -0
  56. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/capture.rs +0 -0
  57. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/main.rs +0 -0
  58. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
  59. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
  60. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
  61. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
  62. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  63. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
  64. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
  65. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
  66. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
  67. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
  68. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
  69. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
  70. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
  71. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
  72. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
  73. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
  74. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
  75. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
  76. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
  77. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
  78. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
  79. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
  80. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  81. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
  82. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
  83. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
  84. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
  85. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
  86. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
  87. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
  88. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
  89. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
  90. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
  91. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
  92. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
  93. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
  94. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  95. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
  96. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/Cargo.toml +0 -0
  97. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/backend.rs +0 -0
  98. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/gdi.rs +0 -0
  99. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/lib.rs +0 -0
  100. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/mock.rs +0 -0
  101. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/oob.rs +0 -0
  102. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/recording.rs +0 -0
  103. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/redact.rs +0 -0
  104. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/visual.rs +0 -0
  105. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/window.rs +0 -0
  106. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-python/Cargo.toml +0 -0
  107. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/Cargo.toml +0 -0
  108. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/src/lib.rs +0 -0
  109. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/src/report.rs +0 -0
  110. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/src/runrecord.rs +0 -0
  111. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
  112. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/Cargo.toml +0 -0
  113. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
  114. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/captures.rs +0 -0
  115. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette.rs +0 -0
  116. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
  117. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/egress.rs +0 -0
  118. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/format.rs +0 -0
  119. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/lib.rs +0 -0
  120. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/secret.rs +0 -0
  121. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/secret_scan.rs +0 -0
  122. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/substitution.rs +0 -0
  123. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/toolcalls.rs +0 -0
  124. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
  125. {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
  126. {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/cli.py +0 -0
  127. {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/py.typed +0 -0
@@ -760,7 +760,7 @@ dependencies = [
760
760
 
761
761
  [[package]]
762
762
  name = "flowproof-adapters"
763
- version = "0.12.0"
763
+ version = "0.12.2"
764
764
  dependencies = [
765
765
  "anyhow",
766
766
  "flowproof-driver",
@@ -779,7 +779,7 @@ dependencies = [
779
779
 
780
780
  [[package]]
781
781
  name = "flowproof-agent"
782
- version = "0.12.0"
782
+ version = "0.12.2"
783
783
  dependencies = [
784
784
  "chrono",
785
785
  "flowproof-driver",
@@ -796,7 +796,7 @@ dependencies = [
796
796
 
797
797
  [[package]]
798
798
  name = "flowproof-cli"
799
- version = "0.12.0"
799
+ version = "0.12.2"
800
800
  dependencies = [
801
801
  "ab_glyph",
802
802
  "chrono",
@@ -817,7 +817,7 @@ dependencies = [
817
817
 
818
818
  [[package]]
819
819
  name = "flowproof-driver"
820
- version = "0.12.0"
820
+ version = "0.12.2"
821
821
  dependencies = [
822
822
  "image",
823
823
  "postgres",
@@ -831,7 +831,7 @@ dependencies = [
831
831
 
832
832
  [[package]]
833
833
  name = "flowproof-python"
834
- version = "0.12.0"
834
+ version = "0.12.2"
835
835
  dependencies = [
836
836
  "flowproof-agent",
837
837
  "flowproof-cli",
@@ -843,7 +843,7 @@ dependencies = [
843
843
 
844
844
  [[package]]
845
845
  name = "flowproof-replay"
846
- version = "0.12.0"
846
+ version = "0.12.2"
847
847
  dependencies = [
848
848
  "chrono",
849
849
  "flowproof-agent",
@@ -857,7 +857,7 @@ dependencies = [
857
857
 
858
858
  [[package]]
859
859
  name = "flowproof-trace"
860
- version = "0.12.0"
860
+ version = "0.12.2"
861
861
  dependencies = [
862
862
  "jsonschema",
863
863
  "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.12.0"
25
+ version = "0.12.2"
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.12.0
3
+ Version: 0.12.2
4
4
  Classifier: Development Status :: 2 - Pre-Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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: early, in active development (v0.2).** Record→replay,
64
+ **Status: pre-alpha, 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: early, in active development (v0.2).** Record→replay,
46
+ **Status: pre-alpha, 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
 
@@ -198,11 +198,26 @@ mod tests {
198
198
  let mut overrides = BTreeMap::new();
199
199
  overrides.insert("ZZ_FLOWPROOF_SORT".to_string(), "z".into());
200
200
  overrides.insert("AA_FLOWPROOF_SORT".to_string(), "a".into());
201
+ // Windows runner images carry this same prefix shape as
202
+ // COMMONPROGRAMFILES and COMMONPROGRAMFILES(X86). Comparing whole
203
+ // NAME=VALUE entries would put '(' before '=' and falsely call the
204
+ // correctly name-sorted block unsorted.
205
+ overrides.insert("MM_FLOWPROOF_SORT".to_string(), "plain".into());
206
+ overrides.insert("MM_FLOWPROOF_SORT(X86)".to_string(), "qualified".into());
201
207
 
202
208
  let entries = parse(&environment_block(&overrides, &[]));
203
- let upper: Vec<String> = entries.iter().map(|e| e.to_uppercase()).collect();
209
+ let upper: Vec<String> = entries
210
+ .iter()
211
+ .map(|entry| {
212
+ entry
213
+ .split_once('=')
214
+ .expect("an environment entry has a name and value")
215
+ .0
216
+ .to_uppercase()
217
+ })
218
+ .collect();
204
219
  let mut sorted = upper.clone();
205
220
  sorted.sort();
206
- assert_eq!(upper, sorted, "the block must be sorted");
221
+ assert_eq!(upper, sorted, "the block must be sorted by name");
207
222
  }
208
223
  }
@@ -15,6 +15,7 @@ use flowproof_driver::{
15
15
  use headless_chrome::browser::tab::{ModifierKey, Tab};
16
16
  use headless_chrome::protocol::cdp::Target::CreateTarget;
17
17
  use headless_chrome::protocol::cdp::{Emulation, Input, Network, Page};
18
+ use headless_chrome::types::Bounds;
18
19
  use headless_chrome::{Browser, LaunchOptions};
19
20
 
20
21
  use crate::AdapterError;
@@ -526,6 +527,16 @@ fn headed_requested() -> bool {
526
527
  std::env::var_os("FLOWPROOF_HEADED").is_some()
527
528
  }
528
529
 
530
+ /// Headed runs are deliberately private. A shared browser isolates each flow
531
+ /// in an incognito context, which Chromium presents as a second window. When
532
+ /// the flow tab closes, the original window (including the keep-alive tab)
533
+ /// becomes visible again and looks like a leaked browser. A visible run is for
534
+ /// a person to watch, so cold-start reuse is worth less than owning one window
535
+ /// whose lifetime exactly matches the flow.
536
+ fn should_share_browser(headed: bool, no_shared: bool) -> bool {
537
+ !headed && !no_shared
538
+ }
539
+
529
540
  /// Build the launch options, split out from [`launch_browser`] so the headless
530
541
  /// decision is testable without starting a browser.
531
542
  fn launch_options_for(
@@ -813,9 +824,14 @@ impl WebAppDriver {
813
824
  }
814
825
 
815
826
  /// A driver on the shared browser (isolated context per flow), or a
816
- /// private browser when `FLOWPROOF_NO_SHARED_BROWSER=1`.
827
+ /// private browser when `FLOWPROOF_NO_SHARED_BROWSER=1` or the browser is
828
+ /// headed. Headed ownership prevents the keep-alive window from surviving
829
+ /// after the visible flow window closes.
817
830
  pub fn new() -> Result<Self, AdapterError> {
818
- if std::env::var_os("FLOWPROOF_NO_SHARED_BROWSER").is_some() {
831
+ if !should_share_browser(
832
+ headed_requested(),
833
+ std::env::var_os("FLOWPROOF_NO_SHARED_BROWSER").is_some(),
834
+ ) {
819
835
  return Ok(Self {
820
836
  browser: launch_browser(&[])?,
821
837
  context_id: None,
@@ -1427,9 +1443,9 @@ fn xpath_literal(text: &str) -> String {
1427
1443
 
1428
1444
  impl Drop for WebAppDriver {
1429
1445
  fn drop(&mut self) {
1430
- // The shared browser outlives this driver: close the tab so pages
1431
- // don't accumulate across a suite. A private browser (opt-out) is
1432
- // torn down with its own process, so nothing to do there.
1446
+ // The shared browser outlives this driver: close the flow tab so pages
1447
+ // don't accumulate across a suite. A private browser (opt-out or
1448
+ // headed) is torn down with its process.
1433
1449
  if self.context_id.is_some() {
1434
1450
  if let Some(tab) = self.tab.take() {
1435
1451
  let _ = tab.close(false);
@@ -1633,6 +1649,16 @@ impl AppDriver for WebAppDriver {
1633
1649
  None => self.browser.new_tab(),
1634
1650
  }
1635
1651
  .map_err(|e| web_err("opening tab", e))?;
1652
+ // A visible flow is the only Chrome window the user should have to
1653
+ // manage. Chromium's default launch bounds can be a narrow utility
1654
+ // window (and an incognito context used to create another one), so
1655
+ // present the privately-owned headed window at the desktop's normal
1656
+ // maximized size. Device emulation below still pins the page viewport
1657
+ // when the spec requests one.
1658
+ if headed_requested() {
1659
+ tab.set_bounds(Bounds::Maximized)
1660
+ .map_err(|e| web_err("maximizing headed browser window", e))?;
1661
+ }
1636
1662
  // Viewport/UA emulation BEFORE navigation, so the app boots into
1637
1663
  // the emulated device (responsive breakpoints, UA sniffing). NOT
1638
1664
  // best-effort: a flow recorded mobile must never run desktop.
@@ -3097,31 +3123,259 @@ impl AppDriver for WebAppDriver {
3097
3123
  }
3098
3124
 
3099
3125
  fn scene(&mut self) -> Result<Option<String>, DriverError> {
3100
- // Enumerate visible interactable elements with stable selectors —
3126
+ // Enumerate visible interactable elements plus readable leaf text —
3101
3127
  // the grounding set an authoring model must choose targets from.
3128
+ // Static text matters for natural `Remember the order number` steps,
3129
+ // not only for assertions.
3102
3130
  // `target` is the provenance-neutral token the model echoes; the
3103
3131
  // bare `css` key is kept one release for older agents.
3104
3132
  const SCENE_JS: &str = r#"
3105
- JSON.stringify(Array.from(document.querySelectorAll(
3106
- 'input, button, a, select, textarea, [role=button], [id]'
3107
- )).filter(el => {
3133
+ (() => {
3134
+ function semanticCss(el) {
3135
+ if (el.id) return '#' + CSS.escape(el.id);
3136
+ for (const attr of ['data-testid', 'data-test', 'data-qa', 'aria-label', 'name']) {
3137
+ const value = el.getAttribute(attr);
3138
+ if (value) {
3139
+ const candidate = el.tagName.toLowerCase() + '[' + attr + '="' +
3140
+ CSS.escape(value) + '"]';
3141
+ if (document.querySelectorAll(candidate).length === 1) return candidate;
3142
+ }
3143
+ }
3144
+ for (const cls of Array.from(el.classList)) {
3145
+ const candidate = el.tagName.toLowerCase() + '.' + CSS.escape(cls);
3146
+ if (document.querySelectorAll(candidate).length === 1) return candidate;
3147
+ }
3148
+ return null;
3149
+ }
3150
+ function cssPath(el) {
3151
+ const semantic = semanticCss(el);
3152
+ if (semantic) return semantic;
3153
+ const parts = [];
3154
+ while (el && el.nodeType === 1 && el !== document.body) {
3155
+ const tag = el.tagName.toLowerCase();
3156
+ const siblings = Array.from(el.parentElement.children)
3157
+ .filter(s => s.tagName === el.tagName);
3158
+ parts.unshift(tag + ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')');
3159
+ el = el.parentElement;
3160
+ }
3161
+ return 'body > ' + parts.join(' > ');
3162
+ }
3163
+ function compoundCss(el) {
3164
+ const classes = Array.from(el.classList);
3165
+ if (!classes.length) return null;
3166
+ return el.tagName.toLowerCase() + classes.map(cls => '.' + CSS.escape(cls)).join('');
3167
+ }
3168
+ function relativeCss(el, container) {
3169
+ const semantic = semanticCss(el);
3170
+ if (semantic && container.querySelectorAll(semantic).length === 1) return semantic;
3171
+ const compound = compoundCss(el);
3172
+ if (!compound) return null;
3173
+ if (container.querySelectorAll(compound).length === 1) return compound;
3174
+
3175
+ // A value cell often shares its base classes with the label
3176
+ // beside it, while the label has one extra presentation class
3177
+ // (`bg-info`, for example). Excluding that distinguishing
3178
+ // class gives the scoped target a stable, non-positional inner
3179
+ // selector without smuggling visible text into CSS.
3180
+ const extras = new Set();
3181
+ for (const other of container.querySelectorAll(compound)) {
3182
+ if (other === el) continue;
3183
+ for (const cls of other.classList) {
3184
+ if (!el.classList.contains(cls)) extras.add(cls);
3185
+ }
3186
+ }
3187
+ for (const extra of extras) {
3188
+ const candidate = compound + ':not(.' + CSS.escape(extra) + ')';
3189
+ if (container.querySelectorAll(candidate).length === 1) return candidate;
3190
+ }
3191
+ return null;
3192
+ }
3193
+ function scopedReadable(el) {
3194
+ const container = el.parentElement;
3195
+ if (!container) return null;
3196
+ const containerCss = semanticCss(container) || compoundCss(container);
3197
+ const innerCss = relativeCss(el, container);
3198
+ if (!containerCss || !innerCss) return null;
3199
+
3200
+ // Use a neighbouring leaf as the row/card identity. Require
3201
+ // that the anchor identifies one matching container on the
3202
+ // current screen; runtime scoped resolution will enforce the
3203
+ // same relationship again on every replay.
3204
+ const siblings = Array.from(container.children);
3205
+ const before = siblings.slice(0, siblings.indexOf(el)).reverse();
3206
+ const after = siblings.slice(siblings.indexOf(el) + 1);
3207
+ for (const anchorEl of before.concat(after)) {
3208
+ const anchor = (anchorEl.textContent || '').trim();
3209
+ if (!anchor || anchor.length > 120) continue;
3210
+ const matching = Array.from(document.querySelectorAll(containerCss)).filter(candidate =>
3211
+ Array.from(candidate.children).some(child =>
3212
+ child !== el && (child.textContent || '').trim() === anchor
3213
+ )
3214
+ );
3215
+ if (matching.length !== 1 || matching[0] !== container) continue;
3216
+ const containerTarget = 'css:' + containerCss;
3217
+ const innerTarget = 'css:' + innerCss;
3218
+ return {
3219
+ token: 'scoped:' + containerTarget + ' containing ' + JSON.stringify(anchor) +
3220
+ ' > ' + innerTarget,
3221
+ container: containerTarget,
3222
+ anchor,
3223
+ inner: innerTarget,
3224
+ };
3225
+ }
3226
+ return null;
3227
+ }
3228
+ const all = Array.from(document.querySelectorAll('body *'));
3229
+ const styledLeaf = el => el.children.length === 0 &&
3230
+ ['DIV', 'SPAN'].includes(el.tagName) &&
3231
+ /(?:background|color)\s*:/i.test(el.getAttribute('style') || '');
3232
+ const interactive = el => el.matches(
3233
+ 'input, button, a, select, textarea, [role=button], [role=checkbox], [role=radio], [role=menuitem], [draggable], [ondrop], .draggable-row, .droparea'
3234
+ ) || (!!el.id && el.children.length === 0 && ['DIV', 'SPAN'].includes(el.tagName)) ||
3235
+ styledLeaf(el);
3236
+ const readableLeaf = el => {
3237
+ if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return false;
3238
+ const text = (el.textContent || '').trim();
3239
+ return text && !Array.from(el.children).some(child =>
3240
+ (child.textContent || '').trim()
3241
+ ) && (semanticCss(el) || scopedReadable(el));
3242
+ };
3243
+ const ordered = all.filter(interactive).concat(
3244
+ all.filter(el => !interactive(el) && readableLeaf(el))
3245
+ );
3246
+ const seen = new Set();
3247
+ const chosen = ordered.filter(el => {
3108
3248
  const r = el.getBoundingClientRect();
3109
- return r.width > 0 && r.height > 0;
3110
- }).slice(0, 100).map((el, i) => {
3111
- const css = el.id ? '#' + el.id
3112
- : el.tagName.toLowerCase() + ':nth-of-type(' +
3113
- (Array.from(document.querySelectorAll(el.tagName)).indexOf(el) + 1) + ')';
3249
+ const style = getComputedStyle(el);
3250
+ // Authoring inventories the rendered PAGE, not only the
3251
+ // current viewport. A user naturally says "enter the date"
3252
+ // even when the field starts below the fold, and every web
3253
+ // action already scrolls its target into view before acting.
3254
+ const rendered = style.display !== 'none' && style.visibility !== 'hidden' &&
3255
+ Number(style.opacity) > 0 && r.width > 0 && r.height > 0;
3256
+ if (!rendered || seen.has(el)) return false;
3257
+ seen.add(el);
3258
+ return true;
3259
+ }).slice(0, 100);
3260
+ const entries = chosen.map(el => {
3261
+ const css = cssPath(el);
3262
+ const scoped = !interactive(el) && !semanticCss(el) ? scopedReadable(el) : null;
3114
3263
  const label = el.labels && el.labels[0] ? el.labels[0].textContent.trim()
3115
3264
  : (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '');
3116
- return {
3117
- target: 'css:' + css,
3265
+ const entry = {
3266
+ target: scoped ? scoped.token : 'css:' + css,
3118
3267
  css,
3119
3268
  tag: el.tagName.toLowerCase(),
3269
+ actionable: interactive(el),
3120
3270
  type: el.getAttribute('type') || undefined,
3121
3271
  text: (el.textContent || '').trim().slice(0, 80) || undefined,
3122
3272
  label: label || undefined,
3273
+ background_color: styledLeaf(el) ? getComputedStyle(el).backgroundColor : undefined,
3123
3274
  };
3124
- }))
3275
+ if (scoped) entry.scope = {
3276
+ container: scoped.container,
3277
+ anchor: scoped.anchor,
3278
+ inner: scoped.inner,
3279
+ };
3280
+ return entry;
3281
+ });
3282
+
3283
+ // A human can ask for the number of displayed rows or the
3284
+ // final cell without naming one current row. Represent those
3285
+ // collection identities directly so the model can ground the
3286
+ // intent without inventing a selector from today's DOM.
3287
+ for (const table of document.querySelectorAll('table')) {
3288
+ const tableCss = semanticCss(table);
3289
+ if (!tableCss) continue;
3290
+ const rows = Array.from(table.querySelectorAll('tr'));
3291
+ if (rows.length) {
3292
+ const css = tableCss + ' tr';
3293
+ entries.push({
3294
+ target: 'css:' + css,
3295
+ css,
3296
+ tag: 'collection',
3297
+ actionable: false,
3298
+ count: rows.length,
3299
+ label: 'rows in ' + tableCss,
3300
+ });
3301
+ }
3302
+ const lastCell = table.querySelector('tr:last-child td:last-child');
3303
+ if (lastCell) {
3304
+ const css = tableCss + ' tr:last-child td:last-child';
3305
+ entries.push({
3306
+ target: 'css:' + css,
3307
+ css,
3308
+ tag: 'td',
3309
+ actionable: false,
3310
+ text: (lastCell.textContent || '').trim().slice(0, 80) || undefined,
3311
+ label: 'last cell in the final row of ' + tableCss,
3312
+ });
3313
+ }
3314
+ }
3315
+
3316
+ // Same-origin iframe values are a normal part of the live
3317
+ // scene. Their synthetic token is translated to Target::Framed
3318
+ // before tracing, just like a scoped row token; no synthetic
3319
+ // authoring syntax is persisted or shown to the human.
3320
+ for (const frameEl of document.querySelectorAll('iframe, frame')) {
3321
+ const frameRect = frameEl.getBoundingClientRect();
3322
+ const frameStyle = getComputedStyle(frameEl);
3323
+ if (frameStyle.display === 'none' || frameStyle.visibility === 'hidden' ||
3324
+ Number(frameStyle.opacity) <= 0 || frameRect.width <= 0 || frameRect.height <= 0) {
3325
+ continue;
3326
+ }
3327
+ const frame = frameEl.getAttribute('title') || frameEl.getAttribute('name') ||
3328
+ frameEl.id || frameEl.getAttribute('aria-label');
3329
+ if (!frame) continue;
3330
+ let frameDoc;
3331
+ try { frameDoc = frameEl.contentDocument; } catch (_) { continue; }
3332
+ if (!frameDoc || !frameDoc.body) continue;
3333
+ const inner = [frameDoc.body].concat(Array.from(frameDoc.querySelectorAll(
3334
+ 'input, button, select, textarea, a, [role=button], [role=checkbox], [role=radio], [role=menuitem], [id]'
3335
+ )));
3336
+ for (const el of inner) {
3337
+ const style = frameDoc.defaultView.getComputedStyle(el);
3338
+ const rect = el.getBoundingClientRect();
3339
+ if (style.display === 'none' || style.visibility === 'hidden' ||
3340
+ Number(style.opacity) <= 0 || rect.width <= 0 || rect.height <= 0) {
3341
+ continue;
3342
+ }
3343
+ let css = el === frameDoc.body ? 'body' : null;
3344
+ if (!css && el.id) css = '#' + CSS.escape(el.id);
3345
+ if (!css) {
3346
+ for (const attr of ['data-testid', 'data-test', 'data-qa', 'aria-label', 'name']) {
3347
+ const value = el.getAttribute(attr);
3348
+ if (!value) continue;
3349
+ const candidate = el.tagName.toLowerCase() + '[' + attr + '="' +
3350
+ CSS.escape(value) + '"]';
3351
+ if (frameDoc.querySelectorAll(candidate).length === 1) {
3352
+ css = candidate;
3353
+ break;
3354
+ }
3355
+ }
3356
+ }
3357
+ if (!css) continue;
3358
+ const innerTarget = 'css:' + css;
3359
+ const token = 'framed:' + JSON.stringify(frame) + ' > ' + innerTarget;
3360
+ const actionable = el.matches(
3361
+ 'input, button, select, textarea, a, [role=button], [role=checkbox], [role=radio], [role=menuitem]'
3362
+ );
3363
+ const label = el.labels && el.labels[0] ? el.labels[0].textContent.trim()
3364
+ : (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '');
3365
+ entries.push({
3366
+ target: token,
3367
+ css,
3368
+ tag: el.tagName.toLowerCase(),
3369
+ actionable,
3370
+ text: el === frameDoc.body ? undefined
3371
+ : (el.value || el.textContent || '').trim().slice(0, 80) || undefined,
3372
+ label: label || (el === frameDoc.body ? 'scroll surface' : undefined),
3373
+ scope: { frame, inner: innerTarget },
3374
+ });
3375
+ }
3376
+ }
3377
+ return JSON.stringify(entries);
3378
+ })()
3125
3379
  "#;
3126
3380
  let value = self
3127
3381
  .tab()?
@@ -3174,6 +3428,17 @@ mod tests {
3174
3428
  assert!(!headed.headless, "FLOWPROOF_HEADED must show the window");
3175
3429
  }
3176
3430
 
3431
+ /// A visible shared browser leaks its keep-alive window into the user's
3432
+ /// desktop after the flow tab closes. Headless suites retain reuse; either
3433
+ /// explicit opt-out or headed mode must select one privately-owned process.
3434
+ #[test]
3435
+ fn headed_runs_never_use_the_shared_keep_alive_browser() {
3436
+ assert!(super::should_share_browser(false, false));
3437
+ assert!(!super::should_share_browser(true, false));
3438
+ assert!(!super::should_share_browser(false, true));
3439
+ assert!(!super::should_share_browser(true, true));
3440
+ }
3441
+
3177
3442
  /// The headed launch failure must name the display, and must not invent one
3178
3443
  /// when headless fails for an unrelated reason.
3179
3444
  ///