1use super::{ActionDefinition, Parameter};
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11mod flexible_deserializers {
13 use serde::{Deserialize, Deserializer};
14
15 pub fn deserialize_u32_flexible<'de, D>(deserializer: D) -> Result<u32, D::Error>
17 where
18 D: Deserializer<'de>,
19 {
20 #[derive(Deserialize)]
21 #[serde(untagged)]
22 enum U32OrString {
23 Number(u32),
24 String(String),
25 }
26
27 match U32OrString::deserialize(deserializer)? {
28 U32OrString::Number(n) => Ok(n),
29 U32OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
30 }
31 }
32
33 pub fn deserialize_option_u32_flexible<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
35 where
36 D: Deserializer<'de>,
37 {
38 #[derive(Deserialize)]
39 #[serde(untagged)]
40 enum U32OrString {
41 Number(u32),
42 String(String),
43 }
44
45 match Option::<U32OrString>::deserialize(deserializer)? {
46 Some(U32OrString::Number(n)) => Ok(Some(n)),
47 Some(U32OrString::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
48 None => Ok(None),
49 }
50 }
51
52 #[allow(dead_code)]
54 pub fn deserialize_u64_flexible<'de, D>(deserializer: D) -> Result<u64, D::Error>
55 where
56 D: Deserializer<'de>,
57 {
58 #[derive(Deserialize)]
59 #[serde(untagged)]
60 enum U64OrString {
61 Number(u64),
62 String(String),
63 }
64
65 match U64OrString::deserialize(deserializer)? {
66 U64OrString::Number(n) => Ok(n),
67 U64OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
68 }
69 }
70
71 pub fn deserialize_option_u64_flexible<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
73 where
74 D: Deserializer<'de>,
75 {
76 #[derive(Deserialize)]
77 #[serde(untagged)]
78 enum U64OrString {
79 Number(u64),
80 String(String),
81 }
82
83 match Option::<U64OrString>::deserialize(deserializer)? {
84 Some(U64OrString::Number(n)) => Ok(Some(n)),
85 Some(U64OrString::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
86 None => Ok(None),
87 }
88 }
89
90 #[allow(dead_code)]
92 pub fn deserialize_u16_flexible<'de, D>(deserializer: D) -> Result<u16, D::Error>
93 where
94 D: Deserializer<'de>,
95 {
96 #[derive(Deserialize)]
97 #[serde(untagged)]
98 enum U16OrString {
99 Number(u16),
100 String(String),
101 }
102
103 match U16OrString::deserialize(deserializer)? {
104 U16OrString::Number(n) => Ok(n),
105 U16OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
106 }
107 }
108
109 pub fn deserialize_option_u16_flexible<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
111 where
112 D: Deserializer<'de>,
113 {
114 #[derive(Deserialize)]
115 #[serde(untagged)]
116 enum U16OrString {
117 Number(u16),
118 String(String),
119 }
120
121 match Option::<U16OrString>::deserialize(deserializer)? {
122 Some(U16OrString::Number(n)) => Ok(Some(n)),
123 Some(U16OrString::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
124 None => Ok(None),
125 }
126 }
127}
128
129type ProtocolGroups = std::collections::HashMap<
131 &'static str,
132 Vec<(String, std::sync::Arc<dyn crate::llm::actions::Server>)>,
133>;
134
135#[derive(Debug, Clone, Deserialize, Serialize)]
137pub struct ServerTaskDefinition {
138 pub task_id: String,
139 pub recurring: bool,
140 #[serde(
141 default,
142 deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
143 )]
144 pub delay_secs: Option<u64>,
145 #[serde(
146 default,
147 deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
148 )]
149 pub interval_secs: Option<u64>,
150 #[serde(
151 default,
152 deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
153 )]
154 pub max_executions: Option<u64>,
155 pub instruction: String,
156 #[serde(default)]
157 pub context: Option<serde_json::Value>,
158}
159
160#[derive(Debug, Clone, Deserialize, Serialize)]
162#[serde(tag = "type", rename_all = "snake_case")]
163pub enum CommonAction {
164 ShowMessage { message: String },
166
167 OpenServer {
169 #[serde(default, skip_serializing_if = "Option::is_none")]
172 mac_address: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 interface: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 host: Option<String>,
179 #[serde(
181 default,
182 skip_serializing_if = "Option::is_none",
183 deserialize_with = "flexible_deserializers::deserialize_option_u16_flexible"
184 )]
185 port: Option<u16>,
186
187 protocol: String,
188 #[serde(default)]
189 send_first: bool,
190 #[serde(default)]
191 initial_memory: Option<String>,
192 #[serde(default)]
194 instruction: String,
195 #[serde(default)]
196 startup_params: Option<serde_json::Value>,
197 #[serde(default)]
199 event_handlers: Option<Vec<serde_json::Value>>,
200 #[serde(default)]
202 scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
203 #[serde(default)]
205 feedback_instructions: Option<String>,
206 },
207
208 CloseServer {
210 #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
211 server_id: u32,
212 },
213
214 CloseAllServers,
216
217 OpenClient {
219 protocol: String,
220 remote_addr: String,
221 #[serde(default)]
223 instruction: String,
224 #[serde(default)]
225 startup_params: Option<serde_json::Value>,
226 #[serde(default)]
227 initial_memory: Option<String>,
228 #[serde(default)]
230 event_handlers: Option<Vec<serde_json::Value>>,
231 #[serde(default)]
233 scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
234 #[serde(default)]
236 feedback_instructions: Option<String>,
237 },
238
239 CloseClient {
241 #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
242 client_id: u32,
243 },
244
245 CloseAllClients,
247
248 CloseConnectionById {
250 #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
251 connection_id: u32,
252 },
253
254 ReconnectClient {
256 #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
257 client_id: u32,
258 },
259
260 UpdateClientInstruction {
262 #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
263 client_id: u32,
264 instruction: String,
265 },
266
267 UpdateServer {
273 #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
274 server_id: u32,
275 #[serde(
276 default,
277 deserialize_with = "flexible_deserializers::deserialize_option_u16_flexible"
278 )]
279 port: Option<u16>,
280 #[serde(default)]
281 host: Option<String>,
282 #[serde(default)]
283 interface: Option<String>,
284 #[serde(default)]
285 mac_address: Option<String>,
286 #[serde(default)]
287 send_first: bool,
288 #[serde(default)]
289 instruction: Option<String>,
290 #[serde(default, alias = "memory")]
291 initial_memory: Option<String>,
292 #[serde(default)]
293 startup_params: Option<serde_json::Value>,
294 #[serde(default)]
295 event_handlers: Option<Vec<serde_json::Value>>,
296 #[serde(default)]
297 scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
298 #[serde(default)]
299 feedback_instructions: Option<String>,
300 },
301
302 UpdateClient {
306 #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
307 client_id: u32,
308 #[serde(default)]
309 remote_addr: Option<String>,
310 #[serde(default)]
311 instruction: Option<String>,
312 #[serde(default, alias = "memory")]
313 initial_memory: Option<String>,
314 #[serde(default)]
315 startup_params: Option<serde_json::Value>,
316 #[serde(default)]
317 event_handlers: Option<Vec<serde_json::Value>>,
318 #[serde(default)]
319 scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
320 #[serde(default)]
321 feedback_instructions: Option<String>,
322 },
323
324 UpdateInstruction { instruction: String },
326
327 ChangeModel { model: String },
329
330 SetMemory { value: String },
332
333 AppendMemory { value: String },
335
336 AppendToLog {
338 output_name: String,
339 content: String,
340 },
341
342 ScheduleTask {
344 task_id: String,
345 recurring: bool,
346 #[serde(
347 default,
348 deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
349 )]
350 delay_secs: Option<u64>,
351 #[serde(
352 default,
353 deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
354 )]
355 interval_secs: Option<u64>,
356 #[serde(
357 default,
358 deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
359 )]
360 max_executions: Option<u64>,
361 #[serde(
362 default,
363 deserialize_with = "flexible_deserializers::deserialize_option_u32_flexible"
364 )]
365 server_id: Option<u32>,
366 #[serde(default)]
367 connection_id: Option<String>,
368 #[serde(
369 default,
370 deserialize_with = "flexible_deserializers::deserialize_option_u32_flexible"
371 )]
372 client_id: Option<u32>,
373 instruction: String,
374 #[serde(default)]
375 context: Option<serde_json::Value>,
376 #[serde(default)]
378 script_runtime: Option<String>,
379 #[serde(default)]
380 script_language: Option<String>,
381 #[serde(default)]
382 script_path: Option<String>,
383 #[serde(default)]
384 script_inline: Option<String>,
385 #[serde(default)]
386 script_handles: Option<Vec<String>>,
387 },
388
389 CancelTask { task_id: String },
391
392 ProvideFeedback {
395 feedback: serde_json::Value,
397 },
398
399 #[cfg(feature = "sqlite")]
401 CreateDatabase {
402 name: String,
403 #[serde(default)]
404 is_memory: bool, #[serde(default)]
406 owner: Option<String>, #[serde(default)]
408 schema_ddl: Option<String>, },
410
411 #[cfg(feature = "sqlite")]
413 DeleteDatabase { database_id: u32 },
414}
415
416impl CommonAction {
417 pub fn from_json(value: &serde_json::Value) -> Result<Self> {
419 let mut value_mut = value.clone();
421 if let Some(obj) = value_mut.as_object_mut() {
422 for wrapper in ["args", "arguments", "parameters"] {
426 if let Some(serde_json::Value::Object(inner)) = obj.remove(wrapper) {
427 for (k, v) in inner {
428 obj.entry(k).or_insert(v);
429 }
430 }
431 }
432 }
433 if let Some(obj) = value_mut.as_object_mut() {
434 if matches!(
436 obj.get("type").and_then(|v| v.as_str()),
437 Some("open_server") | Some("open_client")
438 ) {
439 if let Some(base_stack) = obj.remove("base_stack") {
440 if !obj.contains_key("protocol") {
442 obj.insert("protocol".to_string(), base_stack);
443 }
444 }
445 }
446
447 if matches!(
450 obj.get("type").and_then(|v| v.as_str()),
451 Some("open_server") | Some("open_client")
452 ) {
453 if let (Some(script_inline), Some(script_handles)) = (
454 obj.get("script_inline").and_then(|v| v.as_str()),
455 obj.get("script_handles").and_then(|v| v.as_array()),
456 ) {
457 let mut event_handlers = Vec::new();
459
460 for event_type in script_handles {
462 if let Some(event_type_str) = event_type.as_str() {
463 event_handlers.push(serde_json::json!({
464 "event_pattern": event_type_str,
465 "handler": {
466 "type": "script",
467 "language": "python",
468 "code": script_inline
469 }
470 }));
471 }
472 }
473
474 if !event_handlers.is_empty() {
476 obj.insert(
477 "event_handlers".to_string(),
478 serde_json::json!(event_handlers),
479 );
480 }
481
482 obj.remove("script_inline");
484 obj.remove("script_handles");
485 }
486 }
487 }
488
489 serde_json::from_value(value_mut).context("Failed to parse common action")
490 }
491}
492
493pub fn show_message_action() -> ActionDefinition {
495 ActionDefinition {
496 name: "show_message".to_string(),
497 description: "Display a message to the user controlling NetGet".to_string(),
498 parameters: vec![Parameter {
499 name: "message".to_string(),
500 type_hint: "string".to_string(),
501 description: "Message to display".to_string(),
502 required: true,
503 }],
504 example: json!({
505 "type": "show_message",
506 "message": "Server started successfully on port 8080"
507 }),
508 log_template: None,
509 }
510}
511
512pub fn open_server_action(
514 _selected_mode: crate::state::app_state::ScriptingMode,
515 env: &crate::scripting::ScriptingEnvironment,
516 is_enabled: bool,
517) -> ActionDefinition {
518 let name = "open_server".to_string();
519 let mut description = "Start a new server.\n\n\
520 PARAMETER USAGE RULES:\n\
521 1. ONLY use parameters that are explicitly documented below\n\
522 2. DO NOT invent new parameters, even if they seem logical\n\
523 3. For custom requirements (timeouts, special behavior, etc.):\n\
524 - Put them in the 'instruction' field as natural language\n\n\
525 EXAMPLE - User says 'open HTTP server with 30 second timeout':\n\
526 ❌ WRONG: {\"type\": \"open_server\", \"protocol\": \"http\", \"timeout\": 30}\n\
527 ✅ RIGHT: {\"type\": \"open_server\", \"protocol\": \"http\", \"instruction\": \"HTTP server with 30 second timeout\"}\n\n\
528 TASK SCHEDULING RULES:\n\
529 FOR PERIODIC TASKS (heartbeat, every X seconds/minutes):\n\
530 - Use 'scheduled_tasks' parameter with interval_secs\n\
531 - DO NOT use event_handlers for time-based tasks\n\n\
532 EXAMPLE - User says 'send heartbeat every 10 seconds':\n\
533 ❌ WRONG: {\"event_handlers\": [{\"event_pattern\": \"*\", \"handler\": {...}}]}\n\
534 ✅ RIGHT: {\"scheduled_tasks\": [{\"task_id\": \"heartbeat\", \"recurring\": true, \"interval_secs\": 10, \"instruction\": \"Send heartbeat log\"}]}\n\n\
535 FOR NETWORK EVENTS (data received, connection made):\n\
536 - Use 'event_handlers' parameter\n\
537 - Only for responding to actual network events".to_string();
538
539 if !is_enabled {
540 description.push_str(" ⚠️ DISABLED: You must first read protocol documentation using the read_documentation tool. This tool lists all available protocols (both server and client) and provides detailed configuration information including when to use server vs client mode.");
541 return ActionDefinition {
542 name,
543 description,
544 parameters: vec![],
545 example: json!({}),
546 log_template: None,
547 };
548 }
549
550 let mut parameters = vec![
551 Parameter {
552 name: "mac_address".to_string(),
553 type_hint: "string".to_string(),
554 description: "Optional: MAC address for Layer 2 protocols (e.g., ARP spoofing). Format: \"00:11:22:33:44:55\". Most protocols don't need this.".to_string(),
555 required: false,
556 },
557 Parameter {
558 name: "interface".to_string(),
559 type_hint: "string".to_string(),
560 description: "Optional: Network interface to bind (for raw protocols like ICMP, ARP, DataLink). \
561 Common interface names: \"lo\" or \"lo0\" (loopback), \"eth0\" or \"en0\" (Ethernet), \"wlan0\" (WiFi). \
562 NOTE: Only specify if the protocol specifically requires it (e.g., DataLink). Most port-based protocols (TCP, HTTP, DNS) don't use this. \
563 If you need to discover available interfaces, you can try common names like \"lo\" for loopback or use the system's default interface by omitting this parameter.".to_string(),
564 required: false,
565 },
566 Parameter {
567 name: "host".to_string(),
568 type_hint: "string".to_string(),
569 description: "Optional: Host address to bind (IPv4, IPv6, or hostname). Examples: \"127.0.0.1\" (loopback), \"0.0.0.0\" (all interfaces), \"::\". Protocols will use sensible defaults if omitted.".to_string(),
570 required: false,
571 },
572 Parameter {
573 name: "port".to_string(),
574 type_hint: "number".to_string(),
575 description: "Optional: Port number to listen on. Use 0 to automatically find an available port. Required for port-based protocols (TCP, HTTP, DNS). Raw protocols (ICMP, ARP) don't use this.".to_string(),
576 required: false,
577 },
578 Parameter {
579 name: "protocol".to_string(),
580 type_hint: "string".to_string(),
581 description: format!(
582 "Protocol to use. ALWAYS prefer high-level protocols when user keywords match: \
583 if user says 'dns' or 'dns server' → use 'dns' (NOT 'udp'), \
584 if user says 'http' or 'web server' → use 'http' (NOT 'tcp'), \
585 if user says 'smtp' or 'mail server' → use 'smtp' (NOT 'tcp'). \
586 Only use low-level protocols (tcp, udp) for custom protocols without a specific high-level match. \
587 Available: {}",
588 all_base_stacks(false, None).join(", ")
589 ),
590 required: true,
591 },
592 Parameter {
593 name: "send_first".to_string(),
594 type_hint: "boolean".to_string(),
595 description: "True if server sends data first (FTP, SMTP), false if it waits for client (HTTP)".to_string(),
596 required: false,
597 },
598 Parameter {
599 name: "initial_memory".to_string(),
600 type_hint: "string".to_string(),
601 description: "Optional initial memory as a string. Use for storing persistent context across connections. Example: \"user_count: 0\"".to_string(),
602 required: false,
603 },
604 Parameter {
605 name: "instruction".to_string(),
606 type_hint: "string".to_string(),
607 description: "Detailed instructions for handling network events. \
608 Use this field for custom requirements that don't have dedicated parameters \
609 (e.g., 'with 30 second timeout', 'log all requests to file', 'rate limit to 10 requests per second', etc.)".to_string(),
610 required: true,
611 },
612 Parameter {
613 name: "startup_params".to_string(),
614 type_hint: "object".to_string(),
615 description: "Optional protocol-specific startup parameters. ONLY the \
616 parameters the protocol's documentation declares are accepted: an \
617 undeclared key is refused by name and THE SERVER DOES NOT START. \
618 A protocol that declares none takes no startup_params at all. \
619 Anything the protocol does not declare - what the server should \
620 answer, which mailbox or database exists, what data to serve - \
621 belongs in 'instruction', not here."
622 .to_string(),
623 required: false,
624 },
625 Parameter {
626 name: "scheduled_tasks".to_string(),
627 type_hint: "array".to_string(),
628 description: "Optional: Array of TIME-BASED tasks that execute periodically or after a delay. \
629 \
630 USE WHEN: User says 'every X seconds/minutes', 'heartbeat', 'periodic', 'scheduled', or describes time-based automation. \
631 \
632 EXAMPLES: \
633 - 'send heartbeat every 10 seconds' → scheduled_tasks with interval_secs: 10 \
634 - 'check status every minute' → scheduled_tasks with interval_secs: 60 \
635 - 'cleanup after 30 seconds' → scheduled_tasks with delay_secs: 30 \
636 \
637 DO NOT use event_handlers for periodic tasks - event_handlers respond to network events, NOT time-based triggers! \
638 \
639 Each task has: task_id (string), recurring (boolean), interval_secs (for periodic) OR delay_secs (for one-shot), max_executions (optional), instruction (what to do), context (optional).".to_string(),
640 required: false,
641 },
642 ];
643
644 let handler_mode_guidance = "You can configure different handlers for different events. Each handler specifies an event_pattern (specific event ID or \"*\" for all events) and a handler type (script, static, or llm). Handlers are matched in order - first match wins.";
646
647 let available_runtimes = env.format_available();
648 let event_handlers_description = format!(
649 "Optional: Array of event handlers to configure how events are processed. {}\\n\\n\
650Each handler has:\\n\
651- event_pattern: Event ID to match (e.g., \\\"tcp_data_received\\\") or \\\"*\\\" for all events\\n\
652- handler: Object with:\\n\
653 - type: \\\"script\\\" (inline code), \\\"static\\\" (predefined actions), or \\\"llm\\\" (dynamic processing)\\n\\n\
654REQUIRED FIELDS BY TYPE:\\n\
655 - For script: language ({}), code (inline script)\\n\
656 - For static: actions (array of action objects)\\n\
657 - For llm: instruction (string, REQUIRED) - describes how the LLM should handle this event\\n\\n\
658CRITICAL: LLM handlers MUST include 'instruction' field. Example: {{\\\"type\\\": \\\"llm\\\", \\\"instruction\\\": \\\"Handle HTTP requests...\\\"}}\\n\\n\
659SCRIPT EVENT DATA STRUCTURE:\\n\
660Scripts receive JSON via stdin with this structure:\\n\
661{{\\n\
662 \\\"event_type_id\\\": \\\"http_request\\\", // Event type identifier\\n\
663 \\\"server\\\": {{\\\"id\\\": 1, \\\"port\\\": 8080, \\\"stack\\\": \\\"HTTP\\\", \\\"memory\\\": \\\"\\\", \\\"instruction\\\": \\\"...\\\"}},\\n\
664 \\\"connection\\\": {{\\\"id\\\": \\\"1\\\", \\\"remote_addr\\\": \\\"127.0.0.1:12345\\\"}}, // Optional\\n\
665 \\\"event\\\": {{\\n\
666 // Protocol-specific event data (fields vary by event type)\\n\
667 // For HTTP: method, path, query_string, query, headers, body\\n\
668 // For TCP: data (hex-encoded bytes)\\n\
669 // For DNS: query_id, domain, query_type\\n\
670 }}\\n\
671}}\\n\\n\
672IMPORTANT: Event data is directly under data['event'], NOT data['event']['data']!\\n\
673Access pattern: data['event']['field_name'] (e.g., data['event']['method'])\\n\\n\
674CRITICAL - COMMON MISTAKES TO AVOID:\\n\
675❌ WRONG: data['event']['request']['query_string'] # NO 'request' wrapper!\\n\
676❌ WRONG: data['event']['http_request']['query_string'] # NO 'http_request' wrapper!\\n\
677❌ WRONG: data['event']['data']['method'] # NO 'data' wrapper!\\n\
678✅ RIGHT: data['event']['query_string'] # Direct access\\n\
679✅ RIGHT: data['event']['method'] # Direct access\\n\\n\
680The event_type_id tells you WHAT event occurred, but data fields are DIRECTLY under data['event'].\\n\\n\
681Example HTTP script (sum query parameters x and y):\\n\
682{{\\\"event_pattern\\\": \\\"http_request\\\", \\\"handler\\\": {{\\\"type\\\": \\\"script\\\", \\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"<http_sum_script>\\\"}}}}\\n\\n\
683<http_sum_script>\\n\
684import json\\n\
685import sys\\n\
686\\n\
687data = json.load(sys.stdin)\\n\
688# Access event data: data['event']['field_name']\\n\
689query_params = data['event']['query'] # Pre-parsed query parameters object\\n\
690x = float(query_params['x'])\\n\
691y = float(query_params['y'])\\n\
692result = x + y\\n\
693\\n\
694print(json.dumps({{\\n\
695 'actions': [{{\\n\
696 'type': 'send_http_response',\\n\
697 'status': 200,\\n\
698 'body': str(result)\\n\
699 }}]\\n\
700}}))\\n\
701</http_sum_script>\\n\\n\
702Example TCP script (echo received data):\\n\
703{{\\\"event_pattern\\\": \\\"tcp_data_received\\\", \\\"handler\\\": {{\\\"type\\\": \\\"script\\\", \\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"<tcp_echo_script>\\\"}}}}\\n\\n\
704<tcp_echo_script>\\n\
705import json\\n\
706import sys\\n\
707\\n\
708data = json.load(sys.stdin)\\n\
709# TCP data is hex-encoded in data['event']['data']\\n\
710received_hex = data['event']['data']\\n\
711\\n\
712print(json.dumps({{\\n\
713 'actions': [{{\\n\
714 'type': 'send_tcp_data',\\n\
715 'data': received_hex # Echo back the same hex data\\n\
716 }}]\\n\
717}}))\\n\
718</tcp_echo_script>\\n\\n\
719Example static handler:\\n\
720{{\\\"event_pattern\\\": \\\"*\\\", \\\"handler\\\": {{\\\"type\\\": \\\"static\\\", \\\"actions\\\": [{{\\\"type\\\": \\\"send_data\\\", \\\"data\\\": \\\"Welcome\\\"}}]}}}}\\n\\n\
721Example LLM handler:\\n\
722{{\\\"event_pattern\\\": \\\"http_request\\\", \\\"handler\\\": {{\\\"type\\\": \\\"llm\\\", \\\"instruction\\\": \\\"You are a recipe website\\\"}}}}",
723 handler_mode_guidance,
724 available_runtimes
725 );
726
727 parameters.push(Parameter {
728 name: "event_handlers".to_string(),
729 type_hint: "array".to_string(),
730 description: event_handlers_description,
731 required: false,
732 });
733
734 parameters.push(Parameter {
735 name: "feedback_instructions".to_string(),
736 type_hint: "string".to_string(),
737 description: "Optional: Instructions for automatic server adjustment based on network request feedback. When set, network requests can provide feedback via the 'provide_feedback' action. Feedback is accumulated and debounced (leading edge), then the LLM is invoked with these instructions to decide how to adjust the server behavior (e.g., update instructions, modify handlers, change configuration). Example: \"Adjust response time if clients are timing out\" or \"Learn from failed requests and improve error handling\".".to_string(),
738 required: false,
739 });
740
741 let example = json!({
742 "type": "open_server",
743 "port": 21,
744 "protocol": "tcp",
745 "send_first": true,
746 "initial_memory": "login_count: 0\nfiles: data.txt,readme.md",
747 "instruction": "You are an FTP server. Respond to FTP commands like USER, PASS, LIST, RETR, QUIT with appropriate FTP response codes."
748 });
749
750 ActionDefinition {
751 name,
752 description,
753 parameters,
754 example,
755 log_template: None,
756 }
757}
758
759pub fn close_server_action() -> ActionDefinition {
761 ActionDefinition {
762 name: "close_server".to_string(),
763 description: "Stop a specific server by ID.".to_string(),
764 parameters: vec![Parameter {
765 name: "server_id".to_string(),
766 type_hint: "number".to_string(),
767 description: "Server ID to close (e.g., 1, 2).".to_string(),
768 required: true,
769 }],
770 example: json!({
771 "type": "close_server",
772 "server_id": 1
773 }),
774 log_template: None,
775 }
776}
777
778pub fn close_all_servers_action() -> ActionDefinition {
780 ActionDefinition {
781 name: "close_all_servers".to_string(),
782 description: "Stop all running servers.".to_string(),
783 parameters: vec![],
784 example: json!({
785 "type": "close_all_servers"
786 }),
787 log_template: None,
788 }
789}
790
791pub fn open_client_action(
793 _selected_mode: crate::state::app_state::ScriptingMode,
794 env: &crate::scripting::ScriptingEnvironment,
795 is_enabled: bool,
796) -> ActionDefinition {
797 let name = "open_client".to_string();
798 let mut description = "Connect to a remote server as a client.".to_string();
799
800 if !is_enabled {
801 description.push_str(" ⚠️ DISABLED: You must first read protocol documentation using the read_documentation tool. This tool lists all available protocols (both server and client) and provides detailed configuration information including when to use server vs client mode.");
802 return ActionDefinition {
803 name,
804 description,
805 parameters: vec![],
806 example: serde_json::Value::Null,
807 log_template: None,
808 };
809 }
810
811 let mut parameters = vec![
812 Parameter {
813 name: "protocol".to_string(),
814 type_hint: "string".to_string(),
815 description: "Protocol to use for connection (e.g., 'tcp', 'http', 'redis', 'ssh')".to_string(),
816 required: true,
817 },
818 Parameter {
819 name: "remote_addr".to_string(),
820 type_hint: "string".to_string(),
821 description: "Remote server address as 'hostname:port' or 'IP:port' (e.g., 'example.com:80', '192.168.1.1:6379', 'localhost:8080')".to_string(),
822 required: true,
823 },
824 Parameter {
825 name: "instruction".to_string(),
826 type_hint: "string".to_string(),
827 description: "Detailed instructions for controlling the client (how to send data, interpret responses, make decisions)".to_string(),
828 required: true,
829 },
830 Parameter {
831 name: "initial_memory".to_string(),
832 type_hint: "string".to_string(),
833 description: "Optional initial memory as a string. Use for storing persistent context. Example: \"auth_token: abc123\\nrequest_count: 0\"".to_string(),
834 required: false,
835 },
836 Parameter {
837 name: "startup_params".to_string(),
838 type_hint: "object".to_string(),
839 description: "Optional protocol-specific startup parameters. ONLY the parameters \
840 the protocol's documentation declares are accepted; an undeclared key \
841 is refused by name and the client does not connect. Behavioural \
842 detail belongs in 'instruction', not here. For example, HTTP clients \
843 may accept default headers or user agent settings."
844 .to_string(),
845 required: false,
846 },
847 Parameter {
848 name: "scheduled_tasks".to_string(),
849 type_hint: "array".to_string(),
850 description: "Optional: Array of scheduled tasks to create with this client. Each task will be attached to the client and execute at specified intervals or delays. Tasks are automatically cleaned up when the client disconnects.".to_string(),
851 required: false,
852 },
853 ];
854
855 let handler_mode_guidance = "You can configure different handlers for different client events. Each handler specifies an event_pattern (specific event ID or \"*\" for all events) and a handler type (script, static, or llm). Handlers are matched in order - first match wins.";
857
858 let available_runtimes = env.format_available();
859 let event_handlers_description = format!(
860 "Optional: Array of event handlers to configure how client events are processed. {}\\n\\n\
861Each handler has:\\n\
862- event_pattern: Event ID to match (e.g., \\\"http_response_received\\\") or \\\"*\\\" for all events\\n\
863- handler: Object with:\\n\
864 - type: \\\"script\\\" (inline code), \\\"static\\\" (predefined actions), or \\\"llm\\\" (dynamic processing)\\n\
865 - For script: language ({}), code (inline script)\\n\
866 - For static: actions (array of action objects)\\n\
867 - For llm: instruction (REQUIRED - describes how the LLM should handle this event)\\n\\n\
868Note: Client scripts use the same event data structure as server scripts (see open_server documentation for details).\\n\
869Access pattern: data['event']['field_name'] (e.g., data['event']['status_code'] for HTTP responses)\\n\\n\
870Example script handler: {{\\\"event_pattern\\\": \\\"redis_response_received\\\", \\\"handler\\\": {{\\\"type\\\": \\\"script\\\", \\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"import json,sys;data=json.load(sys.stdin);print(json.dumps({{'actions':[{{'type':'execute_redis_command','command':'PING'}}]}}))\\\"}}}}\\n\\n\
871Example static handler: {{\\\"event_pattern\\\": \\\"*\\\", \\\"handler\\\": {{\\\"type\\\": \\\"static\\\", \\\"actions\\\": [{{\\\"type\\\": \\\"send_http_request\\\", \\\"method\\\": \\\"GET\\\", \\\"path\\\": \\\"/\\\"}}]}}}}\\n\\n\
872Example LLM handler: {{\\\"event_pattern\\\": \\\"http_response_received\\\", \\\"handler\\\": {{\\\"type\\\": \\\"llm\\\", \\\"instruction\\\": \\\"You are a recipe website\\\"}}}}",
873 handler_mode_guidance,
874 available_runtimes
875 );
876
877 parameters.push(Parameter {
878 name: "event_handlers".to_string(),
879 type_hint: "array".to_string(),
880 description: event_handlers_description,
881 required: false,
882 });
883
884 parameters.push(Parameter {
885 name: "feedback_instructions".to_string(),
886 type_hint: "string".to_string(),
887 description: "Optional: Instructions for automatic client adjustment based on server response feedback. When set, server responses can provide feedback via the 'provide_feedback' action. Feedback is accumulated and debounced (leading edge), then the LLM is invoked with these instructions to decide how to adjust the client behavior (e.g., update request strategy, modify retry logic, change authentication method). Example: \"Adjust request rate if server is throttling\" or \"Learn from error responses and modify request format\".".to_string(),
888 required: false,
889 });
890
891 let example = json!({
892 "type": "open_client",
893 "protocol": "http",
894 "remote_addr": "example.com:80",
895 "instruction": "Send a GET request to /api/status and log the response code."
896 });
897
898 ActionDefinition {
899 name,
900 description,
901 parameters,
902 example,
903 log_template: None,
904 }
905}
906
907pub fn close_client_action() -> ActionDefinition {
909 ActionDefinition {
910 name: "close_client".to_string(),
911 description: "Disconnect a specific client by ID.".to_string(),
912 parameters: vec![Parameter {
913 name: "client_id".to_string(),
914 type_hint: "number".to_string(),
915 description: "Client ID to close (e.g., 1, 2).".to_string(),
916 required: true,
917 }],
918 example: json!({
919 "type": "close_client",
920 "client_id": 1
921 }),
922 log_template: None,
923 }
924}
925
926pub fn close_all_clients_action() -> ActionDefinition {
928 ActionDefinition {
929 name: "close_all_clients".to_string(),
930 description: "Disconnect all active clients.".to_string(),
931 parameters: vec![],
932 example: json!({
933 "type": "close_all_clients"
934 }),
935 log_template: None,
936 }
937}
938
939pub fn close_connection_by_id_action() -> ActionDefinition {
941 ActionDefinition {
942 name: "close_connection_by_id".to_string(),
943 description: "Close a specific connection by its unified ID.".to_string(),
944 parameters: vec![Parameter {
945 name: "connection_id".to_string(),
946 type_hint: "number".to_string(),
947 description: "Unified ID of the connection to close (e.g., 3, 5).".to_string(),
948 required: true,
949 }],
950 example: json!({
951 "type": "close_connection_by_id",
952 "connection_id": 3
953 }),
954 log_template: None,
955 }
956}
957
958pub fn reconnect_client_action() -> ActionDefinition {
960 ActionDefinition {
961 name: "reconnect_client".to_string(),
962 description: "Reconnect a disconnected client to its remote server.".to_string(),
963 parameters: vec![Parameter {
964 name: "client_id".to_string(),
965 type_hint: "number".to_string(),
966 description: "Client ID to reconnect (e.g., 1, 2).".to_string(),
967 required: true,
968 }],
969 example: json!({
970 "type": "reconnect_client",
971 "client_id": 1
972 }),
973 log_template: None,
974 }
975}
976
977pub fn update_client_instruction_action() -> ActionDefinition {
979 ActionDefinition {
980 name: "update_client_instruction".to_string(),
981 description:
982 "Update the instruction for a specific client (replaces existing instruction)."
983 .to_string(),
984 parameters: vec![
985 Parameter {
986 name: "client_id".to_string(),
987 type_hint: "number".to_string(),
988 description: "Client ID to update (e.g., 1, 2).".to_string(),
989 required: true,
990 },
991 Parameter {
992 name: "instruction".to_string(),
993 type_hint: "string".to_string(),
994 description: "New instruction for the client.".to_string(),
995 required: true,
996 },
997 ],
998 example: json!({
999 "type": "update_client_instruction",
1000 "client_id": 1,
1001 "instruction": "Switch to POST requests with JSON payload"
1002 }),
1003 log_template: None,
1004 }
1005}
1006
1007pub fn update_server_action() -> ActionDefinition {
1009 ActionDefinition {
1010 name: "update_server".to_string(),
1011 description: "Update a RUNNING server in place, by its id, instead of opening a second \
1012 one. Supply only the fields you want to change. Changing the instruction, memory, \
1013 event_handlers, feedback_instructions or scheduled_tasks is applied WITHOUT dropping \
1014 connections. Changing the port, host, interface, mac_address or startup_params \
1015 requires a clean stop+start (connections are dropped and the server gets a new id). \
1016 Prefer this over open_server when a server for the protocol already exists."
1017 .to_string(),
1018 parameters: vec![
1019 Parameter {
1020 name: "server_id".to_string(),
1021 type_hint: "number".to_string(),
1022 description: "Id of the server to update (from the running servers list)."
1023 .to_string(),
1024 required: true,
1025 },
1026 Parameter {
1027 name: "instruction".to_string(),
1028 type_hint: "string".to_string(),
1029 description: "New LLM instruction (hot-applied).".to_string(),
1030 required: false,
1031 },
1032 Parameter {
1033 name: "event_handlers".to_string(),
1034 type_hint: "array".to_string(),
1035 description: "Replacement deterministic event handlers (hot-applied). Same shape \
1036 as open_server's."
1037 .to_string(),
1038 required: false,
1039 },
1040 Parameter {
1041 name: "startup_params".to_string(),
1042 type_hint: "object".to_string(),
1043 description: "Startup parameters to merge in. Validated against the protocol \
1044 schema; triggers a restart."
1045 .to_string(),
1046 required: false,
1047 },
1048 Parameter {
1049 name: "port".to_string(),
1050 type_hint: "number".to_string(),
1051 description: "New port to bind. Triggers a restart.".to_string(),
1052 required: false,
1053 },
1054 ],
1055 example: json!({
1056 "type": "update_server",
1057 "server_id": 1,
1058 "instruction": "Return 503 for every request now."
1059 }),
1060 log_template: None,
1061 }
1062}
1063
1064pub fn update_client_action() -> ActionDefinition {
1066 ActionDefinition {
1067 name: "update_client".to_string(),
1068 description: "Update a RUNNING client in place, by its id, instead of opening a second \
1069 one. Supply only the fields you want to change. Changing the instruction, memory, \
1070 event_handlers, feedback_instructions or scheduled_tasks is applied in place. \
1071 Changing the remote_addr or startup_params reconnects (new id)."
1072 .to_string(),
1073 parameters: vec![
1074 Parameter {
1075 name: "client_id".to_string(),
1076 type_hint: "number".to_string(),
1077 description: "Id of the client to update.".to_string(),
1078 required: true,
1079 },
1080 Parameter {
1081 name: "instruction".to_string(),
1082 type_hint: "string".to_string(),
1083 description: "New LLM instruction (hot-applied).".to_string(),
1084 required: false,
1085 },
1086 Parameter {
1087 name: "event_handlers".to_string(),
1088 type_hint: "array".to_string(),
1089 description: "Replacement deterministic event handlers (hot-applied).".to_string(),
1090 required: false,
1091 },
1092 Parameter {
1093 name: "remote_addr".to_string(),
1094 type_hint: "string".to_string(),
1095 description: "New remote address 'host:port'. Triggers a reconnect.".to_string(),
1096 required: false,
1097 },
1098 ],
1099 example: json!({
1100 "type": "update_client",
1101 "client_id": 1,
1102 "instruction": "Log every response you receive."
1103 }),
1104 log_template: None,
1105 }
1106}
1107
1108pub fn update_instruction_action() -> ActionDefinition {
1110 ActionDefinition {
1111 name: "update_instruction".to_string(),
1112 description: "Update the current server instruction (combines with existing instruction)"
1113 .to_string(),
1114 parameters: vec![Parameter {
1115 name: "instruction".to_string(),
1116 type_hint: "string".to_string(),
1117 description: "New instruction to add/combine".to_string(),
1118 required: true,
1119 }],
1120 example: json!({
1121 "type": "update_instruction",
1122 "instruction": "For all HTTP requests, return status 404 with 'Not Found' message."
1123 }),
1124 log_template: None,
1125 }
1126}
1127
1128pub fn change_model_action() -> ActionDefinition {
1130 ActionDefinition {
1131 name: "change_model".to_string(),
1132 description: "Switch to a different LLM model".to_string(),
1133 parameters: vec![Parameter {
1134 name: "model".to_string(),
1135 type_hint: "string".to_string(),
1136 description: "Model name (e.g., 'llama3.2:latest')".to_string(),
1137 required: true,
1138 }],
1139 example: json!({
1140 "type": "change_model",
1141 "model": "llama3.2:latest"
1142 }),
1143 log_template: None,
1144 }
1145}
1146
1147pub fn set_memory_action() -> ActionDefinition {
1149 ActionDefinition {
1150 name: "set_memory".to_string(),
1151 description: "Replace the entire global memory with new content. Any existing memory is discarded. Use this to reset or completely rewrite memory state.".to_string(),
1152 parameters: vec![
1153 Parameter {
1154 name: "value".to_string(),
1155 type_hint: "string".to_string(),
1156 description: "New memory value as a string. Replaces all existing memory.".to_string(),
1157 required: true,
1158 },
1159 ],
1160 example: json!({
1161 "type": "set_memory",
1162 "value": "session_id: abc123\nuser_preferences: dark_mode=true\nlast_command: LIST"
1163 }),
1164 log_template: None,
1165 }
1166}
1167
1168pub fn append_memory_action() -> ActionDefinition {
1170 ActionDefinition {
1171 name: "append_memory".to_string(),
1172 description: "Add new content to the end of global memory. Existing memory is preserved and a newline is automatically added before the new content. Use this to incrementally build up memory state.".to_string(),
1173 parameters: vec![
1174 Parameter {
1175 name: "value".to_string(),
1176 type_hint: "string".to_string(),
1177 description: "Text to append as a string. Will be added after existing memory with newline separator.".to_string(),
1178 required: true,
1179 },
1180 ],
1181 example: json!({
1182 "type": "append_memory",
1183 "value": "connection_count: 5\nlast_file_requested: readme.md"
1184 }),
1185 log_template: None,
1186 }
1187}
1188
1189pub fn append_to_log_action() -> ActionDefinition {
1191 ActionDefinition {
1192 name: "append_to_log".to_string(),
1193 description: "If you are asked to log information for the user, use this to append logs to a file. Use this to create access logs, audit trails, or any persistent logging.".to_string(),
1194 parameters: vec![
1195 Parameter {
1196 name: "output_name".to_string(),
1197 type_hint: "string".to_string(),
1198 description: "Name of the log output (e.g., 'access_logs'). Used to construct the log filename netget_<name>_<time>.log, so it may contain only A-Z, a-z, 0-9, '_' and '-' (1-64 characters); anything else is refused.".to_string(),
1199 required: true,
1200 },
1201 Parameter {
1202 name: "content".to_string(),
1203 type_hint: "string".to_string(),
1204 description: "Content to append to the log file.".to_string(),
1205 required: true,
1206 },
1207 ],
1208 example: json!({
1209 "type": "append_to_log",
1210 "output_name": "access_logs",
1211 "content": "127.0.0.1 - - [29/Oct/2025:12:34:56 +0000] \"GET /index.html HTTP/1.1\" 200 1234"
1212 }),
1213 log_template: None,
1214 }
1215}
1216
1217pub fn provide_feedback_action() -> ActionDefinition {
1219 ActionDefinition {
1220 name: "provide_feedback".to_string(),
1221 description: "Provide feedback for automatic server/client adjustment. Only available when feedback_instructions was set during server/client creation. Feedback is accumulated and debounced (leading edge), then periodically the LLM is invoked with the feedback_instructions to decide how to adjust the server/client behavior. Use this to signal issues, patterns, or learning opportunities that should trigger automatic adaptation.".to_string(),
1222 parameters: vec![
1223 Parameter {
1224 name: "feedback".to_string(),
1225 type_hint: "object".to_string(),
1226 description: "Structured feedback data describing what should be learned or adjusted. Can include any relevant context like error rates, performance metrics, client behaviors, failed requests, etc. Example: {\"issue\": \"client_timeout\", \"details\": \"Client disconnected after 5s\", \"suggestion\": \"Increase response speed\"}".to_string(),
1227 required: true,
1228 },
1229 ],
1230 example: json!({
1231 "type": "provide_feedback",
1232 "feedback": {
1233 "issue": "authentication_failed",
1234 "username": "guest",
1235 "attempts": 3,
1236 "suggestion": "Consider blocking IP after multiple failures"
1237 }
1238 }),
1239 log_template: None,
1240 }
1241}
1242
1243pub fn schedule_task_action(
1245 selected_mode: crate::state::app_state::ScriptingMode,
1246 env: &crate::scripting::ScriptingEnvironment,
1247) -> ActionDefinition {
1248 let mut parameters = vec![
1249 Parameter {
1250 name: "task_id".to_string(),
1251 type_hint: "string".to_string(),
1252 description: "Unique identifier for this task (e.g., 'cleanup_logs', 'sse_heartbeat'). Used to reference or cancel the task later.".to_string(),
1253 required: true,
1254 },
1255 Parameter {
1256 name: "recurring".to_string(),
1257 type_hint: "boolean".to_string(),
1258 description: "True for recurring task (executes at intervals), false for one-shot task (executes once after delay).".to_string(),
1259 required: true,
1260 },
1261 Parameter {
1262 name: "delay_secs".to_string(),
1263 type_hint: "number".to_string(),
1264 description: "For one-shot tasks (recurring=false): delay in seconds before executing. For recurring tasks: optional initial delay before first execution (defaults to interval_secs if not provided).".to_string(),
1265 required: false,
1266 },
1267 Parameter {
1268 name: "interval_secs".to_string(),
1269 type_hint: "number".to_string(),
1270 description: "For recurring tasks (recurring=true): interval in seconds between executions. Required when recurring=true.".to_string(),
1271 required: false,
1272 },
1273 Parameter {
1274 name: "max_executions".to_string(),
1275 type_hint: "number".to_string(),
1276 description: "For recurring tasks: maximum number of times to execute. If omitted, task runs indefinitely until cancelled.".to_string(),
1277 required: false,
1278 },
1279 Parameter {
1280 name: "server_id".to_string(),
1281 type_hint: "number".to_string(),
1282 description: "Optional: Server ID to scope this task to. If provided, task uses server's instruction and protocol actions. If omitted, task is global and uses user input actions.".to_string(),
1283 required: false,
1284 },
1285 Parameter {
1286 name: "connection_id".to_string(),
1287 type_hint: "string".to_string(),
1288 description: "Optional: Connection ID (e.g., 'conn-123') to scope this task to a specific connection. Requires server_id to be specified. Task will be automatically cleaned up when the connection closes. Useful for connection-specific timeouts, session cleanup, or per-connection monitoring.".to_string(),
1289 required: false,
1290 },
1291 Parameter {
1292 name: "client_id".to_string(),
1293 type_hint: "number".to_string(),
1294 description: "Optional: Client ID to scope this task to. If provided, task uses client's instruction and protocol actions. Task will be automatically cleaned up when the client disconnects. Useful for client-specific timeouts, reconnection logic, or per-client monitoring.".to_string(),
1295 required: false,
1296 },
1297 Parameter {
1298 name: "instruction".to_string(),
1299 type_hint: "string".to_string(),
1300 description: "Instruction/prompt for LLM when task executes. Describes what the task should do.".to_string(),
1301 required: true,
1302 },
1303 Parameter {
1304 name: "context".to_string(),
1305 type_hint: "object".to_string(),
1306 description: "Optional: Additional context data to pass to LLM when task executes (e.g., thresholds, parameters).".to_string(),
1307 required: false,
1308 },
1309 ];
1310
1311 match selected_mode {
1313 crate::state::app_state::ScriptingMode::On => {
1314 let available_runtimes = env.format_available();
1316 parameters.extend(vec![
1317 Parameter {
1318 name: "script_runtime".to_string(),
1319 type_hint: "string".to_string(),
1320 description: format!(
1321 "Required when script_inline is provided: Choose runtime for script execution. Available: {}",
1322 available_runtimes
1323 ),
1324 required: false,
1325 },
1326 Parameter {
1327 name: "script_inline".to_string(),
1328 type_hint: "string".to_string(),
1329 description: "Optional: Inline script code to handle task execution instead of LLM. Must match the script_runtime language. If provided, script_runtime MUST also be specified.".to_string(),
1330 required: false,
1331 },
1332 Parameter {
1333 name: "script_handles".to_string(),
1334 type_hint: "array".to_string(),
1335 description: "Optional: Event types the script handles (e.g., [\"scheduled_task_cleanup\"]). Defaults to [\"all\"].".to_string(),
1336 required: false,
1337 },
1338 ]);
1339 }
1340 crate::state::app_state::ScriptingMode::Off => {
1341 }
1343 crate::state::app_state::ScriptingMode::Python
1344 | crate::state::app_state::ScriptingMode::JavaScript
1345 | crate::state::app_state::ScriptingMode::Go
1346 | crate::state::app_state::ScriptingMode::Perl => {
1347 let lang = selected_mode.as_str();
1348 parameters.extend(vec![
1349 Parameter {
1350 name: "script_inline".to_string(),
1351 type_hint: "string".to_string(),
1352 description: format!(
1353 "Optional: Inline {} script code to handle task execution instead of LLM. If provided, the script will be executed for each task trigger.",
1354 lang
1355 ),
1356 required: false,
1357 },
1358 Parameter {
1359 name: "script_handles".to_string(),
1360 type_hint: "array".to_string(),
1361 description: "Optional: Event types the script handles (e.g., [\"scheduled_task_cleanup\"]). Defaults to [\"all\"].".to_string(),
1362 required: false,
1363 },
1364 ]);
1365 }
1366 }
1367
1368 ActionDefinition {
1369 name: "schedule_task".to_string(),
1370 description: "Schedule a task (one-shot or recurring). The task will call the LLM or execute a script with the provided instruction. One-shot tasks execute once after a delay and are automatically removed. Recurring tasks execute at intervals until cancelled or max_executions is reached. Useful for delayed operations, timeouts, periodic health checks, heartbeats, SSE messages, metrics collection, etc.".to_string(),
1371 parameters,
1372 example: json!({
1373 "type": "schedule_task",
1374 "task_id": "sse_heartbeat",
1375 "recurring": true,
1376 "interval_secs": 30,
1377 "server_id": 1,
1378 "instruction": "Send SSE heartbeat to all active connections"
1379 }),
1380 log_template: None,
1381 }
1382}
1383
1384pub fn cancel_task_action() -> ActionDefinition {
1386 ActionDefinition {
1387 name: "cancel_task".to_string(),
1388 description: "Cancel a scheduled task by its task_id. Works for both one-shot and recurring tasks. The task is immediately removed and will not execute again.".to_string(),
1389 parameters: vec![Parameter {
1390 name: "task_id".to_string(),
1391 type_hint: "string".to_string(),
1392 description: "ID of the task to cancel (the task_id used when scheduling).".to_string(),
1393 required: true,
1394 }],
1395 example: json!({
1396 "type": "cancel_task",
1397 "task_id": "cleanup_logs"
1398 }),
1399 log_template: None,
1400 }
1401}
1402
1403#[cfg(feature = "sqlite")]
1405pub fn create_database_action() -> ActionDefinition {
1406 ActionDefinition {
1407 name: "create_database".to_string(),
1408 description: "Create a new SQLite database (in-memory or file-based). Use this to store protocol state (e.g., NFS file system, DNS cache, user sessions). The database persists for the lifetime of the owning server/client, or forever if global. You can execute DDL to create tables during creation.".to_string(),
1409 parameters: vec![
1410 Parameter {
1411 name: "name".to_string(),
1412 type_hint: "string".to_string(),
1413 description: "Database name (user-friendly identifier). This will be used to construct the filename as './netget_db_<name>.db' for file-based databases.".to_string(),
1414 required: true,
1415 },
1416 Parameter {
1417 name: "is_memory".to_string(),
1418 type_hint: "boolean".to_string(),
1419 description: "true = in-memory database (fast, data lost on close), false = file-based database (persistent, saved to ./netget_db_<name>.db). Defaults to false (file-based).".to_string(),
1420 required: false,
1421 },
1422 Parameter {
1423 name: "owner".to_string(),
1424 type_hint: "string".to_string(),
1425 description: "Owner scope: 'server-N' (auto-deleted when server closes), 'client-N' (auto-deleted when client disconnects), or 'global' (persists across servers/clients). Omit to default to current context.".to_string(),
1426 required: false,
1427 },
1428 Parameter {
1429 name: "schema_ddl".to_string(),
1430 type_hint: "string".to_string(),
1431 description: "SQL DDL statements to create initial schema (e.g., 'CREATE TABLE files (path TEXT PRIMARY KEY, content BLOB);'). Use semicolons to separate multiple statements.".to_string(),
1432 required: false,
1433 },
1434 ],
1435 example: json!({
1436 "type": "create_database",
1437 "name": "nfs_storage",
1438 "is_memory": true,
1439 "owner": "server-1",
1440 "schema_ddl": "CREATE TABLE files (path TEXT PRIMARY KEY, content BLOB, size INTEGER, modified INTEGER);"
1441 }),
1442 log_template: None,
1443 }
1444}
1445
1446#[cfg(feature = "sqlite")]
1448pub fn delete_database_action() -> ActionDefinition {
1449 ActionDefinition {
1450 name: "delete_database".to_string(),
1451 description: "Delete a database and remove its file (if file-based). This is permanent and cannot be undone. Server/client-owned databases are automatically deleted when the owner closes.".to_string(),
1452 parameters: vec![
1453 Parameter {
1454 name: "database_id".to_string(),
1455 type_hint: "number".to_string(),
1456 description: "Database ID to delete".to_string(),
1457 required: true,
1458 },
1459 ],
1460 example: json!({
1461 "type": "delete_database",
1462 "database_id": 1
1463 }),
1464 log_template: None,
1465 }
1466}
1467
1468pub fn get_all_common_actions(
1478 selected_mode: crate::state::app_state::ScriptingMode,
1479 env: &crate::scripting::ScriptingEnvironment,
1480 is_open_server_enabled: bool,
1481 is_open_client_enabled: bool,
1482) -> Vec<ActionDefinition> {
1483 #[allow(unused_mut)]
1484 let mut actions = vec![];
1485
1486 if is_open_server_enabled {
1489 actions.push(open_server_action(selected_mode, env, true));
1490 }
1491 actions.push(close_server_action());
1492 actions.push(close_all_servers_action());
1493
1494 if is_open_client_enabled {
1497 actions.push(open_client_action(selected_mode, env, true));
1498 }
1499 actions.push(close_client_action());
1500 actions.push(close_all_clients_action());
1501
1502 actions.push(close_connection_by_id_action());
1504
1505 actions.push(reconnect_client_action());
1507 actions.push(update_client_instruction_action());
1508 actions.push(update_client_action());
1509
1510 actions.push(update_server_action());
1512 actions.push(update_instruction_action());
1513 actions.push(set_memory_action());
1514 actions.push(append_memory_action());
1515
1516 actions.push(schedule_task_action(selected_mode, env));
1518 actions.push(cancel_task_action());
1519
1520 actions.push(show_message_action());
1523 actions.push(append_to_log_action());
1524
1525 #[cfg(feature = "sqlite")]
1527 {
1528 actions.push(create_database_action());
1529 actions.push(delete_database_action());
1530 }
1531
1532 actions
1533}
1534
1535pub fn get_user_input_common_actions(
1537 selected_mode: crate::state::app_state::ScriptingMode,
1538 env: &crate::scripting::ScriptingEnvironment,
1539 is_open_server_enabled: bool,
1540 is_open_client_enabled: bool,
1541) -> Vec<ActionDefinition> {
1542 get_all_common_actions(
1543 selected_mode,
1544 env,
1545 is_open_server_enabled,
1546 is_open_client_enabled,
1547 )
1548}
1549
1550pub fn get_network_event_common_actions() -> Vec<ActionDefinition> {
1555 vec![
1556 set_memory_action(),
1558 append_memory_action(),
1559 show_message_action(),
1561 append_to_log_action(),
1562 ]
1566}
1567
1568fn all_base_stacks(
1572 include_disabled: bool,
1573 min_stability: Option<crate::protocol::metadata::DevelopmentState>,
1574) -> Vec<String> {
1575 let registry = crate::protocol::server_registry::registry();
1576
1577 let mut protocols: Vec<String> = registry
1579 .all_protocols()
1580 .into_iter()
1581 .map(|(protocol_name, _protocol)| protocol_name)
1582 .filter(|protocol_name| {
1583 let meta = registry.metadata(protocol_name);
1584 let available = include_disabled
1587 || meta
1588 .as_ref()
1589 .map(|m| m.is_available_to_llm())
1590 .unwrap_or(true);
1591 let meets_min = match (min_stability, meta.as_ref()) {
1594 (Some(min), Some(m)) => m.state >= min,
1595 _ => true,
1596 };
1597 available && meets_min
1598 })
1599 .collect();
1600
1601 protocols.sort();
1603 protocols
1604}
1605
1606pub fn generate_base_stack_documentation(
1609 include_disabled: bool,
1610 min_stability: Option<crate::protocol::metadata::DevelopmentState>,
1611) -> String {
1612 let mut doc = String::from("## Available Base Stacks\n\n");
1613
1614 let registry = crate::protocol::server_registry::registry();
1616 let mut groups: ProtocolGroups = std::collections::HashMap::new();
1617
1618 for protocol_name in all_base_stacks(include_disabled, min_stability) {
1619 if let Some(protocol) = registry.get(&protocol_name) {
1620 let group = protocol.group_name();
1621 groups
1622 .entry(group)
1623 .or_default()
1624 .push((protocol_name.clone(), protocol));
1625 }
1626 }
1627
1628 let mut sorted_group_names: Vec<&'static str> = groups.keys().copied().collect();
1630 sorted_group_names.sort();
1631
1632 for group_name in sorted_group_names {
1633 if let Some(protocols) = groups.get_mut(group_name) {
1634 if protocols.is_empty() {
1635 continue;
1636 }
1637
1638 doc.push_str(&format!("### {}\n", group_name));
1640
1641 protocols.sort_by(|a, b| a.0.cmp(&b.0));
1643
1644 for (protocol_name, protocol) in protocols {
1646 let keywords = protocol.keywords();
1647 if !keywords.is_empty() {
1648 doc.push_str(&format!("{} ({})\n", protocol_name, keywords.join(", ")));
1649 } else {
1650 doc.push_str(&format!("{}\n", protocol_name));
1651 }
1652 }
1653 doc.push('\n');
1654 }
1655 }
1656
1657 doc
1658}
1659
1660#[derive(Debug, serde::Serialize)]
1662pub struct ProtocolDocData {
1663 pub protocol_name: String,
1664 pub both_modes: bool,
1665 pub server: Option<ProtocolModeData>,
1666 pub client: Option<ProtocolModeData>,
1667}
1668
1669#[derive(Debug, serde::Serialize)]
1671pub struct ProtocolModeData {
1672 pub stack_name: String,
1673 pub group_name: String,
1674 pub description: String,
1675 pub example_prompt: String,
1676 pub keywords: Vec<String>,
1677 pub startup_params: Vec<super::ParameterDefinition>,
1678 pub state: String,
1679 pub notes: Option<String>,
1680}
1681
1682pub fn generate_single_protocol_doc_data(protocol_name: &str) -> anyhow::Result<ProtocolDocData> {
1695 let server_registry = crate::protocol::server_registry::registry();
1696 let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
1697
1698 let normalized_name_upper = protocol_name.to_uppercase();
1701 let normalized_name_lower = protocol_name.to_lowercase();
1702
1703 let server_protocol = server_registry.get(&normalized_name_upper);
1704 let client_protocol = client_registry.get(&normalized_name_lower);
1705
1706 if server_protocol.is_none() && client_protocol.is_none() {
1708 return Err(anyhow::anyhow!(
1709 "Protocol '{}' not found in server or client registry",
1710 protocol_name
1711 ));
1712 }
1713
1714 let both_modes = server_protocol.is_some() && client_protocol.is_some();
1715
1716 let server = server_protocol.map(|protocol| {
1718 let metadata = protocol.metadata();
1719 let mut startup_params = protocol.get_startup_parameters();
1721 for param in &mut startup_params {
1722 if let Ok(json_str) = serde_json::to_string(¶m.example) {
1724 param.example = serde_json::Value::String(json_str);
1725 }
1726 }
1727 ProtocolModeData {
1728 stack_name: protocol.stack_name().to_string(),
1729 group_name: protocol.group_name().to_string(),
1730 description: protocol.description().to_string(),
1731 example_prompt: protocol.example_prompt().to_string(),
1732 keywords: protocol.keywords().iter().map(|s| s.to_string()).collect(),
1733 startup_params,
1734 state: format!("{:?}", metadata.state),
1735 notes: metadata.notes.map(|s| s.to_string()),
1736 }
1737 });
1738
1739 let client = client_protocol.map(|protocol| {
1741 let metadata = protocol.metadata();
1742 let mut startup_params = protocol.get_startup_parameters();
1744 for param in &mut startup_params {
1745 if let Ok(json_str) = serde_json::to_string(¶m.example) {
1747 param.example = serde_json::Value::String(json_str);
1748 }
1749 }
1750 ProtocolModeData {
1751 stack_name: protocol.stack_name().to_string(),
1752 group_name: protocol.group_name().to_string(),
1753 description: protocol.description().to_string(),
1754 example_prompt: protocol.example_prompt().to_string(),
1755 keywords: protocol.keywords().iter().map(|s| s.to_string()).collect(),
1756 startup_params,
1757 state: format!("{:?}", metadata.state),
1758 notes: metadata.notes.map(|s| s.to_string()),
1759 }
1760 });
1761
1762 Ok(ProtocolDocData {
1763 protocol_name: protocol_name.to_uppercase(),
1764 both_modes,
1765 server,
1766 client,
1767 })
1768}