1use crate::llm::actions::{
10 protocol_trait::{ActionResult, Protocol, Server},
11 ActionDefinition, Parameter,
12};
13use crate::protocol::log_template::LogTemplate;
14use crate::protocol::{Event, EventType};
15use crate::state::app_state::AppState;
16use anyhow::{Context, Result};
17use serde_json::json;
18use std::sync::LazyLock;
19use tracing::debug;
20
21use super::{generate_fault, generate_success_response, MethodCall, XmlRpcValue};
22
23pub struct XmlRpcProtocol;
25
26impl XmlRpcProtocol {
27 pub fn new() -> Self {
28 Self
29 }
30
31 fn as_integer(value: &serde_json::Value, what: &str) -> Result<i64> {
39 value
40 .as_i64()
41 .or_else(|| value.as_str().and_then(|s| s.trim().parse::<i64>().ok()))
42 .with_context(|| {
43 format!(
44 "value must be an integer for value_type '{}', got {}",
45 what, value
46 )
47 })
48 }
49
50 fn as_double(value: &serde_json::Value) -> Result<f64> {
52 value
53 .as_f64()
54 .or_else(|| value.as_str().and_then(|s| s.trim().parse::<f64>().ok()))
55 .with_context(|| {
56 format!(
57 "value must be a number for value_type 'double', got {}",
58 value
59 )
60 })
61 }
62
63 fn as_boolean(value: &serde_json::Value) -> Result<bool> {
66 if let Some(b) = value.as_bool() {
67 return Ok(b);
68 }
69 if let Some(i) = value.as_i64() {
70 return Ok(i != 0);
71 }
72 match value.as_str().map(|s| s.trim().to_ascii_lowercase()) {
73 Some(ref s) if s == "true" || s == "1" => Ok(true),
74 Some(ref s) if s == "false" || s == "0" => Ok(false),
75 _ => Err(anyhow::anyhow!(
76 "value must be a boolean for value_type 'boolean', got {}",
77 value
78 )),
79 }
80 }
81
82 fn execute_success_response(&self, action: serde_json::Value) -> Result<ActionResult> {
83 let value_type = action
84 .get("value_type")
85 .and_then(|v| v.as_str())
86 .context("Missing 'value_type' parameter")?;
87
88 let value = action.get("value").context("Missing 'value' parameter")?;
89
90 let xmlrpc_value = match value_type {
92 "int" | "i4" => {
93 let i = Self::as_integer(value, value_type)?;
96 let i = i32::try_from(i).map_err(|_| {
97 anyhow::anyhow!(
98 "{} does not fit in an XML-RPC <int> (-2147483648..2147483647); use value_type 'i8'",
99 i
100 )
101 })?;
102 XmlRpcValue::Int(i)
103 }
104 "i8" => {
105 let i = Self::as_integer(value, "i8")?;
106 XmlRpcValue::I8(i)
107 }
108 "boolean" | "bool" => {
109 let b = Self::as_boolean(value)?;
110 XmlRpcValue::Boolean(b)
111 }
112 "string" => {
113 let s = value.as_str().context("Invalid string value")?;
114 XmlRpcValue::String(s.to_string())
115 }
116 "double" => {
117 let d = Self::as_double(value)?;
118 if !d.is_finite() {
119 return Err(anyhow::anyhow!("double value must be finite"));
121 }
122 XmlRpcValue::Double(d)
123 }
124 "array" => {
125 let arr = value.as_array().context("Invalid array value")?;
126 let items: Vec<XmlRpcValue> = arr
127 .iter()
128 .map(|v| self.json_to_xmlrpc_value(v))
129 .collect::<Result<Vec<_>>>()?;
130 XmlRpcValue::Array(items)
131 }
132 "struct" => {
133 let obj = value.as_object().context("Invalid struct value")?;
134 let members: Vec<(String, XmlRpcValue)> = obj
135 .iter()
136 .map(|(k, v)| Ok((k.clone(), self.json_to_xmlrpc_value(v)?)))
137 .collect::<Result<Vec<_>>>()?;
138 XmlRpcValue::Struct(members)
139 }
140 "nil" | "null" => XmlRpcValue::Nil,
141 _ => return Err(anyhow::anyhow!("Unknown value_type: {}", value_type)),
142 };
143
144 let xml = generate_success_response(&xmlrpc_value);
145
146 debug!("XML-RPC success response generated ({} bytes)", xml.len());
147 Ok(ActionResult::Output(xml.as_bytes().to_vec()))
148 }
149
150 fn execute_fault_response(&self, action: serde_json::Value) -> Result<ActionResult> {
151 let code_i64 = match action.get("fault_code") {
159 Some(value) => Self::as_integer(value, "fault_code")?,
160 None => -32603,
161 };
162 let code = i32::try_from(code_i64).map_err(|_| {
163 anyhow::anyhow!("fault_code {} does not fit in an XML-RPC <int>", code_i64)
164 })?;
165
166 let message = action
167 .get("fault_string")
168 .and_then(|v| v.as_str())
169 .context("Missing 'fault_string' parameter")?;
170
171 let xml = generate_fault(code, message);
172
173 debug!("XML-RPC fault response: {} - {}", code, message);
174 Ok(ActionResult::Output(xml.as_bytes().to_vec()))
175 }
176
177 fn execute_list_methods_response(&self, action: serde_json::Value) -> Result<ActionResult> {
178 let methods = action
179 .get("methods")
180 .and_then(|v| v.as_array())
181 .context("Missing 'methods' parameter")?;
182
183 let method_list: Vec<XmlRpcValue> = methods
186 .iter()
187 .map(|v| {
188 v.as_str()
189 .map(|s| XmlRpcValue::String(s.to_string()))
190 .context("every entry of 'methods' must be a method-name string")
191 })
192 .collect::<Result<Vec<_>>>()?;
193
194 let method_count = method_list.len();
195 let response_value = XmlRpcValue::Array(method_list);
196 let xml = generate_success_response(&response_value);
197
198 debug!(
199 "XML-RPC system.listMethods response ({} methods)",
200 method_count
201 );
202 Ok(ActionResult::Output(xml.as_bytes().to_vec()))
203 }
204
205 fn execute_method_help_response(&self, action: serde_json::Value) -> Result<ActionResult> {
206 let help_text = action
207 .get("help_text")
208 .and_then(|v| v.as_str())
209 .context("Missing 'help_text' parameter")?;
210
211 let response_value = XmlRpcValue::String(help_text.to_string());
212 let xml = generate_success_response(&response_value);
213
214 debug!("XML-RPC system.methodHelp response");
215 Ok(ActionResult::Output(xml.as_bytes().to_vec()))
216 }
217
218 fn execute_method_signature_response(&self, action: serde_json::Value) -> Result<ActionResult> {
219 let signatures = action
220 .get("signatures")
221 .and_then(|v| v.as_array())
222 .context("Missing 'signatures' parameter")?;
223
224 let sig_list: Vec<XmlRpcValue> = signatures
228 .iter()
229 .map(|v| {
230 let arr = v.as_array().context(
231 "each entry of 'signatures' must itself be an array of type names, \
232 starting with the return type",
233 )?;
234 let types: Vec<XmlRpcValue> = arr
235 .iter()
236 .map(|t| {
237 t.as_str()
238 .map(|s| XmlRpcValue::String(s.to_string()))
239 .context("signature type names must be strings")
240 })
241 .collect::<Result<Vec<_>>>()?;
242 Ok(XmlRpcValue::Array(types))
243 })
244 .collect::<Result<Vec<_>>>()?;
245
246 let response_value = XmlRpcValue::Array(sig_list);
247 let xml = generate_success_response(&response_value);
248
249 debug!("XML-RPC system.methodSignature response");
250 Ok(ActionResult::Output(xml.as_bytes().to_vec()))
251 }
252
253 fn json_to_xmlrpc_value(&self, value: &serde_json::Value) -> Result<XmlRpcValue> {
255 match value {
256 serde_json::Value::Null => Ok(XmlRpcValue::Nil),
257 serde_json::Value::Bool(b) => Ok(XmlRpcValue::Boolean(*b)),
258 serde_json::Value::Number(n) => {
259 if let Some(i) = n.as_i64() {
260 if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
261 Ok(XmlRpcValue::Int(i as i32))
262 } else {
263 Ok(XmlRpcValue::I8(i))
264 }
265 } else if let Some(f) = n.as_f64() {
266 Ok(XmlRpcValue::Double(f))
267 } else {
268 Err(anyhow::anyhow!("Invalid number"))
269 }
270 }
271 serde_json::Value::String(s) => Ok(XmlRpcValue::String(s.clone())),
272 serde_json::Value::Array(arr) => {
273 let items: Vec<XmlRpcValue> = arr
274 .iter()
275 .map(|v| self.json_to_xmlrpc_value(v))
276 .collect::<Result<Vec<_>>>()?;
277 Ok(XmlRpcValue::Array(items))
278 }
279 serde_json::Value::Object(obj) => {
280 let members: Vec<(String, XmlRpcValue)> = obj
281 .iter()
282 .map(|(k, v)| Ok((k.clone(), self.json_to_xmlrpc_value(v)?)))
283 .collect::<Result<Vec<_>>>()?;
284 Ok(XmlRpcValue::Struct(members))
285 }
286 }
287 }
288}
289
290impl Protocol for XmlRpcProtocol {
292 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
293 Vec::new()
295 }
296 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
297 vec![
298 success_response_action(),
299 fault_response_action(),
300 list_methods_response_action(),
301 method_help_response_action(),
302 method_signature_response_action(),
303 ]
304 }
305 fn protocol_name(&self) -> &'static str {
306 "XML-RPC"
307 }
308 fn get_event_types(&self) -> Vec<EventType> {
309 vec![XMLRPC_METHOD_CALL_EVENT.clone()]
310 }
311 fn stack_name(&self) -> &'static str {
312 "ETH>IP>TCP>HTTP>XML-RPC"
313 }
314 fn keywords(&self) -> Vec<&'static str> {
315 vec!["xmlrpc", "xml-rpc", "xml rpc"]
316 }
317 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
318 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
319
320 ProtocolMetadataV2::builder()
321 .state(DevelopmentState::Experimental)
322 .implementation("Hand-written XML-RPC codec over HTTP POST (quick-xml, hyper 1)")
323 .llm_control("Every method call: a typed return value or a fault code/string")
324 .e2e_testing("curl and mocked in-process HTTP")
325 .notes(
326 "Parameters reach the model with their XML-RPC types: <int>5</int> is the \
327 number 5, not \"5\". <base64> is reported as a structural description, not \
328 an encoded blob. Faults are XML-escaped, so a fault_string containing < or \
329 & is still valid XML. system.multicall is not implemented. Any path is \
330 accepted; there is no auth, no rate limiting, and non-POST returns a fault \
331 body with HTTP 200 rather than 405. Request bodies are capped at 4 MiB and \
332 value nesting at 64 levels.",
333 )
334 .max_inbound_bytes(crate::server::xmlrpc::MAX_REQUEST_BODY_BYTES)
335 .request_only(
336 "XML-RPC is HTTP request/response; a server cannot send a peer anything unprompted",
337 )
338 .build()
339 }
340 fn description(&self) -> &'static str {
341 "XML-RPC server"
342 }
343 fn example_prompt(&self) -> &'static str {
344 "Start an XML-RPC server on port 8080 with methods add(a,b) and greet(name)"
345 }
346 fn group_name(&self) -> &'static str {
347 "AI & API"
348 }
349 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
350 use crate::llm::actions::StartupExamples;
351
352 StartupExamples::new(
353 json!({
355 "type": "open_server",
356 "port": 8080,
357 "base_stack": "xmlrpc",
358 "instruction": "XML-RPC server with add(a,b), subtract(a,b), and system.listMethods. Return proper fault codes for errors"
359 }),
360 json!({
362 "type": "open_server",
363 "port": 8080,
364 "base_stack": "xmlrpc",
365 "event_handlers": [{
366 "event_pattern": "xmlrpc_method_call",
367 "handler": {
368 "type": "script",
369 "language": "python",
370 "code": "method = event.get('method_name', '')\nparams = event.get('params', [])\nif method == 'add' and len(params) >= 2:\n action('xmlrpc_success_response', value_type='int', value=params[0] + params[1])\nelse:\n action('xmlrpc_fault_response', fault_code=-32601, fault_string='Method not found')"
371 }
372 }]
373 }),
374 json!({
376 "type": "open_server",
377 "port": 8080,
378 "base_stack": "xmlrpc",
379 "event_handlers": [{
380 "event_pattern": "xmlrpc_method_call",
381 "handler": {
382 "type": "static",
383 "actions": [{
384 "type": "xmlrpc_success_response",
385 "value_type": "string",
386 "value": "Hello from XML-RPC server"
387 }]
388 }
389 }]
390 }),
391 )
392 }
393}
394
395impl Server for XmlRpcProtocol {
397 fn spawn(
398 &self,
399 ctx: crate::protocol::SpawnContext,
400 ) -> std::pin::Pin<
401 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
402 > {
403 Box::pin(async move {
404 use crate::server::xmlrpc::XmlRpcServer;
405 XmlRpcServer::spawn_with_llm_actions(
406 ctx.legacy_listen_addr(),
407 ctx.llm_client,
408 ctx.state,
409 ctx.status_tx,
410 ctx.server_id,
411 )
412 .await
413 })
414 }
415 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
416 let action_type = action
417 .get("type")
418 .and_then(|v| v.as_str())
419 .context("Missing 'type' field")?;
420
421 match action_type {
422 "xmlrpc_success_response" => self.execute_success_response(action),
423 "xmlrpc_fault_response" => self.execute_fault_response(action),
424 "xmlrpc_list_methods_response" => self.execute_list_methods_response(action),
425 "xmlrpc_method_help_response" => self.execute_method_help_response(action),
426 "xmlrpc_method_signature_response" => self.execute_method_signature_response(action),
427 _ => Err(anyhow::anyhow!("Unknown action type: {}", action_type)),
428 }
429 }
430}
431
432fn success_response_action() -> ActionDefinition {
435 ActionDefinition {
436 name: "xmlrpc_success_response".to_string(),
437 description: "Send XML-RPC success response with a value".to_string(),
438 parameters: vec![
439 Parameter {
440 name: "value_type".to_string(),
441 type_hint: "string".to_string(),
442 description: "Type of the return value (int, i8, boolean, string, double, array, struct, nil)".to_string(),
443 required: true,
444 },
445 Parameter {
446 name: "value".to_string(),
447 type_hint: "any".to_string(),
448 description: "The actual value to return (type must match value_type)".to_string(),
449 required: true,
450 },
451 ],
452 example: json!({
453 "type": "xmlrpc_success_response",
454 "value_type": "int",
455 "value": 42
456 }),
457 log_template: Some(
458 LogTemplate::new()
459 .with_info("-> XML-RPC success type={value_type}")
460 .with_debug("XML-RPC xmlrpc_success_response: type={value_type}"),
461 ),
462 }
463}
464
465fn fault_response_action() -> ActionDefinition {
466 ActionDefinition {
467 name: "xmlrpc_fault_response".to_string(),
468 description: "Send XML-RPC fault/error response".to_string(),
469 parameters: vec![
470 Parameter {
471 name: "fault_code".to_string(),
472 type_hint: "number".to_string(),
473 description: "Fault code (standard codes: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error)".to_string(),
474 required: true,
475 },
476 Parameter {
477 name: "fault_string".to_string(),
478 type_hint: "string".to_string(),
479 description: "Human-readable error message".to_string(),
480 required: true,
481 },
482 ],
483 example: json!({
484 "type": "xmlrpc_fault_response",
485 "fault_code": -32601,
486 "fault_string": "Method not found"
487 }),
488 log_template: Some(
489 LogTemplate::new()
490 .with_info("-> XML-RPC fault {fault_code}: {fault_string}")
491 .with_debug("XML-RPC xmlrpc_fault_response: code={fault_code} message={fault_string}"),
492 ),
493 }
494}
495
496fn list_methods_response_action() -> ActionDefinition {
497 ActionDefinition {
498 name: "xmlrpc_list_methods_response".to_string(),
499 description: "Respond to system.listMethods introspection with array of method names"
500 .to_string(),
501 parameters: vec![Parameter {
502 name: "methods".to_string(),
503 type_hint: "array".to_string(),
504 description: "Array of available method names (strings)".to_string(),
505 required: true,
506 }],
507 example: json!({
508 "type": "xmlrpc_list_methods_response",
509 "methods": ["add", "subtract", "system.listMethods", "system.methodHelp"]
510 }),
511 log_template: Some(
512 LogTemplate::new()
513 .with_info("-> XML-RPC list methods ({methods_len})")
514 .with_debug("XML-RPC xmlrpc_list_methods_response: count={methods_len}"),
515 ),
516 }
517}
518
519fn method_help_response_action() -> ActionDefinition {
520 ActionDefinition {
521 name: "xmlrpc_method_help_response".to_string(),
522 description: "Respond to system.methodHelp introspection with documentation string"
523 .to_string(),
524 parameters: vec![Parameter {
525 name: "help_text".to_string(),
526 type_hint: "string".to_string(),
527 description: "Documentation/help text for the requested method".to_string(),
528 required: true,
529 }],
530 example: json!({
531 "type": "xmlrpc_method_help_response",
532 "help_text": "add(a, b) - Returns the sum of two numbers"
533 }),
534 log_template: Some(
535 LogTemplate::new()
536 .with_info("-> XML-RPC method help")
537 .with_debug("XML-RPC xmlrpc_method_help_response"),
538 ),
539 }
540}
541
542fn method_signature_response_action() -> ActionDefinition {
543 ActionDefinition {
544 name: "xmlrpc_method_signature_response".to_string(),
545 description: "Respond to system.methodSignature introspection with array of signatures".to_string(),
546 parameters: vec![Parameter {
547 name: "signatures".to_string(),
548 type_hint: "array".to_string(),
549 description: "Array of signature arrays. Each signature is [return_type, param1_type, param2_type, ...]. Multiple signatures indicate overloads.".to_string(),
550 required: true,
551 }],
552 example: json!({
553 "type": "xmlrpc_method_signature_response",
554 "signatures": [["int", "int", "int"]]
555 }),
556 log_template: Some(
557 LogTemplate::new()
558 .with_info("-> XML-RPC method signature")
559 .with_debug("XML-RPC xmlrpc_method_signature_response: {signatures_len} sigs"),
560 ),
561 }
562}
563
564pub static XMLRPC_METHOD_CALL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
568 EventType::new(
569 "xmlrpc_method_call",
570 "XML-RPC method call received from client",
571 json!({
572 "type": "xmlrpc_success_response",
573 "value_type": "int",
574 "value": 42
575 }),
576 )
577 .with_parameters(vec![
578 Parameter {
579 name: "method_name".to_string(),
580 type_hint: "string".to_string(),
581 description: "Name of the RPC method being called".to_string(),
582 required: true,
583 },
584 Parameter {
585 name: "params".to_string(),
586 type_hint: "array".to_string(),
587 description: "Array of parameter values (can be integers, strings, booleans, arrays, structs, etc.)".to_string(),
588 required: true,
589 },
590 ])
591 .with_actions(vec![
592 success_response_action(),
593 fault_response_action(),
594 list_methods_response_action(),
595 method_help_response_action(),
596 method_signature_response_action(),
597 ])
598 .with_log_template(
599 LogTemplate::new()
600 .with_info("XML-RPC {method_name}")
601 .with_debug("XML-RPC method={method_name}")
602 .with_trace("XML-RPC: {json_pretty(.)}"),
603 )
604});
605
606pub fn create_method_call_event(method_call: &MethodCall) -> Event {
608 let params_json: Vec<serde_json::Value> = method_call
610 .params
611 .iter()
612 .map(xmlrpc_value_to_json)
613 .collect();
614
615 Event::new(
616 &XMLRPC_METHOD_CALL_EVENT,
617 json!({
618 "method_name": method_call.method_name,
619 "params": params_json,
620 }),
621 )
622}
623
624fn xmlrpc_value_to_json(value: &XmlRpcValue) -> serde_json::Value {
626 match value {
627 XmlRpcValue::Int(i) => json!(i),
628 XmlRpcValue::I8(i) => json!(i),
629 XmlRpcValue::Boolean(b) => json!(b),
630 XmlRpcValue::String(s) => json!(s),
631 XmlRpcValue::Double(d) => json!(d),
632 XmlRpcValue::DateTime(dt) => json!(dt),
633 XmlRpcValue::Base64(bytes) => json!({
638 "xmlrpc_type": "base64",
639 "byte_length": bytes.len(),
640 "text": std::str::from_utf8(bytes).ok(),
641 }),
642 XmlRpcValue::Array(arr) => {
643 let items: Vec<serde_json::Value> = arr.iter().map(xmlrpc_value_to_json).collect();
644 json!(items)
645 }
646 XmlRpcValue::Struct(members) => {
647 let obj: serde_json::Map<String, serde_json::Value> = members
648 .iter()
649 .map(|(k, v)| (k.clone(), xmlrpc_value_to_json(v)))
650 .collect();
651 json!(obj)
652 }
653 XmlRpcValue::Nil => json!(null),
654 }
655}