1use serde::{Deserialize, Serialize};
7
8use crate::agent::{PatternFilter, SnapshotPolicy};
9
10pub const MANIFEST_API_VERSION: &str = "mur.run/v1";
11pub const MANIFEST_KIND_AGENT: &str = "AgentManifest";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct AgentManifest {
17 pub api_version: String,
19 pub kind: String,
21 pub metadata: ManifestMetadata,
22 pub spec: ManifestSpec,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub struct ManifestMetadata {
27 pub name: String,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub workspace: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33#[serde(rename_all = "camelCase")]
34pub struct ManifestSpec {
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub profile_ref: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub sys_prompt_ref: Option<String>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub skills: Vec<String>,
41 #[serde(default)]
42 pub patterns: ManifestPatterns,
43 #[serde(default)]
44 pub resources: ManifestResources,
45 #[serde(default)]
46 pub entitlements: ManifestEntitlements,
47 #[serde(default)]
48 pub federation: ManifestFederation,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52#[serde(rename_all = "camelCase")]
53pub struct ManifestPatterns {
54 #[serde(default)]
55 pub filter: PatternFilter,
56 #[serde(default)]
57 pub snapshot_policy: SnapshotPolicy,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, Default)]
61#[serde(rename_all = "camelCase")]
62pub struct ManifestResources {
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub token_budget_per_day_usd: Option<f64>,
65 #[serde(default)]
66 pub max_concurrent_sessions: u32,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70#[serde(rename_all = "camelCase")]
71pub struct ManifestEntitlements {
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub network: Vec<String>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub filesystem_read: Vec<String>,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub filesystem_write: Vec<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
81#[serde(rename_all = "camelCase")]
82pub struct ManifestFederation {
83 #[serde(default)]
84 pub sync_interval_minutes: u32,
85}
86
87impl AgentManifest {
88 pub fn from_yaml(yaml: &str) -> anyhow::Result<Self> {
90 let m: AgentManifest = serde_yaml_ng::from_str(yaml)?;
91 anyhow::ensure!(
92 m.api_version == MANIFEST_API_VERSION,
93 "expected apiVersion '{}', got '{}'",
94 MANIFEST_API_VERSION,
95 m.api_version
96 );
97 anyhow::ensure!(
98 m.kind == MANIFEST_KIND_AGENT,
99 "expected kind '{}', got '{}'",
100 MANIFEST_KIND_AGENT,
101 m.kind
102 );
103 Ok(m)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 const EXAMPLE: &str = r#"
112apiVersion: mur.run/v1
113kind: AgentManifest
114metadata:
115 name: code-reviewer
116 workspace: default
117spec:
118 sysPromptRef: sys_prompt.md
119 skills:
120 - skills/review-rust.md
121 patterns:
122 filter:
123 tier: [project, core]
124 importanceMin: 0.5
125 snapshotPolicy: pull-on-start
126 resources:
127 tokenBudgetPerDayUsd: 5.0
128 maxConcurrentSessions: 3
129 entitlements:
130 network: [github.com]
131 filesystemRead: [/Users/david/Projects]
132 federation:
133 syncIntervalMinutes: 15
134"#;
135
136 #[test]
137 fn parse_example_manifest() {
138 let m = AgentManifest::from_yaml(EXAMPLE).unwrap();
139 assert_eq!(m.metadata.name, "code-reviewer");
140 assert_eq!(m.spec.skills.len(), 1);
141 assert!((m.spec.resources.token_budget_per_day_usd.unwrap() - 5.0).abs() < f64::EPSILON);
142 assert_eq!(m.spec.federation.sync_interval_minutes, 15);
143 }
144
145 #[test]
146 fn rejects_wrong_api_version() {
147 let bad = EXAMPLE.replace("mur.run/v1", "mur.run/v2");
148 assert!(AgentManifest::from_yaml(&bad).is_err());
149 }
150
151 #[test]
152 fn rejects_wrong_kind() {
153 let bad = EXAMPLE.replace("AgentManifest", "WorkflowManifest");
154 assert!(AgentManifest::from_yaml(&bad).is_err());
155 }
156}