Skip to main content

netget/client/tcp/
actions.rs

1//! TCP client protocol actions implementation
2
3use crate::llm::actions::{
4    client_trait::{Client, ClientActionResult},
5    protocol_trait::Protocol,
6    ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// TCP client connected event
15pub static TCP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "tcp_connected",
18        "TCP client successfully connected to server",
19        serde_json::json!({
20            "type": "send_tcp_data",
21            "data": "48656c6c6f"
22        }),
23    )
24    .with_parameters(vec![Parameter {
25        name: "remote_addr".to_string(),
26        type_hint: "string".to_string(),
27        description: "Remote server address".to_string(),
28        required: true,
29    }])
30});
31
32/// TCP client data received event
33pub static TCP_CLIENT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
34    EventType::new(
35        "tcp_data_received",
36        "Data received from TCP server",
37        json!({"type": "placeholder", "event_id": "tcp_data_received"}),
38    )
39    .with_parameters(vec![
40        Parameter {
41            name: "data_hex".to_string(),
42            type_hint: "string".to_string(),
43            description: "The data received (as hex string)".to_string(),
44            required: true,
45        },
46        Parameter {
47            name: "data_length".to_string(),
48            type_hint: "number".to_string(),
49            description: "Length of data in bytes".to_string(),
50            required: true,
51        },
52    ])
53});
54
55/// TCP client protocol action handler
56pub struct TcpClientProtocol;
57
58impl Default for TcpClientProtocol {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl TcpClientProtocol {
65    pub fn new() -> Self {
66        Self
67    }
68}
69
70// Implement Protocol trait (common functionality)
71impl Protocol for TcpClientProtocol {
72    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
73        vec![
74            ActionDefinition {
75                name: "send_tcp_data".to_string(),
76                description: "Send raw TCP data to the server (UTF-8 string or hex-encoded)"
77                    .to_string(),
78                parameters: vec![
79                    Parameter {
80                        name: "data".to_string(),
81                        type_hint: "string".to_string(),
82                        description: "UTF-8 string data to send (preferred for text)".to_string(),
83                        required: false,
84                    },
85                    Parameter {
86                        name: "data_hex".to_string(),
87                        type_hint: "string".to_string(),
88                        description: "Hexadecimal encoded data to send (for binary data)"
89                            .to_string(),
90                        required: false,
91                    },
92                ],
93                example: json!({
94                    "type": "send_tcp_data",
95                    "data": "Hello World"
96                }),
97                log_template: None,
98            },
99            ActionDefinition {
100                name: "disconnect".to_string(),
101                description: "Disconnect from the TCP server".to_string(),
102                parameters: vec![],
103                example: json!({
104                    "type": "disconnect"
105                }),
106                log_template: None,
107            },
108        ]
109    }
110    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
111        vec![
112            ActionDefinition {
113                name: "send_tcp_data".to_string(),
114                description:
115                    "Send TCP data in response to received data (UTF-8 string or hex-encoded)"
116                        .to_string(),
117                parameters: vec![
118                    Parameter {
119                        name: "data".to_string(),
120                        type_hint: "string".to_string(),
121                        description: "UTF-8 string data to send (preferred for text)".to_string(),
122                        required: false,
123                    },
124                    Parameter {
125                        name: "data_hex".to_string(),
126                        type_hint: "string".to_string(),
127                        description: "Hexadecimal encoded data to send (for binary data)"
128                            .to_string(),
129                        required: false,
130                    },
131                ],
132                example: json!({
133                    "type": "send_tcp_data",
134                    "data": "Hello World"
135                }),
136                log_template: None,
137            },
138            ActionDefinition {
139                name: "wait_for_more".to_string(),
140                description: "Wait for more data before responding".to_string(),
141                parameters: vec![],
142                example: json!({
143                    "type": "wait_for_more"
144                }),
145                log_template: None,
146            },
147        ]
148    }
149    fn protocol_name(&self) -> &'static str {
150        "TCP"
151    }
152    fn get_event_types(&self) -> Vec<EventType> {
153        vec![
154            EventType::new(
155                "tcp_connected",
156                "Triggered when TCP client connects to server",
157                json!({"type": "placeholder", "event_id": "tcp_connected"}),
158            ),
159            EventType::new(
160                "tcp_data_received",
161                "Triggered when TCP client receives data from server",
162                json!({"type": "placeholder", "event_id": "tcp_data_received"}),
163            ),
164        ]
165    }
166    fn stack_name(&self) -> &'static str {
167        "ETH>IP>TCP"
168    }
169    fn keywords(&self) -> Vec<&'static str> {
170        vec!["tcp", "tcp client", "connect to tcp"]
171    }
172    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
173        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
174
175        ProtocolMetadataV2::builder()
176            .state(DevelopmentState::Experimental)
177            .implementation("Tokio TcpStream for raw TCP connections")
178            .llm_control("Full control over sent/received bytes")
179            .e2e_testing("nc (netcat) as test server")
180            .build()
181    }
182    fn description(&self) -> &'static str {
183        "TCP client for connecting to TCP servers"
184    }
185    fn example_prompt(&self) -> &'static str {
186        "Connect to TCP at localhost:8080 and send 'HELLO'"
187    }
188    fn group_name(&self) -> &'static str {
189        "Core"
190    }
191    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
192        use crate::llm::actions::StartupExamples;
193        use serde_json::json;
194
195        StartupExamples::new(
196            // LLM mode: LLM controls TCP client interactions
197            json!({
198                "type": "open_client",
199                "remote_addr": "example.com:9000",
200                "base_stack": "tcp",
201                "instruction": "Connect to the TCP server and send a greeting, then echo any responses"
202            }),
203            // Script mode: Code-based deterministic responses
204            json!({
205                "type": "open_client",
206                "remote_addr": "example.com:9000",
207                "base_stack": "tcp",
208                "event_handlers": [{
209                    "event_pattern": "tcp_data_received",
210                    "handler": {
211                        "type": "script",
212                        "language": "python",
213                        "code": "<tcp_client_handler>"
214                    }
215                }]
216            }),
217            // Static mode: Fixed responses
218            json!({
219                "type": "open_client",
220                "remote_addr": "example.com:9000",
221                "base_stack": "tcp",
222                "event_handlers": [
223                    {
224                        "event_pattern": "tcp_connected",
225                        "handler": {
226                            "type": "static",
227                            "actions": [{
228                                "type": "send_tcp_data",
229                                "data": "HELLO\r\n"
230                            }]
231                        }
232                    },
233                    {
234                        "event_pattern": "tcp_data_received",
235                        "handler": {
236                            "type": "static",
237                            "actions": [{
238                                "type": "disconnect"
239                            }]
240                        }
241                    }
242                ]
243            }),
244        )
245    }
246}
247
248// Implement Client trait (client-specific functionality)
249impl Client for TcpClientProtocol {
250    fn connect(
251        &self,
252        ctx: crate::protocol::ConnectContext,
253    ) -> std::pin::Pin<
254        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
255    > {
256        Box::pin(async move {
257            use crate::client::tcp::TcpClient;
258            TcpClient::connect_with_llm_actions(
259                ctx.remote_addr,
260                ctx.llm_client,
261                ctx.state,
262                ctx.status_tx,
263                ctx.client_id,
264            )
265            .await
266        })
267    }
268    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
269        let action_type = action
270            .get("type")
271            .and_then(|v| v.as_str())
272            .context("Missing 'type' field in action")?;
273
274        match action_type {
275            "send_tcp_data" => {
276                // Support both UTF-8 string (data) and hex-encoded (data_hex)
277                // Prefer UTF-8 for easier LLM interaction
278                let data = if let Some(utf8_data) = action.get("data").and_then(|v| v.as_str()) {
279                    // UTF-8 string provided
280                    utf8_data.as_bytes().to_vec()
281                } else if let Some(hex_data) = action.get("data_hex").and_then(|v| v.as_str()) {
282                    // Hex string provided
283                    hex::decode(hex_data).context("Invalid hex data in data_hex field")?
284                } else {
285                    return Err(anyhow::anyhow!(
286                        "Missing 'data' or 'data_hex' field in send_tcp_data action"
287                    ));
288                };
289
290                Ok(ClientActionResult::SendData(data))
291            }
292            "disconnect" => Ok(ClientActionResult::Disconnect),
293            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
294            _ => Err(anyhow::anyhow!(
295                "Unknown TCP client action: {}",
296                action_type
297            )),
298        }
299    }
300}