flowproof 0.12.1__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 (126) hide show
  1. {flowproof-0.12.1 → flowproof-0.12.2}/Cargo.lock +7 -7
  2. {flowproof-0.12.1 → flowproof-0.12.2}/Cargo.toml +1 -1
  3. {flowproof-0.12.1 → flowproof-0.12.2}/PKG-INFO +1 -1
  4. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs +17 -2
  5. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/web.rs +228 -15
  6. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +90 -31
  7. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/author.rs +367 -9
  8. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/lib.rs +9 -10
  9. flowproof-0.12.2/crates/flowproof-cli/tests/llm_author_e2e.rs +556 -0
  10. {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/__init__.py +1 -1
  11. {flowproof-0.12.1 → flowproof-0.12.2}/pyproject.toml +1 -1
  12. flowproof-0.12.1/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -177
  13. {flowproof-0.12.1 → flowproof-0.12.2}/README.md +0 -0
  14. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/Cargo.toml +0 -0
  15. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
  16. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
  17. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress.rs +0 -0
  18. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
  19. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
  20. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
  21. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
  22. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
  23. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
  24. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
  25. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
  26. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
  27. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
  28. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
  29. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
  30. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/lib.rs +0 -0
  31. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
  32. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
  33. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
  34. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/sap_com.rs +0 -0
  35. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/vision.rs +0 -0
  36. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
  37. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
  38. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/Cargo.toml +0 -0
  39. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/agent_steps.rs +0 -0
  40. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/clarify.rs +0 -0
  41. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/heal.rs +0 -0
  42. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/lib.rs +0 -0
  43. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/llm.rs +0 -0
  44. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/recorder.rs +0 -0
  45. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/rules.rs +0 -0
  46. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/spec.rs +0 -0
  47. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
  48. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/Cargo.toml +0 -0
  49. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/agent_flow.rs +0 -0
  50. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/capture.rs +0 -0
  51. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/main.rs +0 -0
  52. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
  53. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
  54. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
  55. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
  56. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  57. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
  58. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
  59. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
  60. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
  61. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
  62. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
  63. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
  64. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
  65. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
  66. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
  67. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
  68. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
  69. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
  70. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
  71. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
  72. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
  73. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
  74. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
  75. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  76. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
  77. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
  78. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
  79. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
  80. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
  81. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
  82. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
  83. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
  84. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
  85. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
  86. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
  87. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
  88. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
  89. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  90. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
  91. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/Cargo.toml +0 -0
  92. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/app.rs +0 -0
  93. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/backend.rs +0 -0
  94. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/gdi.rs +0 -0
  95. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/lib.rs +0 -0
  96. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/mock.rs +0 -0
  97. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/oob.rs +0 -0
  98. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/recording.rs +0 -0
  99. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/redact.rs +0 -0
  100. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/visual.rs +0 -0
  101. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/window.rs +0 -0
  102. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-python/Cargo.toml +0 -0
  103. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-python/src/lib.rs +0 -0
  104. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/Cargo.toml +0 -0
  105. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/src/lib.rs +0 -0
  106. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/src/report.rs +0 -0
  107. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/src/runrecord.rs +0 -0
  108. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
  109. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/Cargo.toml +0 -0
  110. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
  111. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/captures.rs +0 -0
  112. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette.rs +0 -0
  113. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
  114. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/egress.rs +0 -0
  115. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/format.rs +0 -0
  116. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/lib.rs +0 -0
  117. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/secret.rs +0 -0
  118. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/secret_scan.rs +0 -0
  119. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/substitution.rs +0 -0
  120. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/toolcalls.rs +0 -0
  121. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
  122. {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
  123. {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/cli.py +0 -0
  124. {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/flow.py +0 -0
  125. {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/mcp_server.py +0 -0
  126. {flowproof-0.12.1 → 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.1"
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.1"
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.1"
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.1"
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.1"
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.1"
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.1"
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.1"
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.1
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
@@ -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.
@@ -3134,16 +3160,85 @@ impl AppDriver for WebAppDriver {
3134
3160
  }
3135
3161
  return 'body > ' + parts.join(' > ');
3136
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
+ }
3137
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') || '');
3138
3232
  const interactive = el => el.matches(
3139
- 'input, button, a, select, textarea, [role=button], [role=checkbox], [role=radio], [role=menuitem]'
3140
- );
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);
3141
3236
  const readableLeaf = el => {
3142
3237
  if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return false;
3143
3238
  const text = (el.textContent || '').trim();
3144
- return text && semanticCss(el) && !Array.from(el.children).some(child =>
3239
+ return text && !Array.from(el.children).some(child =>
3145
3240
  (child.textContent || '').trim()
3146
- );
3241
+ ) && (semanticCss(el) || scopedReadable(el));
3147
3242
  };
3148
3243
  const ordered = all.filter(interactive).concat(
3149
3244
  all.filter(el => !interactive(el) && readableLeaf(el))
@@ -3152,27 +3247,134 @@ impl AppDriver for WebAppDriver {
3152
3247
  const chosen = ordered.filter(el => {
3153
3248
  const r = el.getBoundingClientRect();
3154
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.
3155
3254
  const rendered = style.display !== 'none' && style.visibility !== 'hidden' &&
3156
- Number(style.opacity) > 0 && r.width > 0 && r.height > 0 &&
3157
- r.bottom > 0 && r.right > 0 && r.top < innerHeight && r.left < innerWidth;
3255
+ Number(style.opacity) > 0 && r.width > 0 && r.height > 0;
3158
3256
  if (!rendered || seen.has(el)) return false;
3159
3257
  seen.add(el);
3160
3258
  return true;
3161
3259
  }).slice(0, 100);
3162
- return JSON.stringify(chosen.map(el => {
3260
+ const entries = chosen.map(el => {
3163
3261
  const css = cssPath(el);
3262
+ const scoped = !interactive(el) && !semanticCss(el) ? scopedReadable(el) : null;
3164
3263
  const label = el.labels && el.labels[0] ? el.labels[0].textContent.trim()
3165
3264
  : (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '');
3166
- return {
3167
- target: 'css:' + css,
3265
+ const entry = {
3266
+ target: scoped ? scoped.token : 'css:' + css,
3168
3267
  css,
3169
3268
  tag: el.tagName.toLowerCase(),
3170
3269
  actionable: interactive(el),
3171
3270
  type: el.getAttribute('type') || undefined,
3172
3271
  text: (el.textContent || '').trim().slice(0, 80) || undefined,
3173
3272
  label: label || undefined,
3273
+ background_color: styledLeaf(el) ? getComputedStyle(el).backgroundColor : undefined,
3274
+ };
3275
+ if (scoped) entry.scope = {
3276
+ container: scoped.container,
3277
+ anchor: scoped.anchor,
3278
+ inner: scoped.inner,
3174
3279
  };
3175
- }));
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);
3176
3378
  })()
3177
3379
  "#;
3178
3380
  let value = self
@@ -3226,6 +3428,17 @@ mod tests {
3226
3428
  assert!(!headed.headless, "FLOWPROOF_HEADED must show the window");
3227
3429
  }
3228
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
+
3229
3442
  /// The headed launch failure must name the display, and must not invent one
3230
3443
  /// when headless fails for an unrelated reason.
3231
3444
  ///
@@ -40,7 +40,7 @@
40
40
  #![cfg(all(windows, any(feature = "agent", feature = "sap-com")))]
41
41
 
42
42
  use std::io::Read;
43
- use std::net::TcpListener;
43
+ use std::net::{IpAddr, Ipv4Addr, TcpListener, UdpSocket};
44
44
  use std::sync::atomic::{AtomicUsize, Ordering};
45
45
  use std::sync::Arc;
46
46
  use std::time::Duration;
@@ -59,20 +59,20 @@ const RUN_TIMEOUT: Duration = Duration::from_secs(180);
59
59
  /// Bound in the TEST process, not the child's - that is what makes it an
60
60
  /// independent witness rather than a restatement of the child's own opinion.
61
61
  struct Oracle {
62
+ host: Ipv4Addr,
62
63
  port: u16,
63
64
  seen: Arc<AtomicUsize>,
64
65
  }
65
66
 
66
67
  impl Oracle {
67
- /// Bind on loopback and accept in the background until dropped.
68
+ /// Bind on the runner's primary NIC and accept in the background.
68
69
  ///
69
- /// Loopback is a deliberate choice and was checked on a real runner: the
70
- /// spike found WFP's ALE layer classifies and drops loopback connects,
71
- /// with `loopback=true` on the drop record. Had it not, an agent could
72
- /// reach a local service the flow never declared, which would be a hole
73
- /// rather than a convenience.
74
- fn bind() -> std::io::Result<Self> {
75
- let listener = TcpListener::bind("127.0.0.1:0")?;
70
+ /// Flowproof deliberately permits 127.0.0.0/8 so a replay can reach its
71
+ /// local model proxy. Using that address here would test the exception,
72
+ /// not the default-deny policy. The primary NIC still stays entirely on
73
+ /// this host while traversing ALE_AUTH_CONNECT like ordinary traffic.
74
+ fn bind(host: Ipv4Addr) -> std::io::Result<Self> {
75
+ let listener = TcpListener::bind((host, 0))?;
76
76
  let port = listener.local_addr()?.port();
77
77
  let seen = Arc::new(AtomicUsize::new(0));
78
78
  let counter = seen.clone();
@@ -87,7 +87,7 @@ impl Oracle {
87
87
  counter.fetch_add(1, Ordering::SeqCst);
88
88
  }
89
89
  });
90
- Ok(Self { port, seen })
90
+ Ok(Self { host, port, seen })
91
91
  }
92
92
 
93
93
  fn sightings(&self) -> usize {
@@ -96,12 +96,40 @@ impl Oracle {
96
96
 
97
97
  fn allow_entry(&self) -> AllowEntry {
98
98
  AllowEntry {
99
- host: HostMatch::Ip("127.0.0.1".parse().expect("static addr")),
99
+ host: HostMatch::Ip(IpAddr::V4(self.host)),
100
100
  port: Some(self.port),
101
101
  }
102
102
  }
103
103
  }
104
104
 
105
+ /// The address selected by the routing table without sending a packet.
106
+ fn primary_ipv4() -> Option<Ipv4Addr> {
107
+ UdpSocket::bind("0.0.0.0:0")
108
+ .and_then(|socket| {
109
+ socket.connect("192.0.2.1:9")?;
110
+ socket.local_addr()
111
+ })
112
+ .ok()
113
+ .and_then(|addr| match addr.ip() {
114
+ IpAddr::V4(ip) if !ip.is_loopback() && !ip.is_unspecified() => Some(ip),
115
+ _ => None,
116
+ })
117
+ }
118
+
119
+ /// A machine with no non-loopback route cannot exercise the policy honestly.
120
+ fn test_host() -> Option<Ipv4Addr> {
121
+ let host = primary_ipv4();
122
+ if host.is_some() {
123
+ return host;
124
+ }
125
+ assert!(
126
+ std::env::var_os("CI").is_none(),
127
+ "CI must expose a non-loopback IPv4 address for the containment oracle"
128
+ );
129
+ eprintln!("SKIP windows containment E2E: no non-loopback IPv4 address");
130
+ None
131
+ }
132
+
105
133
  /// The probe the contained child runs: connect to each port, write a byte,
106
134
  /// and say what happened.
107
135
  ///
@@ -114,7 +142,7 @@ impl Oracle {
114
142
  /// Only the outer quotes are double quotes. The command line crosses
115
143
  /// `CreateProcessW` as ONE string, and nesting doubles inside it is how this
116
144
  /// kind of probe usually breaks.
117
- fn probe_command(ports: &[u16]) -> String {
145
+ fn probe_command(host: Ipv4Addr, ports: &[u16]) -> String {
118
146
  let list = ports
119
147
  .iter()
120
148
  .map(|p| p.to_string())
@@ -123,7 +151,8 @@ fn probe_command(ports: &[u16]) -> String {
123
151
  format!(
124
152
  "powershell.exe -NoProfile -NonInteractive -Command \
125
153
  \"foreach($p in @({list})){{try{{$c=New-Object Net.Sockets.TcpClient;\
126
- $c.Connect('127.0.0.1',$p);$s=$c.GetStream();\
154
+ $t=$c.ConnectAsync('{host}',$p);\
155
+ if(-not $t.Wait(10000)){{throw 'connect timeout'}};$s=$c.GetStream();\
127
156
  $s.Write([Text.Encoding]::ASCII.GetBytes('fp'),0,2);$s.Flush();$c.Close();\
128
157
  Write-Output ('PROBE|'+$p+'|CONNECTED')}}catch{{\
129
158
  Write-Output ('PROBE|'+$p+'|REFUSED')}}}}\""
@@ -140,7 +169,7 @@ struct Observed {
140
169
  /// Run the probe against both oracles, declaring `allow`.
141
170
  fn probe_run(declared: &Oracle, undeclared: &Oracle, allow: &[AllowEntry]) -> Observed {
142
171
  let outcome = run::run_contained(
143
- &probe_command(&[declared.port, undeclared.port]),
172
+ &probe_command(declared.host, &[declared.port, undeclared.port]),
144
173
  &Default::default(),
145
174
  &[],
146
175
  allow,
@@ -157,6 +186,27 @@ fn probe_run(declared: &Oracle, undeclared: &Oracle, allow: &[AllowEntry]) -> Ob
157
186
  }
158
187
  }
159
188
 
189
+ /// Output capture and the privileged launcher are not witnesses in this test:
190
+ /// the two destination oracles and the WFP audit lane are. GitHub's hosted
191
+ /// Windows runner currently takes the documented CreateProcessWithLogonW
192
+ /// fallback, so accept only those launcher diagnostics while continuing to
193
+ /// reject every audit or collection fault.
194
+ fn assert_evidence_lane_intact(o: &Observed, ev: &str) {
195
+ let unexpected = o
196
+ .outcome
197
+ .faults
198
+ .iter()
199
+ .filter(|fault| {
200
+ !fault.starts_with("the agent was started through CreateProcessWithLogonW")
201
+ && !fault.starts_with("launch privileges were not all enabled")
202
+ })
203
+ .collect::<Vec<_>>();
204
+ assert!(
205
+ unexpected.is_empty(),
206
+ "the containment evidence lane reported faults: {unexpected:?}. {ev}"
207
+ );
208
+ }
209
+
160
210
  /// Everything a failure needs, in one block, because reproducing this locally
161
211
  /// is not an option for most people who will read it.
162
212
  fn evidence(label: &str, o: &Observed) -> String {
@@ -204,14 +254,29 @@ fn host_can_enforce() -> bool {
204
254
  false
205
255
  }
206
256
 
207
- /// The claim, with all three witnesses agreeing.
257
+ /// Prove the positive control first, then invert exactly one declaration.
258
+ ///
259
+ /// These phases mutate process-global Windows state (net-event collection and
260
+ /// the interactive desktop ACL), and the positive control also establishes
261
+ /// that this runner's primary-NIC route works before a refusal is treated as
262
+ /// containment evidence. Keeping both phases in one test makes their order
263
+ /// explicit instead of leaving it to Rust's parallel test scheduler.
208
264
  #[test]
209
- fn an_undeclared_destination_is_refused_and_a_declared_one_is_not() {
265
+ fn declared_and_undeclared_destinations_invert_with_the_policy() {
210
266
  if !host_can_enforce() {
211
267
  return;
212
268
  }
213
- let declared = Oracle::bind().expect("bind the declared oracle");
214
- let undeclared = Oracle::bind().expect("bind the undeclared oracle");
269
+ let Some(host) = test_host() else {
270
+ return;
271
+ };
272
+ assert_both_declared_destinations_connect(host);
273
+ assert_undeclared_destination_is_refused(host);
274
+ }
275
+
276
+ /// The claim, with all three witnesses agreeing.
277
+ fn assert_undeclared_destination_is_refused(host: Ipv4Addr) {
278
+ let declared = Oracle::bind(host).expect("bind the declared oracle");
279
+ let undeclared = Oracle::bind(host).expect("bind the undeclared oracle");
215
280
 
216
281
  let o = probe_run(&declared, &undeclared, &[declared.allow_entry()]);
217
282
  let ev = evidence("enforced", &o);
@@ -220,10 +285,7 @@ fn an_undeclared_destination_is_refused_and_a_declared_one_is_not() {
220
285
  // anything. A run that never installed its filters and then saw no traffic
221
286
  // is not evidence of containment.
222
287
  assert!(o.outcome.is_contained(), "the run was not contained. {ev}");
223
- assert!(
224
- o.outcome.faults.is_empty(),
225
- "a fault is the absence of evidence, so nothing below can be certified over it. {ev}"
226
- );
288
+ assert_evidence_lane_intact(&o, &ev);
227
289
 
228
290
  // Witness 3 first: without it, everything after is consistent with a child
229
291
  // that never ran.
@@ -256,18 +318,14 @@ fn an_undeclared_destination_is_refused_and_a_declared_one_is_not() {
256
318
  /// The negative control: with the same probe and the undeclared port DECLARED,
257
319
  /// the connection must go through.
258
320
  ///
259
- /// This is what makes the test above non-vacuous. If this one ever fails, the
260
- /// test above stops meaning anything - not because containment broke, but
321
+ /// This is what makes the refusal phase non-vacuous. If this one ever fails,
322
+ /// the refusal stops meaning anything - not because containment broke, but
261
323
  /// because the probe stopped reaching the network at all, and a test that
262
324
  /// cannot connect when it is allowed to cannot prove anything by failing to
263
325
  /// connect when it is not.
264
- #[test]
265
- fn the_same_probe_connects_when_the_destination_is_declared() {
266
- if !host_can_enforce() {
267
- return;
268
- }
269
- let declared = Oracle::bind().expect("bind the declared oracle");
270
- let undeclared = Oracle::bind().expect("bind the second oracle");
326
+ fn assert_both_declared_destinations_connect(host: Ipv4Addr) {
327
+ let declared = Oracle::bind(host).expect("bind the declared oracle");
328
+ let undeclared = Oracle::bind(host).expect("bind the second oracle");
271
329
 
272
330
  let o = probe_run(
273
331
  &declared,
@@ -277,6 +335,7 @@ fn the_same_probe_connects_when_the_destination_is_declared() {
277
335
  let ev = evidence("negative-control", &o);
278
336
 
279
337
  assert!(o.outcome.is_contained(), "the run was not contained. {ev}");
338
+ assert_evidence_lane_intact(&o, &ev);
280
339
  assert!(
281
340
  o.declared_sightings >= 1 && o.undeclared_sightings >= 1,
282
341
  "both destinations were declared, so both must have been reached; a probe that \