1use axum::{
5 extract::{Query, State},
6 http::StatusCode,
7 response::{IntoResponse, Json, Response},
8};
9use serde_json::json;
10
11use crate::backend::reactive::InjectionRequest;
12use crate::backend::reactive::registry as agent_registry;
13use crate::backend::subagent_watcher;
14use crate::backend::base;
15
16use super::AppState;
17
18pub(super) async fn handle_reactive_inject(
19 State(state): State<AppState>,
20 Json(req): Json<InjectionRequest>,
21) -> Json<serde_json::Value> {
22 tracing::info!(
23 target_agent = %req.target_agent,
24 source_agent = ?req.source_agent,
25 msg_len = req.message.len(),
26 "reactive inject request received"
27 );
28
29 let resp = state.reactive_handler.inject_message(req.clone());
31 if resp.success {
32 return Json(serde_json::to_value(&resp).unwrap_or_default());
33 }
34
35 let is_not_found = resp
37 .error
38 .as_deref()
39 .map(|e| e.starts_with("agent not found"))
40 .unwrap_or(false);
41
42 if is_not_found {
43 let data_dir = base::get_wave_data_dir();
45 if let Some(entry) = agent_registry::lookup(&data_dir, &req.target_agent) {
46 if entry.local_url != state.local_web_url {
48 let forward_url = format!("{}/agentmux/reactive/inject", entry.local_url);
49 tracing::debug!(
50 target = %req.target_agent,
51 url = %forward_url,
52 "cross-instance inject forward"
53 );
54 let mut fwd = state.http_client.post(&forward_url).json(&req);
55 if !entry.auth_key.is_empty() {
56 fwd = fwd.header("X-AuthKey", &entry.auth_key);
57 }
58 match fwd.send().await {
59 Ok(r) if r.status().is_success() => {
60 if let Ok(body) = r.json::<serde_json::Value>().await {
61 return Json(body);
62 }
63 }
64 Ok(r) => {
65 tracing::warn!(
66 target = %req.target_agent,
67 status = %r.status(),
68 url = %forward_url,
69 "cross-instance forward: non-success status"
70 );
71 }
72 Err(e) => {
73 tracing::warn!(
74 target = %req.target_agent,
75 error = %e,
76 url = %forward_url,
77 "cross-instance forward failed — removing stale registry entry"
78 );
79 agent_registry::remove(&data_dir, &req.target_agent);
80 }
81 }
82 }
83 }
84
85 if let Some((peer_url, peer_auth_key)) = state
89 .lan_discovery
90 .find_agent(&req.target_agent, &state.http_client)
91 .await
92 {
93 let forward_url = format!("{}/agentmux/reactive/inject", peer_url);
94 tracing::debug!(
95 target = %req.target_agent,
96 url = %forward_url,
97 "LAN peer inject forward"
98 );
99 let mut fwd = state.http_client.post(&forward_url).json(&req);
100 if !peer_auth_key.is_empty() {
101 fwd = fwd.header("X-AuthKey", &peer_auth_key);
102 }
103 match fwd.send().await {
104 Ok(r) if r.status().is_success() => {
105 if let Ok(body) = r.json::<serde_json::Value>().await {
106 if body.get("success").and_then(|v| v.as_bool()) == Some(false) {
109 tracing::warn!(
110 target = %req.target_agent,
111 url = %forward_url,
112 "LAN peer inject: success=false — evicting stale cache entry"
113 );
114 state.lan_discovery.evict_agent(&req.target_agent);
115 } else {
116 return Json(body);
117 }
118 }
119 }
120 Ok(r) => {
121 tracing::warn!(
122 target = %req.target_agent,
123 status = %r.status(),
124 url = %forward_url,
125 "LAN peer forward: non-success HTTP status"
126 );
127 }
128 Err(e) => {
129 tracing::warn!(
130 target = %req.target_agent,
131 error = %e,
132 url = %forward_url,
133 "LAN peer forward failed — evicting cache entry"
134 );
135 state.lan_discovery.evict_agent(&req.target_agent);
136 }
137 }
138 }
139 }
140
141 Json(serde_json::to_value(&resp).unwrap_or_default())
143}
144
145pub(super) async fn handle_reactive_agents(
146 State(state): State<AppState>,
147) -> Json<serde_json::Value> {
148 let agents = state.reactive_handler.list_agents();
149 Json(serde_json::to_value(&agents).unwrap_or(json!([])))
150}
151
152#[derive(serde::Deserialize)]
153pub(super) struct AgentQuery {
154 id: Option<String>,
155}
156
157pub(super) async fn handle_reactive_agent(
158 State(state): State<AppState>,
159 Query(params): Query<AgentQuery>,
160) -> Response {
161 let id = match ¶ms.id {
162 Some(id) if !id.is_empty() => id.as_str(),
163 _ => {
164 return (
165 StatusCode::BAD_REQUEST,
166 Json(json!({"error": "missing id param"})),
167 )
168 .into_response()
169 }
170 };
171 match state.reactive_handler.get_agent(id) {
172 Some(agent) => Json(serde_json::to_value(&agent).unwrap_or_default()).into_response(),
173 None => (
174 StatusCode::NOT_FOUND,
175 Json(json!({"error": "agent not found"})),
176 )
177 .into_response(),
178 }
179}
180
181#[derive(serde::Deserialize)]
182pub(super) struct AuditQuery {
183 #[serde(default = "default_audit_limit")]
184 limit: usize,
185}
186fn default_audit_limit() -> usize {
187 100
188}
189
190pub(super) async fn handle_reactive_audit(
191 State(state): State<AppState>,
192 Query(params): Query<AuditQuery>,
193) -> Json<serde_json::Value> {
194 let log = state.reactive_handler.get_audit_log(params.limit);
195 Json(serde_json::to_value(&log).unwrap_or(json!([])))
196}
197
198#[derive(serde::Deserialize)]
199pub(super) struct RegisterRequest {
200 agent_id: String,
201 block_id: String,
202 tab_id: Option<String>,
203}
204
205pub(super) async fn handle_reactive_register(
206 State(state): State<AppState>,
207 Json(req): Json<RegisterRequest>,
208) -> Response {
209 tracing::info!(
210 agent_id = %req.agent_id,
211 block_id = %req.block_id,
212 "reactive register request"
213 );
214 match state
215 .reactive_handler
216 .register_agent(&req.agent_id, &req.block_id, req.tab_id.as_deref())
217 {
218 Ok(()) => {
219 let data_dir = base::get_wave_data_dir();
222 agent_registry::write(&data_dir, &req.agent_id, &state.local_web_url, &req.block_id);
223
224 if let Some(config_dir) = subagent_watcher::derive_claude_config_dir(&req.agent_id) {
228 state.subagent_watcher.watch_agent(&req.agent_id, &req.block_id, config_dir);
229 }
230
231 if let Some(sub) = crate::muxbus::cloud_subscriber::get_global_subscriber() {
233 sub.add_agent(&req.agent_id);
234 }
235
236 state.broker.publish(crate::backend::wps::WaveEvent {
242 event: "agent:reactive-registered".to_string(),
243 scopes: vec![format!("block:{}", req.block_id)],
244 sender: String::new(),
245 persist: 0,
246 data: Some(json!({ "block_id": req.block_id })),
247 });
248
249 Json(json!({"success": true})).into_response()
250 }
251 Err(e) => (
252 StatusCode::BAD_REQUEST,
253 Json(json!({"error": e})),
254 )
255 .into_response(),
256 }
257}
258
259#[derive(serde::Deserialize)]
260pub(super) struct UnregisterRequest {
261 agent_id: String,
262}
263
264pub(super) async fn handle_reactive_unregister(
265 State(state): State<AppState>,
266 Json(req): Json<UnregisterRequest>,
267) -> Json<serde_json::Value> {
268 let block_id = state.reactive_handler.get_agent(&req.agent_id)
270 .map(|r| r.block_id.clone());
271
272 state.reactive_handler.unregister_agent(&req.agent_id);
273 let data_dir = base::get_wave_data_dir();
275 agent_registry::remove(&data_dir, &req.agent_id);
276 state.subagent_watcher.unwatch_agent(&req.agent_id);
279 if let Some(sub) = crate::muxbus::cloud_subscriber::get_global_subscriber() {
281 sub.remove_agent(&req.agent_id);
282 }
283
284 if let Some(bid) = block_id {
286 state.broker.publish(crate::backend::wps::WaveEvent {
287 event: "agent:reactive-unregistered".to_string(),
288 scopes: vec![format!("block:{}", bid)],
289 sender: String::new(),
290 persist: 0,
291 data: Some(json!({ "block_id": bid })),
292 });
293 }
294
295 Json(json!({"success": true}))
296}
297
298pub(super) async fn handle_reactive_poller_stats(
299 State(state): State<AppState>,
300) -> Json<serde_json::Value> {
301 let stats = state.poller.stats();
302 Json(serde_json::to_value(&stats).unwrap_or(json!({})))
303}
304
305#[derive(serde::Deserialize)]
306pub(super) struct PollerConfigRequest {
307 url: Option<String>,
308 token: Option<String>,
309}
310
311pub(super) async fn handle_reactive_poller_config(
312 State(state): State<AppState>,
313 Json(req): Json<PollerConfigRequest>,
314) -> Json<serde_json::Value> {
315 state.poller.reconfigure(req.url, req.token);
316 Json(json!({"success": true}))
317}
318
319pub(super) async fn handle_reactive_poller_status(
320 State(state): State<AppState>,
321) -> Json<serde_json::Value> {
322 let status = state.poller.status();
323 Json(serde_json::to_value(&status).unwrap_or(json!({})))
324}