use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use mnemo_mcp::role_filter::RoleFilterConfig;
use serde::Deserialize;
use thiserror::Error;
#[derive(Debug, Clone, Deserialize)]
pub struct Manifest {
pub keystore_path: PathBuf,
pub audit_log_path: PathBuf,
#[serde(default = "default_allowed_tools")]
pub allowed_tools: BTreeSet<String>,
#[serde(default)]
pub allowed_agents: BTreeSet<String>,
#[serde(default = "default_allowed_parents")]
pub allowed_parents: BTreeSet<String>,
#[serde(default)]
pub tool_catalog_pin_path: Option<PathBuf>,
#[serde(default)]
pub allow_removed_drift: bool,
#[serde(default)]
pub role_filter: Option<RoleFilterConfig>,
}
fn default_allowed_tools() -> BTreeSet<String> {
["mnemo.recall", "mnemo.verify"]
.into_iter()
.map(String::from)
.collect()
}
fn default_allowed_parents() -> BTreeSet<String> {
["claude", "claude-code", "systemd", "supervisord"]
.into_iter()
.map(String::from)
.collect()
}
#[derive(Debug, Error)]
pub enum ManifestError {
#[error("manifest file not found at {path}")]
NotFound { path: PathBuf },
#[error("manifest IO error: {0}")]
Io(#[from] std::io::Error),
#[error("manifest TOML parse error: {0}")]
Parse(#[from] toml::de::Error),
#[error("manifest field {field} is invalid: {reason}")]
Invalid { field: &'static str, reason: String },
}
impl Manifest {
pub fn load(path: &Path) -> Result<Self, ManifestError> {
if !path.exists() {
return Err(ManifestError::NotFound { path: path.into() });
}
let text = std::fs::read_to_string(path)?;
let m: Manifest = toml::from_str(&text)?;
m.validate()?;
Ok(m)
}
fn validate(&self) -> Result<(), ManifestError> {
for t in &self.allowed_tools {
if !KNOWN_TOOLS.contains(&t.as_str()) {
return Err(ManifestError::Invalid {
field: "allowed_tools",
reason: format!("unknown tool {:?}; valid: {:?}", t, KNOWN_TOOLS),
});
}
}
Ok(())
}
}
pub const KNOWN_TOOLS: &[&str] = &[
"mnemo.remember",
"mnemo.recall",
"mnemo.reflect",
"mnemo.forget_subject",
"mnemo.export_audit_log",
"mnemo.verify",
];
#[derive(Debug, Deserialize)]
pub struct Keystore {
pub key_id: String,
pub key_hex: String,
}
impl Keystore {
pub fn load(path: &Path) -> Result<Self, ManifestError> {
if !path.exists() {
return Err(ManifestError::NotFound { path: path.into() });
}
let text = std::fs::read_to_string(path)?;
let k: Keystore = toml::from_str(&text)?;
if k.key_id.is_empty() {
return Err(ManifestError::Invalid {
field: "key_id",
reason: "must not be empty".into(),
});
}
let bytes = hex::decode(&k.key_hex).map_err(|e| ManifestError::Invalid {
field: "key_hex",
reason: format!("not valid hex: {e}"),
})?;
if bytes.len() < 32 {
return Err(ManifestError::Invalid {
field: "key_hex",
reason: format!("must decode to >= 32 bytes (got {})", bytes.len()),
});
}
Ok(k)
}
pub fn key_bytes(&self) -> Result<Vec<u8>, ManifestError> {
hex::decode(&self.key_hex).map_err(|e| ManifestError::Invalid {
field: "key_hex",
reason: format!("not valid hex: {e}"),
})
}
}