agentmux_srv/server/app_api/bundle/
import_for_agent.rs1use super::*;
8
9#[derive(serde::Deserialize, Default, ts_rs::TS)]
10#[ts(export, export_to = "../../../frontend/types/rpc/", rename = "BundleImportForAgentReq")]
11pub(super) struct ImportForAgentReq {
12 pub(crate) agent_id: String,
13 #[serde(default)]
14 #[ts(optional)]
15 pub(crate) file_path: Option<String>,
16 #[serde(default)]
17 #[ts(optional)]
18 pub(crate) zip_base64: Option<String>,
19 #[serde(default)]
20 #[ts(optional)]
21 pub(crate) files: Option<Vec<FileEntry>>,
22}
23
24#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
26#[ts(export, export_to = "../../../frontend/types/rpc/")]
27pub(crate) struct BundleImportForAgentResponse {
28 pub(crate) bundle_id: String,
29 #[ts(type = "number")]
30 pub(crate) memory_files_written: usize,
31 pub(crate) skipped_skills: Vec<String>,
32 pub(crate) warnings: Vec<String>,
33}
34
35pub(super) async fn bundle_import_for_agent_impl(
49 id_store: &crate::backend::storage::store::Store,
50 identity_store: &crate::backend::storage::store::Store,
51 mstore: &crate::backend::storage::store::Store,
52 req: ImportForAgentReq,
53) -> Result<BundleImportForAgentResponse, String> {
54 use crate::backend::bundle_import as bi;
55
56 let agent = mstore
57 .agent_def_get(&req.agent_id)
58 .map_err(|e| format!("bundle.import_for_agent: {e}"))?
59 .ok_or_else(|| format!("bundle.import_for_agent: no agent with id {}", req.agent_id))?;
60
61 let import_lock = bundle_import_for_agent_lock(&agent.id);
71 let _import_guard = import_lock.lock().await;
72
73 let resolved = crate::server::native_memory_handlers::resolve_memory_dir_by_id(mstore, &agent);
89 let verified = resolved.as_ref().is_some_and(|r| {
90 r.provenance != crate::server::native_memory_handlers::MemoryDirProvenance::Unverified
91 });
92 let memory_dir = resolved.map(|r| r.path);
93 let mut warnings: Vec<String> = Vec::new();
94 if let Some(dir) = memory_dir.as_ref().filter(|_| verified) {
95 if let Err(e) = crate::server::native_memory_handlers::refresh_memory_mirror_from_live_fs(&agent.id, dir, id_store) {
96 warnings.push(format!("native memory refresh failed (checking mirror as-is): {e}"));
97 }
98 }
99
100 let existing = id_store
104 .agent_native_memory_list_meta(&agent.id)
105 .map_err(|e| format!("bundle.import_for_agent: {e}"))?;
106 if !existing.is_empty() {
107 return Err(format!(
108 "bundle.import_for_agent: agent {} already has {} native memory file(s) — import into an agent with no existing memory, or clear it first",
109 req.agent_id,
110 existing.len()
111 ));
112 }
113
114 let resolved = resolve_import_input(req.file_path, req.zip_base64, req.files, bi::WarningBudget::unbounded())
115 .map_err(|e| format!("bundle.import_for_agent: {e}"))?;
116 let parsed = bi::parse_bundle_import(&resolved.files)
117 .map_err(|e| format!("bundle.import_for_agent: {e}"))?;
118
119 warnings.extend(resolved.intake_warnings);
120 warnings.extend(parsed.warnings.iter().filter(|w| w.as_str() != bi::MEMORY_COMPONENT_IGNORED_WARNING).cloned());
128
129 let manifest_memory_paths = resolved_memory_paths(&resolved.files);
139 if !manifest_memory_paths.is_empty() && memory_dir.is_none() {
140 return Err(format!(
141 "bundle.import_for_agent: agent {} has no working directory configured; cannot import its memory",
142 req.agent_id
143 ));
144 }
145 let memory_dir = if manifest_memory_paths.is_empty() {
148 memory_dir
149 } else {
150 Some(
151 crate::server::native_memory_handlers::memory_dir_for_write_by_id(mstore, &agent)
152 .map_err(|e| format!("bundle.import_for_agent: {e}"))?,
153 )
154 };
155
156 let bundle_id = uuid::Uuid::new_v4().to_string();
160 let now = now_ms();
165 let mut imported_skill_ids: Vec<String> = Vec::new();
166 let rollback_skills = |ids: &[String]| -> Vec<String> {
171 ids.iter()
172 .filter_map(|id| mstore.skill_delete(identity_store, id).err().map(|e| format!("{id}: {e}")))
173 .collect()
174 };
175 for skill in &parsed.skills {
176 let row = crate::backend::storage::Skill {
177 id: uuid::Uuid::new_v4().to_string(),
178 name: skill.slug.clone(),
179 trigger: skill.slug.clone(),
180 skill_type: crate::backend::agent_config::SKILL_TYPE_AGENT_SKILL.to_string(),
181 description: skill.description.clone(),
182 content: skill.content.clone(),
183 is_global: true,
184 created_at: now,
185 updated_at: now,
186 };
187 match identity_store.skill_upsert_unique_global(&row) {
188 Ok(()) => imported_skill_ids.push(row.id),
189 Err(e) if e.to_string().contains("already exists") => {
190 warnings.push(format!("skill \"{}\" already exists; skipped", skill.slug));
191 }
192 Err(e) => {
193 let rollback_errors = rollback_skills(&imported_skill_ids);
194 let mut msg = format!("bundle.import_for_agent: failed to create skill \"{}\": {e}", skill.slug);
195 if !rollback_errors.is_empty() {
196 msg.push_str(&format!(
197 "; additionally, rollback of {} previously-created skill(s) failed and may have left orphaned global skill row(s): {}",
198 rollback_errors.len(),
199 rollback_errors.join("; ")
200 ));
201 }
202 return Err(msg);
203 }
204 }
205 }
206 let memory = crate::backend::storage::store::Bundle {
207 id: bundle_id.clone(),
208 name: parsed.name,
209 description: parsed.description,
210 is_blank: false,
211 is_global: false,
212 provider: parsed.provider,
213 model: parsed.model,
214 instructions: parsed.instructions,
215 instructions_by_provider: serde_json::to_string(&parsed.instructions_by_provider)
216 .unwrap_or_else(|_| "{}".to_string()),
217 context_files: serde_json::to_string(
218 &parsed.context_files.iter().map(|cf| json!({"path": cf.path, "content": cf.content})).collect::<Vec<_>>(),
219 ).unwrap_or_else(|_| "[]".to_string()),
220 mcp_servers: "[]".to_string(),
222 skills: serde_json::to_string(&imported_skill_ids).unwrap_or_else(|_| "[]".to_string()),
223 sort_order: 0,
224 created_at: now,
225 updated_at: now,
226 is_system: false,
227 };
228 if let Err(e) = id_store.bundle_upsert(&memory) {
229 let rollback_errors = rollback_skills(&imported_skill_ids);
230 let mut msg = format!("bundle.import_for_agent: {e}");
231 if !rollback_errors.is_empty() {
232 msg.push_str(&format!(
233 "; additionally, rollback of {} previously-created skill(s) failed and may have left orphaned global skill row(s): {}",
234 rollback_errors.len(),
235 rollback_errors.join("; ")
236 ));
237 }
238 return Err(msg);
239 }
240
241 warnings.extend(bind_imported_components(
244 mstore,
245 id_store,
246 identity_store,
247 &bundle_id,
248 &imported_skill_ids,
249 ));
250 warnings.extend(mcp_servers_not_imported_warning(&parsed.mcp_servers));
251
252 let mut memory_files_written = 0usize;
258 if let Some(memory_dir) = &memory_dir {
259 if let Err(e) = std::fs::create_dir_all(memory_dir) {
260 warnings.push(format!("native memory: mkdir failed: {e}"));
261 }
262 }
263 if let Some(memory_dir) = memory_dir {
264 for path in manifest_memory_paths {
265 let Some(filename) = path.strip_prefix("memory/") else {
272 warnings.push(format!("{path}: components.memory path is not under memory/; skipped"));
273 continue;
274 };
275 if crate::server::native_memory_handlers::validate_memory_filename(filename).is_err() {
276 warnings.push(format!("memory/{filename}: not a valid memory filename; skipped"));
277 continue;
278 }
279 let Some(content) = resolved.files.iter().find(|f| f.path == path).map(|f| f.content.clone()) else {
280 warnings.push(format!("{path}: referenced in components.memory but not found among the bundle's files; skipped"));
281 continue;
282 };
283 crate::backend::memory_reconcile::record_agentmux_write(
286 &agent.id,
287 &memory_dir,
288 filename,
289 content.as_bytes(),
290 "bundle-import",
291 "",
292 );
293 let dest = memory_dir.join(filename);
294 let tmp = memory_dir.join(format!(".{filename}.{}.tmp", uuid::Uuid::new_v4()));
295 let write_result = std::fs::write(&tmp, &content)
296 .and_then(|_| std::fs::rename(&tmp, &dest));
297 if let Err(e) = write_result {
298 let _ = std::fs::remove_file(&tmp);
299 warnings.push(format!("memory/{filename}: write failed: {e}"));
300 continue;
301 }
302 let dest_meta = std::fs::metadata(&dest).ok();
303 let size_bytes = dest_meta.as_ref().map(|m| m.len() as i64).unwrap_or(content.len() as i64);
304 let mtime_ms = dest_meta
305 .and_then(|m| m.modified().ok())
306 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
307 .map(|d| d.as_millis() as i64)
308 .unwrap_or(0);
309 let metadata_type = crate::server::native_memory_handlers::parse_memory_frontmatter_type(&content);
310 if let Err(e) = id_store.agent_native_memory_upsert(
311 &agent.id,
312 filename,
313 &content,
314 metadata_type.as_deref(),
315 &dest.to_string_lossy(),
316 size_bytes,
317 mtime_ms,
318 ) {
319 warnings.push(format!("memory/{filename}: mirror upsert failed: {e}"));
320 } else {
321 memory_files_written += 1;
322 }
323 }
324 }
325
326 Ok(BundleImportForAgentResponse {
327 bundle_id,
328 memory_files_written,
329 skipped_skills: parsed.skipped_skills,
330 warnings,
331 })
332}
333
334pub(super) fn register_bundle_import_for_agent(engine: &Arc<WshRpcEngine>, state: &AppState) {
335 let id_store = state.id_store.clone();
336 let identity_store = state.identity_store.clone();
337 let mstore = state.mstore.clone();
338 engine.register_typed(
339 COMMAND_BUNDLE_IMPORT_FOR_AGENT,
340 move |req: ImportForAgentReq, _ctx| {
341 let id_store = id_store.clone();
342 let identity_store = identity_store.clone();
343 let mstore = mstore.clone();
344 async move { bundle_import_for_agent_impl(&id_store, &identity_store, &mstore, req).await }
345 },
346 );
347}
348
349pub(super) fn resolved_memory_paths(files: &[crate::backend::bundle_import::BundleImportFile]) -> Vec<String> {
354 let Some(name) = crate::backend::bundle_import::manifest_file_name(|n| files.iter().any(|f| f.path == n)) else {
355 return Vec::new();
356 };
357 let Some(manifest_file) = files.iter().find(|f| f.path == name) else {
358 return Vec::new();
359 };
360 let Ok(manifest) = serde_json::from_str::<serde_json::Value>(&manifest_file.content) else {
361 return Vec::new();
362 };
363 manifest["components"]["memory"]
364 .as_array()
365 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(str::to_string)).collect())
366 .unwrap_or_default()
367}