use std::sync::atomic::{AtomicU64, Ordering};
use wm_core::{Args, Context, CoreError, Output, Result, Sandbox, Tool};
pub const V1_FLAG_ENV: &str = "WM_LANDLOCK_V1";
#[must_use]
pub fn v1_requested() -> bool {
std::env::var(V1_FLAG_ENV).is_ok_and(|v| v == "1")
}
pub type RestrictFn = Box<dyn Fn() -> std::result::Result<(), String> + Send + Sync>;
pub struct ScopedSandboxExecutor {
restrict: RestrictFn,
runs: AtomicU64,
degraded: AtomicU64,
failures: AtomicU64,
}
impl ScopedSandboxExecutor {
#[must_use]
pub fn new(
restrict: impl Fn() -> std::result::Result<(), String> + Send + Sync + 'static,
) -> Self {
Self {
restrict: Box::new(restrict),
runs: AtomicU64::new(0),
degraded: AtomicU64::new(0),
failures: AtomicU64::new(0),
}
}
#[must_use]
pub fn stats(&self) -> (u64, u64, u64) {
(
self.runs.load(Ordering::Relaxed),
self.degraded.load(Ordering::Relaxed),
self.failures.load(Ordering::Relaxed),
)
}
pub fn run(&self, tool: &dyn Tool, ctx: &mut Context, args: Args) -> Result<Output> {
self.runs.fetch_add(1, Ordering::Relaxed);
let outcome = std::thread::scope(|scope| {
scope
.spawn(|| {
if let Err(reason) = (self.restrict)() {
self.degraded.fetch_add(1, Ordering::Relaxed);
tracing::warn!(
tool = tool.name(),
reason = %reason,
"sandbox: per-tool confinement unavailable — running unconfined (loud-degrade)"
);
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| {
CoreError::Tool(format!("sandbox runtime build failed: {e}"))
})?;
runtime.block_on(tool.call(ctx, args))
})
.join()
});
match outcome {
Ok(result) => result,
Err(_panic) => {
self.failures.fetch_add(1, Ordering::Relaxed);
Err(CoreError::Tool(format!(
"sandboxed tool '{}' panicked — contained by the scoped thread",
tool.name()
)))
}
}
}
#[must_use]
pub fn handles(tool: &dyn Tool) -> bool {
tool.effects().sandbox == Sandbox::StoreScoped
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use wm_core::{BrainWave, EffectRow, Gana, ToolStats};
struct ProbeTool {
effects: EffectRow,
stats: ToolStats,
restricted_seen: Option<Arc<AtomicBool>>,
panic: bool,
}
#[async_trait::async_trait]
impl Tool for ProbeTool {
fn name(&self) -> &str {
"probe"
}
fn gana(&self) -> Gana {
Gana::Heart
}
fn effects(&self) -> &EffectRow {
&self.effects
}
async fn call(&self, _ctx: &mut wm_core::Context, _args: Args) -> wm_core::Result<Output> {
assert!(!self.panic, "probe tool panicked");
if let Some(flag) = &self.restricted_seen {
assert!(
flag.load(Ordering::SeqCst),
"tool must run AFTER the restriction callback"
);
}
Ok(serde_json::json!({"ok": true}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
fn probe() -> ProbeTool {
ProbeTool {
effects: EffectRow {
sandbox: Sandbox::StoreScoped,
..Default::default()
},
stats: ToolStats::default(),
restricted_seen: None,
panic: false,
}
}
#[test]
fn restrict_runs_before_tool_and_output_passes_through() {
let restricted = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&restricted);
let executor = ScopedSandboxExecutor::new(move || {
flag.store(true, Ordering::SeqCst);
Ok(())
});
let mut tool = probe();
tool.restricted_seen = Some(Arc::clone(&restricted));
let mut ctx = wm_core::Context::new(BrainWave::Gamma);
let out = executor
.run(&tool, &mut ctx, serde_json::json!({}))
.unwrap();
assert_eq!(out["ok"], true);
assert_eq!(executor.stats(), (1, 0, 0));
}
#[test]
fn confinement_failure_degrades_loud_but_runs() {
let executor = ScopedSandboxExecutor::new(|| Err("kernel says no".to_string()));
let tool = probe();
let mut ctx = wm_core::Context::new(BrainWave::Gamma);
let out = executor
.run(&tool, &mut ctx, serde_json::json!({}))
.unwrap();
assert_eq!(out["ok"], true, "loud-degrade keeps availability up");
assert_eq!(executor.stats(), (1, 1, 0));
}
#[test]
fn tool_panic_is_contained_not_propagated() {
let executor = ScopedSandboxExecutor::new(|| Ok(()));
let mut tool = probe();
tool.panic = true;
let mut ctx = wm_core::Context::new(BrainWave::Gamma);
let result = executor.run(&tool, &mut ctx, serde_json::json!({}));
assert!(result.is_err(), "panic must surface as a tool error");
assert_eq!(executor.stats(), (1, 0, 1));
}
#[test]
fn handles_only_store_scoped_tools() {
let scoped = probe();
assert!(ScopedSandboxExecutor::handles(&scoped));
let inherited = ProbeTool {
effects: EffectRow::pure(),
..probe()
};
assert!(!ScopedSandboxExecutor::handles(&inherited));
}
#[test]
fn env_flag_parses_strictly() {
let parse = |v: Option<&str>| v.is_some_and(|s| s == "1");
assert!(parse(Some("1")));
assert!(!parse(Some("0")));
assert!(!parse(Some("true")));
assert!(!parse(None));
}
}