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