flowrs-engine/crates/flowrs-core/tests/checker_confinement.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

115 lines
4.2 KiB
Rust

//! C1: the agent's checker compiles and runs the program a graph emits, to compare it with
//! the interpreter. The graph is a model's proposal, written for an unauthenticated
//! visitor, so two things must hold and are tested apart:
//!
//! - the public route never *runs* a graph whose generated code performs a real-world
//! effect (the side-effect filter, [`the_checker_does_not_run_a_graph_that_touches_files`]);
//! - any program that *is* run is confined to the checker's scratch directory (the sandbox,
//! [`the_sandbox_confines_a_program_the_checker_runs`]), which backstops the filter and
//! protects the per-widget harness.
//!
//! `audio.save` emits `std::fs::write(<path>, ..)` on a parameter-supplied path. Here the
//! path names a file in a directory the test owns, outside the checker's root.
use flowrs_core::graph::tier_check::{GraphCase, run_graphs};
use flowrs_core::graph::toolchain::Tiers;
use flowrs_core::graph::verify::{Native, Toolchain};
use flowrs_core::graph::widget::builtin_registry;
use flowrs_core::graph::{Graph, NodeId, WireMode};
use std::path::{Path, PathBuf};
/// A fresh, empty directory under the system temp dir, resolved.
fn fresh(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("flowrs-confine-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d.canonicalize().unwrap()
}
/// `audio.tone → audio.save`, saving to `target` (a path the test owns, outside the root).
fn tone_to_file(target: &Path) -> (Graph, NodeId) {
let mut g = Graph::new();
let tone = g.add_node("audio.tone", 0.0, 0.0);
let save = g.add_node("audio.save", 0.0, 0.0);
g.get_node_mut(save)
.unwrap()
.properties
.insert("path".into(), target.display().to_string());
g.add_wire(tone, "audio", save, "audio", WireMode::Copy)
.unwrap();
(g, tone)
}
fn note_of(t: &Tiers) -> String {
match t {
Tiers::Agree(s) | Tiers::Unchecked(s) => s.clone(),
Tiers::Disagree(v) => v.join("; "),
}
}
/// The side-effect filter: `Native::tiers` (the agent's own path) does not run the whole
/// program of a graph that writes files. It says so, and the file is never written. (Before
/// the fix the program ran and the file appeared, agreeing on three values.)
#[test]
fn the_checker_does_not_run_a_graph_that_touches_files() {
let base = fresh("filter");
let outside = fresh("filter-out");
let target = outside.join("saved.wav");
let (g, _) = tone_to_file(&target);
let tiers = Native {
root: base.join("check"),
}
.tiers(&g, builtin_registry());
assert!(
note_of(&tiers).contains("not run here"),
"the whole program was run instead of filtered: {tiers:?}"
);
assert!(!target.exists(), "the checker wrote {}", target.display());
let _ = (
std::fs::remove_dir_all(&base),
std::fs::remove_dir_all(&outside),
);
}
/// The sandbox: a program the checker runs cannot write outside its scratch directory.
/// Driven through `run_graphs` directly, below the filter, so it exercises the confinement
/// of a program that really executes: the case is not skipped (it ran), and the outside
/// file is absent. (Before the fix the write succeeded and the file appeared.)
#[cfg(target_os = "macos")]
#[test]
fn the_sandbox_confines_a_program_the_checker_runs() {
let scratch = fresh("sandbox");
let outside = fresh("sandbox-out");
let target = outside.join("pwned.wav");
let (g, tone) = tone_to_file(&target);
let case = GraphCase {
name: "tone-save".into(),
graph: g,
probes: vec![(tone, "audio".into())],
files: Vec::new(),
console: false,
};
let report = run_graphs(&[case], &scratch).expect("the program compiles and runs");
assert!(
report.skipped.is_empty(),
"the program never ran, so confinement was not exercised: {:?}",
report.skipped
);
assert!(
report.compared >= 1,
"the probed program produced nothing: {report:?}"
);
assert!(
!target.exists(),
"a program the checker ran wrote {}",
target.display()
);
let _ = (
std::fs::remove_dir_all(&scratch),
std::fs::remove_dir_all(&outside),
);
}