Skip to main content

netget/client/rip/
actions.rs

1//! RIP 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/// One place each action is defined, so the async list, the sync list and the event
15/// vocabularies below cannot drift apart.
16fn send_rip_request_action() -> ActionDefinition {
17    ActionDefinition {
18        name: "send_rip_request".to_string(),
19        description: "Send a RIP Request asking the router for its whole routing table".to_string(),
20        parameters: vec![Parameter {
21            name: "version".to_string(),
22            type_hint: "number".to_string(),
23            description: "RIP version: 1 (RFC 1058) or 2 (RFC 2453). No other value is accepted."
24                .to_string(),
25            required: true,
26        }],
27        example: json!({
28            "type": "send_rip_request",
29            "version": 2
30        }),
31        log_template: None,
32    }
33}
34
35fn disconnect_action() -> ActionDefinition {
36    ActionDefinition {
37        name: "disconnect".to_string(),
38        description: "Stop listening and release the socket. RIP is UDP and has no wire close, \
39                      so this ends the session locally."
40            .to_string(),
41        parameters: vec![],
42        example: json!({
43            "type": "disconnect"
44        }),
45        log_template: None,
46    }
47}
48
49fn wait_for_more_action() -> ActionDefinition {
50    ActionDefinition {
51        name: "wait_for_more".to_string(),
52        description: "The routing table was split across several datagrams; wait for the rest \
53                      before deciding."
54            .to_string(),
55        parameters: vec![],
56        example: json!({
57            "type": "wait_for_more"
58        }),
59        log_template: None,
60    }
61}
62
63/// RIP client connected event
64pub static RIP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
65    EventType::new(
66        "rip_connected",
67        "RIP client connected to router",
68        // The example is what the model copies, so it has to be an action the executor
69        // accepts. It used to be `{"type": "placeholder"}`, which `execute_action` rejects.
70        json!({"type": "send_rip_request", "version": 2}),
71    )
72    .with_parameters(vec![Parameter {
73        name: "remote_addr".to_string(),
74        type_hint: "string".to_string(),
75        description: "RIP router address".to_string(),
76        required: true,
77    }])
78    .with_actions(vec![send_rip_request_action(), disconnect_action()])
79});
80
81/// RIP client response received event
82pub static RIP_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
83    EventType::new(
84        "rip_response_received",
85        "RIP response received from router",
86        json!({"type": "disconnect"}),
87    )
88    .with_parameters(vec![
89        Parameter {
90            name: "version".to_string(),
91            type_hint: "number".to_string(),
92            description: "RIP version (1 or 2)".to_string(),
93            required: true,
94        },
95        Parameter {
96            name: "command".to_string(),
97            type_hint: "string".to_string(),
98            description: "RIP command (request or response)".to_string(),
99            required: true,
100        },
101        Parameter {
102            name: "route_count".to_string(),
103            type_hint: "number".to_string(),
104            description: "Number of routes in response".to_string(),
105            required: true,
106        },
107        Parameter {
108            name: "routes".to_string(),
109            type_hint: "array".to_string(),
110            description:
111                "Array of route entries with ip_address, subnet_mask, next_hop, and metric"
112                    .to_string(),
113            required: true,
114        },
115    ])
116    .with_actions(vec![
117        send_rip_request_action(),
118        wait_for_more_action(),
119        disconnect_action(),
120    ])
121});
122
123/// RIP client protocol action handler
124pub struct RipClientProtocol;
125
126impl Default for RipClientProtocol {
127    fn default() -> Self {
128        Self
129    }
130}
131
132impl RipClientProtocol {
133    pub fn new() -> Self {
134        Self
135    }
136}
137
138// Implement Protocol trait (common functionality)
139impl Protocol for RipClientProtocol {
140    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
141        vec![send_rip_request_action(), disconnect_action()]
142    }
143    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
144        vec![send_rip_request_action(), wait_for_more_action()]
145    }
146    fn protocol_name(&self) -> &'static str {
147        "RIP"
148    }
149    // The events the client actually raises, not re-declared copies of them. These used to be
150    // freshly-built `EventType`s with the same two ids but no parameters, so everything reading
151    // the declared surface — the model's event documentation, the registry audits — saw a
152    // stripped version of what `mod.rs` really emits.
153    fn get_event_types(&self) -> Vec<EventType> {
154        vec![
155            RIP_CLIENT_CONNECTED_EVENT.clone(),
156            RIP_CLIENT_RESPONSE_RECEIVED_EVENT.clone(),
157        ]
158    }
159    fn stack_name(&self) -> &'static str {
160        "ETH>IP>UDP>RIP"
161    }
162    fn keywords(&self) -> Vec<&'static str> {
163        vec!["rip", "rip client", "routing information protocol"]
164    }
165    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
166        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
167
168        ProtocolMetadataV2::builder()
169            .state(DevelopmentState::Experimental)
170            .implementation("UDP socket with RIPv1/v2 packet parsing")
171            .llm_control("Query routing tables, analyze routes")
172            .e2e_testing("Mock RIP router or real router in test network")
173            .build()
174    }
175    fn description(&self) -> &'static str {
176        "RIP client for querying routing tables from RIP routers"
177    }
178    fn example_prompt(&self) -> &'static str {
179        "Connect to RIP router at 192.168.1.1:520 and query routing table"
180    }
181    fn group_name(&self) -> &'static str {
182        "Routing"
183    }
184    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
185        use crate::llm::actions::StartupExamples;
186        use serde_json::json;
187
188        StartupExamples::new(
189            // LLM mode: LLM controls RIP routing table queries
190            json!({
191                "type": "open_client",
192                "remote_addr": "192.168.1.1:520",
193                "base_stack": "rip",
194                "instruction": "Query routing table using RIPv2 and analyze routes"
195            }),
196            // Script mode: Code-based RIP response handling
197            json!({
198                "type": "open_client",
199                "remote_addr": "192.168.1.1:520",
200                "base_stack": "rip",
201                "event_handlers": [{
202                    "event_pattern": "rip_response_received",
203                    "handler": {
204                        "type": "script",
205                        "language": "python",
206                        "code": "<rip_client_handler>"
207                    }
208                }]
209            }),
210            // Static mode: Fixed RIP routing table request
211            json!({
212                "type": "open_client",
213                "remote_addr": "192.168.1.1:520",
214                "base_stack": "rip",
215                "event_handlers": [
216                    {
217                        "event_pattern": "rip_connected",
218                        "handler": {
219                            "type": "static",
220                            "actions": [{
221                                "type": "send_rip_request",
222                                "version": 2
223                            }]
224                        }
225                    },
226                    {
227                        "event_pattern": "rip_response_received",
228                        "handler": {
229                            "type": "static",
230                            "actions": [{
231                                "type": "disconnect"
232                            }]
233                        }
234                    }
235                ]
236            }),
237        )
238    }
239}
240
241// Implement Client trait (client-specific functionality)
242impl Client for RipClientProtocol {
243    fn connect(
244        &self,
245        ctx: crate::protocol::ConnectContext,
246    ) -> std::pin::Pin<
247        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
248    > {
249        Box::pin(async move {
250            use crate::client::rip::RipClient;
251            RipClient::connect_with_llm_actions(
252                ctx.remote_addr,
253                ctx.llm_client,
254                ctx.state,
255                ctx.status_tx,
256                ctx.client_id,
257            )
258            .await
259        })
260    }
261    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
262        let action_type = action
263            .get("type")
264            .and_then(|v| v.as_str())
265            .context("Missing 'type' field in action")?;
266
267        match action_type {
268            "send_rip_request" => {
269                let version = action
270                    .get("version")
271                    .and_then(|v| v.as_u64())
272                    .context("Missing 'version' field")?;
273                // RFC 1058 defines version 1 and RFC 2453 version 2; there is no third. Refuse
274                // anything else here rather than in the transport, so the operator and the model
275                // both get a `Rejected` naming the problem instead of a silently-downgraded
276                // datagram.
277                if version != 1 && version != 2 {
278                    return Err(anyhow::anyhow!(
279                        "Invalid RIP 'version' {}: must be 1 (RFC 1058) or 2 (RFC 2453)",
280                        version
281                    ));
282                }
283
284                Ok(ClientActionResult::Custom {
285                    name: "send_rip_request".to_string(),
286                    data: json!({
287                        "version": version
288                    }),
289                })
290            }
291            "disconnect" => Ok(ClientActionResult::Disconnect),
292            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
293            _ => Err(anyhow::anyhow!(
294                "Unknown RIP client action: {}",
295                action_type
296            )),
297        }
298    }
299}