use serde::{Deserialize, Serialize};
use crate::ToolCapability;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FootprintProvenance {
BuiltIn,
UserConfig,
McpSelfDeclared,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpawnClass {
#[default]
None,
Sandboxed,
Privileged,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ResourceSet {
pub workspace_read: bool,
pub network_read: bool,
pub workspace_write: bool,
pub network_write: bool,
}
impl ResourceSet {
#[must_use]
pub fn is_empty(&self) -> bool {
!self.workspace_write && !self.network_write
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Footprint {
pub reads: ResourceSet,
pub writes: ResourceSet,
pub spawns: SpawnClass,
}
impl Footprint {
#[must_use]
pub fn writes_workspace_state(&self) -> bool {
self.writes.workspace_write
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolManifest {
pub name: String,
pub footprint: Footprint,
pub provenance: FootprintProvenance,
}
impl ToolManifest {
#[must_use]
pub fn derive_conservative(
name: &str,
caps: &[ToolCapability],
unified_writes_state: bool,
provenance: FootprintProvenance,
) -> Self {
Self {
name: name.to_string(),
footprint: derive_conservative_footprint(caps, unified_writes_state),
provenance,
}
}
}
#[must_use]
pub fn derive_conservative_footprint(
caps: &[ToolCapability],
unified_writes_state: bool,
) -> Footprint {
let read_only = caps.contains(&ToolCapability::ReadOnly);
let writes_files = caps.contains(&ToolCapability::WritesFiles);
let executes = caps.contains(&ToolCapability::ExecutesCode);
let network = caps.contains(&ToolCapability::Network);
let writes = ResourceSet {
workspace_write: unified_writes_state || writes_files || executes,
network_write: network && !read_only,
..Default::default()
};
let reads = ResourceSet {
workspace_read: read_only || writes_files || executes || !network,
network_read: network,
..Default::default()
};
let spawns = if executes {
SpawnClass::Sandboxed
} else {
SpawnClass::None
};
Footprint {
reads,
writes,
spawns,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_only_file_tool_has_empty_writes() {
let fp = derive_conservative_footprint(&[ToolCapability::ReadOnly], false);
assert!(fp.writes.is_empty());
assert!(fp.reads.workspace_read);
assert!(!fp.writes_workspace_state());
}
#[test]
fn unified_writes_state_union_covers_exec_shell() {
let caps = vec![ToolCapability::ExecutesCode, ToolCapability::Sandboxable];
let without = derive_conservative_footprint(&caps, false);
assert!(without.writes_workspace_state());
let with_union = derive_conservative_footprint(&caps, true);
assert!(with_union.writes_workspace_state());
assert_eq!(with_union.spawns, SpawnClass::Sandboxed);
}
#[test]
fn write_file_footprint_is_workspace_write() {
let fp = derive_conservative_footprint(&[ToolCapability::WritesFiles], true);
assert!(fp.writes.workspace_write);
assert!(!fp.writes.is_empty());
}
#[test]
fn mcp_network_tool_defaults_network_write_when_not_read_only() {
let fp = derive_conservative_footprint(
&[ToolCapability::Network, ToolCapability::RequiresApproval],
false,
);
assert!(fp.reads.network_read);
assert!(fp.writes.network_write);
}
}