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 NFS_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "nfs_connected",
18 "NFS client successfully mounted an NFS export. This is your one turn before the \
19 client goes idle: issue an operation, or wait_for_more to do nothing.",
20 json!({"type": "nfs_list_dir", "path": "/"}),
21 )
22 .with_parameters(vec![
23 Parameter {
24 name: "export_path".to_string(),
25 type_hint: "string".to_string(),
26 description: "NFS export path that was mounted".to_string(),
27 required: true,
28 },
29 Parameter {
30 name: "root_fh".to_string(),
31 type_hint: "string".to_string(),
32 description: "Root file handle (hex-encoded)".to_string(),
33 required: true,
34 },
35 ])
36});
37
38pub static NFS_CLIENT_OPERATION_RESULT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
40 EventType::new(
41 "nfs_operation_result",
42 "Result of an NFS file operation. `success` is false when the server refused or the \
43 path does not exist; `error` then carries the NFS status. Follow up with another \
44 operation, or wait_for_more if there is nothing more to do.",
45 json!({"type": "nfs_list_dir", "path": "/"}),
51 )
52 .with_parameters(vec![
53 Parameter {
54 name: "operation".to_string(),
55 type_hint: "string".to_string(),
56 description: "The operation performed (lookup, read, write, etc.)".to_string(),
57 required: true,
58 },
59 Parameter {
60 name: "result".to_string(),
61 type_hint: "object".to_string(),
62 description: "Operation result data".to_string(),
63 required: true,
64 },
65 ])
66});
67
68pub struct NfsClientProtocol;
70
71impl NfsClientProtocol {
72 pub fn new() -> Self {
73 Self
74 }
75}
76
77impl Protocol for NfsClientProtocol {
78 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
79 vec![
80 ActionDefinition {
81 name: "nfs_lookup".to_string(),
82 description: "Look up a file or directory by path".to_string(),
83 parameters: vec![Parameter {
84 name: "path".to_string(),
85 type_hint: "string".to_string(),
86 description: "Path to file or directory (relative to root)".to_string(),
87 required: true,
88 }],
89 example: json!({
90 "type": "nfs_lookup",
91 "path": "/documents/readme.txt"
92 }),
93 log_template: None,
94 },
95 ActionDefinition {
96 name: "nfs_read_file".to_string(),
97 description: "Read contents of a file".to_string(),
98 parameters: vec![
99 Parameter {
100 name: "path".to_string(),
101 type_hint: "string".to_string(),
102 description: "Path to file to read".to_string(),
103 required: true,
104 },
105 Parameter {
106 name: "offset".to_string(),
107 type_hint: "number".to_string(),
108 description: "Byte offset to start reading from (default: 0)".to_string(),
109 required: false,
110 },
111 Parameter {
112 name: "count".to_string(),
113 type_hint: "number".to_string(),
114 description: "Number of bytes to read (default: 4096)".to_string(),
115 required: false,
116 },
117 ],
118 example: json!({
119 "type": "nfs_read_file",
120 "path": "/readme.txt",
121 "offset": 0,
122 "count": 4096
123 }),
124 log_template: None,
125 },
126 ActionDefinition {
127 name: "nfs_write_file".to_string(),
128 description: "Write data to a file".to_string(),
129 parameters: vec![
130 Parameter {
131 name: "path".to_string(),
132 type_hint: "string".to_string(),
133 description: "Path to file to write".to_string(),
134 required: true,
135 },
136 Parameter {
137 name: "data".to_string(),
138 type_hint: "string".to_string(),
139 description: "Data to write".to_string(),
140 required: true,
141 },
142 Parameter {
143 name: "offset".to_string(),
144 type_hint: "number".to_string(),
145 description: "Byte offset to start writing at (default: 0)".to_string(),
146 required: false,
147 },
148 ],
149 example: json!({
150 "type": "nfs_write_file",
151 "path": "/data.txt",
152 "data": "Hello, World!",
153 "offset": 0
154 }),
155 log_template: None,
156 },
157 ActionDefinition {
158 name: "nfs_list_dir".to_string(),
159 description: "List contents of a directory".to_string(),
160 parameters: vec![Parameter {
161 name: "path".to_string(),
162 type_hint: "string".to_string(),
163 description: "Path to directory to list (default: /)".to_string(),
164 required: false,
165 }],
166 example: json!({
167 "type": "nfs_list_dir",
168 "path": "/documents"
169 }),
170 log_template: None,
171 },
172 ActionDefinition {
173 name: "nfs_get_attr".to_string(),
174 description: "Get file or directory attributes".to_string(),
175 parameters: vec![Parameter {
176 name: "path".to_string(),
177 type_hint: "string".to_string(),
178 description: "Path to file or directory".to_string(),
179 required: true,
180 }],
181 example: json!({
182 "type": "nfs_get_attr",
183 "path": "/readme.txt"
184 }),
185 log_template: None,
186 },
187 ActionDefinition {
188 name: "nfs_create_file".to_string(),
189 description: "Create a new file".to_string(),
190 parameters: vec![
191 Parameter {
192 name: "path".to_string(),
193 type_hint: "string".to_string(),
194 description: "Path to new file".to_string(),
195 required: true,
196 },
197 Parameter {
198 name: "mode".to_string(),
199 type_hint: "number".to_string(),
200 description: "File permissions in octal (default: 0644)".to_string(),
201 required: false,
202 },
203 ],
204 example: json!({
205 "type": "nfs_create_file",
206 "path": "/newfile.txt",
207 "mode": 0o644
208 }),
209 log_template: None,
210 },
211 ActionDefinition {
212 name: "nfs_mkdir".to_string(),
213 description: "Create a new directory".to_string(),
214 parameters: vec![
215 Parameter {
216 name: "path".to_string(),
217 type_hint: "string".to_string(),
218 description: "Path to new directory".to_string(),
219 required: true,
220 },
221 Parameter {
222 name: "mode".to_string(),
223 type_hint: "number".to_string(),
224 description: "Directory permissions in octal (default: 0755)".to_string(),
225 required: false,
226 },
227 ],
228 example: json!({
229 "type": "nfs_mkdir",
230 "path": "/newdir",
231 "mode": 0o755
232 }),
233 log_template: None,
234 },
235 ActionDefinition {
236 name: "nfs_remove".to_string(),
237 description: "Remove a file from the mounted export by path. Unlinks a regular \
238 file only — a directory needs the server's own rmdir and is \
239 refused here — and the removal is immediate and not reversible."
240 .to_string(),
241 parameters: vec![Parameter {
242 name: "path".to_string(),
243 type_hint: "string".to_string(),
244 description: "Path to file to remove".to_string(),
245 required: true,
246 }],
247 example: json!({
248 "type": "nfs_remove",
249 "path": "/oldfile.txt"
250 }),
251 log_template: None,
252 },
253 ActionDefinition {
254 name: "nfs_rmdir".to_string(),
255 description: "Remove a directory".to_string(),
256 parameters: vec![Parameter {
257 name: "path".to_string(),
258 type_hint: "string".to_string(),
259 description: "Path to directory to remove".to_string(),
260 required: true,
261 }],
262 example: json!({
263 "type": "nfs_rmdir",
264 "path": "/olddir"
265 }),
266 log_template: None,
267 },
268 ActionDefinition {
269 name: "disconnect".to_string(),
270 description: "Disconnect from the NFS server".to_string(),
271 parameters: vec![],
272 example: json!({
273 "type": "disconnect"
274 }),
275 log_template: None,
276 },
277 ]
278 }
279
280 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
281 vec![ActionDefinition {
282 name: "wait_for_more".to_string(),
283 description: "Wait for more operations before responding".to_string(),
284 parameters: vec![],
285 example: json!({
286 "type": "wait_for_more"
287 }),
288 log_template: None,
289 }]
290 }
291
292 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
293 use crate::llm::actions::ParameterDefinition;
294 vec![
295 ParameterDefinition {
296 name: "portmapper_port".to_string(),
297 description: "Port to ask for the MOUNT and NFS ports on (default 111). \
298 Override it when the server multiplexes all three RPC programs \
299 on one port, as NetGet's own NFS server does"
300 .to_string(),
301 type_hint: "number".to_string(),
302 required: false,
303 example: json!(111),
304 default: None,
305 },
306 ParameterDefinition {
307 name: "mount_port".to_string(),
308 description: "MOUNT program port. Skips the portmapper lookup for MOUNT"
309 .to_string(),
310 type_hint: "number".to_string(),
311 required: false,
312 example: json!(2049),
313 default: None,
314 },
315 ParameterDefinition {
316 name: "nfs_port".to_string(),
317 description: "NFS program port (normally 2049). Skips the portmapper lookup \
318 for NFS"
319 .to_string(),
320 type_hint: "number".to_string(),
321 required: false,
322 example: json!(2049),
323 default: None,
324 },
325 ParameterDefinition {
326 name: "privileged_source_port".to_string(),
327 description: "Bind the local end to a port below 1024, which many real NFS \
328 servers demand and which requires root. Default true"
329 .to_string(),
330 type_hint: "boolean".to_string(),
331 required: false,
332 example: json!(false),
333 default: None,
334 },
335 ]
336 }
337
338 fn protocol_name(&self) -> &'static str {
339 "NFS"
340 }
341
342 fn get_event_types(&self) -> Vec<EventType> {
350 vec![
351 NFS_CLIENT_CONNECTED_EVENT.clone(),
352 NFS_CLIENT_OPERATION_RESULT_EVENT.clone(),
353 ]
354 }
355
356 fn stack_name(&self) -> &'static str {
357 "ETH>IP>TCP>RPC>NFS"
358 }
359
360 fn keywords(&self) -> Vec<&'static str> {
361 vec!["nfs", "nfs client", "connect to nfs"]
362 }
363
364 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
365 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
366
367 ProtocolMetadataV2::builder()
368 .state(DevelopmentState::Experimental)
369 .implementation("nfs3_client crate for NFSv3 protocol")
370 .llm_control("Full control over file operations (read, write, create, delete)")
371 .e2e_testing("NetGet NFS server as test target")
372 .build()
373 }
374
375 fn description(&self) -> &'static str {
376 "NFS client for mounting and accessing network file systems"
377 }
378
379 fn example_prompt(&self) -> &'static str {
380 "Connect to NFS server at 192.168.1.100:/export/data and read /readme.txt"
381 }
382
383 fn group_name(&self) -> &'static str {
384 "File Sharing"
385 }
386 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
387 use crate::llm::actions::StartupExamples;
388 use serde_json::json;
389
390 StartupExamples::new(
391 json!({
393 "type": "open_client",
394 "remote_addr": "192.168.1.100:/export/data",
395 "base_stack": "nfs",
396 "instruction": "Read /readme.txt and list the root directory"
397 }),
398 json!({
400 "type": "open_client",
401 "remote_addr": "192.168.1.100:/export/data",
402 "base_stack": "nfs",
403 "event_handlers": [{
404 "event_pattern": "nfs_operation_result",
405 "handler": {
406 "type": "script",
407 "language": "python",
408 "code": "<nfs_client_handler>"
409 }
410 }]
411 }),
412 json!({
414 "type": "open_client",
415 "remote_addr": "192.168.1.100:/export/data",
416 "base_stack": "nfs",
417 "event_handlers": [
418 {
419 "event_pattern": "nfs_connected",
420 "handler": {
421 "type": "static",
422 "actions": [{
423 "type": "nfs_list_dir",
424 "path": "/"
425 }]
426 }
427 },
428 {
429 "event_pattern": "nfs_operation_result",
430 "handler": {
431 "type": "static",
432 "actions": [{
433 "type": "disconnect"
434 }]
435 }
436 }
437 ]
438 }),
439 )
440 }
441}
442
443impl Client for NfsClientProtocol {
444 fn connect(
445 &self,
446 ctx: crate::protocol::ConnectContext,
447 ) -> std::pin::Pin<
448 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
449 > {
450 Box::pin(async move {
451 use crate::client::nfs::NfsClient;
452 NfsClient::connect_with_llm_actions(
453 ctx.remote_addr,
454 ctx.llm_client,
455 ctx.state,
456 ctx.status_tx,
457 ctx.client_id,
458 ctx.startup_params,
459 )
460 .await
461 })
462 }
463
464 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
465 let action_type = action
466 .get("type")
467 .and_then(|v| v.as_str())
468 .context("Missing 'type' field in action")?;
469
470 match action_type {
471 "nfs_lookup" | "nfs_read_file" | "nfs_write_file" | "nfs_list_dir" | "nfs_get_attr"
472 | "nfs_create_file" | "nfs_mkdir" | "nfs_remove" | "nfs_rmdir" => {
473 Ok(ClientActionResult::Custom {
475 name: action_type.to_string(),
476 data: action,
477 })
478 }
479 "disconnect" => Ok(ClientActionResult::Disconnect),
480 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
481 _ => Err(anyhow::anyhow!(
482 "Unknown NFS client action: {}",
483 action_type
484 )),
485 }
486 }
487}