agentmux_mcp/tools/
fleet.rs1use super::*;
7
8pub(super) async fn call(name: &str, arguments: &Value, cx: &ToolCtx<'_>) -> Result<String> {
9 let ToolCtx { local_url, auth_key, client, .. } = *cx;
10 match name {
11 "SendMessage" => {
12 let to = arguments
13 .get("to")
14 .and_then(|v| v.as_str())
15 .filter(|s| !s.is_empty())
16 .ok_or_else(|| anyhow::anyhow!("missing required parameter: to"))?;
17 let message = arguments
18 .get("message")
19 .and_then(|v| v.as_str())
20 .filter(|s| !s.is_empty())
21 .ok_or_else(|| anyhow::anyhow!("missing required parameter: message"))?;
22
23 if local_url.is_empty() || auth_key.is_empty() {
24 anyhow::bail!(
25 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
26 Is this agent pane opened via AgentMux?"
27 );
28 }
29
30 let source_agent = std::env::var("AGENTMUX_AGENT_ID")
31 .ok()
32 .filter(|s| !s.is_empty());
33
34 let url = format!(
35 "{}/agentmux/reactive/inject",
36 local_url.trim_end_matches('/')
37 );
38 let req = signed_jekt(source_agent, to, message);
39
40 let resp = client
41 .post(&url)
42 .header(AUTH_KEY_HEADER, auth_key)
43 .json(&req)
44 .send()
45 .await
46 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
47
48 if !resp.status().is_success() {
49 let status = resp.status();
50 let text = resp.text().await.unwrap_or_default();
51 anyhow::bail!("inject failed: HTTP {status} — {text}");
52 }
53
54 let result: Value = resp
55 .json()
56 .await
57 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
58
59 send_message_outcome(to, &result)
60 }
61 "DiscoverAgents" => {
62 if local_url.is_empty() || auth_key.is_empty() {
63 anyhow::bail!(
64 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
65 Is this agent pane opened via AgentMux?"
66 );
67 }
68
69 let url = format!("{}/agentmux/discovery", local_url.trim_end_matches('/'));
70 let resp = client
71 .get(&url)
72 .header(AUTH_KEY_HEADER, auth_key)
73 .send()
74 .await
75 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
76
77 if !resp.status().is_success() {
78 let status = resp.status();
79 let text = resp.text().await.unwrap_or_default();
80 anyhow::bail!("discovery failed: HTTP {status} — {text}");
81 }
82
83 let result: Value = resp
84 .json()
85 .await
86 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
87
88 Ok(serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()))
89 }
90 "FleetList" => {
91 if local_url.is_empty() || auth_key.is_empty() {
95 anyhow::bail!(
96 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
97 Is this agent pane opened via AgentMux?"
98 );
99 }
100
101 let url = format!("{}/agentmux/discovery", local_url.trim_end_matches('/'));
102 let resp = client
103 .get(&url)
104 .header(AUTH_KEY_HEADER, auth_key)
105 .send()
106 .await
107 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
108
109 if !resp.status().is_success() {
110 let status = resp.status();
111 let text = resp.text().await.unwrap_or_default();
112 anyhow::bail!("discovery failed: HTTP {status} — {text}");
113 }
114
115 let result: Value = resp
116 .json()
117 .await
118 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
119
120 Ok(serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()))
121 }
122 "FleetBroadcast" => {
123 let targets = arguments
124 .get("targets")
125 .and_then(|v| v.as_array())
126 .ok_or_else(|| anyhow::anyhow!("missing required parameter: targets"))?
127 .iter()
128 .filter_map(|v| v.as_str().map(str::to_string))
129 .collect::<Vec<_>>();
130 if targets.is_empty() {
131 anyhow::bail!("targets must be a non-empty array of block_id (host/cross-channel) or agent name (LAN/WAN) strings");
132 }
133 let message = arguments
134 .get("message")
135 .and_then(|v| v.as_str())
136 .filter(|s| !s.is_empty())
137 .ok_or_else(|| anyhow::anyhow!("missing required parameter: message"))?;
138
139 if local_url.is_empty() || auth_key.is_empty() {
140 anyhow::bail!(
141 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
142 Is this agent pane opened via AgentMux?"
143 );
144 }
145 let source_agent = std::env::var("AGENTMUX_AGENT_ID")
146 .ok()
147 .filter(|s| !s.is_empty());
148
149 let discovery_url = format!("{}/agentmux/discovery", local_url.trim_end_matches('/'));
182 let discovery_resp = client
183 .get(&discovery_url)
184 .header(AUTH_KEY_HEADER, auth_key)
185 .send()
186 .await
187 .map_err(|e| anyhow::anyhow!("discovery request failed: {e}"))?;
188 if !discovery_resp.status().is_success() {
189 let status = discovery_resp.status();
190 let text = discovery_resp.text().await.unwrap_or_default();
191 anyhow::bail!("discovery failed: HTTP {status} — {text}");
192 }
193 let discovery: Value = discovery_resp
194 .json()
195 .await
196 .map_err(|e| anyhow::anyhow!("discovery response parse failed: {e}"))?;
197 let block_to_agent = build_block_to_agent_map(&discovery);
198
199 const BROADCAST_CHUNK_SIZE: usize = 10;
215 const BROADCAST_CHUNK_PAUSE: std::time::Duration = std::time::Duration::from_millis(1100);
216
217 let url = format!("{}/agentmux/reactive/inject", local_url.trim_end_matches('/'));
218 let mut succeeded: Vec<String> = Vec::new();
219 let mut failed: Vec<serde_json::Value> = Vec::new();
220 for (chunk_idx, chunk) in targets.chunks(BROADCAST_CHUNK_SIZE).enumerate() {
221 if chunk_idx > 0 {
222 tokio::time::sleep(BROADCAST_CHUNK_PAUSE).await;
223 }
224 for target in chunk {
225 let target = target.clone();
226 let target_agent = block_to_agent.get(&target).cloned().unwrap_or_else(|| target.clone());
235 let req = signed_jekt(source_agent.clone(), &target_agent, message);
236 let outcome = async {
237 let resp = client
238 .post(&url)
239 .header(AUTH_KEY_HEADER, auth_key)
240 .json(&req)
241 .send()
242 .await
243 .map_err(|e| format!("request failed: {e}"))?;
244 if !resp.status().is_success() {
245 let status = resp.status();
246 let text = resp.text().await.unwrap_or_default();
247 return Err(format!("HTTP {status} — {text}"));
248 }
249 let result: Value = resp
250 .json()
251 .await
252 .map_err(|e| format!("response parse failed: {e}"))?;
253 if result.get("success").and_then(|v| v.as_bool()) == Some(true) {
254 Ok(())
255 } else {
256 Err(result
257 .get("error")
258 .and_then(|v| v.as_str())
259 .unwrap_or("unknown error")
260 .to_string())
261 }
262 }
263 .await;
264 match outcome {
265 Ok(()) => succeeded.push(target),
266 Err(error) => failed.push(json!({ "id": target, "error": error })),
267 }
268 }
269 }
270
271 let result = json!({ "succeeded": succeeded, "failed": failed });
272 Ok(serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()))
273 }
274 "FleetBulkStop" => {
275 let targets = arguments
276 .get("targets")
277 .and_then(|v| v.as_array())
278 .ok_or_else(|| anyhow::anyhow!("missing required parameter: targets"))?
279 .iter()
280 .filter_map(|v| v.as_str().map(str::to_string))
281 .collect::<Vec<_>>();
282 if targets.is_empty() {
283 anyhow::bail!("targets must be a non-empty array of block_id strings");
284 }
285 let signal = arguments.get("signal").and_then(|v| v.as_str()).map(str::to_string);
286 let staged = arguments.get("staged").cloned();
287
288 if local_url.is_empty() || auth_key.is_empty() {
289 anyhow::bail!(
290 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
291 Is this agent pane opened via AgentMux?"
292 );
293 }
294
295 let url = format!("{}/api/v1/fleet/bulk-stop", local_url.trim_end_matches('/'));
296 let mut body = json!({ "targets": targets, "signal": signal });
297 if let Some(staged) = staged {
298 body["staged"] = staged;
299 }
300 if let Ok(auth) = sign_ui_automation_auth() {
302 body["auth"] = serde_json::to_value(auth).unwrap_or(Value::Null);
303 }
304 let resp = client
305 .post(&url)
306 .header(AUTH_KEY_HEADER, auth_key)
307 .json(&body)
308 .send()
309 .await
310 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
311
312 if !resp.status().is_success() {
313 let status = resp.status();
314 let text = resp.text().await.unwrap_or_default();
315 anyhow::bail!("fleet bulk-stop failed: HTTP {status} — {text}");
316 }
317
318 let mut result: Value = resp
319 .json()
320 .await
321 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
322 if result["status"] == "pending_user_override" {
323 let pending = result["pending"].as_array().cloned().unwrap_or_default();
327 let mut outcomes = Vec::with_capacity(pending.len());
328 for p in &pending {
329 let request_id = p["request_id"].as_str().unwrap_or_default();
330 let now = await_shutdown(client, local_url, auth_key, request_id, true).await;
331 outcomes.push((p["block_id"].as_str().unwrap_or_default().to_string(), now));
332 }
333 result = merge_bulk_stop_outcomes(result, outcomes);
334 }
335
336 Ok(serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()))
337 }
338 "SupervisorNudge" => {
339 let target_agent = arguments
340 .get("target_agent")
341 .and_then(|v| v.as_str())
342 .filter(|s| !s.is_empty())
343 .ok_or_else(|| anyhow::anyhow!("missing required parameter: target_agent"))?;
344 let action = arguments
345 .get("action")
346 .and_then(|v| v.as_str())
347 .filter(|s| !s.is_empty())
348 .ok_or_else(|| anyhow::anyhow!("missing required parameter: action"))?;
349 if action != "nudge" && action != "decline" {
350 anyhow::bail!("invalid action: {action} (expected \"nudge\" or \"decline\")");
351 }
352 let reason = arguments.get("reason").and_then(|v| v.as_str());
353
354 if local_url.is_empty() || auth_key.is_empty() {
355 anyhow::bail!(
356 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
357 Is this agent pane opened via AgentMux?"
358 );
359 }
360
361 let source_agent = std::env::var("AGENTMUX_AGENT_ID")
362 .ok()
363 .filter(|s| !s.is_empty());
364
365 let url = format!(
366 "{}/agentmux/reactive/supervisor-decision",
367 local_url.trim_end_matches('/')
368 );
369 let body = json!({
370 "target_agent": target_agent,
371 "action": action,
372 "reason": reason,
373 "source_agent": source_agent,
374 });
375
376 let resp = client
377 .post(&url)
378 .header(AUTH_KEY_HEADER, auth_key)
379 .json(&body)
380 .send()
381 .await
382 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
383
384 let status = resp.status();
385 let result: Value = resp
386 .json()
387 .await
388 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
389
390 if !status.is_success() {
391 let err = result
392 .get("error")
393 .and_then(|v| v.as_str())
394 .unwrap_or("unknown error");
395 anyhow::bail!("supervisor decision rejected: HTTP {status} — {err}");
396 }
397
398 if result.get("success").and_then(|v| v.as_bool()) != Some(true) {
406 let err = result
407 .get("error")
408 .and_then(|v| v.as_str())
409 .unwrap_or("unknown error");
410 anyhow::bail!("supervisor decision delivery failed: {err}");
411 }
412
413 Ok(serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()))
414 }
415 _ => Err(not_in_family()),
416 }
417}