1pub mod bundle;
2pub mod inventory;
3pub mod policy;
4pub mod prompt;
5
6use std::path::PathBuf;
7
8use crate::compiler::tool_names::{ToolProjectionStatus, project_tool_for_harness};
9use bundle::{LaunchBundle, ScaffoldSlots, Skills, ToolsSpec};
10use policy::{PolicyInput, resolve_policy};
11use prompt::compile_prompt_surface;
12
13use crate::cli::MarsContext;
14use crate::compiler::agents::{AgentProfile, parse_agent_content};
15use crate::compiler::harness_descriptor::CompilerHarnessDescriptor;
16use crate::config::EffectiveProjectConfig;
17use crate::error::{ConfigError, MarsError};
18use crate::frontmatter::SkillsSpec;
19
20pub const LAUNCH_BUNDLE_VERSION: u32 = 3;
21
22pub struct LaunchBundleRequest {
23 pub agent: Option<String>,
24 pub model: Option<String>,
25 pub harness: Option<String>,
26 pub effort: Option<String>,
27 pub approval: Option<String>,
28 pub sandbox: Option<String>,
29 pub extra_skills: Vec<String>,
30 pub models_refresh: crate::models::ModelsRefreshControl,
31}
32
33pub fn build_launch_bundle(
34 ctx: &MarsContext,
35 request: LaunchBundleRequest,
36) -> Result<LaunchBundle, MarsError> {
37 let mut warnings: Vec<String> = Vec::new();
38 let profile: AgentProfile;
39 let agent_body: Option<String>;
40
41 if let Some(agent) = request.agent.as_deref() {
42 let agent_path = agent_file_path(&ctx.project_root, agent);
43 let agent_content =
44 std::fs::read_to_string(&agent_path).map_err(|source| MarsError::Io {
45 operation: "read launch bundle agent".to_string(),
46 path: agent_path.clone(),
47 source,
48 })?;
49
50 let mut parse_diags = Vec::new();
51 let (parsed_profile, frontmatter) = parse_agent_content(&agent_content, &mut parse_diags)
52 .map_err(|err| {
53 MarsError::Config(ConfigError::Invalid {
54 message: format!(
55 "failed to parse agent `{agent}` from {}: {err}",
56 agent_path.display()
57 ),
58 })
59 })?;
60
61 if let Some(fatal) = parse_diags.iter().find(|diag| diag.is_error()) {
62 return Err(MarsError::Config(ConfigError::Invalid {
63 message: format!(
64 "agent `{agent}` has invalid frontmatter in {}: {}",
65 agent_path.display(),
66 fatal.message()
67 ),
68 }));
69 }
70
71 warnings.extend(
72 parse_diags
73 .iter()
74 .map(|diag| format!("agent `{agent}`: {}", diag.message())),
75 );
76 agent_body = Some(frontmatter.body().to_string());
77 profile = parsed_profile;
78 } else {
79 profile = empty_agent_profile();
80 agent_body = None;
81 }
82
83 let effective_project_config = load_effective_project_config_or_default(&ctx.project_root)?;
84 if let Some(message) = crate::compiler::agent_copy::deprecated_fanout_agents_warning(
85 effective_project_config.settings.meridian_agent_copy(),
86 ) {
87 warnings.push(message);
88 }
89 let lock = crate::lock::load_for_runtime_aliases(&ctx.project_root)?;
90 let runtime_aliases = crate::models::merged_runtime_aliases(
91 &lock.dependency_model_aliases,
92 Some(&effective_project_config.models),
93 );
94
95 let policy = resolve_policy(
96 &effective_project_config,
97 PolicyInput {
98 project_root: &ctx.project_root,
99 runtime_aliases: &runtime_aliases,
100 agent: request.agent.as_deref(),
101 profile: &profile,
102 model_override: request.model.as_deref(),
103 harness_override: request.harness.as_deref(),
104 effort_override: request.effort.as_deref(),
105 approval_override: request.approval.as_deref(),
106 sandbox_override: request.sandbox.as_deref(),
107 models_refresh: request.models_refresh,
108 },
109 )?;
110
111 warnings.extend(policy.warnings);
112
113 let mars_dir = ctx.project_root.join(".mars");
114 let effective_skills = resolve_effective_skills(&profile, &policy.routing.harness)?;
115
116 let prompt = compile_prompt_surface(
117 &mars_dir,
118 agent_body.as_deref().unwrap_or(""),
119 &effective_skills,
120 &request.extra_skills,
121 &policy.routing.harness,
122 &policy.routing.model_token,
123 &policy.routing.model,
124 &profile.subagents,
125 effective_project_config.settings.meridian_fanout_agents(),
126 )?;
127
128 warnings.extend(prompt.warnings);
129 let (resolved_tools, tool_warnings) = resolve_bundle_tools(&profile, &policy.routing.harness)?;
130 warnings.extend(tool_warnings);
131
132 Ok(LaunchBundle {
133 version: LAUNCH_BUNDLE_VERSION,
134 agent: request.agent,
135 agent_body,
136 routing: policy.routing,
137 execution_policy: policy.execution_policy,
138 prompt_surface: bundle::PromptSurface {
139 system_instruction: prompt.system_instruction,
140 supplemental_documents: prompt.supplemental_documents,
141 inventory_prompt: prompt.inventory_prompt,
142 },
143 scaffold_slots: ScaffoldSlots::placeholders(),
144 tools: resolved_tools,
145 skills: Skills {
146 loaded: prompt.loaded_skills,
147 available: prompt.available_skills,
148 missing: prompt.missing_skills,
149 },
150 provenance: policy.provenance,
151 warnings,
152 })
153}
154
155fn empty_agent_profile() -> AgentProfile {
156 AgentProfile {
157 name: None,
158 description: None,
159 harness: None,
160 model: None,
161 mode: None,
162 model_invocable: true,
163 user_invocable: true,
164 had_model_invocable_field: false,
165 had_user_invocable_field: false,
166 approval: None,
167 sandbox: None,
168 effort: None,
169 autocompact: None,
170 autocompact_pct: None,
171 skills: SkillsSpec::default(),
172 subagents: Vec::new(),
173 tools: Vec::new(),
174 tools_denied: Vec::new(),
175 disallowed_tools: Vec::new(),
176 harness_overrides: Default::default(),
177 model_policies: Vec::new(),
178 fanout: Vec::new(),
179 }
180}
181
182fn load_effective_project_config_or_default(
183 project_root: &std::path::Path,
184) -> Result<EffectiveProjectConfig, MarsError> {
185 match crate::config::load_effective_project_config(project_root) {
186 Ok(config) => Ok(config),
187 Err(MarsError::Config(ConfigError::NotFound { .. })) => {
188 Ok(EffectiveProjectConfig::default())
189 }
190 Err(err) => Err(err),
191 }
192}
193
194fn agent_file_path(project_root: &std::path::Path, agent: &str) -> PathBuf {
195 project_root
196 .join(".mars")
197 .join("agents")
198 .join(format!("{agent}.md"))
199}
200
201fn resolve_bundle_tools(
202 profile: &crate::compiler::agents::AgentProfile,
203 harness: &str,
204) -> Result<(ToolsSpec, Vec<String>), MarsError> {
205 use crate::compiler::mcp_ref::project_mcp_refs_for_emission;
206
207 let descriptor = parse_harness_descriptor(harness)?;
208 let harness_kind = descriptor.kind;
209 let harness = descriptor.canonical_id;
210
211 let effective_tools = profile.effective_tool_policy(&harness_kind);
212 let mut warnings = Vec::new();
213
214 let allowed = normalize_and_dedupe_tools(
215 &effective_tools.allowed,
216 descriptor,
217 ToolPolicyKind::Allowed,
218 &mut warnings,
219 );
220 let mut disallowed = normalize_and_dedupe_tools(
221 &effective_tools.disallowed,
222 descriptor,
223 ToolPolicyKind::Disallowed,
224 &mut warnings,
225 );
226
227 let mcp_allowed = project_mcp_refs_for_emission(
230 &effective_tools.mcp_allowed,
231 harness_kind,
232 |canonical, reason| {
233 warnings.push(format!(
234 "MCP ref `{canonical}` cannot be represented for {harness}: {}",
235 reason.message()
236 ));
237 },
238 );
239
240 let mcp_disallowed = project_mcp_refs_for_emission(
241 &effective_tools.mcp_disallowed,
242 harness_kind,
243 |canonical, reason| {
244 warnings.push(format!(
245 "disallowed MCP ref `{canonical}` cannot be represented for {harness}: {}",
246 reason.message()
247 ));
248 },
249 );
250 disallowed.extend(mcp_disallowed);
251
252 Ok((
253 ToolsSpec {
254 allowed,
255 disallowed,
256 mcp: mcp_allowed,
257 },
258 warnings,
259 ))
260}
261
262fn normalize_and_dedupe_tools(
263 tools: &[String],
264 descriptor: &CompilerHarnessDescriptor,
265 kind: ToolPolicyKind,
266 warnings: &mut Vec<String>,
267) -> Vec<String> {
268 let harness = descriptor.canonical_id;
269 let mut seen = std::collections::HashSet::new();
270 let mut projected = Vec::new();
271
272 for tool in tools {
273 let normalized = project_tool_for_harness(tool, descriptor.kind);
274 if normalized.status == ToolProjectionStatus::UnknownProjected {
275 match kind {
276 ToolPolicyKind::Allowed => warnings.push(format!(
277 "tool '{tool}' is not a known {harness} tool; projected via {harness} naming convention (verify it exists)"
278 )),
279 ToolPolicyKind::Disallowed => warnings.push(format!(
280 "disallowed tool '{tool}' is not a known {harness} tool; projected via {harness} naming convention (verify it exists)"
281 )),
282 }
283 }
284
285 let trimmed = normalized.name.trim();
286 if trimmed.is_empty() {
287 continue;
288 }
289 if seen.insert(trimmed.to_string()) {
290 projected.push(trimmed.to_string());
291 }
292 }
293
294 projected
295}
296
297#[derive(Clone, Copy, PartialEq, Eq)]
298enum ToolPolicyKind {
299 Allowed,
300 Disallowed,
301}
302
303fn resolve_effective_skills(
304 profile: &crate::compiler::agents::AgentProfile,
305 harness: &str,
306) -> Result<SkillsSpec, MarsError> {
307 let descriptor = parse_harness_descriptor(harness)?;
308 Ok(profile.effective_skills(&descriptor.kind).clone())
309}
310
311fn parse_harness_descriptor(
312 harness: &str,
313) -> Result<&'static CompilerHarnessDescriptor, MarsError> {
314 crate::compiler::harness_descriptor::descriptor_for_canonical_id(harness).ok_or_else(|| {
315 MarsError::Config(ConfigError::Invalid {
316 message: format!(
317 "invalid harness `{harness}` for launch bundle resolution; expected one of: {}",
318 crate::compiler::harness_descriptor::known_canonical_ids()
319 .collect::<Vec<_>>()
320 .join(", ")
321 ),
322 })
323 })
324}