Skip to main content

netget/client/mdns/
actions.rs

1//! mDNS 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::log_template::LogTemplate;
9use crate::protocol::EventType;
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::LazyLock;
14
15/// mDNS client connected event
16pub static MDNS_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17    EventType::new(
18        "mdns_connected",
19        "mDNS client initialized and ready for service discovery",
20        json!({"type": "browse_service", "service_type": "_http._tcp.local"}),
21    )
22    .with_parameters(vec![
23        Parameter {
24            name: "status".to_string(),
25            type_hint: "string".to_string(),
26            description: "Connection status".to_string(),
27            required: true,
28        },
29        Parameter {
30            name: "message".to_string(),
31            type_hint: "string".to_string(),
32            description: "Status message".to_string(),
33            required: false,
34        },
35    ])
36});
37
38/// mDNS service found event
39pub static MDNS_CLIENT_SERVICE_FOUND_EVENT: LazyLock<EventType> = LazyLock::new(|| {
40    EventType::new(
41        "mdns_service_found",
42        "mDNS service instance discovered on the local network",
43        json!({"type": "wait_for_more"}),
44    )
45    .with_parameters(vec![
46        Parameter {
47            name: "service_type".to_string(),
48            type_hint: "string".to_string(),
49            description: "Service type (e.g., '_http._tcp.local')".to_string(),
50            required: true,
51        },
52        Parameter {
53            name: "fullname".to_string(),
54            type_hint: "string".to_string(),
55            description: "Full service instance name".to_string(),
56            required: true,
57        },
58    ])
59});
60
61/// mDNS service resolved event
62pub static MDNS_CLIENT_SERVICE_RESOLVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
63    EventType::new(
64        "mdns_service_resolved",
65        "mDNS service fully resolved with IP addresses and port",
66        json!({"type": "wait_for_more"}),
67    )
68    .with_parameters(vec![
69        Parameter {
70            name: "fullname".to_string(),
71            type_hint: "string".to_string(),
72            description: "Full service instance name".to_string(),
73            required: true,
74        },
75        Parameter {
76            name: "hostname".to_string(),
77            type_hint: "string".to_string(),
78            description: "Service hostname".to_string(),
79            required: true,
80        },
81        Parameter {
82            name: "addresses".to_string(),
83            type_hint: "array".to_string(),
84            description: "List of IP addresses".to_string(),
85            required: true,
86        },
87        Parameter {
88            name: "port".to_string(),
89            type_hint: "number".to_string(),
90            description: "Service port number".to_string(),
91            required: true,
92        },
93        Parameter {
94            name: "properties".to_string(),
95            type_hint: "array".to_string(),
96            description: "Service TXT record properties".to_string(),
97            required: false,
98        },
99    ])
100});
101
102/// mDNS client protocol action handler
103pub struct MdnsClientProtocol;
104
105impl MdnsClientProtocol {
106    pub fn new() -> Self {
107        Self
108    }
109}
110
111// Implement Protocol trait (common functionality)
112impl Protocol for MdnsClientProtocol {
113    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
114        vec![
115                ActionDefinition {
116                    name: "browse_service".to_string(),
117                    description: "Browse for mDNS services of a specific type on the local network".to_string(),
118                    parameters: vec![
119                        Parameter {
120                            name: "service_type".to_string(),
121                            type_hint: "string".to_string(),
122                            description: "Service type to browse for (e.g., '_http._tcp.local', '_ssh._tcp.local', '_printer._tcp.local')".to_string(),
123                            required: true,
124                        },
125                    ],
126                    example: json!({
127                        "type": "browse_service",
128                        "service_type": "_http._tcp.local"
129                    }),
130                log_template: None,
131                },
132                ActionDefinition {
133                    name: "resolve_hostname".to_string(),
134                    description: "Resolve a .local hostname to IP addresses using mDNS".to_string(),
135                    parameters: vec![
136                        Parameter {
137                            name: "hostname".to_string(),
138                            type_hint: "string".to_string(),
139                            description: "Hostname to resolve (e.g., 'myserver.local')".to_string(),
140                            required: true,
141                        },
142                    ],
143                    example: json!({
144                        "type": "resolve_hostname",
145                        "hostname": "myserver.local"
146                    }),
147                log_template: None,
148                },
149                ActionDefinition {
150                    name: "disconnect".to_string(),
151                    description: "Stop mDNS service discovery and disconnect".to_string(),
152                    parameters: vec![],
153                    example: json!({
154                        "type": "disconnect"
155                    }),
156                log_template: None,
157                },
158            ]
159    }
160    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
161        vec![ActionDefinition {
162            name: "wait_for_more".to_string(),
163            description: "Wait for more service discovery events before responding".to_string(),
164            parameters: vec![],
165            example: json!({
166                "type": "wait_for_more"
167            }),
168            log_template: Some(
169                LogTemplate::new()
170                    .with_info("-> mDNS wait for more")
171                    .with_debug("mDNS wait_for_more"),
172            ),
173        }]
174    }
175    fn protocol_name(&self) -> &'static str {
176        "mDNS"
177    }
178    fn get_event_types(&self) -> Vec<EventType> {
179        vec![
180            EventType::new(
181                "mdns_connected",
182                "Triggered when mDNS client is initialized",
183                json!({"type": "wait_for_more"}),
184            ),
185            EventType::new(
186                "mdns_service_found",
187                "Triggered when an mDNS service is discovered",
188                json!({"type": "wait_for_more"}),
189            ),
190            EventType::new(
191                "mdns_service_resolved",
192                "Triggered when an mDNS service is fully resolved with IP and port",
193                json!({"type": "wait_for_more"}),
194            ),
195        ]
196    }
197    fn stack_name(&self) -> &'static str {
198        "ETH>IP>UDP>mDNS"
199    }
200    fn keywords(&self) -> Vec<&'static str> {
201        vec![
202            "mdns",
203            "multicast dns",
204            "service discovery",
205            "zeroconf",
206            "bonjour",
207            ".local",
208        ]
209    }
210    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
211        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
212
213        ProtocolMetadataV2::builder()
214            .state(DevelopmentState::Experimental)
215            .implementation("mdns-sd crate for RFC 6762 and RFC 6763 compliance")
216            .llm_control("Browse services, resolve hostnames, analyze service properties")
217            .e2e_testing("Built-in macOS/Linux mDNS responders for testing")
218            .build()
219    }
220    fn description(&self) -> &'static str {
221        "mDNS client for discovering services on the local network"
222    }
223    fn example_prompt(&self) -> &'static str {
224        "Browse for HTTP services on the local network using mDNS"
225    }
226    fn group_name(&self) -> &'static str {
227        "DNS"
228    }
229
230    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
231        use crate::llm::actions::StartupExamples;
232        use serde_json::json;
233
234        StartupExamples::new(
235            // LLM mode: LLM controls mDNS service discovery
236            json!({
237                "type": "open_client",
238                "remote_addr": "0.0.0.0:5353",
239                "base_stack": "mdns",
240                "instruction": "Browse for HTTP services on the local network and list all discovered web servers"
241            }),
242            // Script mode: Code-based deterministic responses
243            json!({
244                "type": "open_client",
245                "remote_addr": "0.0.0.0:5353",
246                "base_stack": "mdns",
247                "event_handlers": [{
248                    "event_pattern": "mdns_service_resolved",
249                    "handler": {
250                        "type": "script",
251                        "language": "python",
252                        "code": "<mdns_client_handler>"
253                    }
254                }]
255            }),
256            // Static mode: Fixed mDNS browse on connect
257            json!({
258                "type": "open_client",
259                "remote_addr": "0.0.0.0:5353",
260                "base_stack": "mdns",
261                "event_handlers": [
262                    {
263                        "event_pattern": "mdns_connected",
264                        "handler": {
265                            "type": "static",
266                            "actions": [{
267                                "type": "browse_service",
268                                "service_type": "_http._tcp.local"
269                            }]
270                        }
271                    },
272                    {
273                        "event_pattern": "mdns_service_resolved",
274                        "handler": {
275                            "type": "static",
276                            "actions": [{
277                                "type": "wait_for_more"
278                            }]
279                        }
280                    }
281                ]
282            }),
283        )
284    }
285}
286
287// Implement Client trait (client-specific functionality)
288impl Client for MdnsClientProtocol {
289    fn connect(
290        &self,
291        ctx: crate::protocol::ConnectContext,
292    ) -> std::pin::Pin<
293        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
294    > {
295        Box::pin(async move {
296            use crate::client::mdns::MdnsClient;
297            MdnsClient::connect_with_llm_actions(
298                ctx.remote_addr,
299                ctx.llm_client,
300                ctx.state,
301                ctx.status_tx,
302                ctx.client_id,
303            )
304            .await
305        })
306    }
307    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
308        let action_type = action
309            .get("type")
310            .and_then(|v| v.as_str())
311            .context("Missing 'type' field in action")?;
312
313        match action_type {
314            "browse_service" => {
315                let service_type = action
316                    .get("service_type")
317                    .and_then(|v| v.as_str())
318                    .context("Missing 'service_type' field")?;
319
320                Ok(ClientActionResult::Custom {
321                    name: "browse_service".to_string(),
322                    data: json!({
323                        "service_type": service_type
324                    }),
325                })
326            }
327            "resolve_hostname" => {
328                let hostname = action
329                    .get("hostname")
330                    .and_then(|v| v.as_str())
331                    .context("Missing 'hostname' field")?;
332
333                Ok(ClientActionResult::Custom {
334                    name: "resolve_hostname".to_string(),
335                    data: json!({
336                        "hostname": hostname
337                    }),
338                })
339            }
340            "disconnect" => Ok(ClientActionResult::Disconnect),
341            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
342            _ => Err(anyhow::anyhow!(
343                "Unknown mDNS client action: {}",
344                action_type
345            )),
346        }
347    }
348}