//! 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(, ..)` 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), ); }