nornir 0.1.0

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! AI-agent guard rails.
//!
//! Enforces the `[guard].forbidden` list from `nornir.toml` at the
//! filesystem level via `chmod -w`. A misbehaving agent (or a tired
//! human) trying to edit a protected file gets `EACCES` rather than
//! silently corrupting append-only history or generated docs.
//!
//! Operations:
//! - [`apply`]   — chmod -w on every forbidden path that exists
//! - [`release`] — chmod +w on every forbidden path (for intentional edits)
//! - [`status`]  — report current writable bit for each path
//!
//! All paths in the `forbidden` list are interpreted relative to the
//! supplied `workspace_root` (typically the dir containing
//! `workspace_holger/`, `holger/`, `znippy/`, ...).

use std::path::{Path, PathBuf};

use anyhow::{Context, Result};

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PathStatus {
    pub path: PathBuf,
    pub exists: bool,
    pub writable: bool,
    pub changed: bool,
}

pub fn status(workspace_root: &Path, forbidden: &[String]) -> Vec<PathStatus> {
    forbidden
        .iter()
        .map(|rel| {
            let p = workspace_root.join(rel);
            let (exists, writable) = inspect(&p);
            PathStatus { path: p, exists, writable, changed: false }
        })
        .collect()
}

pub fn apply(workspace_root: &Path, forbidden: &[String]) -> Result<Vec<PathStatus>> {
    chmod_each(workspace_root, forbidden, false)
}

pub fn release(workspace_root: &Path, forbidden: &[String]) -> Result<Vec<PathStatus>> {
    chmod_each(workspace_root, forbidden, true)
}

fn inspect(p: &Path) -> (bool, bool) {
    match std::fs::metadata(p) {
        Ok(m) => (true, !m.permissions().readonly()),
        Err(_) => (false, false),
    }
}

#[cfg(unix)]
fn chmod_each(
    workspace_root: &Path,
    forbidden: &[String],
    writable: bool,
) -> Result<Vec<PathStatus>> {
    use std::os::unix::fs::PermissionsExt;
    let mut out = Vec::new();
    for rel in forbidden {
        let p = workspace_root.join(rel);
        if !p.exists() {
            out.push(PathStatus { path: p, exists: false, writable: false, changed: false });
            continue;
        }
        let meta = std::fs::metadata(&p)
            .with_context(|| format!("stat {}", p.display()))?;
        let before = !meta.permissions().readonly();
        let mut perms = meta.permissions();
        let mode = perms.mode();
        let new_mode = if writable {
            mode | 0o200 // u+w
        } else {
            mode & !0o222 // strip write everywhere
        };
        perms.set_mode(new_mode);
        std::fs::set_permissions(&p, perms)
            .with_context(|| format!("chmod {}", p.display()))?;
        let (_, after) = inspect(&p);
        out.push(PathStatus {
            path: p,
            exists: true,
            writable: after,
            changed: before != after,
        });
    }
    Ok(out)
}

#[cfg(not(unix))]
fn chmod_each(
    workspace_root: &Path,
    forbidden: &[String],
    writable: bool,
) -> Result<Vec<PathStatus>> {
    let mut out = Vec::new();
    for rel in forbidden {
        let p = workspace_root.join(rel);
        if !p.exists() {
            out.push(PathStatus { path: p, exists: false, writable: false, changed: false });
            continue;
        }
        let meta = std::fs::metadata(&p)?;
        let before = !meta.permissions().readonly();
        let mut perms = meta.permissions();
        perms.set_readonly(!writable);
        std::fs::set_permissions(&p, perms)?;
        let (_, after) = inspect(&p);
        out.push(PathStatus {
            path: p,
            exists: true,
            writable: after,
            changed: before != after,
        });
    }
    Ok(out)
}