netget/client/socket_file/
actions.rs1use 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
14pub static SOCKET_FILE_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
19 EventType::new(
20 "socket_file_connected",
21 "Socket File client successfully connected to Unix domain socket",
22 json!({
23 "type": "send_socket_file_data",
24 "data": "PING\n"
25 }),
26 )
27 .with_parameters(vec![Parameter {
28 name: "socket_path".to_string(),
29 type_hint: "string".to_string(),
30 description: "Unix domain socket path".to_string(),
31 required: true,
32 }])
33 .with_actions(vec![
34 send_socket_file_data_action("Send data to the Unix domain socket"),
35 disconnect_action(),
36 ])
37});
38
39pub static SOCKET_FILE_CLIENT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
46 EventType::new(
47 "socket_file_data_received",
48 "Data received from Unix domain socket",
49 json!({
50 "type": "send_socket_file_data",
51 "data": "PONG\n"
52 }),
53 )
54 .with_parameters(vec![
55 Parameter {
56 name: "data".to_string(),
57 type_hint: "string".to_string(),
58 description: "The bytes received from the socket. Read it according to 'encoding'."
59 .to_string(),
60 required: true,
61 },
62 Parameter {
63 name: "encoding".to_string(),
64 type_hint: "string".to_string(),
65 description:
66 "How to read 'data': \"utf8\" means literal text, \"hex\" means the bytes \
67 hex-encoded (used whenever they are not all printable ASCII). To echo the bytes \
68 back unchanged, pass the same 'data' and 'encoding' to send_socket_file_data."
69 .to_string(),
70 required: true,
71 },
72 Parameter {
73 name: "data_length".to_string(),
74 type_hint: "number".to_string(),
75 description: "Length of the received data in bytes".to_string(),
76 required: true,
77 },
78 ])
79 .with_actions(vec![
80 send_socket_file_data_action("Send data in response to the bytes just received"),
81 wait_for_more_action(),
82 disconnect_action(),
83 ])
84});
85
86fn decode_outbound_data(action: &serde_json::Value) -> Result<Vec<u8>> {
93 if let Some(data) = action.get("data").and_then(|v| v.as_str()) {
94 let encoding = action
95 .get("encoding")
96 .and_then(|v| v.as_str())
97 .unwrap_or("utf8");
98 return match encoding {
99 "utf8" => Ok(data.as_bytes().to_vec()),
100 "hex" => hex::decode(data).map_err(|e| {
101 anyhow::anyhow!(
102 "'data' was declared as \"encoding\": \"hex\" but is not valid hex ({e}). \
103 Hex payloads are two hex digits per byte with no separators. To send this \
104 value as literal text, omit 'encoding' or set it to \"utf8\"."
105 )
106 }),
107 other => Err(anyhow::anyhow!(
108 "Invalid 'encoding' value {other:?}. Valid values are \"utf8\" (default, send \
109 the characters of 'data' as-is) and \"hex\" (decode 'data' as hex-encoded \
110 bytes)."
111 )),
112 };
113 }
114 if let Some(hex_str) = action.get("data_hex").and_then(|v| v.as_str()) {
115 return hex::decode(hex_str).map_err(|e| {
116 anyhow::anyhow!(
117 "'data_hex' is not valid hex ({e}). Prefer 'data' with the optional 'encoding' \
118 field: text as {{\"data\": \"PING\"}}, bytes as \
119 {{\"data\": \"48656c6c6f\", \"encoding\": \"hex\"}}."
120 )
121 });
122 }
123 Err(anyhow::anyhow!(
124 "Missing 'data' field. Send text as {{\"data\": \"PING\"}} or bytes as \
125 {{\"data\": \"48656c6c6f\", \"encoding\": \"hex\"}}."
126 ))
127}
128
129fn encoding_parameter() -> Parameter {
131 Parameter {
132 name: "encoding".to_string(),
133 type_hint: "string".to_string(),
134 description: "How to convert 'data' into the bytes put on the socket. \"utf8\" (the \
135 default when omitted) sends the characters of 'data' unchanged - use it for text. \
136 \"hex\" decodes 'data' as hex-encoded bytes, two hex digits per byte - use it for \
137 binary, e.g. {\"data\": \"48656c6c6f\", \"encoding\": \"hex\"} sends the 5 bytes \
138 'Hello'. No other values are accepted"
139 .to_string(),
140 required: false,
141 }
142}
143
144fn send_socket_file_data_action(description: &str) -> ActionDefinition {
147 ActionDefinition {
148 name: "send_socket_file_data".to_string(),
149 description: format!(
150 "{description}. The 'data' field holds the payload and the optional 'encoding' field \
151 says how to turn it into bytes: omit 'encoding' (or use \"utf8\") to send the \
152 characters as-is, or set \"encoding\": \"hex\" to send 'data' decoded from hex. \
153 There is no auto-detection."
154 ),
155 parameters: vec![
156 Parameter {
157 name: "data".to_string(),
158 type_hint: "string".to_string(),
159 description: "Data to send. Interpreted according to 'encoding': by default the \
160 characters of this string are sent as-is (UTF-8)."
161 .to_string(),
162 required: true,
163 },
164 encoding_parameter(),
165 ],
166 example: json!({
167 "type": "send_socket_file_data",
168 "data": "PING\n",
169 "encoding": "utf8"
170 }),
171 log_template: Some(
172 crate::protocol::log_template::LogTemplate::new()
173 .with_info("-> SOCK {data_len}B")
174 .with_debug("SOCK send_socket_file_data: data_len={data_len}"),
175 ),
176 }
177}
178
179fn disconnect_action() -> ActionDefinition {
181 ActionDefinition {
182 name: "disconnect".to_string(),
183 description: "Close the Unix domain socket connection and stop reading from it."
184 .to_string(),
185 parameters: vec![],
186 example: json!({
187 "type": "disconnect"
188 }),
189 log_template: Some(
190 crate::protocol::log_template::LogTemplate::new()
191 .with_info("-> SOCK disconnect")
192 .with_debug("SOCK disconnect"),
193 ),
194 }
195}
196
197fn wait_for_more_action() -> ActionDefinition {
199 ActionDefinition {
200 name: "wait_for_more".to_string(),
201 description: "Wait for more data before responding (the bytes received so far are an \
202 incomplete message)."
203 .to_string(),
204 parameters: vec![],
205 example: json!({
206 "type": "wait_for_more"
207 }),
208 log_template: None,
209 }
210}
211
212pub struct SocketFileClientProtocol;
214
215impl SocketFileClientProtocol {
216 pub fn new() -> Self {
217 Self
218 }
219}
220
221impl Default for SocketFileClientProtocol {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl Protocol for SocketFileClientProtocol {
229 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
230 vec![
231 send_socket_file_data_action("Send data to the Unix domain socket"),
232 disconnect_action(),
233 ]
234 }
235 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
236 vec![
237 send_socket_file_data_action("Send data in response to the bytes just received"),
238 wait_for_more_action(),
239 disconnect_action(),
240 ]
241 }
242 fn protocol_name(&self) -> &'static str {
243 "SocketFile"
244 }
245 fn get_event_types(&self) -> Vec<EventType> {
251 vec![
252 SOCKET_FILE_CLIENT_CONNECTED_EVENT.clone(),
253 SOCKET_FILE_CLIENT_DATA_RECEIVED_EVENT.clone(),
254 ]
255 }
256 fn stack_name(&self) -> &'static str {
257 "UnixSocket"
258 }
259 fn keywords(&self) -> Vec<&'static str> {
260 vec![
261 "socket file",
262 "unix socket",
263 "domain socket",
264 "socket-file",
265 "socketfile",
266 ]
267 }
268 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
269 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
270
271 ProtocolMetadataV2::builder()
272 .state(DevelopmentState::Experimental)
273 .implementation("Tokio UnixStream for Unix domain socket connections")
274 .llm_control("Full control over sent/received bytes")
275 .e2e_testing("A real tokio UnixListener peer in-process; no third-party client exists")
276 .notes(
277 "Unix domain socket client. The peer is a local descriptor, so there is no \
278 independent third-party implementation to validate against and no route past \
279 Experimental on the usual evidence. The tests drive a real UnixListener peer \
280 in-process: connect, connect-time send, the peer's reply, the client's answer.",
281 )
282 .build()
283 }
284 fn description(&self) -> &'static str {
285 "Socket File client for connecting to Unix domain sockets"
286 }
287 fn example_prompt(&self) -> &'static str {
288 "Connect to socket file at ./app.sock and send 'HELLO'"
289 }
290 fn group_name(&self) -> &'static str {
291 "Core"
292 }
293
294 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
295 use crate::llm::actions::StartupExamples;
296 use serde_json::json;
297
298 StartupExamples::new(
299 json!({
301 "type": "open_client",
302 "remote_addr": "./app.sock",
303 "base_stack": "socket-file",
304 "instruction": "Connect to Unix socket and send 'PING', echo responses"
305 }),
306 json!({
308 "type": "open_client",
309 "remote_addr": "./app.sock",
310 "base_stack": "socket-file",
311 "event_handlers": [{
312 "event_pattern": "socket_file_data_received",
313 "handler": {
314 "type": "script",
315 "language": "python",
316 "code": "<socket_file_handler>"
317 }
318 }]
319 }),
320 json!({
322 "type": "open_client",
323 "remote_addr": "./app.sock",
324 "base_stack": "socket-file",
325 "event_handlers": [{
326 "event_pattern": "socket_file_connected",
327 "handler": {
328 "type": "static",
329 "actions": [{
330 "type": "send_socket_file_data",
331 "data": "HELLO\n"
332 }]
333 }
334 }]
335 }),
336 )
337 }
338}
339
340impl Client for SocketFileClientProtocol {
342 fn connect(
343 &self,
344 ctx: crate::protocol::ConnectContext,
345 ) -> std::pin::Pin<
346 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
347 > {
348 Box::pin(async move {
349 use crate::client::socket_file::SocketFileClient;
350 SocketFileClient::connect_with_llm_actions(
351 ctx.remote_addr,
352 ctx.llm_client,
353 ctx.state,
354 ctx.status_tx,
355 ctx.client_id,
356 )
357 .await
358 })
359 }
360 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
361 let action_type = action
362 .get("type")
363 .and_then(|v| v.as_str())
364 .context("Missing 'type' field in action")?;
365
366 match action_type {
367 "send_socket_file_data" => {
368 Ok(ClientActionResult::SendData(decode_outbound_data(&action)?))
369 }
370 "disconnect" => Ok(ClientActionResult::Disconnect),
371 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
372 _ => Err(anyhow::anyhow!(
373 "Unknown Socket File client action: {}",
374 action_type
375 )),
376 }
377 }
378}