1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct MuragentManifest {
10 pub schema: String,
11 pub exported_at: String,
12 pub exporter: ExporterInfo,
13 pub agent: AgentRef,
14 pub required_surfaces: Vec<Surface>,
15 #[serde(default)]
16 pub optional_capabilities: Vec<String>,
17 #[serde(default)]
18 pub mcp_servers: Vec<McpServerRef>,
19 pub icon: IconHashes,
20 #[serde(default)]
21 pub sanitized: SanitizedReport,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub hub: Option<HubBlock>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub commander: Option<CommanderBlock>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub deployment: Option<serde_json::Value>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub assignment: Option<serde_json::Value>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ExporterInfo {
36 pub mur_version: String,
37 pub tool: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub min_hub_version: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub min_commander_version: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct AgentRef {
46 pub slug: String,
47 pub display_name: String,
48 pub bundle_id: String,
49 pub url_scheme: String,
50 pub original_uuid: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum Surface {
56 Hub,
57 Commander,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct McpServerRef {
62 pub name: String,
63 pub command_basename: String,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct IconHashes {
68 #[serde(default)]
69 pub formats: Vec<String>,
70 #[serde(default)]
71 pub hash: IconHashMap,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct IconHashMap {
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub icns: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub ico: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub png: Option<String>,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct SanitizedReport {
86 #[serde(default)]
87 pub removed_fields: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct HubBlock {
94 pub appearance: HubAppearance,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub voice: Option<HubVoice>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub pet: Option<HubPet>,
99 #[serde(default)]
100 pub url_scheme_overrides: Vec<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct HubAppearance {
105 pub style_preset: String,
106 pub behavior_preset: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct HubVoice {
111 pub enabled: bool,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct HubPet {
116 pub enabled: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct CommanderBlock {
123 pub chat_platforms: Vec<String>,
124 #[serde(default)]
125 pub workflows: Vec<CommanderWorkflowRef>,
126 #[serde(default)]
127 pub programs: Vec<CommanderProgramRef>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub jira: Option<CommanderJira>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub sub_agents: Option<CommanderSubAgents>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub schedule_defaults: Option<CommanderScheduleDefaults>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct CommanderWorkflowRef {
138 pub name: String,
139 pub file: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub schedule: Option<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct CommanderProgramRef {
146 pub file: String,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct CommanderJira {
151 pub base_url: String,
152 pub secret: String,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CommanderSubAgents {
157 pub max_concurrent: u32,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct CommanderScheduleDefaults {
162 pub timezone: String,
163}
164
165impl MuragentManifest {
168 pub fn is_v2(&self) -> bool {
170 self.schema == "mur-agent/2"
171 }
172
173 pub fn validate_bundle_id(&self) -> Result<(), String> {
175 let expected = format!("run.mur.agent.{}", self.agent.slug);
176 if self.agent.bundle_id != expected {
177 return Err(format!(
178 "bundle_id '{}' does not match expected '{}'",
179 self.agent.bundle_id, expected
180 ));
181 }
182 Ok(())
183 }
184}