1use anyhow::Result;
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25use tokio::sync::mpsc;
26
27use crate::llm::actions::common::ServerTaskDefinition;
28use crate::llm::actions::ParameterDefinition;
29use crate::llm::OllamaClient;
30use crate::state::app_state::AppState;
31use crate::state::{ClientId, ServerId};
32
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct ServerForm {
42 pub protocol: String,
45 pub port: Option<u16>,
47 pub host: Option<String>,
49 pub interface: Option<String>,
51 pub mac_address: Option<String>,
53 #[serde(default)]
56 pub send_first: bool,
57 pub instruction: Option<String>,
59 pub initial_memory: Option<String>,
61 pub startup_params: Option<Value>,
63 pub event_handlers: Option<Vec<Value>>,
65 pub scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
67 pub feedback_instructions: Option<String>,
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
74pub struct ClientForm {
75 pub protocol: String,
78 pub remote_addr: Option<String>,
81 pub instruction: Option<String>,
83 pub initial_memory: Option<String>,
85 pub startup_params: Option<Value>,
87 pub event_handlers: Option<Vec<Value>>,
89 pub scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
91 pub feedback_instructions: Option<String>,
93}
94
95#[derive(Debug, Clone)]
99pub struct UpdateOutcome {
100 pub id: u32,
103 pub restarted: bool,
105 pub summary: String,
107}
108
109pub fn server_declared_params(protocol: &str) -> Option<Vec<ParameterDefinition>> {
113 crate::protocol::server_registry::registry()
114 .resolve(protocol)
115 .ok()
116 .map(|p| p.get_startup_parameters())
117}
118
119pub fn client_declared_params(protocol: &str) -> Option<Vec<ParameterDefinition>> {
122 crate::protocol::CLIENT_REGISTRY
123 .resolve(protocol)
124 .ok()
125 .map(|p| p.get_startup_parameters())
126}
127
128fn validate_params_declared(
137 params: &Value,
138 schema: &[ParameterDefinition],
139 protocol: &str,
140) -> Result<()> {
141 crate::protocol::StartupParams::preflight(params)?;
142 crate::protocol::spawn_context::StartupParams::new_validated(params.clone(), schema.to_vec())
143 .map(|_| ())
144 .map_err(|e| anyhow::anyhow!("Invalid startup_params for {}: {}", protocol, e))
145}
146
147pub(crate) fn validate_server_startup_params(
150 params: &Value,
151 schema: &[ParameterDefinition],
152 protocol: &str,
153) -> Result<()> {
154 validate_params_declared(params, schema, protocol)?;
155 {
156 let names: std::collections::HashSet<_> = schema.iter().map(|p| p.name.as_str()).collect();
157 if [
158 "cert_path",
159 "key_path",
160 "common_name",
161 "san_dns_names",
162 "validity_days",
163 "organization",
164 "organizational_unit",
165 ]
166 .iter()
167 .all(|name| names.contains(name))
168 {
169 let validated =
170 crate::protocol::StartupParams::new_validated(params.clone(), schema.to_vec())?;
171 if names.contains("tls_enabled") {
172 crate::server::tls_cert_manager::extract_tls_config_from_params(&validated)?;
173 } else {
174 crate::server::tls_cert_manager::extract_required_tls_config_from_params(
175 &validated,
176 )?;
177 }
178 }
179 }
180 Ok(())
181}
182
183impl ServerForm {
184 pub async fn create(
187 mut self,
188 state: &AppState,
189 status_tx: mpsc::UnboundedSender<String>,
190 ) -> Result<ServerId> {
191 crate::protocol::StartupParams::preflight_owned(&mut self.startup_params)?;
192 let instruction = self.instruction.clone().unwrap_or_else(|| {
193 format!(
194 "You are a {} server. Handle requests appropriately.",
195 self.protocol
196 )
197 });
198 crate::cli::server_startup::start_server_from_action(
199 state,
200 self.mac_address,
201 self.interface,
202 self.host,
203 self.port,
204 &self.protocol,
205 self.send_first,
206 self.initial_memory,
207 instruction,
208 self.startup_params,
209 self.event_handlers,
210 self.scheduled_tasks,
211 self.feedback_instructions,
212 status_tx,
213 )
214 .await
215 }
216}
217
218impl ClientForm {
219 pub async fn create(
222 mut self,
223 state: &AppState,
224 llm_client: OllamaClient,
225 status_tx: mpsc::UnboundedSender<String>,
226 ) -> Result<ClientId> {
227 crate::protocol::StartupParams::preflight_owned(&mut self.startup_params)?;
228 let remote_addr = self
229 .remote_addr
230 .clone()
231 .ok_or_else(|| anyhow::anyhow!("remote_addr is required to open a client"))?;
232 let instruction = self.instruction.clone().unwrap_or_else(|| {
233 format!(
234 "You are a {} client connected to {}. Handle responses appropriately.",
235 self.protocol, remote_addr
236 )
237 });
238 crate::cli::client_startup::start_client_from_action(
239 state,
240 &self.protocol,
241 &remote_addr,
242 instruction,
243 self.startup_params,
244 self.initial_memory,
245 self.event_handlers,
246 self.scheduled_tasks,
247 self.feedback_instructions,
248 llm_client,
249 Some(status_tx),
250 )
251 .await
252 }
253}
254
255fn merge_params(base: Option<&Value>, overlay: &Value) -> Result<Value> {
259 if !crate::utils::json_budget::within_values_budget(
261 base.into_iter().chain(std::iter::once(overlay)),
262 crate::protocol::spawn_context::MAX_STARTUP_PARAM_BYTES,
263 crate::protocol::spawn_context::MAX_STARTUP_PARAM_NODES,
264 crate::protocol::spawn_context::MAX_STARTUP_PARAM_DEPTH,
265 ) {
266 return Err(anyhow::anyhow!(
267 "startup_params exceeds the merged JSON size, node or depth budget"
268 ));
269 }
270 let map = overlay
271 .as_object()
272 .ok_or_else(|| anyhow::anyhow!("startup_params must be a JSON object"))?;
273 let mut out = base
274 .and_then(|v| v.as_object().cloned())
275 .unwrap_or_default();
276 for (k, v) in map {
277 out.insert(k.clone(), v.clone());
278 }
279 Ok(Value::Object(out))
280}
281
282fn server_needs_restart(form: &ServerForm) -> bool {
285 form.port.is_some()
286 || form.host.is_some()
287 || form.interface.is_some()
288 || form.mac_address.is_some()
289 || form.startup_params.is_some()
290}
291
292pub async fn update_server(
303 state: &AppState,
304 server_id: ServerId,
305 mut form: ServerForm,
306 status_tx: mpsc::UnboundedSender<String>,
307) -> Result<UpdateOutcome> {
308 crate::protocol::StartupParams::preflight_owned(&mut form.startup_params)?;
309 let current = state
310 .with_server_mut(server_id, |current| {
311 crate::protocol::StartupParams::preflight_owned(&mut current.startup_params)?;
312 Ok::<_, crate::protocol::StartupParamError>(current.clone())
313 })
314 .await
315 .ok_or_else(|| anyhow::anyhow!("Server #{} not found", server_id.as_u32()))??;
316
317 if !form.protocol.is_empty() && !form.protocol.eq_ignore_ascii_case(¤t.protocol_name) {
319 return Err(anyhow::anyhow!(
320 "Cannot change protocol of server #{} from '{}' to '{}'. Stop it and open a new one instead.",
321 server_id.as_u32(),
322 current.protocol_name,
323 form.protocol
324 ));
325 }
326
327 let protocol_impl = crate::protocol::server_registry::registry()
330 .resolve(¤t.protocol_name)
331 .map_err(|_| {
332 anyhow::anyhow!("Server protocol '{}' not available", current.protocol_name)
333 })?;
334
335 let prepared_tasks = crate::state::task::prepare_tasks(
340 form.scheduled_tasks.as_deref(),
341 crate::state::task::TaskScope::Server(server_id),
342 )?;
343 let merged_params: Option<Value> = match &form.startup_params {
344 Some(overlay) => Some(merge_params(current.startup_params.as_ref(), overlay)?),
345 None => current.startup_params.clone(),
346 };
347 if let Some(params) = &merged_params {
348 let schema = protocol_impl.get_startup_parameters();
349 validate_server_startup_params(params, &schema, ¤t.protocol_name)?;
350 }
351
352 if let Some(handlers) = &form.event_handlers {
354 crate::events::handler::EventHandler::parse_event_handlers(handlers.clone())
355 .map_err(|e| anyhow::anyhow!("Invalid event_handlers: {}", e))?;
356 }
357
358 if server_needs_restart(&form) {
359 let mut changed = Vec::new();
361 if form.port.is_some() {
362 changed.push("port");
363 }
364 if form.host.is_some() {
365 changed.push("host");
366 }
367 if form.interface.is_some() {
368 changed.push("interface");
369 }
370 if form.mac_address.is_some() {
371 changed.push("mac_address");
372 }
373 if form.startup_params.is_some() {
374 changed.push("startup_params");
375 }
376
377 let instruction = form
379 .instruction
380 .clone()
381 .unwrap_or(current.instruction.clone());
382 let initial_memory = match &form.initial_memory {
383 Some(m) => Some(m.clone()),
384 None if !current.memory.is_empty() => Some(current.memory.clone()),
385 None => None,
386 };
387 let event_handlers: Option<Vec<Value>> = match &form.event_handlers {
390 Some(h) => Some(h.clone()),
391 None => current.event_handler_config.as_ref().and_then(|cfg| {
392 serde_json::to_value(&cfg.handlers)
393 .ok()
394 .and_then(|v| v.as_array().cloned())
395 }),
396 };
397 let feedback_instructions = form
398 .feedback_instructions
399 .clone()
400 .or(current.feedback_instructions.clone());
401 let port = form.port.or(Some(current.port));
402
403 state.remove_server(server_id).await;
405 let _ = status_tx.send(format!(
406 "[SERVER] Restarting server #{} ({}) to apply {}",
407 server_id.as_u32(),
408 current.protocol_name,
409 changed.join(", ")
410 ));
411
412 let new_id = crate::cli::server_startup::start_server_from_action(
413 state,
414 form.mac_address,
415 form.interface,
416 form.host,
417 port,
418 ¤t.protocol_name,
419 form.send_first,
420 initial_memory,
421 instruction,
422 merged_params,
423 event_handlers,
424 form.scheduled_tasks,
425 feedback_instructions,
426 status_tx.clone(),
427 )
428 .await?;
429
430 let summary = format!(
431 "Server #{} restarted as #{} (changed: {}); live connections were dropped.",
432 server_id.as_u32(),
433 new_id.as_u32(),
434 changed.join(", ")
435 );
436 let _ = status_tx.send(format!("[SERVER] {}", summary));
437 let _ = status_tx.send("__UPDATE_UI__".to_string());
438 return Ok(UpdateOutcome {
439 id: new_id.as_u32(),
440 restarted: true,
441 summary,
442 });
443 }
444
445 let mut changed = Vec::new();
447
448 if let Some(instruction) = &form.instruction {
449 state.set_instruction(server_id, instruction.clone()).await;
450 changed.push("instruction");
451 }
452 if let Some(memory) = &form.initial_memory {
453 state.set_memory(server_id, memory.clone()).await;
454 changed.push("memory");
455 }
456 if let Some(handlers) = &form.event_handlers {
457 let config = crate::events::handler::EventHandler::parse_event_handlers(handlers.clone())
459 .map_err(|e| anyhow::anyhow!("Invalid event_handlers: {}", e))?;
460 state
461 .with_server_mut(server_id, |s| {
462 s.event_handler_config = Some(config);
463 })
464 .await;
465 changed.push("event_handlers");
466 }
467 if let Some(fb) = &form.feedback_instructions {
468 state
469 .with_server_mut(server_id, |s| {
470 s.feedback_instructions = Some(fb.clone());
471 })
472 .await;
473 changed.push("feedback_instructions");
474 }
475 if let Some(tasks) = form.scheduled_tasks {
476 let n = tasks.len();
477 for task in prepared_tasks {
478 state.add_task(task).await;
479 }
480 if n > 0 {
481 changed.push("scheduled_tasks");
482 }
483 }
484
485 let summary = if changed.is_empty() {
486 format!("Server #{}: nothing to update.", server_id.as_u32())
487 } else {
488 format!(
489 "Server #{} updated in place ({}); connections preserved.",
490 server_id.as_u32(),
491 changed.join(", ")
492 )
493 };
494 let _ = status_tx.send(format!("[SERVER] {}", summary));
495 let _ = status_tx.send("__UPDATE_UI__".to_string());
496
497 Ok(UpdateOutcome {
498 id: server_id.as_u32(),
499 restarted: false,
500 summary,
501 })
502}
503
504fn client_needs_restart(form: &ClientForm) -> bool {
506 form.remote_addr.is_some() || form.startup_params.is_some()
507}
508
509pub async fn update_client(
513 state: &AppState,
514 client_id: ClientId,
515 mut form: ClientForm,
516 llm_client: OllamaClient,
517 status_tx: mpsc::UnboundedSender<String>,
518) -> Result<UpdateOutcome> {
519 crate::protocol::StartupParams::preflight_owned(&mut form.startup_params)?;
520 let current = state
521 .with_client_mut(client_id, |current| {
522 crate::protocol::StartupParams::preflight_owned(&mut current.startup_params)?;
523 Ok::<_, crate::protocol::StartupParamError>(current.clone())
524 })
525 .await
526 .ok_or_else(|| anyhow::anyhow!("Client #{} not found", client_id.as_u32()))??;
527
528 if !form.protocol.is_empty() && !form.protocol.eq_ignore_ascii_case(¤t.protocol_name) {
529 return Err(anyhow::anyhow!(
530 "Cannot change protocol of client #{} from '{}' to '{}'. Stop it and open a new one instead.",
531 client_id.as_u32(),
532 current.protocol_name,
533 form.protocol
534 ));
535 }
536
537 let protocol_impl = crate::protocol::CLIENT_REGISTRY
540 .resolve(¤t.protocol_name)
541 .map_err(|_| {
542 anyhow::anyhow!("Client protocol '{}' not available", current.protocol_name)
543 })?;
544
545 let prepared_tasks = crate::state::task::prepare_tasks(
547 form.scheduled_tasks.as_deref(),
548 crate::state::task::TaskScope::Client(client_id),
549 )?;
550 let merged_params: Option<Value> = match &form.startup_params {
551 Some(overlay) => Some(merge_params(current.startup_params.as_ref(), overlay)?),
552 None => current.startup_params.clone(),
553 };
554 if let Some(params) = &merged_params {
555 let schema = protocol_impl.get_startup_parameters();
556 validate_params_declared(params, &schema, ¤t.protocol_name)?;
557 }
558
559 if let Some(handlers) = &form.event_handlers {
561 validate_client_handlers(handlers)?;
562 }
563
564 if client_needs_restart(&form) {
565 let mut changed = Vec::new();
566 if form.remote_addr.is_some() {
567 changed.push("remote_addr");
568 }
569 if form.startup_params.is_some() {
570 changed.push("startup_params");
571 }
572
573 let remote_addr = form
574 .remote_addr
575 .clone()
576 .unwrap_or(current.remote_addr.clone());
577 let instruction = form
578 .instruction
579 .clone()
580 .unwrap_or(current.instruction.clone());
581 let initial_memory = match &form.initial_memory {
582 Some(m) => Some(m.clone()),
583 None if !current.memory.is_empty() => Some(current.memory.clone()),
584 None => None,
585 };
586 let event_handlers: Option<Vec<Value>> = match &form.event_handlers {
587 Some(h) => Some(h.clone()),
588 None => current.event_handler_config.as_ref().and_then(|cfg| {
589 serde_json::to_value(&cfg.handlers)
590 .ok()
591 .and_then(|v| v.as_array().cloned())
592 }),
593 };
594 let feedback_instructions = form
595 .feedback_instructions
596 .clone()
597 .or(current.feedback_instructions.clone());
598
599 state.remove_client(client_id).await;
601 let _ = status_tx.send(format!(
602 "[CLIENT] Reconnecting client #{} ({}) to apply {}",
603 client_id.as_u32(),
604 current.protocol_name,
605 changed.join(", ")
606 ));
607
608 let new_id = crate::cli::client_startup::start_client_from_action(
609 state,
610 ¤t.protocol_name,
611 &remote_addr,
612 instruction,
613 merged_params,
614 initial_memory,
615 event_handlers,
616 form.scheduled_tasks,
617 feedback_instructions,
618 llm_client,
619 Some(status_tx.clone()),
620 )
621 .await?;
622
623 let summary = format!(
624 "Client #{} reconnected as #{} (changed: {}).",
625 client_id.as_u32(),
626 new_id.as_u32(),
627 changed.join(", ")
628 );
629 let _ = status_tx.send(format!("[CLIENT] {}", summary));
630 let _ = status_tx.send("__UPDATE_UI__".to_string());
631 return Ok(UpdateOutcome {
632 id: new_id.as_u32(),
633 restarted: true,
634 summary,
635 });
636 }
637
638 let mut changed = Vec::new();
640 if let Some(instruction) = &form.instruction {
641 state
642 .set_instruction_for_client(client_id, instruction.clone())
643 .await;
644 changed.push("instruction");
645 }
646 if let Some(memory) = &form.initial_memory {
647 state.set_memory_for_client(client_id, memory.clone()).await;
648 changed.push("memory");
649 }
650 if let Some(handlers) = &form.event_handlers {
651 let config = parse_client_handlers(handlers)?;
652 state
653 .with_client_mut(client_id, |c| {
654 c.event_handler_config = Some(config);
655 })
656 .await;
657 changed.push("event_handlers");
658 }
659 if let Some(fb) = &form.feedback_instructions {
660 state
661 .with_client_mut(client_id, |c| {
662 c.feedback_instructions = Some(fb.clone());
663 })
664 .await;
665 changed.push("feedback_instructions");
666 }
667 if let Some(tasks) = form.scheduled_tasks {
668 let n = tasks.len();
669 for task in prepared_tasks {
670 state.add_task(task).await;
671 }
672 if n > 0 {
673 changed.push("scheduled_tasks");
674 }
675 }
676
677 let summary = if changed.is_empty() {
678 format!("Client #{}: nothing to update.", client_id.as_u32())
679 } else {
680 format!(
681 "Client #{} updated in place ({}).",
682 client_id.as_u32(),
683 changed.join(", ")
684 )
685 };
686 let _ = status_tx.send(format!("[CLIENT] {}", summary));
687 let _ = status_tx.send("__UPDATE_UI__".to_string());
688
689 Ok(UpdateOutcome {
690 id: client_id.as_u32(),
691 restarted: false,
692 summary,
693 })
694}
695
696fn validate_client_handlers(handlers: &[Value]) -> Result<()> {
698 parse_client_handlers(handlers).map(|_| ())
699}
700
701fn parse_client_handlers(handlers: &[Value]) -> Result<crate::scripting::EventHandlerConfig> {
707 use crate::scripting::{EventHandler, EventHandlerConfig};
708 let mut parsed = Vec::with_capacity(handlers.len());
709 for (i, h) in handlers.iter().enumerate() {
710 let handler: EventHandler = serde_json::from_value(h.clone())
711 .map_err(|e| anyhow::anyhow!("Invalid event_handlers[{}]: {}", i, e))?;
712 parsed.push(handler);
713 }
714 Ok(EventHandlerConfig { handlers: parsed })
715}
716
717#[derive(Debug, Clone, PartialEq, Eq)]
736pub enum FormTarget {
737 CreateServer,
739 CreateClient,
741 UpdateServer(u32),
743 UpdateClient(u32),
745}
746
747#[derive(Debug, Clone, PartialEq, Eq)]
749enum FieldTarget {
750 Port,
751 RemoteAddr,
752 Instruction,
753 Memory,
754 EventHandlers,
755 FeedbackInstructions,
756 StartupParam {
759 type_hint: String,
760 },
761}
762
763#[derive(Debug, Clone)]
765pub struct FormField {
766 pub name: String,
769 pub type_label: String,
772 pub required: bool,
775 pub help: String,
777 pub prefill: String,
779 pub value: Option<String>,
783 target: FieldTarget,
784}
785
786#[derive(Debug, Clone)]
788pub struct InteractiveForm {
789 pub target: FormTarget,
791 pub protocol: String,
793 pub fields: Vec<FormField>,
795 pub index: usize,
797}
798
799#[derive(Debug, Clone, Default)]
802pub struct ServerPrefill {
803 pub instruction: String,
804 pub memory: String,
805 pub port: u16,
806 pub startup_params: Option<Value>,
807 pub event_handlers: Option<Vec<Value>>,
808 pub feedback_instructions: Option<String>,
809}
810
811impl ServerPrefill {
812 pub fn from_server(server: &crate::state::server::ServerInstance) -> Self {
824 Self {
825 instruction: server.instruction.clone(),
826 memory: server.memory.clone(),
827 port: server
828 .local_addr
829 .map(|a| a.port())
830 .filter(|p| *p != 0)
831 .unwrap_or(server.port),
832 startup_params: server.startup_params.clone(),
833 event_handlers: server.event_handler_config.as_ref().and_then(|c| {
834 serde_json::to_value(&c.handlers)
835 .ok()
836 .and_then(|v| v.as_array().cloned())
837 }),
838 feedback_instructions: server.feedback_instructions.clone(),
839 }
840 }
841}
842
843#[derive(Debug, Clone, Default)]
845pub struct ClientPrefill {
846 pub remote_addr: String,
847 pub instruction: String,
848 pub memory: String,
849 pub startup_params: Option<Value>,
850 pub event_handlers: Option<Vec<Value>>,
851 pub feedback_instructions: Option<String>,
852}
853
854fn param_value_to_prefill(v: &Value) -> String {
858 match v {
859 Value::String(s) => s.clone(),
860 other => serde_json::to_string(other).unwrap_or_default(),
861 }
862}
863
864fn startup_param_fields(schema: &[ParameterDefinition], current: Option<&Value>) -> Vec<FormField> {
868 let current_obj = current.and_then(|v| v.as_object());
869 schema
870 .iter()
871 .map(|p| {
872 let prefill = current_obj
873 .and_then(|o| o.get(&p.name))
874 .map(param_value_to_prefill)
875 .unwrap_or_default();
876 FormField {
877 name: p.name.clone(),
878 type_label: p.type_hint.clone(),
879 required: p.required,
880 help: p.description.clone(),
881 prefill,
882 value: None,
883 target: FieldTarget::StartupParam {
884 type_hint: p.type_hint.clone(),
885 },
886 }
887 })
888 .collect()
889}
890
891fn field(name: &str, ty: &str, help: &str, prefill: String, target: FieldTarget) -> FormField {
892 FormField {
893 name: name.to_string(),
894 type_label: ty.to_string(),
895 required: false,
896 help: help.to_string(),
897 prefill,
898 value: None,
899 target,
900 }
901}
902
903fn handlers_to_prefill(handlers: &Option<Vec<Value>>) -> String {
906 match handlers {
907 Some(h) if !h.is_empty() => serde_json::to_string(h).unwrap_or_default(),
908 _ => String::new(),
909 }
910}
911
912impl InteractiveForm {
913 pub fn create_server(protocol: &str, schema: &[ParameterDefinition]) -> Self {
915 let mut fields = common_server_fields(&ServerPrefill::default(), false);
916 fields.extend(startup_param_fields(schema, None));
917 Self {
918 target: FormTarget::CreateServer,
919 protocol: protocol.to_string(),
920 fields,
921 index: 0,
922 }
923 }
924
925 pub fn create_client(protocol: &str, schema: &[ParameterDefinition]) -> Self {
927 let mut fields = common_client_fields(&ClientPrefill::default(), false);
928 fields.extend(startup_param_fields(schema, None));
929 Self {
930 target: FormTarget::CreateClient,
931 protocol: protocol.to_string(),
932 fields,
933 index: 0,
934 }
935 }
936
937 pub fn update_server(
940 id: u32,
941 protocol: &str,
942 schema: &[ParameterDefinition],
943 current: &ServerPrefill,
944 ) -> Self {
945 let mut fields = common_server_fields(current, true);
946 fields.extend(startup_param_fields(
947 schema,
948 current.startup_params.as_ref(),
949 ));
950 Self {
951 target: FormTarget::UpdateServer(id),
952 protocol: protocol.to_string(),
953 fields,
954 index: 0,
955 }
956 }
957
958 pub fn update_client(
960 id: u32,
961 protocol: &str,
962 schema: &[ParameterDefinition],
963 current: &ClientPrefill,
964 ) -> Self {
965 let mut fields = common_client_fields(current, true);
966 fields.extend(startup_param_fields(
967 schema,
968 current.startup_params.as_ref(),
969 ));
970 Self {
971 target: FormTarget::UpdateClient(id),
972 protocol: protocol.to_string(),
973 fields,
974 index: 0,
975 }
976 }
977
978 pub fn current_field(&self) -> Option<&FormField> {
980 self.fields.get(self.index)
981 }
982
983 pub fn prompt(&self) -> String {
985 match self.current_field() {
986 None => "Form complete.".to_string(),
987 Some(f) => {
988 let req = if f.required { ", required" } else { "" };
989 let hint = if f.required {
990 " (required)"
991 } else if matches!(
992 self.target,
993 FormTarget::CreateServer | FormTarget::CreateClient
994 ) {
995 " (empty = protocol default)"
996 } else {
997 " (empty = keep current)"
998 };
999 format!(
1000 "Field {}/{}: {} ({}{}) — {}{}",
1001 self.index + 1,
1002 self.fields.len(),
1003 f.name,
1004 f.type_label,
1005 req,
1006 f.help,
1007 hint,
1008 )
1009 }
1010 }
1011 }
1012
1013 pub fn title(&self) -> String {
1015 match self.target {
1016 FormTarget::CreateServer => format!("Create {} server", self.protocol),
1017 FormTarget::CreateClient => format!("Create {} client", self.protocol),
1018 FormTarget::UpdateServer(id) => {
1019 format!("Update {} server #{}", self.protocol, id)
1020 }
1021 FormTarget::UpdateClient(id) => {
1022 format!("Update {} client #{}", self.protocol, id)
1023 }
1024 }
1025 }
1026
1027 pub fn current_prefill(&self) -> String {
1029 self.current_field()
1030 .map(|f| f.prefill.clone())
1031 .unwrap_or_default()
1032 }
1033
1034 pub fn submit_current(&mut self, raw: &str) {
1036 if let Some(f) = self.fields.get_mut(self.index) {
1037 f.value = Some(raw.trim().to_string());
1038 }
1039 self.index += 1;
1040 }
1041
1042 pub fn set_field_value(&mut self, name: &str, value: &str) -> bool {
1045 if let Some(f) = self.fields.iter_mut().find(|f| f.name == name) {
1046 f.value = Some(value.trim().to_string());
1047 true
1048 } else {
1049 false
1050 }
1051 }
1052
1053 pub fn is_complete(&self) -> bool {
1055 self.index >= self.fields.len()
1056 }
1057
1058 fn is_client(&self) -> bool {
1059 matches!(
1060 self.target,
1061 FormTarget::CreateClient | FormTarget::UpdateClient(_)
1062 )
1063 }
1064
1065 fn is_update(&self) -> bool {
1066 matches!(
1067 self.target,
1068 FormTarget::UpdateServer(_) | FormTarget::UpdateClient(_)
1069 )
1070 }
1071
1072 fn effective_value<'a>(&self, f: &'a FormField) -> Option<&'a str> {
1082 let trimmed = f.value.as_deref().unwrap_or("").trim();
1083 if trimmed.is_empty() {
1084 return None;
1085 }
1086 if self.is_update() && trimmed == f.prefill.trim() {
1087 return None;
1088 }
1089 Some(trimmed)
1090 }
1091
1092 pub fn into_server_form(&self) -> Result<ServerForm> {
1095 if self.is_client() {
1096 anyhow::bail!("into_server_form called on a client form");
1097 }
1098 let mut form = ServerForm {
1099 protocol: self.protocol.clone(),
1100 ..Default::default()
1101 };
1102 let mut params = serde_json::Map::new();
1103 for f in &self.fields {
1104 let v = match self.effective_value(f) {
1105 Some(v) => v,
1106 None => continue, };
1108 apply_common_field(f, v, &mut params, |t, val| match t {
1109 FieldTarget::Port => {
1110 form.port = Some(parse_port(val, &f.name)?);
1111 Ok(())
1112 }
1113 FieldTarget::Instruction => {
1114 form.instruction = Some(val.to_string());
1115 Ok(())
1116 }
1117 FieldTarget::Memory => {
1118 form.initial_memory = Some(val.to_string());
1119 Ok(())
1120 }
1121 FieldTarget::EventHandlers => {
1122 form.event_handlers = Some(parse_handlers(val)?);
1123 Ok(())
1124 }
1125 FieldTarget::FeedbackInstructions => {
1126 form.feedback_instructions = Some(val.to_string());
1127 Ok(())
1128 }
1129 FieldTarget::RemoteAddr => Ok(()), FieldTarget::StartupParam { .. } => unreachable!(),
1131 })?;
1132 }
1133 if !params.is_empty() {
1134 form.startup_params = Some(Value::Object(params));
1135 }
1136 Ok(form)
1137 }
1138
1139 pub fn into_client_form(&self) -> Result<ClientForm> {
1141 if !self.is_client() {
1142 anyhow::bail!("into_client_form called on a server form");
1143 }
1144 let mut form = ClientForm {
1145 protocol: self.protocol.clone(),
1146 ..Default::default()
1147 };
1148 let mut params = serde_json::Map::new();
1149 for f in &self.fields {
1150 let v = match self.effective_value(f) {
1151 Some(v) => v,
1152 None => continue,
1153 };
1154 apply_common_field(f, v, &mut params, |t, val| match t {
1155 FieldTarget::RemoteAddr => {
1156 form.remote_addr = Some(val.to_string());
1157 Ok(())
1158 }
1159 FieldTarget::Instruction => {
1160 form.instruction = Some(val.to_string());
1161 Ok(())
1162 }
1163 FieldTarget::Memory => {
1164 form.initial_memory = Some(val.to_string());
1165 Ok(())
1166 }
1167 FieldTarget::EventHandlers => {
1168 form.event_handlers = Some(parse_handlers(val)?);
1169 Ok(())
1170 }
1171 FieldTarget::FeedbackInstructions => {
1172 form.feedback_instructions = Some(val.to_string());
1173 Ok(())
1174 }
1175 FieldTarget::Port => Ok(()), FieldTarget::StartupParam { .. } => unreachable!(),
1177 })?;
1178 }
1179 if !params.is_empty() {
1180 form.startup_params = Some(Value::Object(params));
1181 }
1182 Ok(form)
1183 }
1184}
1185
1186fn common_server_fields(current: &ServerPrefill, update: bool) -> Vec<FormField> {
1189 let (port, instr, mem, fb) = if update {
1190 (
1191 current.port.to_string(),
1192 current.instruction.clone(),
1193 current.memory.clone(),
1194 current.feedback_instructions.clone().unwrap_or_default(),
1195 )
1196 } else {
1197 ("0".to_string(), String::new(), String::new(), String::new())
1198 };
1199 vec![
1200 field(
1201 "port",
1202 "u16",
1203 "Port to bind (0 = OS-assigns a free port)",
1204 port,
1205 FieldTarget::Port,
1206 ),
1207 field(
1208 "instruction",
1209 "string",
1210 "Natural-language LLM instruction (fallback handling path)",
1211 instr,
1212 FieldTarget::Instruction,
1213 ),
1214 field(
1215 "initial_memory",
1216 "string",
1217 "Seed the instance's LLM memory",
1218 mem,
1219 FieldTarget::Memory,
1220 ),
1221 field(
1222 "event_handlers",
1223 "json array",
1224 "Deterministic handlers, e.g. [{\"event_pattern\":\"http_request\",\"handler\":{\"type\":\"static\",...}}]",
1225 if update { handlers_to_prefill(¤t.event_handlers) } else { String::new() },
1226 FieldTarget::EventHandlers,
1227 ),
1228 field(
1229 "feedback_instructions",
1230 "string",
1231 "Instructions for the automatic feedback loop",
1232 fb,
1233 FieldTarget::FeedbackInstructions,
1234 ),
1235 ]
1236}
1237
1238fn common_client_fields(current: &ClientPrefill, update: bool) -> Vec<FormField> {
1240 let (instr, mem, fb) = if update {
1241 (
1242 current.instruction.clone(),
1243 current.memory.clone(),
1244 current.feedback_instructions.clone().unwrap_or_default(),
1245 )
1246 } else {
1247 (String::new(), String::new(), String::new())
1248 };
1249 let mut remote = field(
1250 "remote_addr",
1251 "string",
1252 "Remote server address, host:port",
1253 if update {
1254 current.remote_addr.clone()
1255 } else {
1256 String::new()
1257 },
1258 FieldTarget::RemoteAddr,
1259 );
1260 remote.required = !update;
1262 vec![
1263 remote,
1264 field(
1265 "instruction",
1266 "string",
1267 "Natural-language LLM instruction (fallback handling path)",
1268 instr,
1269 FieldTarget::Instruction,
1270 ),
1271 field(
1272 "initial_memory",
1273 "string",
1274 "Seed the instance's LLM memory",
1275 mem,
1276 FieldTarget::Memory,
1277 ),
1278 field(
1279 "event_handlers",
1280 "json array",
1281 "Deterministic handlers (JSON array)",
1282 if update {
1283 handlers_to_prefill(¤t.event_handlers)
1284 } else {
1285 String::new()
1286 },
1287 FieldTarget::EventHandlers,
1288 ),
1289 field(
1290 "feedback_instructions",
1291 "string",
1292 "Instructions for the automatic feedback loop",
1293 fb,
1294 FieldTarget::FeedbackInstructions,
1295 ),
1296 ]
1297}
1298
1299fn apply_common_field(
1303 f: &FormField,
1304 v: &str,
1305 params: &mut serde_json::Map<String, Value>,
1306 set_common: impl FnOnce(&FieldTarget, &str) -> Result<()>,
1307) -> Result<()> {
1308 if let FieldTarget::StartupParam { type_hint } = &f.target {
1309 params.insert(f.name.clone(), coerce_param_value(v, type_hint, &f.name)?);
1310 Ok(())
1311 } else {
1312 set_common(&f.target, v)
1313 }
1314}
1315
1316fn parse_port(v: &str, name: &str) -> Result<u16> {
1317 v.parse::<u16>().map_err(|_| {
1318 anyhow::anyhow!(
1319 "field '{}' must be a port number 0-65535, got '{}'",
1320 name,
1321 v
1322 )
1323 })
1324}
1325
1326fn parse_handlers(v: &str) -> Result<Vec<Value>> {
1327 let parsed: Value = serde_json::from_str(v)
1328 .map_err(|e| anyhow::anyhow!("field 'event_handlers' is not valid JSON: {}", e))?;
1329 parsed
1330 .as_array()
1331 .cloned()
1332 .ok_or_else(|| anyhow::anyhow!("field 'event_handlers' must be a JSON array"))
1333}
1334
1335fn coerce_param_value(raw: &str, type_hint: &str, name: &str) -> Result<Value> {
1340 let hint = type_hint.to_lowercase();
1341 if hint.contains("bool") {
1342 return raw
1343 .parse::<bool>()
1344 .map(Value::Bool)
1345 .map_err(|_| anyhow::anyhow!("field '{}' must be true/false, got '{}'", name, raw));
1346 }
1347 if hint.contains("int")
1348 || hint.contains("number")
1349 || hint.contains("u16")
1350 || hint.contains("u32")
1351 {
1352 if let Ok(n) = raw.parse::<i64>() {
1353 return Ok(Value::from(n));
1354 }
1355 if let Ok(fl) = raw.parse::<f64>() {
1356 return Ok(Value::from(fl));
1357 }
1358 anyhow::bail!("field '{}' must be a number, got '{}'", name, raw);
1359 }
1360 if hint.contains("object") || hint.contains("array") || hint.contains("json") {
1361 return serde_json::from_str(raw)
1362 .map_err(|e| anyhow::anyhow!("field '{}' must be valid JSON: {}", name, e));
1363 }
1364 Ok(Value::String(raw.to_string()))
1365}