use std::path::{Path, PathBuf};
use agent_mesh_protocol::{Caveats, CountBound, Scope};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaveatsSource {
ConfigFile(PathBuf),
UnconfinedDefault,
MalformedConfig(PathBuf),
}
#[derive(Debug, Clone)]
pub struct GrantedCaveats {
pub caveats: Caveats,
pub source: CaveatsSource,
}
impl GrantedCaveats {
#[must_use]
pub fn load() -> Self {
let path = newt_core::Config::user_config_path();
match Self::resolve_path(path.as_deref()) {
Ok(g) => g,
Err(e) => {
eprintln!(
"WARNING: newt-mcp-server found malformed user config [caveats]: {e}; \
resource access is DENIED (fail-closed default)."
);
Self::deny_all(path.as_deref())
}
}
}
pub fn resolve(home: Option<&Path>) -> anyhow::Result<Self> {
let path = home.map(|home| home.join(".newt").join("config.toml"));
Self::resolve_path(path.as_deref())
}
fn resolve_path(path: Option<&Path>) -> anyhow::Result<Self> {
if let Some(path) = path {
if path.is_file() {
let caveats = load_from_config(path)?;
return Ok(Self {
caveats,
source: CaveatsSource::ConfigFile(path.to_path_buf()),
});
}
}
Ok(Self {
caveats: Caveats::top(),
source: CaveatsSource::UnconfinedDefault,
})
}
fn deny_all(path: Option<&Path>) -> Self {
Self {
caveats: Caveats {
fs_read: Scope::none(),
fs_write: Scope::none(),
exec: Scope::none(),
net: Scope::none(),
max_calls: CountBound::AtMost(0),
valid_for_generation: Scope::none(),
},
source: CaveatsSource::MalformedConfig(
path.unwrap_or_else(|| Path::new("/dev/failure"))
.to_path_buf(),
),
}
}
#[must_use]
pub fn banner(&self) -> String {
match &self.source {
CaveatsSource::ConfigFile(p) => {
format!(
"newt-mcp-server: shell_run confined by {} [caveats]",
p.display()
)
}
CaveatsSource::MalformedConfig(p) => {
format!(
"newt-mcp-server: shell_run is fail-closed by malformed config at {}",
p.display()
)
}
CaveatsSource::UnconfinedDefault => {
"WARNING: newt-mcp-server shell_run is running UNCONFINED \
(no user config [caveats]); shell_run can run any \
command with full ambient authority. Add a [caveats] table to \
the user config.toml (e.g. exec = { only = [\"git\", \"cargo\"] }) \
to confine it."
.to_string()
}
}
}
pub fn warn_to_stderr(&self) {
eprintln!("{}", self.banner());
}
}
#[derive(serde::Deserialize)]
struct Config {
caveats: Option<Caveats>,
}
fn load_from_config(path: &Path) -> anyhow::Result<Caveats> {
let text = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("cannot read {}: {e}", path.display()))?;
let cfg: Config = toml::from_str(&text)
.map_err(|e| anyhow::anyhow!("cannot parse {} [caveats]: {e}", path.display()))?;
Ok(cfg.caveats.unwrap_or_else(Caveats::top))
}
#[cfg(test)]
mod tests {
use super::*;
use agent_mesh_protocol::{CountBound, Scope};
fn tempdir() -> PathBuf {
let base = std::env::temp_dir().join(format!(
"newt-mcp-caveats-test-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
std::fs::create_dir_all(&base).unwrap();
base
}
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[test]
fn config_toml_is_parsed_with_the_mesh_shape() {
let dir = tempdir();
let nd = dir.join(".newt");
std::fs::create_dir_all(&nd).unwrap();
std::fs::write(
nd.join("config.toml"),
r#"
[caveats]
fs_read = "all"
fs_write = "all"
exec = { only = ["git", "cargo"] }
net = "all"
max_calls = { at_most = 5 }
valid_for_generation = "all"
"#,
)
.unwrap();
let g = GrantedCaveats::resolve(Some(&dir)).unwrap();
assert!(matches!(g.source, CaveatsSource::ConfigFile(_)));
assert_eq!(
g.caveats.exec,
Scope::only(["git".to_string(), "cargo".to_string()])
);
assert_eq!(g.caveats.max_calls, CountBound::AtMost(5));
assert!(!g.banner().starts_with("WARNING"));
}
#[test]
fn missing_config_is_unconfined_top_with_warning_banner() {
let dir = tempdir(); let g = GrantedCaveats::resolve(Some(&dir)).unwrap();
assert_eq!(g.source, CaveatsSource::UnconfinedDefault);
assert_eq!(g.caveats, Caveats::top());
assert!(g.banner().contains("UNCONFINED"), "banner: {}", g.banner());
assert!(g.banner().starts_with("WARNING"));
}
#[test]
fn missing_home_falls_through_to_unconfined_default() {
let g = GrantedCaveats::resolve(None).unwrap();
assert_eq!(g.source, CaveatsSource::UnconfinedDefault);
}
#[test]
fn config_without_caveats_table_falls_through_to_top() {
let dir = tempdir();
let nd = dir.join(".newt");
std::fs::create_dir_all(&nd).unwrap();
std::fs::write(nd.join("config.toml"), "model = \"llama3.1:8b\"\n").unwrap();
let g = GrantedCaveats::resolve(Some(&dir)).unwrap();
assert_eq!(g.caveats, Caveats::top());
assert!(matches!(g.source, CaveatsSource::ConfigFile(_)));
}
#[test]
fn malformed_config_is_an_error() {
let dir = tempdir();
let nd = dir.join(".newt");
std::fs::create_dir_all(&nd).unwrap();
std::fs::write(nd.join("config.toml"), "[caveats]\nexec = { not = valid\n").unwrap();
let err = GrantedCaveats::resolve(Some(&dir)).unwrap_err();
assert!(err.to_string().contains("config.toml"), "got: {err}");
}
}