1use std::sync::Arc;
6use chrono::Utc;
7
8use crate::backend::rpc::engine::WshRpcEngine;
9use crate::backend::rpc_types::{
10 COMMAND_LIST_AGENTS, COMMAND_CREATE_AGENT, COMMAND_UPDATE_AGENT,
11 COMMAND_DELETE_AGENT, COMMAND_GET_AGENT_CONTENT, COMMAND_SET_AGENT_CONTENT,
12 COMMAND_GET_ALL_AGENT_CONTENT,
13 COMMAND_IMPORT_AGENT_FROM_CLAW, COMMAND_IMPORT_AGENTS, COMMAND_EXPORT_AGENTS,
14 COMMAND_RESEED_AGENTS,
15 COMMAND_CONTAINER_RUNTIME_AVAILABLE, COMMAND_CONTAINER_IMAGE_CHECK,
16 CommandContainerImageCheckData, ContainerImageCheckResult,
17 CommandListAgentDefinitionsData,
18 CommandContainerRuntimeAvailableData, CommandReseedAgentsData, CommandExportAgentsData,
19 ContainerRuntimeAvailableResult, ReseedAgentsResult,
20 CommandCreateAgentDefinitionData, CommandUpdateAgentDefinitionData, CommandDeleteAgentDefinitionData,
21 CommandGetAgentContentData, CommandSetAgentContentData, CommandGetAllAgentContentData,
22 CommandImportAgentFromClawData,
23 CommandImportAgentDefinitionsData, ImportAgentDefinitionsResult,
24 ExportAgentDefinitionsResult, AgentDefinitionExport, AgentSkillExport,
25};
26use crate::backend::storage::{AgentDefinition, AgentContent, AgentSkill};
27
28use super::super::AppState;
29
30pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
31 let mstore_lfa = state.mstore.clone();
46 engine.register_typed(COMMAND_LIST_AGENTS, move |data: serde_json::Value, _ctx| {
58 let mstore = mstore_lfa.clone();
59 async move {
60 let cmd: CommandListAgentDefinitionsData =
66 serde_json::from_value(data).unwrap_or_default();
67 let agents = mstore.agent_def_list().map_err(|e| format!("listagents: {e}"))?;
68 let is_seeded_filter = cmd.is_seeded;
69 let include_hidden = cmd.include_hidden;
70 let filtered: Vec<_> = agents
71 .into_iter()
72 .filter(|a| match is_seeded_filter {
73 Some(flag) => a.is_seeded == flag,
74 None => true,
75 })
76 .filter(|a| {
82 include_hidden || a.is_seeded != 1 || a.user_hidden == 0
83 })
84 .collect();
85 Ok(filtered)
86 }
87 });
88
89 let mstore_cfa = state.mstore.clone();
91 let id_store_cfa = state.id_store.clone();
92 let broker_cfa = state.broker.clone();
93 engine.register_typed(
94 COMMAND_CREATE_AGENT,
95 move |cmd: CommandCreateAgentDefinitionData, _ctx| {
96 let mstore = mstore_cfa.clone();
97 let id_store = id_store_cfa.clone();
98 let broker = broker_cfa.clone();
99 async move {
100 let now = agentmux_common::time::now_ms();
101 let mut agent = AgentDefinition {
106 id: uuid::Uuid::new_v4().to_string(),
107 slug: String::new(),
108 name: cmd.name,
109 icon: cmd.icon,
110 provider: cmd.provider,
111 description: cmd.description,
112 working_directory: cmd.working_directory,
113 shell: cmd.shell,
114 provider_flags: cmd.provider_flags,
115 auto_start: cmd.auto_start,
116 restart_on_crash: cmd.restart_on_crash,
117 idle_timeout_minutes: cmd.idle_timeout_minutes,
118 created_at: now,
119 agent_type: cmd.agent_type,
120 environment: cmd.environment,
121 agent_bus_id: cmd.agent_bus_id,
122 is_seeded: 0,
123 accounts: String::new(),
124 parent_id: String::new(),
125 branch_label: String::new(),
126 updated_at: now,
127 user_hidden: 0,
128 container_image: String::new(),
129 container_volumes: "[]".to_string(),
130 container_name: String::new(),
131 use_ambient_login: 0,
133 model_vendor_base_url: String::new(),
136 auto_continue_enabled: 0,
139 memory_id: String::new(),
140 conversation_visibility: crate::backend::storage::agents::default_conversation_visibility(),
141 };
142 mstore.agent_def_insert(&mut agent).map_err(|e| format!("createagent: {e}"))?;
143 mstore.agent_def_provision_and_bind_bundle(&id_store, &mut agent, now);
150 let _ = mstore.agent_content_set(&crate::backend::storage::store::AgentContent {
155 agent_id: agent.id.clone(),
156 content_type: "ui:color".to_string(),
157 content: crate::backend::agent_color::pick_agent_color(&agent.id).to_string(),
158 updated_at: now,
159 });
160 broker.publish_event::<crate::backend::mps_events::AgentsChanged>(&(), vec![], 0);
161 Ok(agent)
162 }
163 },
164 );
165
166 let mstore_ufa = state.mstore.clone();
168 let broker_ufa = state.broker.clone();
169 engine.register_typed(
170 COMMAND_UPDATE_AGENT,
171 move |cmd: CommandUpdateAgentDefinitionData, _ctx| {
172 let mstore = mstore_ufa.clone();
173 let broker = broker_ufa.clone();
174 async move {
175 let existing = mstore.agent_def_list().map_err(|e| format!("updateagent: {e}"))?;
177 let old = existing.iter().find(|a| a.id == cmd.id)
178 .ok_or_else(|| format!("updateagent: agent {} not found", cmd.id))?;
179 crate::server::app_api::check_provider_unchanged(&old.id, &old.provider, &cmd.provider)
180 .map_err(|e| format!("updateagent: {e}"))?;
181 let chosen_model_vendor_base_url = match &cmd.model_vendor_base_url {
187 Some(url) => {
188 crate::server::app_api::validate_vendor_base_url(&cmd.provider, url)
189 .map_err(|e| format!("updateagent: {e}"))?;
190 url.clone()
191 }
192 None => old.model_vendor_base_url.clone(),
193 };
194 let mut agent = AgentDefinition {
198 id: cmd.id,
199 slug: old.slug.clone(),
200 name: cmd.name,
201 icon: cmd.icon,
202 provider: cmd.provider,
203 description: cmd.description,
204 working_directory: cmd.working_directory,
205 shell: cmd.shell,
206 provider_flags: cmd.provider_flags,
207 auto_start: cmd.auto_start,
208 restart_on_crash: cmd.restart_on_crash,
209 idle_timeout_minutes: cmd.idle_timeout_minutes,
210 created_at: old.created_at,
211 agent_type: cmd.agent_type,
212 environment: cmd.environment,
213 agent_bus_id: cmd.agent_bus_id,
214 is_seeded: old.is_seeded,
215 accounts: if cmd.accounts.is_empty() { old.accounts.clone() } else { cmd.accounts },
221 parent_id: old.parent_id.clone(),
225 branch_label: old.branch_label.clone(),
226 updated_at: old.updated_at,
230 user_hidden: old.user_hidden,
236 container_image: if cmd.container_image.is_empty() { old.container_image.clone() } else { cmd.container_image },
242 container_volumes: if cmd.container_volumes == "[]" { old.container_volumes.clone() } else { cmd.container_volumes },
243 container_name: old.container_name.clone(),
245 use_ambient_login: old.use_ambient_login,
247 model_vendor_base_url: chosen_model_vendor_base_url,
248 auto_continue_enabled: cmd.auto_continue_enabled.unwrap_or(old.auto_continue_enabled),
253 memory_id: old.memory_id.clone(),
259 conversation_visibility: old.conversation_visibility.clone(),
266 };
267 let found = mstore.agent_def_update(&mut agent).map_err(|e| format!("updateagent: {e}"))?;
268 if !found {
269 return Err(format!("updateagent: agent {} not found", agent.id));
270 }
271 broker.publish_event::<crate::backend::mps_events::AgentsChanged>(&(), vec![], 0);
272 Ok(agent)
273 }
274 },
275 );
276
277 let mstore_dfa = state.mstore.clone();
279 let broker_dfa = state.broker.clone();
280 let identity_store_dfa = state.identity_store.clone();
281 let id_store_dfa = state.id_store.clone();
282 engine.register_typed(
283 COMMAND_DELETE_AGENT,
284 move |cmd: CommandDeleteAgentDefinitionData, _ctx| {
285 let mstore = mstore_dfa.clone();
286 let broker = broker_dfa.clone();
287 let identity_store = identity_store_dfa.clone();
288 let id_store = id_store_dfa.clone();
289 async move {
290 mstore.agent_def_delete(&cmd.id).map_err(|e| format!("deleteagent: {e}"))?;
291 super::purge_identity_store_rows(&identity_store, &id_store, &cmd.id, "deleteagent");
292 broker.publish_event::<crate::backend::mps_events::AgentsChanged>(&(), vec![], 0);
293 Ok(())
294 }
295 },
296 );
297
298 let container_manager_cra = state.container_manager.clone();
309 engine.register_typed(
310 COMMAND_CONTAINER_RUNTIME_AVAILABLE,
311 move |_cmd: Option<CommandContainerRuntimeAvailableData>, _ctx| {
312 let cm = container_manager_cra.clone();
313 async move {
314 let available = cm.is_available().await;
315 Ok(ContainerRuntimeAvailableResult { available })
316 }
317 },
318 );
319
320 let container_manager_cic = state.container_manager.clone();
324 engine.register_typed(
325 COMMAND_CONTAINER_IMAGE_CHECK,
326 move |cmd: Option<CommandContainerImageCheckData>, _ctx| {
327 let cm = container_manager_cic.clone();
328 async move {
329 let image = cmd.unwrap_or_default().image;
330 let manager = cm.get().await;
331 let status = crate::backend::container_image::check_image_access(manager.as_ref(), &image).await;
332 Ok(ContainerImageCheckResult { status })
333 }
334 },
335 );
336
337 let mstore_gfc = state.mstore.clone();
339 engine.register_typed(
340 COMMAND_GET_AGENT_CONTENT,
341 move |cmd: CommandGetAgentContentData, _ctx| {
342 let mstore = mstore_gfc.clone();
343 async move {
344 let content = mstore.agent_content_get(&cmd.agent_id, &cmd.content_type)
345 .map_err(|e| format!("getagentcontent: {e}"))?;
346 Ok(content)
352 }
353 },
354 );
355
356 let mstore_sfc = state.mstore.clone();
358 let broker_sfc = state.broker.clone();
359 engine.register_typed(
360 COMMAND_SET_AGENT_CONTENT,
361 move |cmd: CommandSetAgentContentData, _ctx| {
362 let mstore = mstore_sfc.clone();
363 let broker = broker_sfc.clone();
364 async move {
365 let now = agentmux_common::time::now_ms();
366 let content = AgentContent {
367 agent_id: cmd.agent_id,
368 content_type: cmd.content_type,
369 content: cmd.content,
370 updated_at: now,
371 };
372 mstore.agent_content_set(&content).map_err(|e| format!("setagentcontent: {e}"))?;
373 broker.publish_event::<crate::backend::mps_events::AgentContentChanged>(&(), vec![], 0);
374 Ok(content)
375 }
376 },
377 );
378
379 let mstore_gafc = state.mstore.clone();
381 engine.register_typed(
382 COMMAND_GET_ALL_AGENT_CONTENT,
383 move |cmd: CommandGetAllAgentContentData, _ctx| {
384 let mstore = mstore_gafc.clone();
385 async move {
386 let contents = mstore.agent_content_get_all(&cmd.agent_id)
387 .map_err(|e| format!("getallagentcontent: {e}"))?;
388 Ok(contents)
389 }
390 },
391 );
392
393 let mstore_ifc = state.mstore.clone();
397 let id_store_ifc = state.id_store.clone();
398 let broker_ifc = state.broker.clone();
399 engine.register_typed(
400 COMMAND_IMPORT_AGENT_FROM_CLAW,
401 move |cmd: CommandImportAgentFromClawData, _ctx| {
402 let mstore = mstore_ifc.clone();
403 let id_store = id_store_ifc.clone();
404 let broker = broker_ifc.clone();
405 async move {
406
407 let workspace_path = std::path::Path::new(&cmd.workspace_path);
408 if !workspace_path.exists() {
409 return Err(format!("importagentfromclaw: path does not exist: {}", cmd.workspace_path));
410 }
411
412 let now = agentmux_common::time::now_ms();
413
414 let mut provider = "claude".to_string();
416 let settings_path = workspace_path.join(".claude").join("settings.json");
417 if settings_path.exists() {
418 if let Ok(settings_str) = std::fs::read_to_string(&settings_path) {
419 if let Ok(settings) = serde_json::from_str::<serde_json::Value>(&settings_str) {
420 if let Some(p) = settings.get("provider").and_then(|v| v.as_str()) {
421 provider = p.to_string();
422 }
423 }
424 }
425 }
426
427 let mut agent = AgentDefinition {
431 id: uuid::Uuid::new_v4().to_string(),
432 slug: String::new(),
433 name: cmd.agent_name.clone(),
434 icon: "\u{2726}".to_string(),
435 provider,
436 description: format!("Imported from {}", cmd.workspace_path),
437 working_directory: cmd.workspace_path.clone(),
438 shell: String::new(),
439 provider_flags: String::new(),
440 auto_start: 0,
441 restart_on_crash: 0,
442 idle_timeout_minutes: 0,
443 created_at: now,
444 agent_type: "standalone".to_string(),
445 environment: String::new(),
446 agent_bus_id: String::new(),
447 is_seeded: 0,
448 accounts: String::new(),
449 parent_id: String::new(),
450 branch_label: String::new(),
451 updated_at: now,
452 user_hidden: 0,
453 container_image: String::new(),
454 container_volumes: "[]".to_string(),
455 container_name: String::new(),
456 use_ambient_login: 0,
457 model_vendor_base_url: String::new(),
458 auto_continue_enabled: 0,
459 memory_id: String::new(),
460 conversation_visibility: crate::backend::storage::agents::default_conversation_visibility(),
461 };
462 mstore.agent_def_insert(&mut agent).map_err(|e| format!("importagentfromclaw: {e}"))?;
463 mstore.agent_def_provision_and_bind_bundle(&id_store, &mut agent, now);
464
465 let claude_md_path = workspace_path.join("CLAUDE.md");
467 if claude_md_path.exists() {
468 if let Ok(content) = std::fs::read_to_string(&claude_md_path) {
469 let fc = AgentContent {
470 agent_id: agent.id.clone(),
471 content_type: "agentmd".to_string(),
472 content,
473 updated_at: now,
474 };
475 let _ = mstore.agent_content_set(&fc);
476 }
477 }
478
479 let mcp_path = workspace_path.join(".mcp.json");
481 if mcp_path.exists() {
482 if let Ok(content) = std::fs::read_to_string(&mcp_path) {
483 let fc = AgentContent {
484 agent_id: agent.id.clone(),
485 content_type: "mcp".to_string(),
486 content,
487 updated_at: now,
488 };
489 let _ = mstore.agent_content_set(&fc);
490 }
491 }
492
493 broker.publish_event::<crate::backend::mps_events::AgentsChanged>(&(), vec![], 0);
494 Ok(agent)
495 }
496 },
497 );
498
499 let mstore_rsfa = state.mstore.clone();
501 let broker_rsfa = state.broker.clone();
502 engine.register_typed(
503 COMMAND_RESEED_AGENTS,
504 move |_cmd: Option<CommandReseedAgentsData>, _ctx| {
505 let mstore = mstore_rsfa.clone();
506 let broker = broker_rsfa.clone();
507 async move {
508 let deleted = mstore.agent_def_delete_seeded()
510 .map_err(|e| format!("reseedagents: delete seeded: {e}"))?;
511
512 let report = crate::backend::agent_seed::seed_agents(&mstore)
514 .map_err(|e| format!("reseedagents: seed: {e}"))?;
515
516 broker.publish_event::<crate::backend::mps_events::AgentsChanged>(&(), vec![], 0);
517 Ok(ReseedAgentsResult {
518 deleted,
519 created: report.created,
520 skipped: report.skipped,
521 })
522 }
523 },
524 );
525
526 let mstore_ifa = state.mstore.clone();
528 let id_store_ifa = state.id_store.clone();
529 let broker_ifa = state.broker.clone();
530 engine.register_typed(
531 COMMAND_IMPORT_AGENTS,
532 move |cmd: CommandImportAgentDefinitionsData, _ctx| {
533 let mstore = mstore_ifa.clone();
534 let id_store = id_store_ifa.clone();
535 let broker = broker_ifa.clone();
536 async move {
537
538 let now = agentmux_common::time::now_ms();
539
540 let mut imported: Vec<String> = Vec::new();
541 let mut skipped: Vec<String> = Vec::new();
542 let mut failed: Vec<String> = Vec::new();
543
544 for agent_import in cmd.agents {
545 let existing = mstore.agent_def_list()
547 .unwrap_or_default()
548 .into_iter()
549 .any(|a| a.slug == agent_import.id);
550
551 if existing {
552 skipped.push(agent_import.name.clone());
553 continue;
554 }
555
556 let mut agent = AgentDefinition {
557 id: uuid::Uuid::new_v4().to_string(),
558 slug: agent_import.id.clone(),
559 name: agent_import.name.clone(),
560 icon: agent_import.icon.clone(),
561 provider: agent_import.provider.clone(),
562 description: agent_import.description.clone(),
563 working_directory: agent_import.working_directory.clone(),
564 shell: agent_import.shell.clone(),
565 provider_flags: String::new(),
566 auto_start: 0,
567 restart_on_crash: if agent_import.restart_on_crash { 1 } else { 0 },
568 idle_timeout_minutes: 0,
569 created_at: now,
570 agent_type: agent_import.agent_type.clone(),
571 environment: agent_import.environment.clone(),
572 agent_bus_id: agent_import.agent_bus_id.clone(),
573 is_seeded: 0,
574 accounts: String::new(),
575 parent_id: String::new(),
576 branch_label: String::new(),
577 updated_at: now,
578 user_hidden: 0,
579 container_image: String::new(),
580 container_volumes: "[]".to_string(),
581 container_name: String::new(),
582 use_ambient_login: 0,
583 model_vendor_base_url: String::new(),
584 auto_continue_enabled: 0,
585 memory_id: String::new(),
586 conversation_visibility: crate::backend::storage::agents::default_conversation_visibility(),
587 };
588
589 if let Err(e) = mstore.agent_def_insert(&mut agent) {
590 failed.push(format!("{}: {e}", agent_import.name));
591 continue;
592 }
593 mstore.agent_def_provision_and_bind_bundle(&id_store, &mut agent, now);
594
595 let mut content_ok = true;
597 for (content_type, content) in &agent_import.content {
598 let fc = AgentContent {
599 agent_id: agent.id.clone(),
600 content_type: content_type.clone(),
601 content: content.clone(),
602 updated_at: now,
603 };
604 if let Err(e) = mstore.agent_content_set(&fc) {
605 tracing::warn!("import: failed to set content for agent {}: {e}", agent.id);
606 content_ok = false;
607 }
608 }
609
610 let mut skills_ok = true;
612 for skill_import in &agent_import.skills {
613 let skill = AgentSkill {
614 id: uuid::Uuid::new_v4().to_string(),
615 agent_id: agent.id.clone(),
616 name: skill_import.name.clone(),
617 trigger: skill_import.trigger.clone(),
618 skill_type: skill_import.skill_type.clone(),
619 description: skill_import.description.clone(),
620 content: skill_import.content.clone(),
621 created_at: now,
622 };
623 if let Err(e) = mstore.agent_skill_insert(&skill) {
624 tracing::warn!("import: failed to insert skill '{}' for agent {}: {e}", skill.name, agent.id);
625 skills_ok = false;
626 }
627 }
628
629 if content_ok && skills_ok {
630 imported.push(agent_import.name.clone());
631 } else {
632 failed.push(agent_import.name.clone());
633 }
634 }
635
636 broker.publish_event::<crate::backend::mps_events::AgentsChanged>(&(), vec![], 0);
637
638 let result = ImportAgentDefinitionsResult { imported, skipped, failed };
639 Ok(result)
640 }
641 },
642 );
643
644 let mstore_efa = state.mstore.clone();
646 let id_store_efa = state.id_store.clone();
647 engine.register_typed(
648 COMMAND_EXPORT_AGENTS,
649 move |_cmd: Option<CommandExportAgentsData>, _ctx| {
650 let mstore = mstore_efa.clone();
651 let id_store = id_store_efa.clone();
652 async move {
653 let agents = mstore.agent_def_list()
654 .map_err(|e| format!("exportagents: list: {e}"))?;
655
656 let mut agent_exports: Vec<AgentDefinitionExport> = Vec::new();
657
658 for agent in agents {
659 let content_map = mstore.agent_content_get_all(&agent.id)
660 .unwrap_or_default()
661 .into_iter()
662 .map(|fc| (fc.content_type, fc.content))
663 .collect::<std::collections::HashMap<String, String>>();
664
665 let skills = mstore.agent_skill_list(&agent.id)
666 .unwrap_or_default()
667 .into_iter()
668 .map(|s| AgentSkillExport {
669 name: s.name,
670 trigger: s.trigger,
671 skill_type: s.skill_type,
672 description: s.description,
673 content: s.content,
674 })
675 .collect::<Vec<_>>();
676
677 let effective_provider = id_store.resolve_effective_provider_id(&agent);
681 agent_exports.push(AgentDefinitionExport {
682 id: agent.slug.clone(),
683 name: agent.name,
684 icon: agent.icon,
685 description: agent.description,
686 provider: effective_provider,
687 shell: agent.shell,
688 working_directory: agent.working_directory,
689 agent_bus_id: agent.agent_bus_id,
690 agent_type: agent.agent_type,
691 environment: agent.environment,
692 restart_on_crash: agent.restart_on_crash != 0,
693 content: content_map,
694 skills,
695 });
696 }
697
698 let exported_at = Utc::now().to_rfc3339();
699
700 let result = ExportAgentDefinitionsResult {
701 version: 4,
702 exported_at,
703 source: "agentmux-export".to_string(),
704 agents: agent_exports,
705 };
706 Ok(result)
707 }
708 },
709 );
710
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use crate::backend::rpc_types::RpcMessage;
717 use crate::backend::storage::bundles::Bundle;
718 use crate::server::tests::test_state;
719
720 fn seed_bundle(state: &AppState, id: &str, provider: &str) {
721 let bundle = Bundle {
722 id: id.to_string(),
723 name: "Bundle".to_string(),
724 description: String::new(),
725 is_blank: false,
726 is_global: false,
727 provider: provider.to_string(),
728 model: String::new(),
729 instructions: String::new(),
730 instructions_by_provider: "{}".to_string(),
731 context_files: "[]".to_string(),
732 mcp_servers: "[]".to_string(),
733 skills: "[]".to_string(),
734 sort_order: 0,
735 created_at: 0,
736 updated_at: 0,
737 is_system: false,
738 };
739 state.id_store.bundle_upsert(&bundle).unwrap();
740 }
741
742 fn seed_drifted_agent(state: &AppState, def_id: &str, bundle_id: &str) {
747 let mut def = AgentDefinition {
748 conversation_visibility: crate::backend::storage::agents::default_conversation_visibility(),
749 id: def_id.to_string(),
750 slug: def_id.to_string(),
751 name: "Drifted Agent".to_string(),
752 icon: String::new(),
753 provider: "codex".to_string(),
754 description: String::new(),
755 working_directory: String::new(),
756 shell: String::new(),
757 provider_flags: String::new(),
758 auto_start: 0,
759 restart_on_crash: 0,
760 idle_timeout_minutes: 0,
761 created_at: 0,
762 agent_type: "host".to_string(),
763 environment: String::new(),
764 agent_bus_id: String::new(),
765 is_seeded: 0,
766 accounts: String::new(),
767 parent_id: String::new(),
768 branch_label: String::new(),
769 updated_at: 0,
770 user_hidden: 0,
771 container_image: String::new(),
772 container_volumes: "[]".to_string(),
773 container_name: String::new(),
774 use_ambient_login: 0,
775 auto_continue_enabled: 0,
776 model_vendor_base_url: String::new(),
777 memory_id: bundle_id.to_string(),
778 };
779 state.mstore.agent_def_insert(&mut def).unwrap();
780 }
781
782 #[tokio::test]
783 async fn exportagents_carries_the_agents_own_provider_not_its_bundles() {
784 let state = test_state();
785 seed_bundle(&state, "bundle-claude", "claude");
786 seed_drifted_agent(&state, "agent-drift", "bundle-claude");
787
788 let (engine, mut output_rx) = WshRpcEngine::new();
789 register(&engine, &state);
790
791 engine.handle_message(RpcMessage {
792 command: COMMAND_EXPORT_AGENTS.to_string(),
793 reqid: "req-1".to_string(),
794 data: Some(serde_json::json!({})),
795 ..Default::default()
796 });
797 let resp = tokio::time::timeout(std::time::Duration::from_secs(2), output_rx.recv())
798 .await
799 .unwrap()
800 .unwrap();
801 assert!(resp.error.is_empty(), "unexpected error: {}", resp.error);
802 let result: ExportAgentDefinitionsResult =
803 serde_json::from_value(resp.data.expect("expected result data")).unwrap();
804
805 let exported = result
806 .agents
807 .iter()
808 .find(|a| a.id == "agent-drift")
809 .expect("exported agent should be present");
810 assert_eq!(
811 exported.provider, "codex",
812 "the agent owns its harness; the bundle's provider is a hint (SPEC_AGENT_BUNDLE_FORMAT_V0_3_2026_10_05.md §3.1)"
813 );
814 }
815}