flowproof 0.2.0__tar.gz → 0.2.1__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 (66) hide show
  1. {flowproof-0.2.0 → flowproof-0.2.1}/Cargo.lock +8 -7
  2. {flowproof-0.2.0 → flowproof-0.2.1}/Cargo.toml +1 -1
  3. flowproof-0.2.1/PKG-INFO +66 -0
  4. flowproof-0.2.1/README.md +47 -0
  5. flowproof-0.2.1/crates/flowproof-agent/src/clarify.rs +177 -0
  6. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/lib.rs +2 -0
  7. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/recorder.rs +151 -10
  8. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/rules.rs +62 -1
  9. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/spec.rs +76 -1
  10. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/Cargo.toml +1 -0
  11. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/src/lib.rs +172 -2
  12. flowproof-0.2.1/crates/flowproof-cli/tests/api_pipeline.rs +177 -0
  13. flowproof-0.2.1/crates/flowproof-cli/tests/examples_resolve.rs +64 -0
  14. flowproof-0.2.1/crates/flowproof-cli/tests/suite_env_from.rs +86 -0
  15. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/oob.rs +30 -7
  16. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-python/src/lib.rs +20 -6
  17. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/src/lib.rs +18 -3
  18. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/schema/trace-v1.schema.json +9 -1
  19. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/src/format.rs +4 -0
  20. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/src/secret.rs +63 -0
  21. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +1 -0
  22. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/tests/schema_conformance.rs +3 -1
  23. {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/__init__.py +8 -6
  24. {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/flow.py +24 -1
  25. {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/mcp_server.py +8 -1
  26. {flowproof-0.2.0 → flowproof-0.2.1}/pyproject.toml +2 -2
  27. flowproof-0.2.0/PKG-INFO +0 -62
  28. flowproof-0.2.0/README.md +0 -43
  29. flowproof-0.2.0/crates/flowproof-cli/tests/api_pipeline.rs +0 -79
  30. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/Cargo.toml +0 -0
  31. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/lib.rs +0 -0
  32. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/sap_com.rs +0 -0
  33. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/vision.rs +0 -0
  34. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/web.rs +0 -0
  35. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/Cargo.toml +0 -0
  36. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/author.rs +0 -0
  37. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/heal.rs +0 -0
  38. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/llm.rs +0 -0
  39. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/src/main.rs +0 -0
  40. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  41. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
  42. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
  43. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  44. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
  45. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
  46. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
  47. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
  48. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  49. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
  50. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/Cargo.toml +0 -0
  51. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/app.rs +0 -0
  52. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/backend.rs +0 -0
  53. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/gdi.rs +0 -0
  54. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/lib.rs +0 -0
  55. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/mock.rs +0 -0
  56. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/recording.rs +0 -0
  57. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/redact.rs +0 -0
  58. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/window.rs +0 -0
  59. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-python/Cargo.toml +0 -0
  60. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/Cargo.toml +0 -0
  61. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/src/report.rs +0 -0
  62. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
  63. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/Cargo.toml +0 -0
  64. {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/src/lib.rs +0 -0
  65. {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/cli.py +0 -0
  66. {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/py.typed +0 -0
@@ -722,7 +722,7 @@ dependencies = [
722
722
 
723
723
  [[package]]
724
724
  name = "flowproof-adapters"
725
- version = "0.2.0"
725
+ version = "0.2.1"
726
726
  dependencies = [
727
727
  "anyhow",
728
728
  "flowproof-driver",
@@ -738,7 +738,7 @@ dependencies = [
738
738
 
739
739
  [[package]]
740
740
  name = "flowproof-agent"
741
- version = "0.2.0"
741
+ version = "0.2.1"
742
742
  dependencies = [
743
743
  "chrono",
744
744
  "flowproof-driver",
@@ -753,7 +753,7 @@ dependencies = [
753
753
 
754
754
  [[package]]
755
755
  name = "flowproof-cli"
756
- version = "0.2.0"
756
+ version = "0.2.1"
757
757
  dependencies = [
758
758
  "ab_glyph",
759
759
  "clap",
@@ -765,12 +765,13 @@ dependencies = [
765
765
  "image",
766
766
  "imageproc",
767
767
  "serde_json",
768
+ "serde_yaml",
768
769
  "tiny_http",
769
770
  ]
770
771
 
771
772
  [[package]]
772
773
  name = "flowproof-driver"
773
- version = "0.2.0"
774
+ version = "0.2.1"
774
775
  dependencies = [
775
776
  "image",
776
777
  "postgres",
@@ -784,7 +785,7 @@ dependencies = [
784
785
 
785
786
  [[package]]
786
787
  name = "flowproof-python"
787
- version = "0.2.0"
788
+ version = "0.2.1"
788
789
  dependencies = [
789
790
  "flowproof-agent",
790
791
  "flowproof-cli",
@@ -796,7 +797,7 @@ dependencies = [
796
797
 
797
798
  [[package]]
798
799
  name = "flowproof-replay"
799
- version = "0.2.0"
800
+ version = "0.2.1"
800
801
  dependencies = [
801
802
  "chrono",
802
803
  "flowproof-agent",
@@ -809,7 +810,7 @@ dependencies = [
809
810
 
810
811
  [[package]]
811
812
  name = "flowproof-trace"
812
- version = "0.2.0"
813
+ version = "0.2.1"
813
814
  dependencies = [
814
815
  "jsonschema",
815
816
  "serde",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproof-replay", "crates/flowproof-agent", "crates/flowproof-adapters", "crates/flowproof-cli", "crates/flowproof-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.0"
6
+ version = "0.2.1"
7
7
  edition = "2021"
8
8
  license = "Apache-2.0"
9
9
  repository = "https://github.com/automators-com/flowproof"
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowproof
3
+ Version: 0.2.1
4
+ Classifier: Development Status :: 2 - Pre-Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Rust
8
+ Classifier: Topic :: Software Development :: Testing
9
+ Requires-Dist: mcp>=1.2 ; python_full_version >= '3.10' and extra == 'mcp'
10
+ Provides-Extra: mcp
11
+ Summary: Generic open-source automation framework for the AI-agent era: automated testing and agentic process automation across web, desktop, and Citrix.
12
+ Author-email: Automators <hello@automators.com>
13
+ License-Expression: Apache-2.0
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
16
+ Project-URL: Homepage, https://github.com/automators-com/flowproof
17
+ Project-URL: Repository, https://github.com/automators-com/flowproof
18
+
19
+ # flowproof
20
+
21
+ A generic open-source automation framework for the AI-agent era:
22
+ automated testing and agentic process automation across web, desktop,
23
+ and Citrix.
24
+
25
+ **Agents author, a deterministic engine executes.** A flow is described in
26
+ YAML with natural-language steps, recorded once against the live app, and
27
+ replayed deterministically in CI with zero LLM calls. Built agent-native:
28
+ every operation returns structured results a program can reason over, and
29
+ agents can drive record/run/get_trace/heal over MCP
30
+ (`pip install flowproof[mcp]`, run `flowproof-mcp`).
31
+
32
+ ```yaml
33
+ # calc.flow.yaml
34
+ name: Add two numbers
35
+ app: calc
36
+ steps:
37
+ - Type 5
38
+ - Press plus
39
+ - Type 3
40
+ - Press equals
41
+ - assert: display shows 8
42
+ ```
43
+
44
+ ```python
45
+ from flowproof import Flow
46
+
47
+ flow = Flow("calc.flow.yaml")
48
+ flow.record() # performs the flow live, writes the trace
49
+ result = flow.run() # deterministic replay -> RunResult
50
+ assert result.passed
51
+ ```
52
+
53
+ Or from the shell: `flowproof record calc.flow.yaml`, then
54
+ `flowproof run calc.flow.yaml` (add `--json` for the structured report).
55
+
56
+ The wheel bundles the Rust engine; no separate install. Targets: `web`
57
+ (headless Chromium, any OS), Windows desktop via UI Automation, `sap`
58
+ (SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
59
+ (no UI — HTTP/SQL assertion flows, any OS).
60
+
61
+ **Status: early, in active development (v0.2).** Record→replay,
62
+ model-grounded authoring, healing with reviewable diffs, suites, run
63
+ recordings, and the MCP server all work and are tested in CI.
64
+
65
+ Docs and source: [github.com/automators-com/flowproof](https://github.com/automators-com/flowproof)
66
+
@@ -0,0 +1,47 @@
1
+ # flowproof
2
+
3
+ A generic open-source automation framework for the AI-agent era:
4
+ automated testing and agentic process automation across web, desktop,
5
+ and Citrix.
6
+
7
+ **Agents author, a deterministic engine executes.** A flow is described in
8
+ YAML with natural-language steps, recorded once against the live app, and
9
+ replayed deterministically in CI with zero LLM calls. Built agent-native:
10
+ every operation returns structured results a program can reason over, and
11
+ agents can drive record/run/get_trace/heal over MCP
12
+ (`pip install flowproof[mcp]`, run `flowproof-mcp`).
13
+
14
+ ```yaml
15
+ # calc.flow.yaml
16
+ name: Add two numbers
17
+ app: calc
18
+ steps:
19
+ - Type 5
20
+ - Press plus
21
+ - Type 3
22
+ - Press equals
23
+ - assert: display shows 8
24
+ ```
25
+
26
+ ```python
27
+ from flowproof import Flow
28
+
29
+ flow = Flow("calc.flow.yaml")
30
+ flow.record() # performs the flow live, writes the trace
31
+ result = flow.run() # deterministic replay -> RunResult
32
+ assert result.passed
33
+ ```
34
+
35
+ Or from the shell: `flowproof record calc.flow.yaml`, then
36
+ `flowproof run calc.flow.yaml` (add `--json` for the structured report).
37
+
38
+ The wheel bundles the Rust engine; no separate install. Targets: `web`
39
+ (headless Chromium, any OS), Windows desktop via UI Automation, `sap`
40
+ (SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
41
+ (no UI — HTTP/SQL assertion flows, any OS).
42
+
43
+ **Status: early, in active development (v0.2).** Record→replay,
44
+ model-grounded authoring, healing with reviewable diffs, suites, run
45
+ recordings, and the MCP server all work and are tested in CI.
46
+
47
+ Docs and source: [github.com/automators-com/flowproof](https://github.com/automators-com/flowproof)
@@ -0,0 +1,177 @@
1
+ //! Structured "this step could not be authored" payloads.
2
+ //!
3
+ //! flowproof deliberately has no in-loop clarification: when neither the
4
+ //! rules nor the model can turn a step into a grounded action, recording
5
+ //! stops and the *driving* agent (a human, or an MCP caller like DataMaker's
6
+ //! agent) resolves the ambiguity — by consulting an external source of
7
+ //! truth, rewriting the vague step into concrete grammar, and re-recording.
8
+ //! This module is the machine-readable half of that loop: everything the
9
+ //! driving agent needs to know *what* was ambiguous and *what the live
10
+ //! screen offered* at the moment authoring gave up.
11
+
12
+ use serde::{Deserialize, Serialize};
13
+
14
+ /// Where authoring gave up on the step.
15
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16
+ #[serde(rename_all = "snake_case")]
17
+ pub enum ClarifyStage {
18
+ /// The rules couldn't parse the step and no model backend is
19
+ /// configured — the payload carries the rules diagnostic.
20
+ NoModel,
21
+ /// The model was consulted (with one self-correcting retry) and still
22
+ /// couldn't ground the step to a listed element.
23
+ Model,
24
+ }
25
+
26
+ impl std::fmt::Display for ClarifyStage {
27
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28
+ f.write_str(match self {
29
+ ClarifyStage::NoModel => "no model backend",
30
+ ClarifyStage::Model => "model could not ground",
31
+ })
32
+ }
33
+ }
34
+
35
+ /// One interactable element from the live scene at the moment of failure —
36
+ /// the same grounding set the author saw, in structured form so the driving
37
+ /// agent can enumerate fields ("which of these should the vague step
38
+ /// change?") instead of re-scraping the app.
39
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40
+ pub struct SceneElement {
41
+ /// Neutral target token (`css:…`, `id:…`, `text:…`) — usable verbatim
42
+ /// as a quoted target in a rewritten step.
43
+ pub target: String,
44
+ #[serde(default, skip_serializing_if = "Option::is_none")]
45
+ pub tag: Option<String>,
46
+ /// Associated label / aria-label / placeholder, when the driver saw one.
47
+ #[serde(default, skip_serializing_if = "Option::is_none")]
48
+ pub label: Option<String>,
49
+ #[serde(default, skip_serializing_if = "Option::is_none")]
50
+ pub text: Option<String>,
51
+ /// Input type (`text`, `password`, …), when applicable.
52
+ #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
53
+ pub input_type: Option<String>,
54
+ }
55
+
56
+ /// The full clarification payload. Text fields keep `${VAR}` references
57
+ /// raw — nothing here ever holds a resolved secret.
58
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59
+ pub struct Clarification {
60
+ /// The spec step that could not be authored, verbatim.
61
+ pub step: String,
62
+ /// 0-based index of that step in the spec.
63
+ pub step_index: usize,
64
+ pub stage: ClarifyStage,
65
+ /// Human-readable diagnostic from the stage that gave up.
66
+ pub reason: String,
67
+ /// The rules diagnostic, when the model stage was reached (the rules
68
+ /// failed first — their error often names the accepted forms).
69
+ #[serde(default, skip_serializing_if = "Option::is_none")]
70
+ pub rules_error: Option<String>,
71
+ /// Intents already performed, in order — the app is left in the state
72
+ /// AFTER these, which is the state `scene` describes.
73
+ pub completed_steps: Vec<String>,
74
+ /// Interactable inventory of the live screen (may be empty when the
75
+ /// driver has no scene support).
76
+ pub scene: Vec<SceneElement>,
77
+ /// What to do next, for agents that don't know the loop.
78
+ pub hint: String,
79
+ }
80
+
81
+ impl Clarification {
82
+ pub const HINT: &'static str = "Rewrite the step using the grammar in docs/authoring.md, \
83
+ targeting a listed element (quote its label, or use its target token verbatim), \
84
+ then re-record. Consult your data source for domain questions the scene cannot \
85
+ answer (e.g. which fields are required).";
86
+ }
87
+
88
+ /// Parse a driver scene JSON into the structured inventory. Mirrors the
89
+ /// author's grounding rules (`author.rs::scene_targets`): a modern element
90
+ /// carries a `target` token; a legacy web element with only a `css` key is
91
+ /// lifted to `css:<sel>`. Anything unparseable yields an empty inventory —
92
+ /// a clarification without a scene is still useful.
93
+ pub fn scene_inventory(scene: &str) -> Vec<SceneElement> {
94
+ serde_json::from_str::<Vec<serde_json::Value>>(scene)
95
+ .unwrap_or_default()
96
+ .iter()
97
+ .filter_map(|e| {
98
+ let target = e["target"]
99
+ .as_str()
100
+ .map(str::to_string)
101
+ .or_else(|| e["css"].as_str().map(|css| format!("css:{css}")))?;
102
+ let field = |key: &str| e[key].as_str().map(str::to_string);
103
+ Some(SceneElement {
104
+ target,
105
+ tag: field("tag"),
106
+ label: field("label"),
107
+ text: field("text"),
108
+ input_type: field("type"),
109
+ })
110
+ })
111
+ .collect()
112
+ }
113
+
114
+ #[cfg(test)]
115
+ mod tests {
116
+ use super::*;
117
+
118
+ #[test]
119
+ fn parses_a_web_shaped_scene() {
120
+ let scene = r##"[
121
+ {"target": "css:#price", "css": "#price", "tag": "input",
122
+ "type": "text", "label": "Net Price"},
123
+ {"target": "text:Save", "tag": "button", "text": "Save"}
124
+ ]"##;
125
+ let inv = scene_inventory(scene);
126
+ assert_eq!(inv.len(), 2);
127
+ assert_eq!(inv[0].target, "css:#price");
128
+ assert_eq!(inv[0].label.as_deref(), Some("Net Price"));
129
+ assert_eq!(inv[0].input_type.as_deref(), Some("text"));
130
+ assert_eq!(inv[1].target, "text:Save");
131
+ assert_eq!(inv[1].text.as_deref(), Some("Save"));
132
+ assert!(inv[1].label.is_none());
133
+ }
134
+
135
+ #[test]
136
+ fn lifts_legacy_bare_css_entries() {
137
+ let inv = scene_inventory(r##"[{"css": "#total", "tag": "span"}]"##);
138
+ assert_eq!(inv.len(), 1);
139
+ assert_eq!(inv[0].target, "css:#total");
140
+ }
141
+
142
+ #[test]
143
+ fn garbage_scene_yields_empty_inventory() {
144
+ assert!(scene_inventory("not json").is_empty());
145
+ assert!(scene_inventory("[{\"no_target\": 1}]").is_empty());
146
+ }
147
+
148
+ #[test]
149
+ fn payload_serializes_to_the_documented_field_names() {
150
+ let c = Clarification {
151
+ step: "make required field changes".into(),
152
+ step_index: 4,
153
+ stage: ClarifyStage::Model,
154
+ reason: "target 'invented' is not one of the listed elements".into(),
155
+ rules_error: Some("no rule matches".into()),
156
+ completed_steps: vec!["Press the \"Edit\" button".into()],
157
+ scene: vec![SceneElement {
158
+ target: "css:#price".into(),
159
+ tag: Some("input".into()),
160
+ label: Some("Net Price".into()),
161
+ text: None,
162
+ input_type: Some("text".into()),
163
+ }],
164
+ hint: Clarification::HINT.into(),
165
+ };
166
+ let v = serde_json::to_value(&c).expect("serializes");
167
+ assert_eq!(v["stage"], "model");
168
+ assert_eq!(v["step_index"], 4);
169
+ assert_eq!(v["scene"][0]["type"], "text");
170
+ assert_eq!(v["scene"][0]["label"], "Net Price");
171
+ // Optional fields absent when None.
172
+ assert!(v["scene"][0].get("text").is_none());
173
+ // Round-trips.
174
+ let back: Clarification = serde_json::from_value(v).expect("deserializes");
175
+ assert_eq!(back, c);
176
+ }
177
+ }
@@ -5,12 +5,14 @@
5
5
  //! scene graph. The replayer never touches this crate.
6
6
 
7
7
  pub mod author;
8
+ pub mod clarify;
8
9
  pub mod heal;
9
10
  pub mod llm;
10
11
  pub mod recorder;
11
12
  pub mod rules;
12
13
  pub mod spec;
13
14
 
15
+ pub use clarify::{Clarification, ClarifyStage};
14
16
  pub use heal::{heal, heal_with_author, HealError, HealReport};
15
17
  pub use llm::{HttpModelClient, ModelClient};
16
18
  pub use recorder::{record, record_with_author, Author, RecordError, RecordSummary};
@@ -76,6 +76,12 @@ pub enum RecordError {
76
76
  NoAuthor { step: String, rules_error: String },
77
77
  #[error("driver cannot describe its scene; LLM authoring is unavailable for app '{0}'")]
78
78
  NoScene(String),
79
+ #[error(
80
+ "cannot author step '{}' ({}): {} — a structured clarification payload with the \
81
+ live-screen inventory is available via `record --json` or the MCP record tool",
82
+ .0.step, .0.stage, .0.reason
83
+ )]
84
+ NeedsClarification(Box<crate::clarify::Clarification>),
79
85
  }
80
86
 
81
87
  /// Outcome of a recording session.
@@ -372,6 +378,8 @@ fn step_for(id: usize, intent: &str, app: &str, action: &ResolvedAction) -> Step
372
378
  ResolvedAction::AssertApi {
373
379
  method,
374
380
  url,
381
+ headers,
382
+ body,
375
383
  status,
376
384
  body_contains,
377
385
  timeout_ms,
@@ -384,10 +392,13 @@ fn step_for(id: usize, intent: &str, app: &str, action: &ResolvedAction) -> Step
384
392
  (
385
393
  Vec::new(),
386
394
  Action::Assert(Assertion::Api {
395
+ // Raw clones: body string leaves and header values keep
396
+ // their ${VAR} refs — tokens never enter the trace.
387
397
  request: flowproof_trace::format::ApiRequest {
388
398
  method: method.clone(),
389
399
  url: url.clone(),
390
- body: None,
400
+ body: body.clone(),
401
+ headers: headers.clone(),
391
402
  },
392
403
  status: *status,
393
404
  expect: Some(serde_json::Value::Object(expect)),
@@ -601,11 +612,38 @@ fn author_actions<D: AppDriver, C: ModelClient>(
601
612
  if author == Author::Rules {
602
613
  return Err(RecordError::Rules(rules_error));
603
614
  }
604
- let Some(client) = client.as_mut() else {
605
- return Err(RecordError::NoAuthor {
615
+ // Ambiguity from here on ends in a structured clarification:
616
+ // the driving agent — not flowproof — resolves it and re-records.
617
+ // `prior` holds the intents already performed, so its length is
618
+ // this step's index and the live scene reflects their effects.
619
+ let clarify = |stage, reason: String, rules_err: Option<String>, scene: Vec<_>| {
620
+ RecordError::NeedsClarification(Box::new(crate::clarify::Clarification {
606
621
  step: intent.to_string(),
607
- rules_error: rules_error.to_string(),
608
- });
622
+ step_index: prior.len(),
623
+ stage,
624
+ reason,
625
+ rules_error: rules_err,
626
+ completed_steps: prior.to_vec(),
627
+ scene,
628
+ hint: crate::clarify::Clarification::HINT.into(),
629
+ }))
630
+ };
631
+ let Some(client) = client.as_mut() else {
632
+ let inventory = driver
633
+ .scene()
634
+ .ok()
635
+ .flatten()
636
+ .map(|s| crate::clarify::scene_inventory(&s))
637
+ .unwrap_or_default();
638
+ return Err(clarify(
639
+ crate::clarify::ClarifyStage::NoModel,
640
+ format!(
641
+ "no model backend is configured (set FLOWPROOF_AI_PROVIDER / \
642
+ FLOWPROOF_AI_API_KEY to enable LLM authoring): {rules_error}"
643
+ ),
644
+ Some(rules_error.to_string()),
645
+ inventory,
646
+ ));
609
647
  };
610
648
  let scene = driver
611
649
  .scene()?
@@ -618,9 +656,21 @@ fn author_actions<D: AppDriver, C: ModelClient>(
618
656
  intent,
619
657
  scene: &scene,
620
658
  };
621
- let action = author_step(*client, &ctx)?;
622
- *llm_used = true;
623
- Ok(vec![action])
659
+ match author_step(*client, &ctx) {
660
+ Ok(action) => {
661
+ *llm_used = true;
662
+ Ok(vec![action])
663
+ }
664
+ // Grounding failure after the retry = genuine ambiguity.
665
+ // Config errors (bad key, network) stay plain errors.
666
+ Err(crate::AgentError::Authoring { reason, .. }) => Err(clarify(
667
+ crate::clarify::ClarifyStage::Model,
668
+ reason,
669
+ Some(rules_error.to_string()),
670
+ crate::clarify::scene_inventory(&scene),
671
+ )),
672
+ Err(other) => Err(other.into()),
673
+ }
624
674
  }
625
675
  }
626
676
  }
@@ -734,16 +784,33 @@ pub fn record_with_client<D: AppDriver, C: ModelClient>(
734
784
  ResolvedAction::AssertApi {
735
785
  method,
736
786
  url,
787
+ headers,
788
+ body,
737
789
  status,
738
790
  body_contains,
739
791
  timeout_ms,
740
792
  } => {
793
+ // Resolved like `equals` above: the trace keeps the raw
794
+ // ${VAR}; only the live probe sees values — including
795
+ // header tokens and body string leaves.
741
796
  let probe = flowproof_driver::oob::OobProbe::Api {
742
797
  method: method.clone(),
743
798
  url: flowproof_trace::secret::resolve_refs(url)?,
744
- body: None,
799
+ body: match body {
800
+ Some(b) => Some(flowproof_trace::secret::resolve_refs_in_json(b)?),
801
+ None => None,
802
+ },
803
+ headers: headers
804
+ .iter()
805
+ .map(|(k, v)| {
806
+ Ok((k.clone(), flowproof_trace::secret::resolve_refs(v)?))
807
+ })
808
+ .collect::<Result<_, flowproof_trace::secret::MissingSecret>>()?,
745
809
  status: *status,
746
- body_contains: body_contains.clone(),
810
+ body_contains: match body_contains {
811
+ Some(needle) => Some(flowproof_trace::secret::resolve_refs(needle)?),
812
+ None => None,
813
+ },
747
814
  };
748
815
  poll_oob(&probe, *timeout_ms, &spec_step.intent())?
749
816
  }
@@ -1212,6 +1279,80 @@ steps:
1212
1279
  assert_eq!(client.calls, 0);
1213
1280
  }
1214
1281
 
1282
+ #[test]
1283
+ fn no_model_backend_yields_clarification_with_scene() {
1284
+ let spec = FlowSpec::parse(
1285
+ "name: Vague
1286
+ app: web
1287
+ url: https://e.test/x
1288
+ steps:
1289
+ - Type acme into the \"Supplier\" field
1290
+ - make required field changes
1291
+ ",
1292
+ )
1293
+ .expect("parses");
1294
+ let mut driver = MockAppDriver::new(&["Supplier"]);
1295
+ driver.scene = Some(
1296
+ r##"[{"target":"css:#price","tag":"input","type":"text","label":"Net Price"}]"##.into(),
1297
+ );
1298
+ let out = std::env::temp_dir().join("flowproof-clarify-nomodel.trace.jsonl");
1299
+ let err = record_with_client(
1300
+ &spec,
1301
+ &mut driver,
1302
+ &out,
1303
+ Author::Auto,
1304
+ Option::<&mut CountingClient>::None,
1305
+ )
1306
+ .expect_err("vague step with no model must need clarification");
1307
+ let RecordError::NeedsClarification(c) = err else {
1308
+ panic!("expected NeedsClarification, got: {err}");
1309
+ };
1310
+ assert_eq!(c.step, "make required field changes");
1311
+ assert_eq!(c.step_index, 1);
1312
+ assert_eq!(c.stage, crate::ClarifyStage::NoModel);
1313
+ assert_eq!(
1314
+ c.completed_steps,
1315
+ vec!["Type acme into the \"Supplier\" field"]
1316
+ );
1317
+ // The live inventory reached the payload — the driving agent can
1318
+ // see the "Net Price" field exists and rewrite the step.
1319
+ assert_eq!(c.scene.len(), 1);
1320
+ assert_eq!(c.scene[0].label.as_deref(), Some("Net Price"));
1321
+ assert!(c.rules_error.is_some());
1322
+ std::fs::remove_file(&out).ok();
1323
+ }
1324
+
1325
+ #[test]
1326
+ fn ungrounded_model_yields_clarification() {
1327
+ let spec = FlowSpec::parse(
1328
+ "name: Vague
1329
+ app: web
1330
+ url: https://e.test/x
1331
+ steps:
1332
+ - make required field changes
1333
+ ",
1334
+ )
1335
+ .expect("parses");
1336
+ let mut driver = MockAppDriver::new(&["#price"]);
1337
+ driver.scene = Some(r##"[{"target":"css:#price","tag":"input"}]"##.into());
1338
+ // The model invents a selector on both attempts — grounding fails.
1339
+ let mut client = CountingClient {
1340
+ reply: r##"{"action":"click","target":"css:#invented"}"##.into(),
1341
+ calls: 0,
1342
+ };
1343
+ let out = std::env::temp_dir().join("flowproof-clarify-model.trace.jsonl");
1344
+ let err = record_with_client(&spec, &mut driver, &out, Author::Auto, Some(&mut client))
1345
+ .expect_err("ungroundable step must need clarification");
1346
+ let RecordError::NeedsClarification(c) = err else {
1347
+ panic!("expected NeedsClarification, got: {err}");
1348
+ };
1349
+ assert_eq!(c.stage, crate::ClarifyStage::Model);
1350
+ assert_eq!(client.calls, 2, "one attempt + one self-correcting retry");
1351
+ assert!(c.rules_error.is_some(), "rules diagnostic travels along");
1352
+ assert_eq!(c.scene[0].target, "css:#price");
1353
+ std::fs::remove_file(&out).ok();
1354
+ }
1355
+
1215
1356
  #[test]
1216
1357
  fn unknown_app_is_rejected() {
1217
1358
  let spec =