1use axum::{
11 extract::{Query, State},
12 routing::{get, post},
13 Json, Router,
14};
15use serde::{Deserialize, Serialize};
16
17use crate::acp::{get_presets, AcpPaths, DistributionType, RuntimeType, WarmupStatus};
18use crate::error::ServerError;
19use crate::shell_env;
20use crate::state::AppState;
21
22const ACP_REGISTRY_URL: &str =
24 "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
25
26pub fn router() -> Router<AppState> {
27 Router::new()
28 .route("/registry", get(get_registry).post(refresh_registry))
29 .route("/install", post(install_agent).delete(uninstall_agent))
30 .route("/runtime", get(get_runtime_status).post(ensure_runtime))
31 .route("/warmup", get(get_warmup_status).post(warmup_agent))
32}
33
34#[derive(Debug, Deserialize)]
37struct RegistryQuery {
38 id: Option<String>,
39 #[allow(dead_code)]
40 refresh: Option<bool>,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44pub struct RegistryAgent {
45 pub id: String,
46 pub name: String,
47 pub version: String,
48 pub description: String,
49 pub repository: Option<String>,
50 pub authors: Vec<String>,
51 pub license: String,
52 pub icon: Option<String>,
53 pub distribution: serde_json::Value,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub struct AcpRegistry {
58 pub version: String,
59 pub agents: Vec<RegistryAgent>,
60}
61
62#[derive(Debug, Serialize)]
63struct AgentWithStatus {
64 agent: RegistryAgent,
65 available: bool,
66 installed: bool,
67 uninstallable: bool,
68 #[serde(rename = "distributionTypes")]
69 distribution_types: Vec<String>,
70 source: &'static str,
71}
72
73#[derive(Debug, Serialize)]
74#[allow(dead_code)]
75struct RegistryResponse {
76 agents: Vec<AgentWithStatus>,
77 platform: Option<String>,
78 #[serde(rename = "runtimeAvailability")]
79 runtime_availability: RuntimeAvailability,
80}
81
82#[derive(Debug, Serialize)]
83#[allow(dead_code)]
84struct RuntimeAvailability {
85 npx: bool,
86 uvx: bool,
87}
88
89#[derive(Debug, Deserialize)]
90struct InstallRequest {
91 #[serde(rename = "agentId")]
92 agent_id: String,
93 #[serde(rename = "distributionType")]
94 distribution_type: Option<String>,
95}
96
97async fn get_registry(
101 State(state): State<AppState>,
102 Query(query): Query<RegistryQuery>,
103) -> Result<Json<serde_json::Value>, ServerError> {
104 let _ = state.acp_installation_state.load().await;
106
107 let registry = fetch_registry().await?;
109
110 let npx_available = shell_env::which("npx").is_some();
112 let uvx_available = shell_env::which("uv").is_some();
113
114 if let Some(agent_id) = query.id {
116 if let Some(agent) = registry.agents.into_iter().find(|a| a.id == agent_id) {
117 let status = get_agent_status(&state, &agent, npx_available, uvx_available).await;
118 return Ok(Json(serde_json::json!({
119 "agent": agent,
120 "available": status.available,
121 "installed": status.installed,
122 "uninstallable": status.uninstallable,
123 "platform": detect_platform(),
124 "distributionType": status.resolved_distribution_type,
125 })));
126 } else {
127 return Err(ServerError::NotFound(format!(
128 "Agent '{agent_id}' not found"
129 )));
130 }
131 }
132
133 let registry_ids: std::collections::HashSet<String> = registry
135 .agents
136 .iter()
137 .map(|agent| agent.id.clone())
138 .collect();
139 let mut agents_with_status = Vec::new();
140 for agent in registry.agents {
141 let dist_types = get_distribution_types(&agent.distribution);
142 let status = get_agent_status(&state, &agent, npx_available, uvx_available).await;
143 agents_with_status.push(AgentWithStatus {
144 agent,
145 available: status.available,
146 installed: status.installed,
147 uninstallable: status.uninstallable,
148 distribution_types: dist_types,
149 source: "registry",
150 });
151 }
152
153 for preset in get_presets() {
154 if preset.id == "claude" {
155 continue;
156 }
157 if registry_ids.contains(&preset.id) {
158 continue;
159 }
160
161 let resolved =
162 resolve_preset_command(&preset).and_then(|command| shell_env::which(&command));
163 agents_with_status.push(AgentWithStatus {
164 agent: RegistryAgent {
165 id: preset.id,
166 name: preset.name,
167 version: String::new(),
168 description: preset.description,
169 repository: None,
170 authors: vec![],
171 license: String::new(),
172 icon: None,
173 distribution: serde_json::json!({}),
174 },
175 available: resolved.is_some(),
176 installed: resolved.is_some(),
177 uninstallable: false,
178 distribution_types: vec![],
179 source: "builtin",
180 });
181 }
182
183 Ok(Json(serde_json::json!({
184 "agents": agents_with_status,
185 "platform": detect_platform(),
186 "runtimeAvailability": {
187 "npx": npx_available,
188 "uvx": uvx_available,
189 }
190 })))
191}
192
193async fn install_agent(
195 State(state): State<AppState>,
196 Json(req): Json<InstallRequest>,
197) -> Result<Json<serde_json::Value>, ServerError> {
198 let registry = fetch_registry().await?;
199
200 let agent = registry
201 .agents
202 .into_iter()
203 .find(|a| a.id == req.agent_id)
204 .ok_or_else(|| {
205 ServerError::NotFound(format!("Agent '{}' not found in registry", req.agent_id))
206 })?;
207
208 let dist_types = get_distribution_types(&agent.distribution);
209 let npx_available = shell_env::which("npx").is_some();
210 let uvx_available = shell_env::which("uv").is_some();
211
212 let dist_type = req.distribution_type.unwrap_or_else(|| {
214 if dist_types.contains(&"npx".to_string()) && npx_available {
215 "npx".to_string()
216 } else if dist_types.contains(&"uvx".to_string()) && uvx_available {
217 "uvx".to_string()
218 } else if dist_types.contains(&"binary".to_string()) {
219 "binary".to_string()
220 } else {
221 "npx".to_string()
222 }
223 });
224
225 tracing::info!(
226 "[ACP Install] Installing agent: {} via {}",
227 req.agent_id,
228 dist_type
229 );
230
231 let version = if agent.version.is_empty() {
232 "latest".to_string()
233 } else {
234 agent.version.clone()
235 };
236
237 match dist_type.as_str() {
238 "npx" => {
239 let npx_system = shell_env::which("npx").is_some();
241 if !npx_system {
242 tracing::info!("[ACP Install] npx not found on PATH — downloading managed Node.js");
243 state
244 .acp_runtime_manager
245 .ensure_runtime(&RuntimeType::Npx)
246 .await
247 .map_err(|e| {
248 ServerError::Internal(format!("Failed to ensure npx runtime: {e}"))
249 })?;
250 }
251
252 let package = agent
254 .distribution
255 .get("npx")
256 .and_then(|v| v.get("package"))
257 .and_then(|v| v.as_str())
258 .map(|s| s.to_string());
259
260 state
261 .acp_installation_state
262 .mark_installed(
263 &req.agent_id,
264 &version,
265 DistributionType::Npx,
266 None,
267 package,
268 )
269 .await
270 .map_err(|e| ServerError::Internal(format!("Failed to save state: {e}")))?;
271
272 state
274 .acp_warmup_service
275 .warmup_in_background(&req.agent_id)
276 .await;
277
278 Ok(Json(serde_json::json!({
279 "success": true,
280 "agentId": req.agent_id,
281 "distributionType": dist_type,
282 "message": format!("Agent '{}' configured for npx (warmup started)", agent.name)
283 })))
284 }
285 "uvx" => {
286 let uvx_system = shell_env::which("uv").is_some();
288 if !uvx_system {
289 tracing::info!("[ACP Install] uv not found on PATH — downloading managed uv");
290 state
291 .acp_runtime_manager
292 .ensure_runtime(&RuntimeType::Uvx)
293 .await
294 .map_err(|e| {
295 ServerError::Internal(format!("Failed to ensure uvx runtime: {e}"))
296 })?;
297 }
298
299 let package = agent
301 .distribution
302 .get("uvx")
303 .and_then(|v| v.get("package"))
304 .and_then(|v| v.as_str())
305 .map(|s| s.to_string());
306
307 state
308 .acp_installation_state
309 .mark_installed(
310 &req.agent_id,
311 &version,
312 DistributionType::Uvx,
313 None,
314 package,
315 )
316 .await
317 .map_err(|e| ServerError::Internal(format!("Failed to save state: {e}")))?;
318
319 state
321 .acp_warmup_service
322 .warmup_in_background(&req.agent_id)
323 .await;
324
325 Ok(Json(serde_json::json!({
326 "success": true,
327 "agentId": req.agent_id,
328 "distributionType": dist_type,
329 "message": format!("Agent '{}' configured for uvx (warmup started)", agent.name)
330 })))
331 }
332 "binary" => {
333 let platform = AcpPaths::current_platform();
335 let binary_config = agent
336 .distribution
337 .get("binary")
338 .and_then(|v| v.get(&platform))
339 .ok_or_else(|| {
340 ServerError::BadRequest(format!("No binary available for platform: {platform}"))
341 })?;
342
343 let binary_info: crate::acp::BinaryInfo = serde_json::from_value(binary_config.clone())
344 .map_err(|e| ServerError::Internal(format!("Failed to parse binary info: {e}")))?;
345
346 let exe_path = state
347 .acp_binary_manager
348 .install_binary(&req.agent_id, &version, &binary_info)
349 .await
350 .map_err(|e| ServerError::Internal(format!("Binary installation failed: {e}")))?;
351
352 let exe_path_str = exe_path.to_string_lossy().to_string();
353 state
354 .acp_installation_state
355 .mark_installed(
356 &req.agent_id,
357 &version,
358 DistributionType::Binary,
359 Some(exe_path_str.clone()),
360 None,
361 )
362 .await
363 .map_err(|e| ServerError::Internal(format!("Failed to save state: {e}")))?;
364
365 Ok(Json(serde_json::json!({
366 "success": true,
367 "agentId": req.agent_id,
368 "distributionType": dist_type,
369 "installedPath": exe_path_str,
370 "message": format!("Agent '{}' binary installed successfully", agent.name)
371 })))
372 }
373 _ => Err(ServerError::BadRequest(format!(
374 "Unknown distribution type: {dist_type}"
375 ))),
376 }
377}
378
379async fn uninstall_agent(
381 State(state): State<AppState>,
382 Json(req): Json<InstallRequest>,
383) -> Result<Json<serde_json::Value>, ServerError> {
384 tracing::info!("[ACP Install] Uninstalling agent: {}", req.agent_id);
385
386 if let Some(info) = state
388 .acp_installation_state
389 .get_installed_info(&req.agent_id)
390 .await
391 {
392 if info.dist_type == DistributionType::Binary {
393 state
395 .acp_binary_manager
396 .uninstall(&req.agent_id)
397 .await
398 .map_err(|e| ServerError::Internal(format!("Failed to remove binary: {e}")))?;
399 }
400 }
401
402 state
404 .acp_installation_state
405 .uninstall(&req.agent_id)
406 .await
407 .map_err(|e| ServerError::Internal(format!("Failed to update state: {e}")))?;
408
409 Ok(Json(serde_json::json!({
410 "success": true,
411 "agentId": req.agent_id,
412 "message": format!("Agent '{}' uninstalled", req.agent_id)
413 })))
414}
415
416pub async fn fetch_registry() -> Result<AcpRegistry, ServerError> {
420 let response = reqwest::get(ACP_REGISTRY_URL)
421 .await
422 .map_err(|e| ServerError::Internal(format!("Failed to fetch registry: {e}")))?;
423
424 if !response.status().is_success() {
425 return Err(ServerError::Internal(format!(
426 "Registry fetch failed: {}",
427 response.status()
428 )));
429 }
430
431 let registry: AcpRegistry = response
432 .json()
433 .await
434 .map_err(|e| ServerError::Internal(format!("Failed to parse registry: {e}")))?;
435
436 Ok(registry)
437}
438
439fn get_distribution_types(distribution: &serde_json::Value) -> Vec<String> {
441 let mut types = Vec::new();
442 if distribution.get("npx").is_some() {
443 types.push("npx".to_string());
444 }
445 if distribution.get("uvx").is_some() {
446 types.push("uvx".to_string());
447 }
448 if distribution.get("binary").is_some() {
449 types.push("binary".to_string());
450 }
451 types
452}
453
454#[derive(Debug)]
455struct RegistryAgentStatus {
456 available: bool,
457 installed: bool,
458 uninstallable: bool,
459 resolved_distribution_type: Option<&'static str>,
460}
461
462async fn get_agent_status(
463 state: &AppState,
464 agent: &RegistryAgent,
465 npx_available: bool,
466 uvx_available: bool,
467) -> RegistryAgentStatus {
468 let installed_info = state
469 .acp_installation_state
470 .get_installed_info(&agent.id)
471 .await;
472
473 if let Some(info) = installed_info {
474 if info.dist_type == DistributionType::Binary {
475 return RegistryAgentStatus {
476 available: true,
477 installed: true,
478 uninstallable: true,
479 resolved_distribution_type: Some("binary"),
480 };
481 }
482 }
483
484 let dist = &agent.distribution;
485 if dist.get("npx").is_some() && npx_available {
486 return RegistryAgentStatus {
487 available: true,
488 installed: false,
489 uninstallable: false,
490 resolved_distribution_type: Some("npx"),
491 };
492 }
493
494 if dist.get("uvx").is_some() && uvx_available {
495 return RegistryAgentStatus {
496 available: true,
497 installed: false,
498 uninstallable: false,
499 resolved_distribution_type: Some("uvx"),
500 };
501 }
502
503 RegistryAgentStatus {
504 available: false,
505 installed: false,
506 uninstallable: false,
507 resolved_distribution_type: None,
508 }
509}
510
511fn resolve_preset_command(preset: &crate::acp::AcpPreset) -> Option<String> {
512 if let Some(env_var) = &preset.env_bin_override {
513 if let Ok(value) = std::env::var(env_var) {
514 let trimmed = value.trim();
515 if !trimmed.is_empty() {
516 return Some(trimmed.to_string());
517 }
518 }
519 }
520
521 let trimmed = preset.command.trim();
522 if trimmed.is_empty() {
523 None
524 } else {
525 Some(trimmed.to_string())
526 }
527}
528
529pub fn detect_platform() -> Option<String> {
531 let os = std::env::consts::OS;
532 let arch = std::env::consts::ARCH;
533
534 let platform = match (os, arch) {
535 ("macos", "aarch64") => "darwin-aarch64",
536 ("macos", "x86_64") => "darwin-x86_64",
537 ("linux", "aarch64") => "linux-aarch64",
538 ("linux", "x86_64") => "linux-x86_64",
539 ("windows", "aarch64") => "windows-aarch64",
540 ("windows", "x86_64") => "windows-x86_64",
541 _ => return None,
542 };
543
544 Some(platform.to_string())
545}
546
547async fn refresh_registry(
549 State(_state): State<AppState>,
550) -> Result<Json<serde_json::Value>, ServerError> {
551 let registry = fetch_registry().await?;
552 Ok(Json(serde_json::json!({
553 "success": true,
554 "version": registry.version,
555 "agentCount": registry.agents.len(),
556 "message": "Registry cache refreshed"
557 })))
558}
559
560#[derive(Debug, serde::Deserialize)]
563struct EnsureRuntimeRequest {
564 runtime: String,
566}
567
568async fn get_runtime_status(
570 State(state): State<AppState>,
571) -> Result<Json<serde_json::Value>, ServerError> {
572 use crate::acp::runtime_manager::current_platform;
573
574 let rm = &state.acp_runtime_manager;
575 let platform = current_platform();
576
577 let check_with_version = |rt: RuntimeType| async move {
579 let managed = rm.get_managed_runtime(&rt).await;
580 let system = rm.get_system_runtime(&rt);
581 let version = rm.get_version(&rt).await;
582 serde_json::json!({
583 "available": managed.is_some() || system.is_some(),
584 "managed": managed.as_ref().map(|i| i.path.to_string_lossy().to_string()),
585 "system": system.as_ref().map(|i| i.path.to_string_lossy().to_string()),
586 "version": version,
587 })
588 };
589
590 let (node, npx, uv, uvx) = tokio::join!(
591 check_with_version(RuntimeType::Node),
592 check_with_version(RuntimeType::Npx),
593 check_with_version(RuntimeType::Uv),
594 check_with_version(RuntimeType::Uvx),
595 );
596
597 Ok(Json(serde_json::json!({
598 "platform": platform,
599 "runtimes": {
600 "node": node,
601 "npx": npx,
602 "uv": uv,
603 "uvx": uvx,
604 }
605 })))
606}
607
608async fn ensure_runtime(
612 State(state): State<AppState>,
613 Json(req): Json<EnsureRuntimeRequest>,
614) -> Result<Json<serde_json::Value>, ServerError> {
615 let rt = match req.runtime.as_str() {
616 "node" => RuntimeType::Node,
617 "npx" => RuntimeType::Npx,
618 "uv" => RuntimeType::Uv,
619 "uvx" => RuntimeType::Uvx,
620 other => {
621 return Err(ServerError::BadRequest(format!(
622 "Unknown runtime '{other}'. Use node, npx, uv, or uvx."
623 )));
624 }
625 };
626
627 tracing::info!("[ACP Runtime] Ensuring runtime: {:?}", rt);
628 let info = state
629 .acp_runtime_manager
630 .ensure_runtime(&rt)
631 .await
632 .map_err(|e| ServerError::Internal(format!("Failed to ensure runtime: {e}")))?;
633
634 let version = state.acp_runtime_manager.get_version(&rt).await;
636
637 Ok(Json(serde_json::json!({
638 "success": true,
639 "runtime": req.runtime,
640 "path": info.path.to_string_lossy(),
641 "version": version.or(info.version),
642 "managed": info.is_managed,
643 })))
644}
645
646#[derive(Debug, serde::Deserialize)]
649struct WarmupRequest {
650 #[serde(rename = "agentId")]
651 agent_id: String,
652 #[serde(default)]
654 sync: bool,
655}
656
657#[derive(Debug, serde::Deserialize)]
658struct WarmupQuery {
659 id: Option<String>,
660}
661
662async fn get_warmup_status(
664 State(state): State<AppState>,
665 Query(query): Query<WarmupQuery>,
666) -> Result<Json<serde_json::Value>, ServerError> {
667 if let Some(agent_id) = query.id {
668 let status = state.acp_warmup_service.get_status(&agent_id).await;
669 return Ok(Json(
670 serde_json::to_value(status).map_err(|e| ServerError::Internal(e.to_string()))?,
671 ));
672 }
673 let statuses: Vec<WarmupStatus> = state.acp_warmup_service.get_all_statuses().await;
674 Ok(Json(serde_json::json!({ "statuses": statuses })))
675}
676
677async fn warmup_agent(
679 State(state): State<AppState>,
680 Json(req): Json<WarmupRequest>,
681) -> Result<Json<serde_json::Value>, ServerError> {
682 tracing::info!("[ACP Warmup] Warming up agent: {}", req.agent_id);
683
684 if req.sync {
685 let ok = state
686 .acp_warmup_service
687 .warmup(&req.agent_id)
688 .await
689 .unwrap_or(false);
690
691 let status = state.acp_warmup_service.get_status(&req.agent_id).await;
692 return Ok(Json(serde_json::json!({
693 "agentId": req.agent_id,
694 "success": ok,
695 "status": status,
696 })));
697 }
698
699 state
700 .acp_warmup_service
701 .warmup_in_background(&req.agent_id)
702 .await;
703
704 Ok(Json(serde_json::json!({
705 "agentId": req.agent_id,
706 "started": true,
707 "message": format!("Warmup started for agent '{}' in the background", req.agent_id),
708 })))
709}