1pub mod actions;
3pub mod response_guard;
4
5pub use actions::XmlRpcClientProtocol;
6
7use anyhow::{Context, Result};
8use std::collections::BTreeMap;
9use std::net::SocketAddr;
10use std::sync::Arc;
11use tokio::sync::mpsc;
12use tracing::{error, info};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::xmlrpc::actions::{
16 XMLRPC_CLIENT_CONNECTED_EVENT, XMLRPC_CLIENT_RESPONSE_RECEIVED_EVENT,
17};
18use crate::llm::actions::client_trait::{Client, ClientActionResult};
19use crate::llm::ollama_client::OllamaClient;
20use crate::llm::ClientLlmResult;
21use crate::logging::emit::Log;
22use crate::protocol::Event;
23use crate::state::app_state::AppState;
24use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
25use crate::state::{AccessLogOwner, ClientId, ClientStatus};
26
27enum Applied {
31 Ran(String),
33 Disconnect,
35}
36
37enum CallOutcome {
39 Result(serde_json::Value),
41 Fault(String),
43}
44
45#[derive(Clone, Copy)]
47enum Dispatch {
48 Inline,
51 Deferred,
55}
56
57const DEFAULT_TIMEOUT_SECS: u64 = 30;
60
61const MAX_VALUE_DEPTH: usize = 64;
72
73pub struct XmlRpcClient;
75
76impl XmlRpcClient {
77 pub async fn connect_with_llm_actions(
79 remote_addr: String,
80 _llm_client: OllamaClient,
81 app_state: Arc<AppState>,
82 status_tx: mpsc::UnboundedSender<String>,
83 client_id: ClientId,
84 startup_params: Option<crate::protocol::StartupParams>,
85 ) -> Result<SocketAddr> {
86 let timeout_secs = match &startup_params {
93 Some(params) => params.get_optional_u64("timeout_secs")?,
94 None => None,
95 }
96 .unwrap_or(DEFAULT_TIMEOUT_SECS);
97
98 info!(
99 "XML-RPC client {} initialized for {}",
100 client_id, remote_addr
101 );
102
103 let server_url =
105 if remote_addr.starts_with("http://") || remote_addr.starts_with("https://") {
106 remote_addr.clone()
107 } else {
108 format!("http://{}", remote_addr)
109 };
110
111 app_state
113 .with_client_mut(client_id, |client| {
114 client.set_protocol_field("server_url".to_string(), serde_json::json!(server_url));
115 client.set_protocol_field(
116 "timeout_secs".to_string(),
117 serde_json::json!(timeout_secs),
118 );
119 })
120 .await;
121
122 app_state
124 .update_client_status(client_id, ClientStatus::Connected)
125 .await;
126 Log::new(Some(&status_tx)).info(format!(
127 "XML-RPC client {} ready for {}",
128 client_id, server_url
129 ));
130 let _ = status_tx.send("__UPDATE_UI__".to_string());
131
132 let command_rx =
137 crate::client::command_support::register_command_channel(&app_state, client_id).await;
138 let cmd_task = tokio::spawn(Self::command_loop(
139 command_rx,
140 client_id,
141 app_state.clone(),
142 _llm_client.clone(),
143 status_tx.clone(),
144 ));
145 app_state.register_client_task(client_id, cmd_task).await;
146
147 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
149 let protocol = Arc::new(crate::client::xmlrpc::actions::XmlRpcClientProtocol::new());
150 let event = Event::new(
151 &XMLRPC_CLIENT_CONNECTED_EVENT,
152 serde_json::json!({
153 "server_url": server_url,
154 }),
155 );
156
157 let memory = app_state
158 .get_memory_for_client(client_id)
159 .await
160 .unwrap_or_default();
161
162 if let Ok(ClientLlmResult {
163 actions,
164 memory_updates,
165 }) = call_llm_for_client(
166 &_llm_client,
167 &app_state,
168 client_id.to_string(),
169 &instruction,
170 &memory,
171 Some(&event),
172 protocol.as_ref(),
173 &status_tx,
174 )
175 .await
176 {
177 if let Some(mem) = memory_updates {
179 app_state.set_memory_for_client(client_id, mem).await;
180 }
181
182 for action in actions {
184 let result = match protocol.execute_action(action) {
185 Ok(result) => result,
186 Err(e) => {
187 error!("XML-RPC client {} rejected action: {}", client_id, e);
188 continue;
189 }
190 };
191
192 if matches!(result, ClientActionResult::Disconnect) {
193 info!(
194 "XML-RPC client {} disconnecting on initial action",
195 client_id
196 );
197 let _ = Self::apply_action(
198 result,
199 Dispatch::Inline,
200 client_id,
201 &app_state,
202 &_llm_client,
203 &status_tx,
204 )
205 .await;
206 app_state.remove_client_handle(client_id).await;
208 let _ = status_tx.send("__UPDATE_UI__".to_string());
209 return Ok("0.0.0.0:0".parse().unwrap());
210 }
211
212 let app_state_clone = app_state.clone();
215 let llm_client_clone = _llm_client.clone();
216 let status_tx_clone = status_tx.clone();
217 let task_registrar = app_state.clone();
220 let task_handle = tokio::spawn(async move {
221 if let Err(e) = Self::apply_action(
222 result,
223 Dispatch::Inline,
224 client_id,
225 &app_state_clone,
226 &llm_client_clone,
227 &status_tx_clone,
228 )
229 .await
230 {
231 error!("XML-RPC client {} action failed: {}", client_id, e);
232 }
233 });
234 task_registrar
235 .register_client_task(client_id, task_handle)
236 .await;
237 }
238 }
239 }
240
241 Ok("0.0.0.0:0".parse().unwrap())
246 }
247
248 async fn apply_action(
251 result: ClientActionResult,
252 dispatch: Dispatch,
253 client_id: ClientId,
254 app_state: &Arc<AppState>,
255 llm_client: &OllamaClient,
256 status_tx: &mpsc::UnboundedSender<String>,
257 ) -> Result<Applied> {
258 match result {
259 ClientActionResult::Custom { name, data } if name == "xmlrpc_call" => {
260 let method = data
261 .get("method_name")
262 .and_then(|v| v.as_str())
263 .context("Missing 'method_name' in xmlrpc_call")?
264 .to_string();
265 let params = data
266 .get("params")
267 .and_then(|v| v.as_array())
268 .cloned()
269 .unwrap_or_default();
270 let param_count = params.len();
271
272 let outcome =
273 Self::perform_call(client_id, &method, params, app_state, status_tx).await?;
274 let detail = match &outcome {
275 CallOutcome::Result(_) => format!(
276 "xmlrpc_call '{}' with {} param(s): server returned a result",
277 method, param_count
278 ),
279 CallOutcome::Fault(fault) => format!(
280 "xmlrpc_call '{}' with {} param(s): server returned fault: {}",
281 method, param_count, fault
282 ),
283 };
284
285 match dispatch {
286 Dispatch::Inline => {
287 Self::notify_call_result(
288 client_id,
289 method,
290 outcome,
291 app_state.clone(),
292 llm_client.clone(),
293 status_tx.clone(),
294 )
295 .await;
296 }
297 Dispatch::Deferred => {
298 let handle = tokio::spawn(Self::notify_call_result(
299 client_id,
300 method,
301 outcome,
302 app_state.clone(),
303 llm_client.clone(),
304 status_tx.clone(),
305 ));
306 app_state.register_client_task(client_id, handle).await;
308 }
309 }
310 Ok(Applied::Ran(detail))
311 }
312 ClientActionResult::Disconnect => {
313 info!("XML-RPC client {} disconnecting", client_id);
314 app_state
315 .update_client_status(client_id, ClientStatus::Disconnected)
316 .await;
317 let _ = status_tx.send("__UPDATE_UI__".to_string());
318 Ok(Applied::Disconnect)
319 }
320 ClientActionResult::Custom { name, .. } => Err(anyhow::anyhow!(
321 "XML-RPC client cannot execute custom result '{}'",
322 name
323 )),
324 _ => Ok(Applied::Ran(
326 "no method called (action produced no XML-RPC request)".to_string(),
327 )),
328 }
329 }
330
331 async fn command_loop(
336 mut command_rx: mpsc::Receiver<ClientCommand>,
337 client_id: ClientId,
338 app_state: Arc<AppState>,
339 llm_client: OllamaClient,
340 status_tx: mpsc::UnboundedSender<String>,
341 ) {
342 use crate::llm::actions::protocol_trait::Protocol;
343
344 let protocol = crate::client::xmlrpc::actions::XmlRpcClientProtocol::new();
345
346 while let Some(command) = command_rx.recv().await {
347 let action = command.action.clone();
348 let outcome = match protocol.execute_action(action.clone()) {
349 Err(e) => Ok(ClientSendOutcome::Rejected {
350 error: e.to_string(),
351 }),
352 Ok(result) => Self::apply_action(
353 result,
354 Dispatch::Deferred,
355 client_id,
356 &app_state,
357 &llm_client,
358 &status_tx,
359 )
360 .await
361 .map(|applied| match applied {
362 Applied::Disconnect => ClientSendOutcome::Disconnected,
363 Applied::Ran(detail) => ClientSendOutcome::Executed { detail },
364 }),
365 };
366
367 let outcome_json = match &outcome {
368 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
369 Err(e) => serde_json::json!({"error": e.to_string()}),
370 };
371 app_state
372 .record_access_log(
373 AccessLogOwner::Client(client_id.as_u32()),
374 protocol.protocol_name(),
375 None,
376 "injected_action",
377 action,
378 vec![outcome_json],
379 )
380 .await;
381
382 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
383 if let Err(e) = &outcome {
384 error!("XML-RPC client {} injected action failed: {}", client_id, e);
385 let _ = status_tx.send(format!(
386 "[WARN] Client {} injected action failed: {}",
387 client_id, e
388 ));
389 }
390 let _ = status_tx.send("__UPDATE_UI__".to_string());
391 crate::client::command_support::reply(command, outcome);
392
393 if disconnect {
394 break;
395 }
396 }
397
398 app_state.remove_client_handle(client_id).await;
399 let _ = status_tx.send("__UPDATE_UI__".to_string());
400 info!("XML-RPC client {} command loop ended", client_id);
401 }
402
403 pub fn call_method(
405 client_id: ClientId,
406 method_name: String,
407 params: Vec<serde_json::Value>,
408 app_state: Arc<AppState>,
409 llm_client: OllamaClient,
410 status_tx: mpsc::UnboundedSender<String>,
411 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>> {
412 Box::pin(async move {
413 Self::call_method_impl(
414 client_id,
415 method_name,
416 params,
417 app_state,
418 llm_client,
419 status_tx,
420 )
421 .await
422 })
423 }
424
425 async fn call_method_impl(
427 client_id: ClientId,
428 method_name: String,
429 params: Vec<serde_json::Value>,
430 app_state: Arc<AppState>,
431 llm_client: OllamaClient,
432 status_tx: mpsc::UnboundedSender<String>,
433 ) -> Result<()> {
434 let outcome =
435 Self::perform_call(client_id, &method_name, params, &app_state, &status_tx).await?;
436 let failed = match &outcome {
437 CallOutcome::Fault(fault) => Some(fault.clone()),
438 CallOutcome::Result(_) => None,
439 };
440 Self::notify_call_result(
441 client_id,
442 method_name,
443 outcome,
444 app_state,
445 llm_client,
446 status_tx,
447 )
448 .await;
449 match failed {
450 Some(fault) => Err(anyhow::anyhow!("XML-RPC error: {}", fault)),
451 None => Ok(()),
452 }
453 }
454
455 async fn perform_call(
463 client_id: ClientId,
464 method_name: &str,
465 params: Vec<serde_json::Value>,
466 app_state: &Arc<AppState>,
467 status_tx: &mpsc::UnboundedSender<String>,
468 ) -> Result<CallOutcome> {
469 let (server_url, timeout_secs) = app_state
471 .with_client_mut(client_id, |client| {
472 (
473 client
474 .get_protocol_field("server_url")
475 .and_then(|v| v.as_str())
476 .map(|s| s.to_string()),
477 client
478 .get_protocol_field("timeout_secs")
479 .and_then(|v| v.as_u64()),
480 )
481 })
482 .await
483 .unwrap_or((None, None));
484 let server_url = server_url.context("No server URL found")?;
485 let timeout = std::time::Duration::from_secs(timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS));
486
487 info!(
488 "XML-RPC client {} calling method: {} with {} params",
489 client_id,
490 method_name,
491 params.len()
492 );
493
494 let mut xmlrpc_params = Vec::new();
496 for param in params {
497 let xmlrpc_value = Self::json_to_xmlrpc_value(param)?;
498 xmlrpc_params.push(xmlrpc_value);
499 }
500
501 let method_owned = method_name.to_string();
502 let log_name = method_name.to_string();
503
504 let request_xml = {
514 let mut request = xmlrpc::Request::new(&method_owned);
515 for param in &xmlrpc_params {
516 request = request.arg(param.clone());
517 }
518 let mut buf = Vec::new();
519 request
520 .write_as_xml(&mut buf)
521 .context("Failed to serialise the XML-RPC request")?;
522 buf
523 };
524
525 let http = response_guard::http_client_for(&server_url, timeout).await;
526 let fetch = async {
527 let response = http
528 .post(&server_url)
529 .header(reqwest::header::USER_AGENT, "Rust xmlrpc")
530 .header(reqwest::header::CONTENT_TYPE, "text/xml; charset=utf-8")
531 .body(request_xml)
532 .send()
533 .await?;
534 response_guard::check_response(&response)?;
535 response_guard::read_body_capped(response).await
536 };
537
538 let body = match tokio::time::timeout(timeout, fetch).await {
543 Ok(Ok(body)) => body,
544 Ok(Err(e)) => {
545 if let Some(refusal) = e.downcast_ref::<response_guard::ResponseRefusal>() {
547 Log::new(Some(status_tx)).error(format!(
548 "XML-RPC client {} refused the reply to '{}' decision={} ({})",
549 client_id,
550 log_name,
551 refusal.decision_tag(),
552 refusal
553 ));
554 } else {
555 Log::new(Some(status_tx)).error(format!(
556 "XML-RPC client {} call to {} failed: {}",
557 client_id, log_name, e
558 ));
559 }
560 return Err(e);
561 }
562 Err(_) => {
563 Log::new(Some(status_tx)).error(format!(
564 "XML-RPC client {} call to {} timed out after {}s",
565 client_id,
566 log_name,
567 timeout.as_secs()
568 ));
569 return Err(anyhow::anyhow!(
570 "XML-RPC call '{}' timed out after {}s (timeout_secs)",
571 log_name,
572 timeout.as_secs()
573 ));
574 }
575 };
576
577 let scan_and_parse = tokio::task::spawn_blocking(move || {
580 response_guard::scan_depth(&body)?;
581 let mut request = xmlrpc::Request::new(&method_owned);
582 for param in xmlrpc_params {
583 request = request.arg(param);
584 }
585 Ok::<_, anyhow::Error>(request.call(response_guard::PrefetchedTransport::new(body)))
586 })
587 .await;
588
589 match scan_and_parse {
590 Ok(Ok(Ok(response))) => {
591 info!(
592 "XML-RPC client {} received response for {}",
593 client_id, log_name
594 );
595 Ok(CallOutcome::Result(Self::xmlrpc_value_to_json(&response)))
596 }
597 Ok(Ok(Err(err))) if err.fault().is_some() => {
599 let fault_msg = err.to_string();
600 error!(
601 "XML-RPC client {} fault for {}: {}",
602 client_id, log_name, fault_msg
603 );
604 Ok(CallOutcome::Fault(fault_msg))
605 }
606 Ok(Ok(Err(err))) => {
608 Log::new(Some(status_tx)).error(format!(
609 "XML-RPC client {} could not parse the reply to {}: {}",
610 client_id, log_name, err
611 ));
612 Err(anyhow::anyhow!(
613 "XML-RPC reply to '{}' could not be parsed: {}",
614 log_name,
615 err
616 ))
617 }
618 Ok(Err(refusal)) => {
619 if let Some(r) = refusal.downcast_ref::<response_guard::ResponseRefusal>() {
620 Log::new(Some(status_tx)).error(format!(
621 "XML-RPC client {} refused the reply to '{}' decision={} ({})",
622 client_id,
623 log_name,
624 r.decision_tag(),
625 r
626 ));
627 }
628 Err(refusal)
629 }
630 Err(e) => {
631 Log::new(Some(status_tx))
632 .error(format!("XML-RPC client {} call failed: {}", client_id, e));
633 Err(e.into())
634 }
635 }
636 }
637
638 fn notify_call_result(
641 client_id: ClientId,
642 method_name: String,
643 outcome: CallOutcome,
644 app_state: Arc<AppState>,
645 llm_client: OllamaClient,
646 status_tx: mpsc::UnboundedSender<String>,
647 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
648 Box::pin(async move {
649 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
650 return;
651 };
652
653 let protocol = Arc::new(crate::client::xmlrpc::actions::XmlRpcClientProtocol::new());
654 let data = match &outcome {
655 CallOutcome::Result(result) => serde_json::json!({
656 "method_name": method_name,
657 "result": result,
658 }),
659 CallOutcome::Fault(fault) => serde_json::json!({
660 "method_name": method_name,
661 "fault": { "error": fault },
662 }),
663 };
664 let event = Event::new(&XMLRPC_CLIENT_RESPONSE_RECEIVED_EVENT, data);
665
666 let memory = app_state
667 .get_memory_for_client(client_id)
668 .await
669 .unwrap_or_default();
670
671 match call_llm_for_client(
672 &llm_client,
673 &app_state,
674 client_id.to_string(),
675 &instruction,
676 &memory,
677 Some(&event),
678 protocol.as_ref(),
679 &status_tx,
680 )
681 .await
682 {
683 Ok(ClientLlmResult {
684 actions,
685 memory_updates,
686 }) => {
687 if let Some(mem) = memory_updates {
688 app_state.set_memory_for_client(client_id, mem).await;
689 }
690
691 for action in actions {
693 let result = match protocol.execute_action(action) {
694 Ok(result) => result,
695 Err(e) => {
696 error!("XML-RPC client {} rejected action: {}", client_id, e);
697 continue;
698 }
699 };
700 match Self::apply_action(
701 result,
702 Dispatch::Inline,
703 client_id,
704 &app_state,
705 &llm_client,
706 &status_tx,
707 )
708 .await
709 {
710 Ok(Applied::Ran(detail)) => {
711 info!("XML-RPC client {}: {}", client_id, detail)
712 }
713 Ok(Applied::Disconnect) => break,
714 Err(e) => {
715 error!("XML-RPC client {} action failed: {}", client_id, e)
716 }
717 }
718 }
719 }
720 Err(e) => {
721 error!("LLM error for XML-RPC client {}: {}", client_id, e);
722 }
723 }
724 })
725 }
726
727 fn json_to_xmlrpc_value(json: serde_json::Value) -> Result<xmlrpc::Value> {
733 Self::json_to_xmlrpc_value_at(json, 0)
734 }
735
736 fn json_to_xmlrpc_value_at(json: serde_json::Value, depth: usize) -> Result<xmlrpc::Value> {
737 if depth >= MAX_VALUE_DEPTH {
738 anyhow::bail!("parameter nesting deeper than {} levels", MAX_VALUE_DEPTH);
739 }
740 match json {
741 serde_json::Value::Null => Ok(xmlrpc::Value::String("".to_string())),
742 serde_json::Value::Bool(b) => Ok(xmlrpc::Value::Bool(b)),
743 serde_json::Value::Number(n) => {
744 if let Some(i) = n.as_i64() {
745 if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
746 Ok(xmlrpc::Value::Int(i as i32))
747 } else {
748 Ok(xmlrpc::Value::Int64(i))
749 }
750 } else if let Some(f) = n.as_f64() {
751 Ok(xmlrpc::Value::Double(f))
752 } else {
753 Err(anyhow::anyhow!("Invalid number"))
754 }
755 }
756 serde_json::Value::String(s) => Ok(xmlrpc::Value::String(s)),
757 serde_json::Value::Array(arr) => {
758 let mut xmlrpc_arr = Vec::new();
759 for item in arr {
760 xmlrpc_arr.push(Self::json_to_xmlrpc_value_at(item, depth + 1)?);
761 }
762 Ok(xmlrpc::Value::Array(xmlrpc_arr))
763 }
764 serde_json::Value::Object(obj) => {
765 let mut xmlrpc_struct = BTreeMap::new();
766 for (key, value) in obj {
767 xmlrpc_struct.insert(key, Self::json_to_xmlrpc_value_at(value, depth + 1)?);
768 }
769 Ok(xmlrpc::Value::Struct(xmlrpc_struct))
770 }
771 }
772 }
773
774 fn xmlrpc_value_to_json(value: &xmlrpc::Value) -> serde_json::Value {
790 Self::xmlrpc_value_to_json_at(value, 0)
791 }
792
793 fn xmlrpc_value_to_json_at(value: &xmlrpc::Value, depth: usize) -> serde_json::Value {
794 if depth >= MAX_VALUE_DEPTH {
795 return serde_json::json!(format!(
796 "<nesting deeper than {} levels was not decoded>",
797 MAX_VALUE_DEPTH
798 ));
799 }
800 match value {
801 xmlrpc::Value::Int(i) => serde_json::json!(i),
802 xmlrpc::Value::Int64(i) => serde_json::json!(i),
803 xmlrpc::Value::Bool(b) => serde_json::json!(b),
804 xmlrpc::Value::String(s) => serde_json::json!(s),
805 xmlrpc::Value::Double(f) => serde_json::json!(f),
806 xmlrpc::Value::DateTime(dt) => serde_json::json!(dt.to_string()),
807 xmlrpc::Value::Base64(b) => {
808 serde_json::json!(base64::Engine::encode(
810 &base64::engine::general_purpose::STANDARD,
811 b
812 ))
813 }
814 xmlrpc::Value::Array(arr) => {
815 let json_arr: Vec<_> = arr
816 .iter()
817 .map(|v| Self::xmlrpc_value_to_json_at(v, depth + 1))
818 .collect();
819 serde_json::json!(json_arr)
820 }
821 xmlrpc::Value::Struct(s) => {
822 let mut obj = serde_json::Map::new();
823 for (key, value) in s {
824 obj.insert(key.clone(), Self::xmlrpc_value_to_json_at(value, depth + 1));
825 }
826 serde_json::json!(obj)
827 }
828 xmlrpc::Value::Nil => serde_json::Value::Null,
829 }
830 }
831}