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
14pub static BOOTP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "bootp_connected",
18 "BOOTP client ready to send requests",
19 json!({"type": "placeholder", "event_id": "bootp_connected"}),
20 )
21 .with_parameters(vec![Parameter {
22 name: "server_addr".to_string(),
23 type_hint: "string".to_string(),
24 description: "BOOTP/DHCP server address".to_string(),
25 required: true,
26 }])
27});
28
29pub static BOOTP_REPLY_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31 EventType::new(
32 "bootp_reply_received",
33 "BOOTP reply received from server",
34 json!({"type": "placeholder", "event_id": "bootp_reply_received"}),
35 )
36 .with_parameters(vec![
37 Parameter {
38 name: "assigned_ip".to_string(),
39 type_hint: "string".to_string(),
40 description: "IP address assigned by BOOTP server (yiaddr)".to_string(),
41 required: true,
42 },
43 Parameter {
44 name: "server_ip".to_string(),
45 type_hint: "string".to_string(),
46 description: "BOOTP server IP address (siaddr)".to_string(),
47 required: true,
48 },
49 Parameter {
50 name: "boot_filename".to_string(),
51 type_hint: "string".to_string(),
52 description: "Boot file name (e.g., 'pxelinux.0')".to_string(),
53 required: false,
54 },
55 Parameter {
56 name: "gateway_ip".to_string(),
57 type_hint: "string".to_string(),
58 description: "Gateway IP address (giaddr)".to_string(),
59 required: false,
60 },
61 ])
62});
63
64#[derive(Default)]
66pub struct BootpClientProtocol;
67
68impl BootpClientProtocol {
69 pub fn new() -> Self {
70 Self
71 }
72}
73
74impl Protocol for BootpClientProtocol {
76 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
77 vec![
78 ActionDefinition {
79 name: "send_bootp_request".to_string(),
80 description: "Send a BOOTP request to discover boot server and IP address"
81 .to_string(),
82 parameters: vec![
83 Parameter {
84 name: "client_mac".to_string(),
85 type_hint: "string".to_string(),
86 description: "Client MAC address (format: 00:11:22:33:44:55)".to_string(),
87 required: true,
88 },
89 Parameter {
90 name: "broadcast".to_string(),
91 type_hint: "boolean".to_string(),
92 description: "Use broadcast (true) or unicast (false)".to_string(),
93 required: false,
94 },
95 ],
96 example: json!({
97 "type": "send_bootp_request",
98 "client_mac": "00:11:22:33:44:55",
99 "broadcast": true
100 }),
101 log_template: None,
102 },
103 ActionDefinition {
104 name: "disconnect".to_string(),
105 description: "Close the BOOTP client".to_string(),
106 parameters: vec![],
107 example: json!({
108 "type": "disconnect"
109 }),
110 log_template: None,
111 },
112 ]
113 }
114 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
115 vec![
116 ActionDefinition {
117 name: "send_bootp_request".to_string(),
118 description: "Send another BOOTP request in response to a reply".to_string(),
119 parameters: vec![
120 Parameter {
121 name: "client_mac".to_string(),
122 type_hint: "string".to_string(),
123 description: "Client MAC address (format: 00:11:22:33:44:55)".to_string(),
124 required: true,
125 },
126 Parameter {
127 name: "broadcast".to_string(),
128 type_hint: "boolean".to_string(),
129 description: "Use broadcast (true) or unicast (false)".to_string(),
130 required: false,
131 },
132 ],
133 example: json!({
134 "type": "send_bootp_request",
135 "client_mac": "00:11:22:33:44:55",
136 "broadcast": true
137 }),
138 log_template: None,
139 },
140 ActionDefinition {
141 name: "wait_for_more".to_string(),
142 description: "Wait for more BOOTP replies before responding".to_string(),
143 parameters: vec![],
144 example: json!({
145 "type": "wait_for_more"
146 }),
147 log_template: None,
148 },
149 ]
150 }
151 fn protocol_name(&self) -> &'static str {
152 "BOOTP"
153 }
154 fn get_event_types(&self) -> Vec<EventType> {
155 vec![
156 EventType::new(
157 "bootp_connected",
158 "Triggered when BOOTP client is ready",
159 json!({"type": "placeholder", "event_id": "bootp_connected"}),
160 ),
161 EventType::new(
162 "bootp_reply_received",
163 "Triggered when BOOTP reply is received from server",
164 json!({"type": "placeholder", "event_id": "bootp_reply_received"}),
165 ),
166 ]
167 }
168 fn stack_name(&self) -> &'static str {
169 "ETH>IP>UDP>BOOTP"
170 }
171 fn keywords(&self) -> Vec<&'static str> {
172 vec!["bootp", "bootp client", "bootstrap protocol"]
173 }
174 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
175 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
176
177 ProtocolMetadataV2::builder()
178 .state(DevelopmentState::Experimental)
179 .implementation("dhcproto crate for BOOTP packet encoding/decoding")
180 .llm_control("Full control over BOOTP requests and reply interpretation")
181 .e2e_testing("dnsmasq or isc-dhcp-server for BOOTP testing")
182 .build()
183 }
184 fn description(&self) -> &'static str {
185 "BOOTP client for diskless workstation boot discovery (PXE boot, TFTP server location)"
186 }
187 fn example_prompt(&self) -> &'static str {
188 "Connect to BOOTP server at 192.168.1.1:67 and request boot information for MAC 00:11:22:33:44:55"
189 }
190 fn group_name(&self) -> &'static str {
191 "Network Services"
192 }
193
194 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
195 use crate::llm::actions::StartupExamples;
196 use serde_json::json;
197
198 StartupExamples::new(
199 json!({
201 "type": "open_client",
202 "remote_addr": "192.168.1.1:67",
203 "base_stack": "bootp",
204 "instruction": "Request boot information for MAC 00:11:22:33:44:55 and report the boot server and filename"
205 }),
206 json!({
208 "type": "open_client",
209 "remote_addr": "192.168.1.1:67",
210 "base_stack": "bootp",
211 "event_handlers": [{
212 "event_pattern": "bootp_reply_received",
213 "handler": {
214 "type": "script",
215 "language": "python",
216 "code": "<bootp_client_handler>"
217 }
218 }]
219 }),
220 json!({
222 "type": "open_client",
223 "remote_addr": "192.168.1.1:67",
224 "base_stack": "bootp",
225 "event_handlers": [
226 {
227 "event_pattern": "bootp_connected",
228 "handler": {
229 "type": "static",
230 "actions": [{
231 "type": "send_bootp_request",
232 "client_mac": "00:11:22:33:44:55",
233 "broadcast": true
234 }]
235 }
236 },
237 {
238 "event_pattern": "bootp_reply_received",
239 "handler": {
240 "type": "static",
241 "actions": [{
242 "type": "disconnect"
243 }]
244 }
245 }
246 ]
247 }),
248 )
249 }
250}
251
252impl Client for BootpClientProtocol {
254 fn connect(
255 &self,
256 ctx: crate::protocol::ConnectContext,
257 ) -> std::pin::Pin<
258 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
259 > {
260 Box::pin(async move {
261 use crate::client::bootp::BootpClient;
262 BootpClient::connect_with_llm_actions(
263 ctx.remote_addr,
264 ctx.llm_client,
265 ctx.state,
266 ctx.status_tx,
267 ctx.client_id,
268 )
269 .await
270 })
271 }
272 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
273 let action_type = action
274 .get("type")
275 .and_then(|v| v.as_str())
276 .context("Missing 'type' field in action")?;
277
278 match action_type {
279 "send_bootp_request" => {
280 let client_mac = action
281 .get("client_mac")
282 .and_then(|v| v.as_str())
283 .context("Missing 'client_mac' field")?;
284
285 let broadcast = action
286 .get("broadcast")
287 .and_then(|v| v.as_bool())
288 .unwrap_or(true);
289
290 Ok(ClientActionResult::Custom {
291 name: "send_bootp_request".to_string(),
292 data: json!({
293 "client_mac": client_mac,
294 "broadcast": broadcast,
295 }),
296 })
297 }
298 "disconnect" => Ok(ClientActionResult::Disconnect),
299 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
300 _ => Err(anyhow::anyhow!(
301 "Unknown BOOTP client action: {}",
302 action_type
303 )),
304 }
305 }
306}