newt-core 0.6.5

Newt-Agent core types, errors, and the NeMoCode-style tier router
Documentation
//! `Caveats` — the attenuated authority lattice consumed at every tool
//! dispatch site.
//!
//! Issue #95 collapsed the hand-mirrored `newt_core::caveats` module into a
//! re-export of [`agent_mesh_protocol::caveats`]: the signed wire types and the
//! enforcement-side types are now literally the same Rust type, so there is no
//! drift surface between them and no JSON bridge to maintain.
//!
//! What stayed in `newt-core` is the **enforcement convenience layer** —
//! per-axis `permits_*` adaptors and the `permits_one_more` budget check the
//! call sites in `newt-coder` and `newt-tui` rely on. Those aren't part of
//! `agent-mesh-protocol`'s 0.6 surface (the upstream crate ships only the
//! lattice algebra: `top`, `leq`, `meet`), so we hang them off the re-exported
//! types via extension traits ([`CaveatsExt`], [`CountBoundExt`],
//! [`ScopeExt`]). The call sites read exactly the way they did before #95:
//!
//! ```text
//!     if !caveats.permits_fs_write(path) { … }
//!     if !caveats.max_calls.permits_one_more(used) { … }
//! ```
//!
//! Path-prefix and host-suffix matching is *not* in scope here; the lattice
//! deals in set membership only. Enforcement sites (e.g. `tui_permits_path`)
//! layer prefix semantics on top.

pub use agent_mesh_protocol::caveats::{Caveats, CountBound, Scope};

/// Per-axis "permits this concrete item?" check.
///
/// `All` permits everything; `Only(s)` permits exactly the members of `s`.
/// Defined as a trait because the upstream `agent-mesh-protocol::Scope` ships
/// only the lattice algebra; this is the dispatch-site adaptor. Constructors
/// (`Scope::only`, `Scope::none`) are inherent on the upstream type — no
/// re-definition needed.
pub trait ScopeExt<T: Ord + Clone> {
    /// Does this scope authorize `item`?
    fn permits(&self, item: &T) -> bool;
}

impl<T: Ord + Clone> ScopeExt<T> for Scope<T> {
    fn permits(&self, item: &T) -> bool {
        match self {
            Self::All => true,
            Self::Only(set) => set.contains(item),
        }
    }
}

/// "Does this bound permit one more call?" — the dispatch-site form of the
/// `max_calls` axis. Defined as an extension trait because the upstream
/// `CountBound` type ships only the lattice operations.
pub trait CountBoundExt {
    /// Does this bound permit one more call when `used_so_far` calls have
    /// already been counted against it?
    ///
    /// `Unlimited` always permits; `AtMost(n)` permits iff `used_so_far < n`.
    fn permits_one_more(&self, used_so_far: u64) -> bool;
}

impl CountBoundExt for CountBound {
    fn permits_one_more(&self, used_so_far: u64) -> bool {
        match self {
            Self::Unlimited => true,
            Self::AtMost(n) => used_so_far < *n,
        }
    }
}

/// Per-axis "permits this concrete item?" adaptors for the [`Caveats`]
/// lattice. These don't change the algebra; they're convenience adaptors so
/// dispatch sites read like prose.
pub trait CaveatsExt {
    /// Does this authority permit reading `path`?
    ///
    /// `path` is matched by exact string equality against the members of
    /// `fs_read` (or `All` accepts everything). Path-prefix semantics are out
    /// of scope at this layer — see the module docs.
    fn permits_fs_read(&self, path: &str) -> bool;

    /// Does this authority permit writing `path`?
    fn permits_fs_write(&self, path: &str) -> bool;

    /// Does this authority permit executing `cmd`?
    fn permits_exec(&self, cmd: &str) -> bool;

    /// Does this authority permit a network call to `host`?
    fn permits_net(&self, host: &str) -> bool;
}

impl CaveatsExt for Caveats {
    fn permits_fs_read(&self, path: &str) -> bool {
        self.fs_read.permits(&path.to_string())
    }

    fn permits_fs_write(&self, path: &str) -> bool {
        self.fs_write.permits(&path.to_string())
    }

    fn permits_exec(&self, cmd: &str) -> bool {
        self.exec.permits(&cmd.to_string())
    }

    fn permits_net(&self, host: &str) -> bool {
        self.net.permits(&host.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn scope_all_permits_everything() {
        let s: Scope<String> = Scope::All;
        assert!(s.permits(&"anything".to_string()));
        assert!(s.permits(&"".to_string()));
    }

    #[test]
    fn scope_only_permits_exact_members() {
        let s = Scope::<String>::only(["a".to_string(), "b".to_string()]);
        assert!(s.permits(&"a".to_string()));
        assert!(s.permits(&"b".to_string()));
        assert!(!s.permits(&"c".to_string()));
        assert!(!s.permits(&"".to_string()));
    }

    #[test]
    fn scope_none_permits_nothing() {
        let s: Scope<String> = Scope::none();
        assert!(!s.permits(&"a".to_string()));
    }

    #[test]
    fn count_bound_permits_one_more() {
        assert!(CountBound::Unlimited.permits_one_more(0));
        assert!(CountBound::Unlimited.permits_one_more(99_999));
        assert!(CountBound::AtMost(3).permits_one_more(0));
        assert!(CountBound::AtMost(3).permits_one_more(2));
        assert!(!CountBound::AtMost(3).permits_one_more(3));
        assert!(!CountBound::AtMost(3).permits_one_more(99));
        // Edge: AtMost(0) refuses immediately.
        assert!(!CountBound::AtMost(0).permits_one_more(0));
    }

    #[test]
    fn caveats_top_permits_everything() {
        let c = Caveats::top();
        assert!(c.permits_fs_read("/anywhere"));
        assert!(c.permits_fs_write("/anywhere"));
        assert!(c.permits_exec("rm"));
        assert!(c.permits_net("evil.example.com"));
        assert!(c.max_calls.permits_one_more(1_000_000));
    }

    #[test]
    fn caveats_attenuated_denies_outside_scope() {
        let c = Caveats {
            fs_write: Scope::only(["allowed.rs".to_string()]),
            net: Scope::only(["allowed.example.com".to_string()]),
            max_calls: CountBound::AtMost(2),
            ..Caveats::top()
        };
        assert!(c.permits_fs_write("allowed.rs"));
        assert!(!c.permits_fs_write("forbidden.rs"));
        assert!(c.permits_net("allowed.example.com"));
        assert!(!c.permits_net("evil.example.com"));
        assert!(c.max_calls.permits_one_more(1));
        assert!(!c.max_calls.permits_one_more(2));
    }
}