use crate::invariant::rules::util::{list_files_with_ext, rel};
use crate::invariant::{Category, Context, Invariant, Outcome};
use std::fs;
const SHELL_CRATES: &[&str] = &["koala-cli"];
const SKIP_FILES: &[&str] = &["crates/koala-core/src/invariant/rules/side_effect_boundary.rs"];
const FORBIDDEN_PATTERNS: &[&str] = &[
"std::net::TcpListener",
"std::net::TcpStream",
"std::net::UdpSocket",
"tokio::net::",
];
pub struct SideEffectBoundary;
impl Invariant for SideEffectBoundary {
fn id(&self) -> &'static str {
"arch.side-effect-boundary"
}
fn category(&self) -> Category {
Category::Arch
}
fn intent(&self) -> &'static str {
"Network listeners (TcpListener / TcpStream / tokio::net) are \
confined to shell crates (currently `koala-cli`). Library \
crates stay free of socket I/O so they can be tested without \
binding ports."
}
fn adr(&self) -> Option<&'static str> {
Some("ADR-0001")
}
fn evaluate(&self, ctx: &Context) -> Outcome {
let mut hits: Vec<String> = Vec::new();
for path in list_files_with_ext(ctx.root(), "rs") {
let rel_str = rel(&path, ctx.root());
if !rel_str.starts_with("crates/") {
continue;
}
let normalised = rel_str.replace('\\', "/");
if normalised.contains("/tests/") {
continue;
}
let crate_name = match normalised.strip_prefix("crates/") {
Some(rest) => rest.split('/').next().unwrap_or(""),
None => continue,
};
if SHELL_CRATES.contains(&crate_name) {
continue;
}
if SKIP_FILES.iter().any(|s| normalised == *s) {
continue;
}
let Ok(text) = fs::read_to_string(&path) else {
continue;
};
for line in text.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("//") || trimmed.starts_with("/*") {
continue;
}
for pat in FORBIDDEN_PATTERNS {
if trimmed.contains(pat) {
hits.push(format!("{rel_str}: {pat}"));
}
}
}
}
if hits.is_empty() {
Outcome::pass()
} else {
hits.sort();
hits.dedup();
Outcome::fail_repro(
format!(
"{} network-side-effect leak(s) outside shell crates:\n {}",
hits.len(),
hits.join("\n ")
),
"rg -n 'std::net::|tokio::net::' crates/ --glob '!crates/koala-cli/**'",
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_src(root: &std::path::Path, rel: &str, body: &str) {
let p = root.join(rel);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, body).unwrap();
}
#[test]
fn pure_lib_passes() {
let tmp = TempDir::new().unwrap();
write_src(
tmp.path(),
"crates/koala-core/src/lib.rs",
"pub fn add(a: i32, b: i32) -> i32 { a + b }\n",
);
let ctx = Context::new(tmp.path().to_path_buf());
assert!(matches!(
SideEffectBoundary.evaluate(&ctx),
Outcome::Pass { .. }
));
}
#[test]
fn subprocess_spawn_is_allowed_in_domain() {
let tmp = TempDir::new().unwrap();
write_src(
tmp.path(),
"crates/koala-workflow/src/diff.rs",
"use std::process::Command;\npub fn git_diff() { let _ = Command::new(\"git\"); }\n",
);
let ctx = Context::new(tmp.path().to_path_buf());
assert!(matches!(
SideEffectBoundary.evaluate(&ctx),
Outcome::Pass { .. }
));
}
#[test]
fn shell_crate_is_exempt() {
let tmp = TempDir::new().unwrap();
write_src(
tmp.path(),
"crates/koala-cli/src/main.rs",
"fn main() { let _ = std::net::TcpListener::bind(\"0.0.0.0:0\"); }\n",
);
let ctx = Context::new(tmp.path().to_path_buf());
assert!(matches!(
SideEffectBoundary.evaluate(&ctx),
Outcome::Pass { .. }
));
}
#[test]
fn io_outside_shell_rejected() {
let tmp = TempDir::new().unwrap();
write_src(
tmp.path(),
"crates/koala-core/src/server.rs",
"use std::net::TcpListener;\npub fn serve() { let _ = TcpListener::bind(\"0.0.0.0:0\"); }\n",
);
let ctx = Context::new(tmp.path().to_path_buf());
let out = SideEffectBoundary.evaluate(&ctx);
assert!(matches!(out, Outcome::Fail { .. }), "{out:?}");
}
#[test]
fn tests_dir_is_exempt() {
let tmp = TempDir::new().unwrap();
write_src(
tmp.path(),
"crates/koala-core/tests/server_test.rs",
"#[test] fn s() { let _ = std::net::TcpListener::bind(\"0.0.0.0:0\"); }\n",
);
let ctx = Context::new(tmp.path().to_path_buf());
assert!(matches!(
SideEffectBoundary.evaluate(&ctx),
Outcome::Pass { .. }
));
}
}