1use 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
14fn 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
63pub static RIP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
65 EventType::new(
66 "rip_connected",
67 "RIP client connected to router",
68 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
81pub 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
123pub 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
138impl 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 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 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 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 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
241impl 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 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}