flowrs-engine/crates/flowrs-core/tests/secrets.rs
flowRS 2b6f30d348 flowRS engine, cut from flowRS f907e0756a7c
Built alone from an empty CARGO_HOME, its tests passed, and flowrsc checked, ran and exported a
document from the conformance corpus; the export built alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 08:54:33 -05:00

854 lines
30 KiB
Rust

//! Credentials as references, never values (`graph::secret`): a canary credential put in the
//! store and referred to by a Particle node must occur nowhere a value leaves a run or a
//! document: the steps a run returns, its log, its console, the files it wrote, the
//! `.flowrs` and `.fdsl` texts, and the generated program. Each test follows one channel a
//! value can take from the node that holds the credential, as `tests/taint.rs` does for
//! neural data.
use flowrs_core::graph::runtime::Runtime;
use flowrs_core::graph::secret::{self, MARKER, Secret, SecretTaint};
use flowrs_core::graph::widget::{ExecContext, WidgetRegistry, builtin_registry};
use flowrs_core::graph::{Graph, GraphExecutor, NodeId};
use flowrs_core::{Value, WireMode};
use std::collections::HashMap;
use std::sync::Arc;
/// The canary. Built at run time so the source carries no credential-shaped literal.
fn canary() -> String {
format!("{}-{}-{}", "flowrs", "canary", "7f3a9c41d2e8b6aa")
}
/// The name the canary is stored under.
const NAME: &str = "PARTICLE_CANARY";
fn runtime() -> Arc<Runtime> {
let rt = Runtime::new();
rt.put_secret(NAME, Secret::new(canary())).unwrap();
rt
}
struct G {
g: Graph,
}
impl G {
fn new() -> Self {
G { g: Graph::new() }
}
fn node(&mut self, ty: &str, props: &[(&str, &str)]) -> NodeId {
let n = self.g.add_node(ty, 0.0, 0.0);
for (k, v) in props {
self.g
.nodes
.get_mut(&n)
.unwrap()
.properties
.insert((*k).into(), (*v).into());
}
n
}
fn wire(&mut self, a: NodeId, ap: &str, b: NodeId, bp: &str) {
self.g.add_wire(a, ap, b, bp, WireMode::Clone).unwrap();
}
/// A Particle function call holding the canary by reference.
fn source(&mut self) -> NodeId {
let reference = secret::make_reference(NAME);
self.node(
"cloud.particle.function",
&[
("token", reference.as_str()),
("device_id", "dev"),
("function", "led"),
],
)
}
}
struct Run {
outputs: HashMap<(NodeId, String), Value>,
taint: SecretTaint,
console: String,
log: String,
written: String,
unexecuted: String,
}
fn run(g: &Graph, rt: &Arc<Runtime>, withhold: bool) -> Run {
let mut ex =
GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone()).withhold(withhold);
let before = rt.log().events.len();
let steps = ex.run(g).expect("runs");
let mut outputs = HashMap::new();
let mut inputs = String::new();
for s in steps {
inputs.push_str(&format!("{:?}", s.inputs));
for (p, v) in s.outputs {
outputs.insert((s.node_id, p), v);
}
}
let log = format!("{:?}{inputs}", &rt.log().events[before..]);
Run {
outputs,
taint: ex.secret_taint().clone(),
console: ex.console().to_string(),
log,
// As text: a `Vec<u8>`'s `Debug` would hide a credential from the check.
written: ex
.written()
.iter()
.map(|(k, v)| format!("{k}={}", String::from_utf8_lossy(v)))
.collect::<Vec<_>>()
.join("\n"),
unexecuted: format!("{:?}", ex.unexecuted()),
}
}
/// No form of the canary (itself, JSON-escaped, percent-encoded, base64) in `text`.
fn assert_clean(text: &str, what: &str) {
assert!(
!secret::holds(text, &[canary()]),
"{what} carries the credential: {text}"
);
}
fn assert_run_clean(r: &Run) {
assert_clean(&format!("{:?}", r.outputs), "the steps");
assert_clean(&r.log, "the log");
assert_clean(&r.console, "the console");
assert_clean(&r.written, "the files written");
assert_clean(&r.unexecuted, "the reasons a node did not run");
}
/// Whether `v` is the marker.
fn is_marker(v: &Value) -> bool {
matches!(v, Value::String(s) if s == MARKER)
}
#[test]
fn a_reference_resolves_from_the_store_at_t0_and_never_in_an_agents_run() {
let rt = runtime();
let params = HashMap::from([("token".to_string(), secret::make_reference(NAME))]);
let ctx = ExecContext::new(params.clone()).with_runtime(rt.clone());
assert_eq!(ctx.secret("token"), canary());
let mut agent = ExecContext::new(params).with_runtime(rt.clone());
agent.secrets_withheld = true;
assert_eq!(agent.secret("token"), "", "an agent's run resolves nothing");
let missing = ExecContext::new(HashMap::from([(
"token".to_string(),
"secret:NOT_SET".to_string(),
)]))
.with_runtime(rt.clone());
assert_eq!(
missing.secret("token"),
"",
"a name not in the store is empty"
);
let typed = ExecContext::new(HashMap::from([("token".to_string(), "typed".to_string())]));
assert_eq!(
typed.secret("token"),
"typed",
"an old document's value still runs"
);
assert!(rt.put_secret("not a name", Secret::new("x")).is_err());
assert_eq!(rt.secret_names(), [NAME]);
}
#[test]
fn a_secret_is_withheld_from_the_replay_log() {
let rt = runtime();
let mut g = G::new();
let src = g.source();
let p = run(&g.g, &rt, false);
assert!(p.taint.sources.contains(&src));
assert!(p.taint.port(src, "auth"), "auth carries the token");
assert!(!p.taint.port(src, "url"), "the url does not");
assert!(is_marker(&p.outputs[&(src, "auth".into())]));
assert!(
matches!(&p.outputs[&(src, "url".into())], Value::String(u) if u.contains("/v1/devices/dev/led"))
);
assert!(
p.log.contains(MARKER),
"the log records the marker where the value was"
);
assert!(
p.log.contains(&secret::make_reference(NAME)),
"the log keeps the reference, which is a name"
);
assert_run_clean(&p);
// The timeline (probes over the log) reads the same marker.
let tl = flowrs_core::graph::timeline::Timeline::new(&rt.log());
assert_clean(&format!("{tl:?}"), "the timeline");
// The whole log, setting the credential included, holds no value.
assert_clean(&format!("{:?}", rt.log()), "the log from the start");
// And it replays: the log names which credentials were set, never their values, and
// every value derived from one is a marker in both runs.
flowrs_core::graph::runtime::replay(&rt.log(), &g.g, WidgetRegistry::with_builtins())
.expect("a log with a credential replays");
}
#[test]
fn taint_follows_a_secret_through_io_format() {
let rt = runtime();
let mut g = G::new();
let src = g.source();
let fmt = g.node("io.format", &[("format", "header: {}")]);
g.wire(src, "auth", fmt, "value");
let upper = g.node("text.case", &[]);
g.wire(fmt, "string", upper, "text");
let len = g.node("text.length", &[]);
g.wire(upper, "out", len, "text");
let print = g.node("io.print", &[]);
g.wire(fmt, "string", print, "value");
let print2 = g.node("PrintLn", &[]);
g.wire(upper, "out", print2, "input");
// A neighbour free of the credential is untouched.
let hello = g.node("StringLiteral", &[("value", "hello")]);
let print3 = g.node("PrintLn", &[]);
g.wire(hello, "value", print3, "input");
let p = run(&g.g, &rt, false);
for (n, port) in [(fmt, "string"), (upper, "out"), (len, "length")] {
assert!(p.taint.port(n, port), "node {} {port}", n.0);
if let Some(v) = p.outputs.get(&(n, port.to_string())) {
assert!(is_marker(v), "node {} {port} shows the marker", n.0);
}
}
assert!(p.taint.node(print) && p.taint.node(print2));
assert!(!p.taint.node(print3) && !p.taint.node(hello));
assert!(p.console.contains("hello"), "{}", p.console);
assert!(
p.console.contains(secret::CONSOLE_LINE.trim()),
"{}",
p.console
);
assert_run_clean(&p);
}
#[test]
fn a_secret_never_appears_in_generated_code() {
let mut g = G::new();
let src = g.source();
let fmt = g.node("io.format", &[]);
g.wire(src, "auth", fmt, "value");
let detect = g.node(
"cloud.roboflow.detect",
&[("api_key", "secret:ROBOFLOW_KEY"), ("model", "cars")],
);
// A credential typed into the document: compiled as a read of a derived name.
let typed = format!("typed-{}", canary());
let old = g.node("cloud.particle.variable", &[("token", typed.as_str())]);
let program = g.g.to_rust().expect("generates");
assert_clean(&program, "the program");
assert!(
!program.contains(&typed),
"the typed value is not compiled in"
);
assert!(program.contains("std::env::var(\"FLOWRS_SECRET_PARTICLE_CANARY\")"));
assert!(program.contains("std::env::var(\"FLOWRS_SECRET_ROBOFLOW_KEY\")"));
let derived = secret::env_var(&secret::derived_name(old, "token"));
assert!(
program.contains(&format!("std::env::var({derived:?})")),
"{derived} read: {program}"
);
// The other assembler, the profile-driven one, the same way.
let profiled = flowrs_core::codegen::rust::RustCodegen::generate(
&g.g,
&WidgetRegistry::with_builtins(),
&flowrs_core::graph::profile::Profile::learning(),
)
.expect("generates");
assert!(
!profiled.contains(&typed),
"the typed value is not compiled in"
);
assert!(
profiled.contains(&format!("std::env::var({derived:?})")),
"{derived} read: {profiled}"
);
let _ = detect;
}
#[test]
fn export_writes_a_reference_only() {
let rt = runtime();
let mut g = G::new();
let src = g.source();
let fmt = g.node("io.format", &[]);
g.wire(src, "auth", fmt, "value");
// Run it, so the store's value has been used, then write every text a graph leaves as.
let p = run(&g.g, &rt, false);
assert_run_clean(&p);
let flowrs = flowrs_core::graph::document::save(&g.g);
let fdsl = flowrs_core::graph::dsl::print::print(&g.g, builtin_registry());
let program = g.g.to_rust().unwrap();
for (what, text) in [
(".flowrs", &flowrs),
(".fdsl", &fdsl),
("the program", &program),
] {
assert_clean(text, what);
}
assert!(flowrs.contains("secret:PARTICLE_CANARY"), "{flowrs}");
assert!(fdsl.contains("secret:PARTICLE_CANARY"), "{fdsl}");
// Both read back to the same reference.
let back = flowrs_core::graph::document::load(&flowrs).unwrap();
assert_eq!(
back.nodes[&src].properties["token"],
secret::make_reference(NAME)
);
let parsed = flowrs_core::graph::dsl::parse_graph(&fdsl).unwrap().graph;
assert!(
parsed
.nodes
.values()
.any(|n| n.properties.get("token") == Some(&secret::make_reference(NAME)))
);
}
#[test]
fn an_old_document_with_a_literal_token_loads_and_gets_a_finding() {
let typed = format!("old-{}", canary());
let mut g = G::new();
let src = g.node(
"cloud.particle.function",
&[("token", typed.as_str()), ("device_id", "dev")],
);
let fmt = g.node("io.format", &[]);
g.wire(src, "auth", fmt, "value");
let text = flowrs_core::graph::document::save(&g.g);
// It loads, and saves back byte for byte: the document is the person's.
let loaded = flowrs_core::graph::document::load(&text).expect("an old document loads");
assert_eq!(flowrs_core::graph::document::save(&loaded), text);
// The diagnostics say so on the node, without repeating the value.
let diags = flowrs_core::graph::diagnostics::check(&loaded);
let finding = diags
.iter()
.find(|d| d.node == src && d.message.contains("credential's value"))
.expect("a finding on the node");
assert_eq!(
finding.severity,
flowrs_core::graph::diagnostics::Severity::Warning,
"a warning: the node still runs"
);
assert!(!finding.message.contains(&typed));
assert!(finding.message.contains("secret:NAME"));
// It still runs on the value, and no run shows it.
let rt = Runtime::new();
let p = run(&loaded, &rt, false);
assert!(p.taint.sources.contains(&src));
assert!(!format!("{:?}{}{}", p.outputs, p.log, p.console).contains(&typed));
assert!(
p.log.contains(MARKER),
"the parameter is logged as the marker"
);
// An agent sees it withheld, and the program never compiles it in.
let shown = secret::redacted(&loaded, builtin_registry());
assert!(!flowrs_core::graph::document::save(&shown).contains(&typed));
assert!(!loaded.to_rust().unwrap().contains(&typed));
assert_eq!(secret::literal_values(&loaded, builtin_registry()), [typed]);
}
#[test]
fn a_held_wire_carries_a_secret_to_the_next_step() {
// The token's length, summed on a held wire: what the next step reads came from it.
let rt = runtime();
let mut g = G::new();
let src = g.source();
let len = g.node("text.length", &[]);
g.wire(src, "auth", len, "text");
let add = g.node("Add", &[]);
g.wire(len, "length", add, "a");
let fb =
g.g.add_wire(add, "result", add, "b", WireMode::Copy)
.unwrap();
g.g.wires.get_mut(&fb).unwrap().feedback = true;
let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone());
let before = rt.log().events.len();
let first = ex.step(&g.g).unwrap();
let key = flowrs_core::graph::held::key(&g.g.wires[&fb]);
// The real value is held, as the program holds it: the run computed on the credential.
let want = ("Bearer ".len() + canary().len()) as i64;
assert!(
matches!(rt.held.get(&key), Some(Value::Int(n)) if n == want),
"the held value is computed on the real token: {:?}",
rt.held.get(&key)
);
let second = ex.step(&g.g).unwrap();
assert!(
ex.secret_taint().port(add, "result"),
"the reader is tainted"
);
assert!(ex.secret_taint().input(add, "b"), "through the held wire");
let shown = format!("{:?}{:?}", step_outputs(&first), step_outputs(&second));
assert_clean(&shown, "the steps");
assert!(
!shown.contains(&format!("Int({want})")),
"the steps show no sum: {shown}"
);
let log = format!("{:?}", &rt.log().events[before..]);
assert!(
!log.contains(&format!("Int({want})")),
"the log holds no sum: {log}"
);
rt.clear("held");
}
fn step_outputs(steps: &[flowrs_core::graph::executor::ExecutionStep]) -> Vec<String> {
steps
.iter()
.map(|s| format!("{:?} {:?}", s.inputs, s.outputs))
.collect()
}
#[test]
fn a_loop_steered_by_a_secret_taints_its_body() {
let rt = runtime();
let mut g = G::new();
let src = g.source();
let split = g.node("text.split", &[("separator", " ")]);
g.wire(src, "auth", split, "text");
let lp = g.node("ForLoop", &[]);
g.wire(split, "parts", lp, "iterator");
let v = g.node("VecNew", &[]);
let push = g.node("VecPush", &[]);
g.wire(v, "vec", push, "vec");
let one = g.node("IntLiteral", &[("value", "1")]);
g.wire(one, "value", push, "item");
g.wire(push, "vec", lp, "body");
let p = run(&g.g, &rt, false);
assert!(p.taint.node(lp), "the loop ran on the credential");
assert!(p.taint.port(push, "vec"), "its body did");
assert_run_clean(&p);
}
#[test]
fn the_program_scope_and_a_file_the_run_wrote() {
let rt = runtime();
let mut g = G::new();
let src = g.source();
let block = g.node("CodeBlock", &[("src", "format!(\"t,{}\\n1,2\\n\", input)")]);
g.wire(src, "auth", block, "inputs");
let name = g.node("StringLiteral", &[("value", "leak.csv")]);
let write = g.node("WriteFile", &[]);
g.wire(name, "value", write, "path");
g.wire(block, "outputs", write, "contents");
// A code block wired to nothing that carries the credential reads the scope.
let later = g.node("CodeBlockNode", &[("src", "println!(\"{}\", input);")]);
// A widget that loads the file the run wrote: no wire ties it to the credential.
let load = g.node("data.csv.load", &[("path", "leak.csv")]);
let name2 = g.node("StringLiteral", &[("value", "leak.csv")]);
let read = g.node("ReadFile", &[]);
g.wire(name2, "value", read, "path");
let p = run(&g.g, &rt, false);
assert!(p.taint.node(block) && p.taint.node(write));
assert!(p.taint.node(later), "the scope carried it");
assert!(p.taint.node(read), "the file carried it to ReadFile");
assert!(
p.outputs.contains_key(&(load, "data".to_string())),
"the loader ran after the write: {:?}",
p.unexecuted
);
assert!(
p.taint.port(load, "data"),
"the file carried it to the loader"
);
assert!(
p.written.contains(MARKER),
"the written file is withheld: {}",
p.written
);
assert_run_clean(&p);
}
#[test]
fn an_in_place_write_back_reaches_the_bindings_other_readers() {
// `v.push(auth)` then `v.len()` through the wire `v` came on.
let rt = runtime();
let mut g = G::new();
let src = g.source();
let v = g.node("VecNew", &[]);
let push = g.node("VecPush", &[]);
g.wire(v, "vec", push, "vec");
g.wire(src, "auth", push, "item");
let len = g.node("VecLen", &[]);
g.wire(v, "vec", len, "vec");
let p = run(&g.g, &rt, false);
assert!(p.taint.port(len, "length"), "the binding's other reader");
assert_run_clean(&p);
}
#[test]
fn an_agent_run_resolves_no_credential() {
let rt = runtime();
let mut g = G::new();
let src = g.source();
let len = g.node("text.length", &[]);
g.wire(src, "auth", len, "text");
let a = run(&g.g, &rt, true);
assert!(a.taint.port(len, "length"), "still tainted");
assert!(is_marker(&a.outputs[&(src, "auth".into())]));
assert_run_clean(&a);
// What an agent's step holds was computed on nothing: the token's length, summed on a
// held wire, is the length of the scheme alone. The person's step holds the real one.
let mut h = G::new();
let src = h.source();
let len = h.node("text.length", &[]);
h.wire(src, "auth", len, "text");
let add = h.node("Add", &[]);
h.wire(len, "length", add, "a");
let fb =
h.g.add_wire(add, "result", add, "b", WireMode::Copy)
.unwrap();
h.g.wires.get_mut(&fb).unwrap().feedback = true;
let key = flowrs_core::graph::held::key(&h.g.wires[&fb]);
for (withhold, want) in [
(true, "Bearer ".len() as i64),
(false, ("Bearer ".len() + canary().len()) as i64),
] {
rt.clear("held");
let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone())
.withhold(withhold);
ex.step(&h.g).unwrap();
assert!(
matches!(rt.held.get(&key), Some(Value::Int(n)) if n == want),
"withhold={withhold}: {:?}",
rt.held.get(&key)
);
}
rt.clear("held");
}
#[test]
fn a_graph_without_credentials_is_untouched() {
let rt = runtime();
let mut g = G::new();
let hello = g.node("StringLiteral", &[("value", "hello")]);
let print = g.node("PrintLn", &[]);
g.wire(hello, "value", print, "input");
// A Particle node with no token set brings nothing in.
let unset = g.node("cloud.particle.devices", &[]);
let p = run(&g.g, &rt, false);
assert!(!p.taint.any());
assert!(matches!(&p.outputs[&(unset, "auth".into())], Value::String(a) if a == "Bearer "));
assert!(!p.log.contains(MARKER));
assert_eq!(p.console, "hello\n");
}
#[test]
fn an_agent_may_not_write_a_credentials_value() {
let reg = WidgetRegistry::with_builtins();
let mut g = Graph::new();
let n = g.add_node("cloud.particle.function", 0.0, 0.0);
let typed = format!("agent-{}", canary());
let err = flowrs_core::graph::agent::set_declared_properties(
&mut g,
&reg,
n,
&[("token".into(), typed.clone())],
)
.unwrap_err();
assert!(err.contains("secret:NAME"), "{err}");
assert!(
!err.contains(&typed),
"the refusal does not repeat the value"
);
assert!(g.nodes[&n].properties.is_empty(), "unchanged");
flowrs_core::graph::agent::set_declared_properties(
&mut g,
&reg,
n,
&[("token".into(), "secret:PARTICLE".into())],
)
.expect("a reference is written");
}
/// Every parameter whose name says it holds a credential is declared one, with an empty
/// default; every declared one is a real parameter.
#[test]
fn every_credential_parameter_is_declared() {
let shaped = regex_lite::Regex::new(
r"(?i)(^|_)(token|api_?key|apikey|password|passwd|secret|credential|bearer|private_?key|access_?key|client_?secret)($|_)",
)
.unwrap();
let mut declared = 0usize;
for w in builtin_registry().all() {
let id = w.id();
let params = w.parameters();
for p in &params {
// A tokenizer is not a token.
if shaped.is_match(&p.name) {
assert!(
w.secret_parameters().contains(&p.name.as_str()),
"{id}.{} reads like a credential and is not declared one",
p.name
);
}
}
for s in w.secret_parameters() {
declared += 1;
let p = params
.iter()
.find(|p| p.name == *s)
.unwrap_or_else(|| panic!("{id} declares credential {s} but no such parameter"));
assert!(p.default.is_empty(), "{id}.{s} has a default credential");
assert_eq!(p.ty, "String", "{id}.{s}");
}
if let Some(outs) = w.secret_outputs() {
assert!(!w.secret_parameters().is_empty(), "{id}");
let names: Vec<String> = w.outputs().into_iter().map(|p| p.name).collect();
for o in outs {
assert!(names.iter().any(|n| n == o), "{id} names no output {o}");
}
}
}
assert_eq!(declared, 6, "five Particle tokens and Roboflow's key");
}
/// Every widget with a credential parameter, run with the canary set: every output it does
/// not name as carrying the credential is free of it in every encoding, and the ones it
/// names do carry it (or there is no point naming them).
#[test]
fn every_credential_bearing_widget_keeps_it_to_the_outputs_it_names() {
let rt = runtime();
let mut checked = 0usize;
for w in builtin_registry().all() {
if w.secret_parameters().is_empty() {
continue;
}
let id = w.id();
let mut props: HashMap<String, String> = w
.parameters()
.into_iter()
.map(|p| (p.name, p.default))
.collect();
for s in w.secret_parameters() {
props.insert((*s).to_string(), secret::make_reference(NAME));
}
let ctx = ExecContext::for_widget(w, &props).with_runtime(rt.clone());
// Every text input wired, so a widget that echoes an input cannot hide behind an
// empty one.
let inputs: HashMap<String, Value> = w
.inputs()
.into_iter()
.map(|p| (p.name, Value::String("x".into())))
.collect();
let out = w.execute(&ctx, inputs);
let named = w.secret_outputs().unwrap_or(&[]);
let mut carried = false;
for (port, v) in &out {
let text = format!("{v:?}");
if named.contains(&port.as_str()) || w.secret_outputs().is_none() {
carried |= secret::holds(&text, &[canary()]);
continue;
}
assert!(
!secret::holds(&text, &[canary()]),
"{id}.{port} carries the credential and is not named in secret_outputs"
);
}
assert!(
carried,
"{id}: the credential reached none of its named outputs"
);
checked += 1;
}
assert_eq!(checked, 6);
}
/// The edit checker refuses an agent's edit that writes a credential's value, and lets one
/// through that writes a reference; a value the person's document already held is theirs,
/// said by the diagnostics, and not the edit's to fix.
#[test]
fn an_agents_edit_may_not_write_a_credentials_value() {
use flowrs_core::graph::verify::{self, Expect, InBrowser, Stage, Status};
let mut g = Graph::new();
let n = g.add_node("cloud.particle.function", 0.0, 0.0);
g.nodes
.get_mut(&n)
.unwrap()
.properties
.insert("token".into(), "secret:PARTICLE".into());
let typed = format!("edit-{}", canary());
let refused = verify::check_patch(
&g,
&format!("set n{} {{ token: {typed:?} }};", n.0),
builtin_registry(),
&InBrowser,
&Expect::default(),
);
let v = &refused.verification;
// Refused where the patch is applied (`set_declared_properties`).
assert!(
matches!(v.status(Stage::Apply), Some(Status::Failed(_))),
"{}",
v.feedback()
);
let f = v
.errors()
.find(|f| f.message.contains("credential parameter"))
.expect("the refusal names the parameter");
assert!(!f.message.contains(&typed));
// Nothing the checker reports (its findings, its evidence) repeats the value.
let reported = serde_json::to_string(&v.to_json()).unwrap();
assert!(!reported.contains(&typed), "{reported}");
let fine = verify::check_patch(
&g,
&format!("set n{} {{ token: \"secret:OTHER\" }};", n.0),
builtin_registry(),
&InBrowser,
&Expect::default(),
);
assert!(
fine.verification
.errors()
.all(|f| !f.message.contains("credential parameter")),
"{}",
fine.verification.summary()
);
// A value the document already held: the edit touches something else and passes it by.
let mut old = g.clone();
old.nodes
.get_mut(&n)
.unwrap()
.properties
.insert("token".into(), typed.clone());
let other = verify::check_patch(
&old,
&format!("set n{} {{ device_id: \"lab\" }};", n.0),
builtin_registry(),
&InBrowser,
&Expect::default(),
);
assert!(
other
.verification
.errors()
.all(|f| !f.message.contains("credential parameter")),
"{}",
other.verification.summary()
);
let reported = serde_json::to_string(&other.verification.to_json()).unwrap();
assert!(
!reported.contains(&typed),
"the old value is not echoed: {reported}"
);
// A whole graph an agent hands back is held to it at the structure stage: a value it
// wrote fails, one the base already held does not.
let origin = flowrs_core::graph::dsl::Origin::default();
let whole = verify::check_graph(
Some(&g),
&old,
&origin,
builtin_registry(),
&InBrowser,
&Expect::default(),
);
assert!(
matches!(whole.status(Stage::Structure), Some(Status::Failed(_))),
"{}",
whole.feedback()
);
assert!(
whole
.errors()
.any(|f| f.message.contains("credential parameter"))
);
let kept = verify::check_graph(
Some(&old),
&old,
&origin,
builtin_registry(),
&InBrowser,
&Expect::default(),
);
assert!(
kept.errors()
.all(|f| !f.message.contains("credential parameter")),
"{}",
kept.feedback()
);
}
/// A password typed into a front-panel control is saved as the literal's value: the
/// diagnostics say so, and no run returns it, whatever it flows into.
#[test]
fn a_password_control_is_said_and_removed_by_value() {
let typed = format!("pw-{}", canary());
let mut g = G::new();
let quoted = format!("{typed:?}");
let ctl = g.node(
"StringLiteral",
&[("value", quoted.as_str()), ("widget", "password")],
);
let fmt = g.node("io.format", &[("format", "login {}")]);
g.wire(ctl, "value", fmt, "value");
let print = g.node("PrintLn", &[]);
g.wire(fmt, "string", print, "input");
assert_eq!(secret::password_controls(&g.g), [(ctl, typed.clone())]);
let d = flowrs_core::graph::diagnostics::check(&g.g);
assert!(
d.iter()
.any(|d| d.node == ctl && d.message.contains("password control"))
);
let p = run(&g.g, &Runtime::new(), false);
let all = format!("{:?}{}{}", p.outputs, p.log, p.console);
assert!(!all.contains(&typed), "{all}");
assert!(
p.console.contains("login"),
"the run still ran: {}",
p.console
);
}
/// An evidence bundle names a credential parameter by its reference, never a value: one an
/// edit wrote is refused before evidence is gathered, and this holds the bundle to it even if
/// one were gathered.
#[test]
fn an_evidence_bundle_carries_no_credential() {
use flowrs_core::graph::evidence::{self, Bench, Budget, Context, Edit};
use flowrs_core::graph::verify::{self, Expect, InBrowser};
let typed = format!("ev-{}", canary());
let mut base = Graph::new();
let n = base.add_node("cloud.particle.function", 0.0, 0.0);
let mut edited = base.clone();
for (g, token) in [
(&mut base, "secret:PARTICLE"),
(&mut edited, typed.as_str()),
] {
g.nodes
.get_mut(&n)
.unwrap()
.properties
.insert("token".into(), token.to_string());
}
let origin = flowrs_core::graph::dsl::Origin::default();
let v = verify::check_graph(
Some(&base),
&edited,
&origin,
builtin_registry(),
&InBrowser,
&Expect::default(),
);
let changes = flowrs_core::graph::diff::diff(&base, &edited);
let bundle = evidence::gather(
&Edit {
base: &base,
graph: &edited,
changes: &changes,
origin: &origin,
registry: builtin_registry(),
verification: &v,
},
&InBrowser,
&Context {
bench: Bench::default(),
budget: Budget::default(),
},
);
let text = serde_json::to_string(&bundle).unwrap();
assert!(
text.contains("token"),
"the bundle speaks of the parameter: {text}"
);
assert!(!text.contains(&typed), "{text}");
}