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