use serde::{Deserialize, Serialize};
pub const MANIFEST_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct HarnessManifest {
pub version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agents: Option<AgentSet>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skills: Option<SkillSet>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<InstructionLayers>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<StyleSelection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp: Option<McpServers>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub models: Option<ModelTiers>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AgentSet {
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub source: ContentSource,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SkillSet {
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub source: ContentSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ContentSource {
#[default]
Bundled,
Catalog,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct InstructionLayers {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contextual: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<bool>,
}
impl InstructionLayers {
#[must_use]
pub fn merge(self, higher: InstructionLayers) -> InstructionLayers {
InstructionLayers {
system: higher.system.or(self.system),
contextual: higher.contextual.or(self.contextual),
domain: higher.domain.or(self.domain),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct StyleSelection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct McpServers {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trusty_memory: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trusty_search: Option<bool>,
}
impl McpServers {
#[must_use]
pub fn merge(self, higher: McpServers) -> McpServers {
McpServers {
trusty_memory: higher.trusty_memory.or(self.trusty_memory),
trusty_search: higher.trusty_search.or(self.trusty_search),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ModelTiers {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lightweight: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub standard: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub high: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intensive: Option<String>,
}
impl HarnessManifest {
pub fn from_toml(raw: &str) -> Result<Self, toml::de::Error> {
toml::from_str(raw)
}
pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
toml::to_string(self)
}
#[must_use]
pub fn merge(self, higher: HarnessManifest) -> HarnessManifest {
HarnessManifest {
version: higher.version.or(self.version),
agents: higher.agents.or(self.agents),
skills: higher.skills.or(self.skills),
style: higher.style.or(self.style),
models: higher.models.or(self.models),
instructions: merge_optional(self.instructions, higher.instructions, |lo, hi| {
lo.merge(hi)
}),
mcp: merge_optional(self.mcp, higher.mcp, |lo, hi| lo.merge(hi)),
}
}
}
fn merge_optional<T>(lower: Option<T>, higher: Option<T>, f: impl FnOnce(T, T) -> T) -> Option<T> {
match (lower, higher) {
(Some(lo), Some(hi)) => Some(f(lo, hi)),
(lo, hi) => hi.or(lo),
}
}
pub fn selection_matches(name: &str, include: &[String], exclude: &[String]) -> bool {
let included = include.is_empty() || include.iter().any(|p| glob_matches(p, name));
let excluded = exclude.iter().any(|p| glob_matches(p, name));
included && !excluded
}
fn glob_matches(pattern: &str, name: &str) -> bool {
if !pattern.contains('*') {
return pattern == name;
}
if pattern == "*" {
return true;
}
let segments: Vec<&str> = pattern.split('*').collect();
let anchored_start = !segments.first().is_none_or(|s| s.is_empty());
let anchored_end = !segments.last().is_none_or(|s| s.is_empty());
let mut rest = name;
let non_empty: Vec<&str> = segments.iter().copied().filter(|s| !s.is_empty()).collect();
for (idx, seg) in non_empty.iter().enumerate() {
let is_first = idx == 0;
let is_last = idx == non_empty.len() - 1;
if is_first && anchored_start {
match rest.strip_prefix(seg) {
Some(tail) => rest = tail,
None => return false,
}
} else if is_last && anchored_end {
return rest.ends_with(seg);
} else {
match rest.find(seg) {
Some(pos) => rest = &rest[pos + seg.len()..],
None => return false,
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_roundtrip() {
let manifest = HarnessManifest {
version: Some(MANIFEST_VERSION),
agents: Some(AgentSet {
include: vec!["rust-engineer".into(), "qa".into()],
exclude: vec!["*-ops".into()],
source: ContentSource::Catalog,
}),
skills: Some(SkillSet {
include: vec!["example-skill".into()],
exclude: vec![],
source: ContentSource::Bundled,
}),
instructions: Some(InstructionLayers {
system: Some(true),
contextual: Some(true),
domain: Some(false),
}),
style: Some(StyleSelection {
active: Some("trusty-mpm-teacher".into()),
}),
mcp: Some(McpServers {
trusty_memory: Some(true),
trusty_search: Some(false),
}),
models: Some(ModelTiers {
lightweight: Some("haiku".into()),
standard: None,
high: None,
intensive: Some("opus".into()),
}),
};
let toml = manifest.to_toml().expect("serialize");
let parsed = HarnessManifest::from_toml(&toml).expect("parse");
assert_eq!(manifest, parsed);
}
#[test]
fn manifest_partial_parse() {
let toml = r#"
version = 1
[style]
active = "trusty-mpm-research"
"#;
let parsed = HarnessManifest::from_toml(toml).expect("partial parse");
assert_eq!(parsed.version, Some(1));
assert_eq!(
parsed.style.as_ref().and_then(|s| s.active.as_deref()),
Some("trusty-mpm-research")
);
assert!(parsed.agents.is_none());
assert!(parsed.skills.is_none());
assert!(parsed.mcp.is_none());
}
#[test]
fn manifest_merge_overrides() {
let lower = HarnessManifest {
version: Some(1),
agents: Some(AgentSet {
include: vec!["a".into()],
..AgentSet::default()
}),
style: Some(StyleSelection {
active: Some("trusty-mpm".into()),
}),
mcp: Some(McpServers {
trusty_memory: Some(true),
trusty_search: Some(true),
}),
..HarnessManifest::default()
};
let higher = HarnessManifest {
style: Some(StyleSelection {
active: Some("trusty-mpm-teacher".into()),
}),
..HarnessManifest::default()
};
let merged = lower.merge(higher);
assert_eq!(
merged.style.as_ref().and_then(|s| s.active.as_deref()),
Some("trusty-mpm-teacher")
);
assert_eq!(
merged.agents.as_ref().map(|a| a.include.clone()),
Some(vec!["a".to_string()])
);
assert_eq!(
merged.mcp.as_ref().and_then(|m| m.trusty_search),
Some(true)
);
}
#[test]
fn manifest_merge_keeps_lower_when_absent() {
let lower = HarnessManifest {
version: Some(1),
style: Some(StyleSelection {
active: Some("trusty-mpm".into()),
}),
..HarnessManifest::default()
};
let merged = lower.clone().merge(HarnessManifest::default());
assert_eq!(lower, merged);
}
#[test]
fn manifest_merge_mcp_field_level() {
let lower = HarnessManifest {
mcp: Some(McpServers {
trusty_memory: Some(true),
trusty_search: Some(true),
}),
..HarnessManifest::default()
};
let higher = HarnessManifest {
mcp: Some(McpServers {
trusty_memory: None,
trusty_search: Some(false),
}),
..HarnessManifest::default()
};
let merged = lower.merge(higher);
let mcp = merged.mcp.expect("mcp section present");
assert_eq!(
mcp.trusty_memory,
Some(true),
"the unmentioned toggle must survive the partial override"
);
assert_eq!(
mcp.trusty_search,
Some(false),
"the overridden toggle must take the higher layer's value"
);
}
#[test]
fn instruction_layers_field_merge() {
let lower = HarnessManifest {
instructions: Some(InstructionLayers {
system: Some(true),
contextual: Some(true),
domain: Some(true),
}),
..HarnessManifest::default()
};
let higher = HarnessManifest {
instructions: Some(InstructionLayers {
system: None,
contextual: None,
domain: Some(false),
}),
..HarnessManifest::default()
};
let layers = lower.merge(higher).instructions.expect("layers present");
assert_eq!(layers.system, Some(true), "system inherited from lower");
assert_eq!(
layers.contextual,
Some(true),
"contextual inherited from lower"
);
assert_eq!(layers.domain, Some(false), "domain overridden by higher");
}
#[test]
fn selection_matches() {
assert!(super::selection_matches("anything", &[], &[]));
let inc = vec!["rust-engineer".to_string()];
assert!(super::selection_matches("rust-engineer", &inc, &[]));
assert!(!super::selection_matches("qa", &inc, &[]));
let glob_inc = vec!["*-engineer".to_string()];
assert!(super::selection_matches("rust-engineer", &glob_inc, &[]));
assert!(!super::selection_matches("rust-ops", &glob_inc, &[]));
let multi_inc = vec!["*-engineer-*".to_string()];
assert!(super::selection_matches(
"rust-engineer-ops",
&multi_inc,
&[]
));
assert!(!super::selection_matches("rust-engineer", &multi_inc, &[]));
let exc = vec!["rust-engineer".to_string()];
assert!(!super::selection_matches("rust-engineer", &[], &exc));
let inc_and_exc = ["rust-engineer".to_string()];
assert!(!super::selection_matches(
"rust-engineer",
&inc_and_exc,
&exc
));
}
#[test]
fn glob_matching() {
assert!(super::glob_matches("*", "anything"));
assert!(super::glob_matches("*", ""));
assert!(super::glob_matches("rust-engineer", "rust-engineer"));
assert!(!super::glob_matches("rust-engineer", "qa"));
assert!(!super::glob_matches("rust-engineer", "rust-engineer-2"));
assert!(super::glob_matches("*-engineer", "rust-engineer"));
assert!(!super::glob_matches("*-engineer", "rust-engineer-ops"));
assert!(super::glob_matches("engineer-*", "engineer-rust"));
assert!(!super::glob_matches("engineer-*", "rust-engineer"));
assert!(super::glob_matches("rust-*", "rust-engineer"));
assert!(super::glob_matches("a*b", "axxb"));
assert!(super::glob_matches("a*b", "ab"), "interior * matches empty");
assert!(!super::glob_matches("a*b", "axx"));
assert!(super::glob_matches("*-engineer-*", "rust-engineer-ops"));
assert!(super::glob_matches("*-engineer-*", "x-engineer-y"));
assert!(
!super::glob_matches("*-engineer-*", "rust-engineer"),
"no trailing segment after -engineer- → no match"
);
assert!(super::glob_matches("a*b*c", "aXbYc"));
assert!(super::glob_matches("a*b*c", "abc"));
assert!(
!super::glob_matches("a*b*c", "acb"),
"segments must appear in order"
);
assert!(
!super::glob_matches("a*b*c", "aXbY"),
"missing the trailing c suffix"
);
}
#[test]
fn content_source_roundtrip() {
let serialized = toml::to_string(&AgentSet {
source: ContentSource::Catalog,
..AgentSet::default()
})
.unwrap();
assert!(serialized.contains("catalog"));
}
#[test]
fn agent_set_source_default_is_bundled() {
let toml = "[agents]\ninclude = []\n";
let parsed = HarnessManifest::from_toml(toml).unwrap();
assert_eq!(parsed.agents.unwrap().source, ContentSource::Bundled);
}
}