use std::ffi::OsString;
use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::Parser as _;
use serde_json::Value;
use super::servers::{self, Scan};
use super::{Pins, pins, set_up_hosts, wrap};
use crate::cli::{Cli, Command, McpCommand, McpProxyArgs};
use crate::home::Home;
use crate::integrity;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Launch {
pub server: String,
pub sandbox: bool,
pub writes: Vec<PathBuf>,
pub command: Vec<OsString>,
}
impl Launch {
pub fn of(args: &McpProxyArgs) -> Self {
Self {
server: args.server.clone(),
sandbox: args.sandbox,
writes: args.writes.clone(),
command: args.command.clone(),
}
}
pub fn of_entry(entry: &Value) -> Option<Self> {
let args = wrap::proxy_args(entry)?;
let cli = Cli::try_parse_from(std::iter::once("openmoat".to_owned()).chain(args)).ok()?;
match cli.command? {
Command::Mcp {
command: McpCommand::Proxy(proxy),
} => Some(Self::of(&proxy)),
_ => None,
}
}
fn shown(&self) -> String {
self.command
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(" ")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Startup {
Start,
Refuse(String),
}
pub fn check(home: &Home, launch: &Launch, cwd: Option<&Path>) -> Startup {
gather(home, launch, cwd).unwrap_or_else(|e| {
Startup::Refuse(format!(
"OpenMoat cannot check MCP server `{}` before starting it ({e:#}), so it does not \
start it",
launch.server
))
})
}
fn gather(home: &Home, launch: &Launch, cwd: Option<&Path>) -> Result<Startup> {
let name = &launch.server;
let verified = match integrity::check(home) {
Ok(verified) => verified,
Err(e) => {
return Ok(Startup::Refuse(format!(
"the policy lock cannot be read ({e:#}), so OpenMoat trusts no MCP server \
definition and does not start `{name}`"
)));
}
};
if let Some(deny) = verified.checked.deny {
return Ok(Startup::Refuse(format!(
"the policy lock shows drift ({}), so OpenMoat trusts no MCP server definition and \
does not start `{name}`; run `openmoat doctor` in a terminal",
deny.reasons.join("; ")
)));
}
if let Err(e) = verified.home.load_policy() {
return Ok(Startup::Refuse(format!(
"the policy cannot be read ({e:#}), so OpenMoat trusts no MCP server definition and \
does not start `{name}`"
)));
}
let Some(pins) = verified.lock.mcp_servers.as_ref() else {
return Ok(Startup::Refuse(format!(
"OpenMoat has not pinned the agents' MCP server definitions yet, so `{name}` is not \
accepted; run `openmoat mcp accept {name}` in a terminal"
)));
};
let binary = crate::install::hook_binary()?;
let mut sources = Vec::new();
for host in set_up_hosts(home, &binary) {
sources.extend(servers::sources(home, host, cwd)?);
}
let scan = servers::scan(&sources);
Ok(decide(&scan, pins, launch, |var| {
std::env::var_os(var).is_some()
}))
}
pub fn decide(
scan: &Scan,
pins: &Pins,
launch: &Launch,
has_env: impl Fn(&str) -> bool,
) -> Startup {
let name = &launch.server;
let accept = format!("a person must review it: run `openmoat mcp accept {name}` in a terminal");
if let Some((path, why)) = scan.unreadable.first() {
return Startup::Refuse(format!(
"{} cannot be read ({why}), so OpenMoat cannot tell which definition starts MCP \
server `{name}` and does not start it; fix the file, then {accept}",
path.display()
));
}
let candidates: Vec<&servers::Server> = scan
.servers
.iter()
.filter(|s| Launch::of_entry(&s.entry).as_ref() == Some(launch))
.filter(|s| env_names(&s.entry).all(&has_env))
.collect();
if candidates.is_empty() {
return Startup::Refuse(format!(
"MCP server `{name}` is new: no definition a person accepted starts `{}`, so OpenMoat \
does not start it; {accept}",
launch.shown()
));
}
let changes = pins::changes(pins, scan);
let pending: Vec<String> = candidates
.iter()
.filter_map(|server| {
changes
.iter()
.find(|c| c.id == server.id && c.blocks())
.map(pins::Change::describe)
})
.collect();
if pending.is_empty() {
return Startup::Start;
}
Startup::Refuse(format!(
"{}; OpenMoat starts it only once a person has reviewed it: run `openmoat mcp accept \
{name}` in a terminal",
pending.join("; ")
))
}
fn env_names(entry: &Value) -> impl Iterator<Item = &str> {
entry
.get("env")
.and_then(Value::as_object)
.into_iter()
.flat_map(|env| env.keys().map(String::as_str))
}
#[cfg(test)]
mod tests;