flowproof 0.4.1__tar.gz → 0.5.0__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.4.1 → flowproof-0.5.0}/Cargo.lock +7 -7
- {flowproof-0.4.1 → flowproof-0.5.0}/Cargo.toml +1 -1
- {flowproof-0.4.1 → flowproof-0.5.0}/PKG-INFO +1 -1
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/recorder.rs +60 -4
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/rules.rs +5 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/spec.rs +43 -8
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/src/agent_flow.rs +36 -70
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/src/lib.rs +373 -159
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/agent_flow_e2e.rs +18 -3
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/api_pipeline.rs +137 -0
- flowproof-0.5.0/crates/flowproof-cli/tests/audit_record_e2e.rs +218 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/web_e2e.rs +172 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/oob.rs +14 -9
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-replay/src/lib.rs +118 -8
- flowproof-0.5.0/crates/flowproof-replay/src/runrecord.rs +633 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/lib.rs +1 -0
- flowproof-0.5.0/crates/flowproof-trace/src/secret_scan.rs +190 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/flowproof/__init__.py +1 -1
- {flowproof-0.4.1 → flowproof-0.5.0}/pyproject.toml +1 -1
- {flowproof-0.4.1 → flowproof-0.5.0}/README.md +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/egress.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/lib.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/sap_com.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-adapters/src/web.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/agent_steps.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/author.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/clarify.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/heal.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/lib.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-agent/src/llm.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/Cargo.toml +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/src/capture.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/app.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/lib.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/recording.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/visual.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-python/src/lib.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/cassette.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/egress.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/format.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/secret.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/substitution.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/src/toolcalls.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/flowproof/cli.py +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/flowproof/flow.py +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/flowproof/mcp_server.py +0 -0
- {flowproof-0.4.1 → flowproof-0.5.0}/flowproof/py.typed +0 -0
|
@@ -760,7 +760,7 @@ dependencies = [
|
|
|
760
760
|
|
|
761
761
|
[[package]]
|
|
762
762
|
name = "flowproof-adapters"
|
|
763
|
-
version = "0.
|
|
763
|
+
version = "0.5.0"
|
|
764
764
|
dependencies = [
|
|
765
765
|
"anyhow",
|
|
766
766
|
"flowproof-driver",
|
|
@@ -779,7 +779,7 @@ dependencies = [
|
|
|
779
779
|
|
|
780
780
|
[[package]]
|
|
781
781
|
name = "flowproof-agent"
|
|
782
|
-
version = "0.
|
|
782
|
+
version = "0.5.0"
|
|
783
783
|
dependencies = [
|
|
784
784
|
"chrono",
|
|
785
785
|
"flowproof-driver",
|
|
@@ -795,7 +795,7 @@ dependencies = [
|
|
|
795
795
|
|
|
796
796
|
[[package]]
|
|
797
797
|
name = "flowproof-cli"
|
|
798
|
-
version = "0.
|
|
798
|
+
version = "0.5.0"
|
|
799
799
|
dependencies = [
|
|
800
800
|
"ab_glyph",
|
|
801
801
|
"chrono",
|
|
@@ -816,7 +816,7 @@ dependencies = [
|
|
|
816
816
|
|
|
817
817
|
[[package]]
|
|
818
818
|
name = "flowproof-driver"
|
|
819
|
-
version = "0.
|
|
819
|
+
version = "0.5.0"
|
|
820
820
|
dependencies = [
|
|
821
821
|
"image",
|
|
822
822
|
"postgres",
|
|
@@ -830,7 +830,7 @@ dependencies = [
|
|
|
830
830
|
|
|
831
831
|
[[package]]
|
|
832
832
|
name = "flowproof-python"
|
|
833
|
-
version = "0.
|
|
833
|
+
version = "0.5.0"
|
|
834
834
|
dependencies = [
|
|
835
835
|
"flowproof-agent",
|
|
836
836
|
"flowproof-cli",
|
|
@@ -842,7 +842,7 @@ dependencies = [
|
|
|
842
842
|
|
|
843
843
|
[[package]]
|
|
844
844
|
name = "flowproof-replay"
|
|
845
|
-
version = "0.
|
|
845
|
+
version = "0.5.0"
|
|
846
846
|
dependencies = [
|
|
847
847
|
"chrono",
|
|
848
848
|
"flowproof-agent",
|
|
@@ -856,7 +856,7 @@ dependencies = [
|
|
|
856
856
|
|
|
857
857
|
[[package]]
|
|
858
858
|
name = "flowproof-trace"
|
|
859
|
-
version = "0.
|
|
859
|
+
version = "0.5.0"
|
|
860
860
|
dependencies = [
|
|
861
861
|
"jsonschema",
|
|
862
862
|
"regex",
|
|
@@ -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.
|
|
6
|
+
version = "0.5.0"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
repository = "https://github.com/automators-com/flowproof"
|
|
@@ -63,6 +63,11 @@ pub enum RecordError {
|
|
|
63
63
|
Driver(#[from] flowproof_driver::DriverError),
|
|
64
64
|
#[error(transparent)]
|
|
65
65
|
Secret(#[from] flowproof_trace::secret::MissingSecret),
|
|
66
|
+
/// A declared secret leaked into the run's observable corpus, or the
|
|
67
|
+
/// corpus could not be scanned (a too-short secret, a corpus-less flow
|
|
68
|
+
/// kind). Names variables and the corpus element, never the value.
|
|
69
|
+
#[error("{0}")]
|
|
70
|
+
SecretLeak(String),
|
|
66
71
|
#[error("cannot write trace {path}: {source}")]
|
|
67
72
|
Io {
|
|
68
73
|
path: String,
|
|
@@ -1225,15 +1230,18 @@ fn web_browser_from_setup(
|
|
|
1225
1230
|
/// Poll an out-of-band probe until it holds or the bound elapses — the row
|
|
1226
1231
|
/// may still be committing, the API still converging. Configuration errors
|
|
1227
1232
|
/// (missing connection env) fail immediately.
|
|
1233
|
+
/// Returns the probe's response body (an `Api` probe's response text, the
|
|
1234
|
+
/// corpus a secret-leak scan reads; `None` for `Sql`) once the expectation
|
|
1235
|
+
/// holds.
|
|
1228
1236
|
fn poll_oob(
|
|
1229
1237
|
probe: &flowproof_driver::oob::OobProbe,
|
|
1230
1238
|
timeout_ms: u64,
|
|
1231
1239
|
intent: &str,
|
|
1232
|
-
) -> Result<
|
|
1240
|
+
) -> Result<Option<String>, RecordError> {
|
|
1233
1241
|
let deadline = std::time::Instant::now() + Duration::from_millis(timeout_ms);
|
|
1234
1242
|
loop {
|
|
1235
1243
|
match flowproof_driver::oob::check(probe)? {
|
|
1236
|
-
Ok(
|
|
1244
|
+
Ok(body) => return Ok(body),
|
|
1237
1245
|
Err(reason) => {
|
|
1238
1246
|
if std::time::Instant::now() >= deadline {
|
|
1239
1247
|
return Err(RecordError::AssertMismatch {
|
|
@@ -1518,6 +1526,22 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
1518
1526
|
old_steps: Option<&[Step]>,
|
|
1519
1527
|
) -> Result<RecordSummary, RecordError> {
|
|
1520
1528
|
let mut reuse = old_steps.map(ReuseCursor::new);
|
|
1529
|
+
// `assert_no_secret_leak` selectors, grouped by asserting step. The scan
|
|
1530
|
+
// is a whole-run store-guard: it runs on the in-memory corpus BEFORE the
|
|
1531
|
+
// trace is minted, so a leak fails the run and NO trace is written. A flow
|
|
1532
|
+
// that never uses the feature builds no corpus and behaves byte-for-byte
|
|
1533
|
+
// as before.
|
|
1534
|
+
let leak_assertions = spec.secret_leak_assertions();
|
|
1535
|
+
let scan_secrets = !leak_assertions.is_empty();
|
|
1536
|
+
// Honesty, not a vacuous pass: a flow kind with no readable corpus is
|
|
1537
|
+
// refused at execution (the same rule `assert_no_egress` follows), before
|
|
1538
|
+
// launching anything, so nothing is minted for a control that cannot be
|
|
1539
|
+
// checked. web and api are covered; agent never reaches this recorder.
|
|
1540
|
+
if scan_secrets && !flowproof_trace::secret_scan::has_readable_corpus(spec.app.id()) {
|
|
1541
|
+
return Err(RecordError::SecretLeak(
|
|
1542
|
+
flowproof_trace::secret_scan::capability_error(spec.app.id()),
|
|
1543
|
+
));
|
|
1544
|
+
}
|
|
1521
1545
|
let target = launch_target(spec)?;
|
|
1522
1546
|
// The session is an inline mapping by now: a `session: <name>` ref is
|
|
1523
1547
|
// dereferenced to its identity's inline setup at flow load, so record
|
|
@@ -1581,6 +1605,12 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
1581
1605
|
let mut captures: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
|
1582
1606
|
let mut prior_intents: Vec<String> = Vec::new();
|
|
1583
1607
|
let mut llm_used = false;
|
|
1608
|
+
// The secret-leak corpus, held in memory for this run only, exactly like a
|
|
1609
|
+
// resolved `${VAR}` value: (a) the web surface text read at each step
|
|
1610
|
+
// boundary (the same text `page shows` reads, not the page source) and
|
|
1611
|
+
// (b) every `assert_api` response body probed. Populated only when the
|
|
1612
|
+
// flow asserts `assert_no_secret_leak`.
|
|
1613
|
+
let mut secret_corpus: Vec<(String, String)> = Vec::new();
|
|
1584
1614
|
for spec_step in &spec.steps {
|
|
1585
1615
|
let intent = spec_step.intent().to_string();
|
|
1586
1616
|
let actions = author_actions(
|
|
@@ -1714,7 +1744,7 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
1714
1744
|
None => None,
|
|
1715
1745
|
},
|
|
1716
1746
|
};
|
|
1717
|
-
poll_oob(&probe, *timeout_ms, &spec_step.intent())
|
|
1747
|
+
poll_oob(&probe, *timeout_ms, &spec_step.intent())?;
|
|
1718
1748
|
}
|
|
1719
1749
|
ResolvedAction::AssertApi {
|
|
1720
1750
|
method,
|
|
@@ -1747,7 +1777,14 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
1747
1777
|
None => None,
|
|
1748
1778
|
},
|
|
1749
1779
|
};
|
|
1750
|
-
poll_oob(&probe, *timeout_ms, &spec_step.intent())
|
|
1780
|
+
let body = poll_oob(&probe, *timeout_ms, &spec_step.intent())?;
|
|
1781
|
+
// The response body joins the corpus a secret-leak scan
|
|
1782
|
+
// reads. Held in memory, never written to the trace.
|
|
1783
|
+
if scan_secrets {
|
|
1784
|
+
if let Some(text) = body {
|
|
1785
|
+
secret_corpus.push(("an assert_api response body".to_string(), text));
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1751
1788
|
}
|
|
1752
1789
|
ResolvedAction::PressKey { key, modifiers } => {
|
|
1753
1790
|
let mods: Vec<flowproof_driver::KeyMod> =
|
|
@@ -2063,6 +2100,25 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
2063
2100
|
}
|
|
2064
2101
|
}
|
|
2065
2102
|
}
|
|
2103
|
+
// Step boundary: sample the web surface text (the same text `page
|
|
2104
|
+
// shows` reads, not the page source). Per-step, not continuous: a
|
|
2105
|
+
// secret that flashes into the DOM between boundaries is invisible to
|
|
2106
|
+
// this control, an exclusion the audit output echoes.
|
|
2107
|
+
if scan_secrets && spec.app.id() == "web" {
|
|
2108
|
+
secret_corpus.push((
|
|
2109
|
+
"the surface text at a step boundary".to_string(),
|
|
2110
|
+
driver.surface_text()?,
|
|
2111
|
+
));
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// STORE-GUARD: scan the in-memory corpus BEFORE any trace is minted, by
|
|
2116
|
+
// the SAME mechanism replay uses. A leak (or a too-short secret, or a
|
|
2117
|
+
// corpus-less kind) fails the run here, so the leaked value never reaches
|
|
2118
|
+
// disk and no trace is written.
|
|
2119
|
+
if scan_secrets {
|
|
2120
|
+
flowproof_trace::secret_scan::scan_corpus(&leak_assertions, &secret_corpus)
|
|
2121
|
+
.map_err(RecordError::SecretLeak)?;
|
|
2066
2122
|
}
|
|
2067
2123
|
|
|
2068
2124
|
let recording = recorder.and_then(flowproof_driver::RunRecorder::finish);
|
|
@@ -471,6 +471,11 @@ fn resolve_step_inner(app: &str, step: &SpecStep) -> Result<Vec<ResolvedAction>,
|
|
|
471
471
|
.map_or(ASSERT_TIMEOUT_MS, |s| s * 1000),
|
|
472
472
|
}]);
|
|
473
473
|
}
|
|
474
|
+
// A whole-run control assertion, not a per-step driver action: the
|
|
475
|
+
// record-time store-guard and the replay scan enforce it out of band
|
|
476
|
+
// against the captured corpus. It performs nothing on the UI, so it
|
|
477
|
+
// resolves to zero actions on every app kind.
|
|
478
|
+
SpecStep::AssertNoSecretLeak { .. } => return Ok(Vec::new()),
|
|
474
479
|
SpecStep::AssertScreenshot { assert_screenshot } => {
|
|
475
480
|
if assert_screenshot.name.trim().is_empty()
|
|
476
481
|
|| assert_screenshot.name.contains(['/', '\\'])
|
|
@@ -406,6 +406,11 @@ impl FlowSpec {
|
|
|
406
406
|
let bad = |m: String| Err(SpecError::Agent(m));
|
|
407
407
|
let is_agent = self.app.id() == "agent";
|
|
408
408
|
|
|
409
|
+
// `assert_no_secret_leak` is deliberately NOT here: v1 extends it to
|
|
410
|
+
// web and api flows, and any flow kind still without a readable corpus
|
|
411
|
+
// is refused at execution as a CAPABILITY error (never a vacuous
|
|
412
|
+
// parse-time rejection), the same honesty rule `assert_no_egress`
|
|
413
|
+
// follows at execution.
|
|
409
414
|
let agent_only_step = self.steps.iter().find(|step| {
|
|
410
415
|
matches!(
|
|
411
416
|
step,
|
|
@@ -413,7 +418,6 @@ impl FlowSpec {
|
|
|
413
418
|
| SpecStep::AssertToolCall { .. }
|
|
414
419
|
| SpecStep::AssertNoToolCall { .. }
|
|
415
420
|
| SpecStep::AssertNoEgress
|
|
416
|
-
| SpecStep::AssertNoSecretLeak { .. }
|
|
417
421
|
)
|
|
418
422
|
});
|
|
419
423
|
|
|
@@ -816,6 +820,28 @@ impl FlowSpec {
|
|
|
816
820
|
out
|
|
817
821
|
}
|
|
818
822
|
|
|
823
|
+
/// The `assert_no_secret_leak` steps as corpus-scan assertions: each
|
|
824
|
+
/// step's `${VAR}` selectors paired with its 1-based position in `steps:`,
|
|
825
|
+
/// for the failure message. The single source both the record store-guard
|
|
826
|
+
/// and every replay build their scan from, so a web/api flow scans the
|
|
827
|
+
/// same names at both phases exactly as an agent flow does.
|
|
828
|
+
pub fn secret_leak_assertions(&self) -> Vec<flowproof_trace::secret_scan::LeakAssertion> {
|
|
829
|
+
self.steps
|
|
830
|
+
.iter()
|
|
831
|
+
.enumerate()
|
|
832
|
+
.filter_map(|(i, step)| match step {
|
|
833
|
+
SpecStep::AssertNoSecretLeak {
|
|
834
|
+
assert_no_secret_leak,
|
|
835
|
+
} => Some(flowproof_trace::secret_scan::LeakAssertion {
|
|
836
|
+
// 1-based, so the message reads as an author counts steps.
|
|
837
|
+
step_index: i + 1,
|
|
838
|
+
selectors: assert_no_secret_leak.clone(),
|
|
839
|
+
}),
|
|
840
|
+
_ => None,
|
|
841
|
+
})
|
|
842
|
+
.collect()
|
|
843
|
+
}
|
|
844
|
+
|
|
819
845
|
/// Dereference a `session: <name>` ref against a suite's `identities:`
|
|
820
846
|
/// map, replacing it with the named identity's inline setup - a
|
|
821
847
|
/// LOAD-TIME COPY, so the trace stays self-contained (it carries the
|
|
@@ -2571,16 +2597,25 @@ mod security_spine_tests {
|
|
|
2571
2597
|
}
|
|
2572
2598
|
}
|
|
2573
2599
|
|
|
2574
|
-
///
|
|
2575
|
-
///
|
|
2600
|
+
/// Web and api flows now carry `assert_no_secret_leak`: v1 extends the
|
|
2601
|
+
/// corpus scan to their surface text and `assert_api` bodies, so the parse
|
|
2602
|
+
/// gate that once made it agent-only is gone. The honesty for corpus-less
|
|
2603
|
+
/// kinds moved to execution (a capability error), not parse.
|
|
2576
2604
|
#[test]
|
|
2577
|
-
fn
|
|
2578
|
-
let
|
|
2605
|
+
fn assert_no_secret_leak_parses_on_web_and_api_flows() {
|
|
2606
|
+
let web = spec(
|
|
2579
2607
|
"name: n\napp: web\nurl: http://x\nsteps:\n - assert_no_secret_leak: ${DB_PASSWORD}\n",
|
|
2580
2608
|
)
|
|
2581
|
-
.
|
|
2582
|
-
|
|
2583
|
-
|
|
2609
|
+
.expect("secret-leak on web now parses");
|
|
2610
|
+
assert_eq!(
|
|
2611
|
+
web.secret_leak_assertions(),
|
|
2612
|
+
vec![flowproof_trace::secret_scan::LeakAssertion {
|
|
2613
|
+
step_index: 1,
|
|
2614
|
+
selectors: vec!["${DB_PASSWORD}".into()],
|
|
2615
|
+
}]
|
|
2616
|
+
);
|
|
2617
|
+
spec("name: n\napp: api\nsteps:\n - assert_no_secret_leak: ${API_TOKEN}\n")
|
|
2618
|
+
.expect("secret-leak on api now parses");
|
|
2584
2619
|
}
|
|
2585
2620
|
|
|
2586
2621
|
/// It round-trips through serialize/reparse: one selector as a string,
|
|
@@ -556,23 +556,10 @@ struct Plan {
|
|
|
556
556
|
/// The `assert_no_secret_leak` steps, each naming one or more `${VAR}`
|
|
557
557
|
/// selectors and its 1-based position in the flow's `steps:` (for the
|
|
558
558
|
/// failure message). Only the variable NAMES travel here - never a value.
|
|
559
|
-
|
|
559
|
+
/// The shared type web and api flows also build their scan from.
|
|
560
|
+
secret_leaks: Vec<flowproof_trace::secret_scan::LeakAssertion>,
|
|
560
561
|
}
|
|
561
562
|
|
|
562
|
-
/// One `assert_no_secret_leak` step: the `${VAR}` selectors it declares and
|
|
563
|
-
/// its 1-based step position (named in the failure message).
|
|
564
|
-
struct SecretLeakStep {
|
|
565
|
-
step_index: usize,
|
|
566
|
-
selectors: Vec<String>,
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/// The smallest resolved secret length the corpus scan will accept.
|
|
570
|
-
/// Scanning for a one- or two-character value would fire on almost any
|
|
571
|
-
/// output (the doc's `"1"` example), so a secret shorter than this is
|
|
572
|
-
/// REFUSED at execution rather than asserted imprecisely - a control that
|
|
573
|
-
/// cannot be checked precisely is refused, not weakened.
|
|
574
|
-
const MIN_SECRET_LEN: usize = 4;
|
|
575
|
-
|
|
576
563
|
impl Plan {
|
|
577
564
|
/// The port the proxy binds: the fixed `proxy_port` for an http driver,
|
|
578
565
|
/// ephemeral (`0`) for a process.
|
|
@@ -691,21 +678,14 @@ fn plan(spec: &FlowSpec) -> Result<Plan, String> {
|
|
|
691
678
|
let mut tool_calls = Vec::new();
|
|
692
679
|
let mut forbidden = Vec::new();
|
|
693
680
|
let mut reply_contains = Vec::new();
|
|
694
|
-
|
|
695
|
-
|
|
681
|
+
// The secret-leak assertions come from the shared spec accessor, the same
|
|
682
|
+
// source web and api flows build their scan from.
|
|
683
|
+
let secret_leaks = spec.secret_leak_assertions();
|
|
684
|
+
for step in spec.steps.iter() {
|
|
696
685
|
match step {
|
|
697
686
|
SpecStep::AssertToolCall { assert_tool_call } => {
|
|
698
687
|
tool_calls.push(parse_expectation(assert_tool_call)?);
|
|
699
688
|
}
|
|
700
|
-
SpecStep::AssertNoSecretLeak {
|
|
701
|
-
assert_no_secret_leak,
|
|
702
|
-
} => {
|
|
703
|
-
secret_leaks.push(SecretLeakStep {
|
|
704
|
-
// 1-based, so the message reads as an author counts steps.
|
|
705
|
-
step_index: i + 1,
|
|
706
|
-
selectors: assert_no_secret_leak.clone(),
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
689
|
SpecStep::AssertNoToolCall {
|
|
710
690
|
assert_no_tool_call,
|
|
711
691
|
} => {
|
|
@@ -767,6 +747,29 @@ pub fn engages_egress(spec: &FlowSpec) -> bool {
|
|
|
767
747
|
declares_allow || asserts_no_egress
|
|
768
748
|
}
|
|
769
749
|
|
|
750
|
+
/// The egress destinations containment DENIED during the recorded run, read
|
|
751
|
+
/// from the trace's egress lane as value-free `destination (protocol)`
|
|
752
|
+
/// descriptors. Empty for a flow with no egress lane, an unreadable trace, or
|
|
753
|
+
/// a run that attempted no undeclared egress - so it is safe to call on any
|
|
754
|
+
/// agent trace. Feeds the run record's `evidence.blocked`.
|
|
755
|
+
pub fn egress_blocked(trace_path: &Path) -> Vec<String> {
|
|
756
|
+
let Ok(raw) = std::fs::read_to_string(trace_path) else {
|
|
757
|
+
return Vec::new();
|
|
758
|
+
};
|
|
759
|
+
let Ok(trace) = serde_json::from_str::<AgentTrace>(&raw) else {
|
|
760
|
+
return Vec::new();
|
|
761
|
+
};
|
|
762
|
+
trace
|
|
763
|
+
.egress
|
|
764
|
+
.map(|e| {
|
|
765
|
+
e.blocked
|
|
766
|
+
.iter()
|
|
767
|
+
.map(|ev| format!("{} ({})", ev.destination, ev.protocol))
|
|
768
|
+
.collect()
|
|
769
|
+
})
|
|
770
|
+
.unwrap_or_default()
|
|
771
|
+
}
|
|
772
|
+
|
|
770
773
|
/// The containment tier a flow's run achieves, on this platform. Pure and
|
|
771
774
|
/// cheap (a kernel probe on Linux); computed both here, to print the report's
|
|
772
775
|
/// tier line, and inside record/replay, to gate `assert_no_egress` and build
|
|
@@ -878,14 +881,12 @@ fn secret_corpus(
|
|
|
878
881
|
corpus
|
|
879
882
|
}
|
|
880
883
|
|
|
881
|
-
/// Scan the run's captured corpus for any declared secret, by the SAME
|
|
882
|
-
/// mechanism
|
|
883
|
-
/// verdict.
|
|
884
|
-
///
|
|
885
|
-
///
|
|
886
|
-
///
|
|
887
|
-
/// A resolved secret under the minimum length is refused (both phases), in
|
|
888
|
-
/// the same shape as the shipped `MissingSecret` error.
|
|
884
|
+
/// Scan the run's captured corpus for any declared secret, by the SAME shared
|
|
885
|
+
/// mechanism web, api, and replay use, so an unchanged system yields the same
|
|
886
|
+
/// verdict. Builds the agent corpus (model-boundary trajectory + MCP lanes)
|
|
887
|
+
/// and delegates the resolve/refuse/substring-scan to
|
|
888
|
+
/// [`flowproof_trace::secret_scan::scan_corpus`]; only variable NAMES are ever
|
|
889
|
+
/// stored or printed.
|
|
889
890
|
fn check_secret_leak(
|
|
890
891
|
plan: &Plan,
|
|
891
892
|
cassette: &Cassette,
|
|
@@ -895,42 +896,7 @@ fn check_secret_leak(
|
|
|
895
896
|
return Ok(());
|
|
896
897
|
}
|
|
897
898
|
let corpus = secret_corpus(cassette, mcp);
|
|
898
|
-
|
|
899
|
-
// (selector, corpus element) for every variable that appeared,
|
|
900
|
-
// collected then sorted so two leaks report in a stable order.
|
|
901
|
-
let mut hits: Vec<(String, String)> = Vec::new();
|
|
902
|
-
for selector in &leak.selectors {
|
|
903
|
-
let value =
|
|
904
|
-
flowproof_trace::secret::resolve_refs(selector).map_err(|e| e.to_string())?;
|
|
905
|
-
if value.chars().count() < MIN_SECRET_LEN {
|
|
906
|
-
// Named like MissingSecret: the variable and the minimum,
|
|
907
|
-
// never the value.
|
|
908
|
-
return Err(format!(
|
|
909
|
-
"assert_no_secret_leak (step {}): {selector} resolves to a value shorter \
|
|
910
|
-
than the {MIN_SECRET_LEN}-character minimum needed to scan for it \
|
|
911
|
-
precisely; a secret that short cannot be asserted without false positives",
|
|
912
|
-
leak.step_index
|
|
913
|
-
));
|
|
914
|
-
}
|
|
915
|
-
if let Some((element, _)) = corpus.iter().find(|(_, text)| text.contains(&value)) {
|
|
916
|
-
hits.push((selector.clone(), element.clone()));
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
if !hits.is_empty() {
|
|
920
|
-
hits.sort();
|
|
921
|
-
let list = hits
|
|
922
|
-
.iter()
|
|
923
|
-
.map(|(selector, element)| format!("{selector} in {element}"))
|
|
924
|
-
.collect::<Vec<_>>()
|
|
925
|
-
.join("; ");
|
|
926
|
-
return Err(format!(
|
|
927
|
-
"assert_no_secret_leak (step {}): a declared secret appeared in the run \
|
|
928
|
-
output: {list}",
|
|
929
|
-
leak.step_index
|
|
930
|
-
));
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
Ok(())
|
|
899
|
+
flowproof_trace::secret_scan::scan_corpus(&plan.secret_leaks, &corpus)
|
|
934
900
|
}
|
|
935
901
|
|
|
936
902
|
fn parse_expectation(text: &str) -> Result<ToolCallExpectation, String> {
|