1pub mod actions;
26pub mod guard;
27
28use anyhow::{Context, Result};
29use std::net::SocketAddr;
30use std::sync::Arc;
31use tokio::sync::mpsc;
32use tracing::{debug, error, info, warn};
33
34#[cfg(unix)]
35use async_trait::async_trait;
36#[cfg(unix)]
37use nfsserve::nfs::{fattr3, fileid3, filename3, ftype3, nfspath3, nfsstat3, nfstime3, sattr3};
38#[cfg(unix)]
39use nfsserve::vfs::{DirEntry, NFSFileSystem, ReadDirResult};
40
41use crate::llm::action_helper::call_llm;
42use crate::llm::ollama_client::OllamaClient;
43use crate::logging::emit::Log;
44use crate::protocol::Event;
45use crate::server::NfsProtocol;
46use crate::state::app_state::AppState;
47use actions::NFS_OPERATION_EVENT;
48
49pub struct NfsServer;
51
52impl NfsServer {
53 #[cfg(unix)]
55 pub async fn spawn_with_llm_actions(
56 listen_addr: SocketAddr,
57 llm_client: OllamaClient,
58 app_state: Arc<AppState>,
59 status_tx: mpsc::UnboundedSender<String>,
60 server_id: crate::state::ServerId,
61 ) -> Result<SocketAddr> {
62 use nfsserve::tcp::{NFSTcp, NFSTcpListener};
63
64 Log::new(Some(&status_tx)).info(format!(
65 "NFS server (LLM-controlled) starting on {}",
66 listen_addr
67 ));
68
69 let protocol = Arc::new(NfsProtocol::new());
70
71 let filesystem = LlmNfsFileSystem::new(
73 llm_client,
74 app_state.clone(),
75 server_id,
76 protocol,
77 status_tx.clone(),
78 );
79
80 let public = tokio::net::TcpListener::bind(listen_addr)
85 .await
86 .with_context(|| format!("Failed to bind NFS listener on {}", listen_addr))?;
87 let actual_addr = public
88 .local_addr()
89 .context("Failed to read the NFS listener's address")?;
90
91 let nfs_listener = NFSTcpListener::bind("127.0.0.1:0", filesystem)
92 .await
93 .context("Failed to bind the internal NFS backend listener")?;
94 let backend_addr: SocketAddr =
95 SocketAddr::from(([127, 0, 0, 1], nfs_listener.get_listen_port()));
96
97 Log::new(Some(&status_tx)).info(format!(
98 "NFS server listening on {} (record screen in front of nfsserve on {})",
99 actual_addr, backend_addr
100 ));
101
102 let backend_status_tx = status_tx.clone();
104 let backend_handle = tokio::spawn(async move {
105 info!("NFS server handler started");
106
107 if let Err(e) = nfs_listener.handle_forever().await {
109 Log::new(Some(&backend_status_tx)).error(format!("NFS server error: {}", e));
110 }
111 });
112
113 app_state
115 .register_server_task(server_id, backend_handle)
116 .await;
117
118 let screen_state = app_state.clone();
119 let screen_handle = tokio::spawn(async move {
120 guard::serve_screened(public, backend_addr, screen_state, server_id, status_tx).await;
121 });
122 app_state
123 .register_server_task(server_id, screen_handle)
124 .await;
125
126 Ok(actual_addr)
127 }
128
129 #[cfg(not(unix))]
131 pub async fn spawn_with_llm_actions(
132 listen_addr: SocketAddr,
133 _llm_client: OllamaClient,
134 _app_state: Arc<AppState>,
135 status_tx: mpsc::UnboundedSender<String>,
136 _server_id: crate::state::ServerId,
137 ) -> Result<SocketAddr> {
138 Log::new(Some(&status_tx)).error("NFS feature not enabled at compile time");
139 Err(anyhow::anyhow!("NFS feature not enabled"))
140 }
141}
142
143#[cfg(unix)]
145pub struct LlmNfsFileSystem {
146 llm_client: OllamaClient,
147 app_state: Arc<AppState>,
148 server_id: crate::state::ServerId,
149 protocol: Arc<NfsProtocol>,
150 status_tx: mpsc::UnboundedSender<String>,
151}
152
153#[cfg(unix)]
154impl LlmNfsFileSystem {
155 pub fn new(
157 llm_client: OllamaClient,
158 app_state: Arc<AppState>,
159 server_id: crate::state::ServerId,
160 protocol: Arc<NfsProtocol>,
161 status_tx: mpsc::UnboundedSender<String>,
162 ) -> Self {
163 Self {
164 llm_client,
165 app_state,
166 server_id,
167 protocol,
168 status_tx,
169 }
170 }
171
172 async fn consult_llm(
174 &self,
175 operation: &str,
176 params: serde_json::Value,
177 ) -> Result<Vec<serde_json::Value>> {
178 Log::new(Some(&self.status_tx)).debug(format!(
179 "Consulting LLM for NFS {} operation: {:?}",
180 operation, params
181 ));
182
183 let event = Event::new(
185 &NFS_OPERATION_EVENT,
186 serde_json::json!({
187 "operation": operation,
188 "params": params
189 }),
190 );
191
192 Log::new(Some(&self.status_tx))
193 .trace(format!("Calling LLM for NFS {} operation", operation));
194
195 let execution_result = call_llm(
197 &self.llm_client,
198 &self.app_state,
199 self.server_id,
200 None, &event,
202 self.protocol.as_ref(),
203 )
204 .await?;
205
206 let log = Log::new(Some(&self.status_tx));
208 for message in &execution_result.messages {
209 log.info(message);
210 }
211
212 debug!(
213 "LLM returned {} actions for NFS {}",
214 execution_result.raw_actions.len(),
215 operation
216 );
217
218 Ok(execution_result.raw_actions)
220 }
221
222 fn llm_failure(&self, operation: &str, e: &anyhow::Error) -> nfsstat3 {
239 let log = Log::new(Some(&self.status_tx));
240 if crate::llm::is_overload_error(e) {
241 log.error(format!(
242 "NFS {}: LLM overloaded ({}) - answered NFS3ERR_JUKEBOX, client may retry",
243 operation, e
244 ));
245 nfsstat3::NFS3ERR_JUKEBOX
246 } else {
247 log.error(format!(
248 "NFS {}: LLM call failed ({}) - answered NFS3ERR_SERVERFAULT",
249 operation, e
250 ));
251 nfsstat3::NFS3ERR_SERVERFAULT
252 }
253 }
254
255 fn llm_no_answer(&self, operation: &str, expected: &str) -> nfsstat3 {
262 Log::new(Some(&self.status_tx)).error(format!(
263 "NFS {}: no {} action in LLM response - answered NFS3ERR_SERVERFAULT",
264 operation, expected
265 ));
266 nfsstat3::NFS3ERR_SERVERFAULT
267 }
268
269 fn parse_ftype(&self, file_type: &str) -> ftype3 {
271 match file_type {
272 "regular" | "file" => ftype3::NF3REG,
273 "directory" | "dir" => ftype3::NF3DIR,
274 "symlink" | "link" => ftype3::NF3LNK,
275 "block" => ftype3::NF3BLK,
276 "char" => ftype3::NF3CHR,
277 "socket" => ftype3::NF3SOCK,
278 "fifo" => ftype3::NF3FIFO,
279 _ => ftype3::NF3REG, }
281 }
282
283 fn parse_nfstime(&self, timestamp: Option<u64>) -> nfstime3 {
285 let ts = timestamp.unwrap_or_else(|| {
286 crate::utils::clock::SystemTime::now()
290 .duration_since(crate::utils::clock::UNIX_EPOCH)
291 .unwrap_or_default()
292 .as_secs()
293 });
294 nfstime3 {
295 seconds: ts as u32,
296 nseconds: 0,
297 }
298 }
299
300 fn build_fattr3(&self, response: &serde_json::Value) -> Result<fattr3> {
302 let file_type = response
303 .get("file_type")
304 .and_then(|v| v.as_str())
305 .unwrap_or("regular");
306
307 let mode = response
308 .get("mode")
309 .and_then(|v| v.as_u64())
310 .unwrap_or(0o644) as u32;
311
312 let size = response.get("size").and_then(|v| v.as_u64()).unwrap_or(0);
313
314 let uid = response.get("uid").and_then(|v| v.as_u64()).unwrap_or(1000) as u32;
315
316 let gid = response.get("gid").and_then(|v| v.as_u64()).unwrap_or(1000) as u32;
317
318 let atime = self.parse_nfstime(response.get("atime").and_then(|v| v.as_u64()));
319 let mtime = self.parse_nfstime(response.get("mtime").and_then(|v| v.as_u64()));
320 let ctime = self.parse_nfstime(response.get("ctime").and_then(|v| v.as_u64()));
321
322 Ok(fattr3 {
323 ftype: self.parse_ftype(file_type),
324 mode,
325 nlink: 1,
326 uid,
327 gid,
328 size,
329 used: size,
330 rdev: nfsserve::nfs::specdata3 {
331 specdata1: 0,
332 specdata2: 0,
333 },
334 fsid: 0,
335 fileid: response.get("fileid").and_then(|v| v.as_u64()).unwrap_or(0),
336 atime,
337 mtime,
338 ctime,
339 })
340 }
341}
342
343#[cfg(unix)]
344#[async_trait]
345impl NFSFileSystem for LlmNfsFileSystem {
346 fn root_dir(&self) -> fileid3 {
347 1
349 }
350
351 fn capabilities(&self) -> nfsserve::vfs::VFSCapabilities {
352 nfsserve::vfs::VFSCapabilities::ReadWrite
354 }
355
356 async fn path_to_id(&self, path: &[u8]) -> Result<fileid3, nfsstat3> {
371 let components: Vec<&[u8]> = path
372 .split(|&b| b == b'/')
373 .filter(|c| !c.is_empty())
374 .collect();
375
376 if components.len() > guard::MAX_PATH_COMPONENTS {
377 Log::new(Some(&self.status_tx)).error(format!(
378 "NFS path_to_id: refused a {}-component path decision=fail_closed_path_components \
379 (limit {}) - each component is one LLM call",
380 components.len(),
381 guard::MAX_PATH_COMPONENTS
382 ));
383 return Err(nfsstat3::NFS3ERR_NAMETOOLONG);
384 }
385
386 let mut fid = self.root_dir();
387 for component in components {
388 fid = self.lookup(fid, &component.into()).await?;
389 }
390 Ok(fid)
391 }
392
393 async fn lookup(&self, dirid: fileid3, filename: &filename3) -> Result<fileid3, nfsstat3> {
394 let filename_str = String::from_utf8_lossy(filename).to_string();
396
397 let params = serde_json::json!({
398 "dirid": dirid,
399 "filename": filename_str,
400 });
401
402 let result = self.consult_llm("lookup", params).await;
404
405 match result {
406 Ok(actions) => {
407 for action in actions {
409 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_lookup_response") {
410 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
411 debug!("NFS lookup failed: {}", error);
412 return Err(nfsstat3::NFS3ERR_NOENT);
413 }
414
415 if let Some(fileid) = action.get("fileid").and_then(|v| v.as_u64()) {
416 debug!("NFS lookup found fileid: {}", fileid);
417 return Ok(fileid);
418 }
419 }
420 }
421 Err(self.llm_no_answer("lookup", "nfs_lookup_response"))
422 }
423 Err(e) => Err(self.llm_failure("lookup", &e)),
424 }
425 }
426
427 async fn getattr(&self, id: fileid3) -> Result<fattr3, nfsstat3> {
428 let params = serde_json::json!({
429 "fileid": id,
430 });
431
432 let result = self.consult_llm("getattr", params).await;
433
434 match result {
435 Ok(actions) => {
436 for action in actions {
437 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_getattr_response") {
438 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
439 debug!("NFS getattr failed: {}", error);
440 return Err(nfsstat3::NFS3ERR_NOENT);
441 }
442
443 match self.build_fattr3(&action) {
444 Ok(mut attrs) => {
445 attrs.fileid = id; debug!("NFS getattr succeeded for fileid {}", id);
447 return Ok(attrs);
448 }
449 Err(e) => {
450 error!("Failed to build fattr3: {}", e);
451 return Err(nfsstat3::NFS3ERR_IO);
452 }
453 }
454 }
455 }
456 Err(self.llm_no_answer("getattr", "nfs_getattr_response"))
457 }
458 Err(e) => Err(self.llm_failure("getattr", &e)),
459 }
460 }
461
462 async fn setattr(&self, id: fileid3, setattr: sattr3) -> Result<fattr3, nfsstat3> {
463 use nfsserve::nfs::{set_gid3, set_mode3, set_size3, set_uid3};
464
465 let mode_val = match setattr.mode {
467 set_mode3::mode(v) => Some(v as u64),
468 set_mode3::Void => None,
469 };
470 let uid_val = match setattr.uid {
471 set_uid3::uid(v) => Some(v as u64),
472 set_uid3::Void => None,
473 };
474 let gid_val = match setattr.gid {
475 set_gid3::gid(v) => Some(v as u64),
476 set_gid3::Void => None,
477 };
478 let size_val = match setattr.size {
479 set_size3::size(v) => Some(v),
480 set_size3::Void => None,
481 };
482
483 let params = serde_json::json!({
484 "fileid": id,
485 "mode": mode_val,
486 "uid": uid_val,
487 "gid": gid_val,
488 "size": size_val,
489 });
490
491 let result = self.consult_llm("setattr", params).await;
492
493 match result {
494 Ok(actions) => {
495 for action in actions {
496 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_setattr_response") {
497 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
498 debug!("NFS setattr failed: {}", error);
499 return Err(nfsstat3::NFS3ERR_ACCES);
500 }
501
502 match self.build_fattr3(&action) {
503 Ok(mut attrs) => {
504 attrs.fileid = id;
505 debug!("NFS setattr succeeded for fileid {}", id);
506 return Ok(attrs);
507 }
508 Err(e) => {
509 error!("Failed to build fattr3: {}", e);
510 return Err(nfsstat3::NFS3ERR_IO);
511 }
512 }
513 }
514 }
515 Err(self.llm_no_answer("setattr", "nfs_setattr_response"))
516 }
517 Err(e) => Err(self.llm_failure("setattr", &e)),
518 }
519 }
520
521 async fn read(
522 &self,
523 id: fileid3,
524 offset: u64,
525 count: u32,
526 ) -> Result<(Vec<u8>, bool), nfsstat3> {
527 let params = serde_json::json!({
528 "fileid": id,
529 "offset": offset,
530 "count": count,
531 });
532
533 let result = self.consult_llm("read", params).await;
534
535 match result {
536 Ok(actions) => {
537 for action in actions {
538 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_read_response") {
539 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
540 debug!("NFS read failed: {}", error);
541 return Err(nfsstat3::NFS3ERR_ACCES);
542 }
543
544 let Some(data) = action.get("data").and_then(|v| v.as_str()) else {
552 warn!(
553 "NFS read: nfs_read_response for fileid {} omitted the required \
554 `data` field (decision=fail_closed_no_data); refusing rather \
555 than reporting a successful read of an empty file",
556 id
557 );
558 return Err(nfsstat3::NFS3ERR_SERVERFAULT);
559 };
560 let data = data.as_bytes().to_vec();
561
562 let eof = action.get("eof").and_then(|v| v.as_bool()).unwrap_or(true);
563
564 debug!(
565 "NFS read {} bytes from fileid {}, eof={}",
566 data.len(),
567 id,
568 eof
569 );
570 return Ok((data, eof));
571 }
572 }
573 Err(self.llm_no_answer("read", "nfs_read_response"))
574 }
575 Err(e) => Err(self.llm_failure("read", &e)),
576 }
577 }
578
579 async fn write(&self, id: fileid3, offset: u64, data: &[u8]) -> Result<fattr3, nfsstat3> {
580 let data_str = String::from_utf8_lossy(data).to_string();
583 let params = serde_json::json!({
584 "fileid": id,
585 "offset": offset,
586 "data": data_str,
587 "size": data.len(),
588 });
589
590 let result = self.consult_llm("write", params).await;
591
592 match result {
593 Ok(actions) => {
594 for action in actions {
595 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_write_response") {
596 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
597 debug!("NFS write failed: {}", error);
598 return Err(nfsstat3::NFS3ERR_ACCES);
599 }
600
601 match self.build_fattr3(&action) {
602 Ok(mut attrs) => {
603 attrs.fileid = id;
604 debug!("NFS write {} bytes to fileid {}", data.len(), id);
605 return Ok(attrs);
606 }
607 Err(e) => {
608 error!("Failed to build fattr3: {}", e);
609 return Err(nfsstat3::NFS3ERR_IO);
610 }
611 }
612 }
613 }
614 Err(self.llm_no_answer("write", "nfs_write_response"))
615 }
616 Err(e) => Err(self.llm_failure("write", &e)),
617 }
618 }
619
620 async fn create(
621 &self,
622 dirid: fileid3,
623 filename: &filename3,
624 attr: sattr3,
625 ) -> Result<(fileid3, fattr3), nfsstat3> {
626 use nfsserve::nfs::{set_gid3, set_mode3, set_uid3};
627
628 let filename_str = String::from_utf8_lossy(filename).to_string();
629 let mode_val = match attr.mode {
630 set_mode3::mode(v) => Some(v as u64),
631 set_mode3::Void => None,
632 };
633 let uid_val = match attr.uid {
634 set_uid3::uid(v) => Some(v as u64),
635 set_uid3::Void => None,
636 };
637 let gid_val = match attr.gid {
638 set_gid3::gid(v) => Some(v as u64),
639 set_gid3::Void => None,
640 };
641
642 let params = serde_json::json!({
643 "dirid": dirid,
644 "filename": filename_str,
645 "mode": mode_val,
646 "uid": uid_val,
647 "gid": gid_val,
648 });
649
650 let result = self.consult_llm("create", params).await;
651
652 match result {
653 Ok(actions) => {
654 for action in actions {
655 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_create_response") {
656 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
657 debug!("NFS create failed: {}", error);
658 return Err(nfsstat3::NFS3ERR_ACCES);
659 }
660
661 let Some(fileid) = action.get("fileid").and_then(|v| v.as_u64()) else {
665 warn!(
666 "NFS create: nfs_create_response for {} omitted the required \
667 `fileid` (decision=fail_closed_no_fileid); refusing rather \
668 than reporting success with the invalid fileid 0",
669 filename_str
670 );
671 return Err(nfsstat3::NFS3ERR_SERVERFAULT);
672 };
673
674 match self.build_fattr3(&action) {
675 Ok(mut attrs) => {
676 attrs.fileid = fileid;
677 debug!(
678 "NFS create succeeded: {} with fileid {}",
679 filename_str, fileid
680 );
681 return Ok((fileid, attrs));
682 }
683 Err(e) => {
684 error!("Failed to build fattr3: {}", e);
685 return Err(nfsstat3::NFS3ERR_IO);
686 }
687 }
688 }
689 }
690 Err(self.llm_no_answer("create", "nfs_create_response"))
691 }
692 Err(e) => Err(self.llm_failure("create", &e)),
693 }
694 }
695
696 async fn create_exclusive(
697 &self,
698 dirid: fileid3,
699 filename: &filename3,
700 ) -> Result<fileid3, nfsstat3> {
701 let filename_str = String::from_utf8_lossy(filename).to_string();
703 let params = serde_json::json!({
704 "dirid": dirid,
705 "filename": filename_str,
706 "exclusive": true,
707 });
708
709 let result = self.consult_llm("create", params).await;
710
711 match result {
712 Ok(actions) => {
713 for action in actions {
714 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_create_response") {
715 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
716 debug!("NFS create_exclusive failed: {}", error);
717 return Err(nfsstat3::NFS3ERR_EXIST);
718 }
719
720 if let Some(fileid) = action.get("fileid").and_then(|v| v.as_u64()) {
721 debug!(
722 "NFS create_exclusive succeeded: {} with fileid {}",
723 filename_str, fileid
724 );
725 return Ok(fileid);
726 }
727 }
728 }
729 Err(self.llm_no_answer("create", "nfs_create_response"))
730 }
731 Err(e) => Err(self.llm_failure("create_exclusive", &e)),
732 }
733 }
734
735 async fn mkdir(
736 &self,
737 dirid: fileid3,
738 dirname: &filename3,
739 ) -> Result<(fileid3, fattr3), nfsstat3> {
740 let dirname_str = String::from_utf8_lossy(dirname).to_string();
741 let params = serde_json::json!({
742 "dirid": dirid,
743 "dirname": dirname_str,
744 });
745
746 let result = self.consult_llm("mkdir", params).await;
747
748 match result {
749 Ok(actions) => {
750 for action in actions {
751 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_mkdir_response") {
752 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
753 debug!("NFS mkdir failed: {}", error);
754 return Err(nfsstat3::NFS3ERR_ACCES);
755 }
756
757 let fileid = action
763 .get("fileid")
764 .or_else(|| action.get("dirid"))
765 .and_then(|v| v.as_u64())
766 .unwrap_or(0);
767
768 match self.build_fattr3(&action) {
769 Ok(mut attrs) => {
770 attrs.fileid = fileid;
771 attrs.ftype = ftype3::NF3DIR; debug!(
773 "NFS mkdir succeeded: {} with dirid {}",
774 dirname_str, fileid
775 );
776 return Ok((fileid, attrs));
777 }
778 Err(e) => {
779 error!("Failed to build fattr3: {}", e);
780 return Err(nfsstat3::NFS3ERR_IO);
781 }
782 }
783 }
784 }
785 Err(self.llm_no_answer("mkdir", "nfs_mkdir_response"))
786 }
787 Err(e) => Err(self.llm_failure("mkdir", &e)),
788 }
789 }
790
791 async fn remove(&self, dirid: fileid3, filename: &filename3) -> Result<(), nfsstat3> {
792 let filename_str = String::from_utf8_lossy(filename).to_string();
793 let params = serde_json::json!({
794 "dirid": dirid,
795 "filename": filename_str,
796 });
797
798 let result = self.consult_llm("remove", params).await;
799
800 match result {
801 Ok(actions) => {
802 for action in actions {
803 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_remove_response") {
804 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
805 debug!("NFS remove failed: {}", error);
806 return Err(nfsstat3::NFS3ERR_NOENT);
807 }
808
809 match action.get("success").and_then(|v| v.as_bool()) {
816 Some(true) => {
817 debug!("NFS remove succeeded: {}", filename_str);
818 return Ok(());
819 }
820 Some(false) => {
821 debug!(
822 "NFS remove refused by handler for {} \
823 (decision=model_reject)",
824 filename_str
825 );
826 return Err(nfsstat3::NFS3ERR_ACCES);
827 }
828 None => {
829 warn!(
830 "NFS remove: nfs_remove_response for {} omitted the required \
831 `success` boolean (decision=fail_closed_no_success); \
832 refusing rather than assuming the file was removed",
833 filename_str
834 );
835 return Err(nfsstat3::NFS3ERR_SERVERFAULT);
836 }
837 }
838 }
839 }
840 Err(self.llm_no_answer("remove", "nfs_remove_response"))
841 }
842 Err(e) => Err(self.llm_failure("remove", &e)),
843 }
844 }
845
846 async fn rename(
847 &self,
848 from_dirid: fileid3,
849 from_filename: &filename3,
850 to_dirid: fileid3,
851 to_filename: &filename3,
852 ) -> Result<(), nfsstat3> {
853 let from_name = String::from_utf8_lossy(from_filename).to_string();
854 let to_name = String::from_utf8_lossy(to_filename).to_string();
855 let params = serde_json::json!({
856 "from_dirid": from_dirid,
857 "from_filename": from_name,
858 "to_dirid": to_dirid,
859 "to_filename": to_name,
860 });
861
862 let result = self.consult_llm("rename", params).await;
863
864 match result {
865 Ok(actions) => {
866 for action in actions {
867 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_rename_response") {
868 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
869 debug!("NFS rename failed: {}", error);
870 return Err(nfsstat3::NFS3ERR_ACCES);
871 }
872
873 match action.get("success").and_then(|v| v.as_bool()) {
877 Some(true) => {
878 debug!("NFS rename succeeded: {} -> {}", from_name, to_name);
879 return Ok(());
880 }
881 Some(false) => {
882 debug!(
883 "NFS rename refused by handler for {} -> {} \
884 (decision=model_reject)",
885 from_name, to_name
886 );
887 return Err(nfsstat3::NFS3ERR_ACCES);
888 }
889 None => {
890 warn!(
891 "NFS rename: nfs_rename_response for {} -> {} omitted the \
892 required `success` boolean \
893 (decision=fail_closed_no_success); refusing rather than \
894 assuming the rename happened",
895 from_name, to_name
896 );
897 return Err(nfsstat3::NFS3ERR_SERVERFAULT);
898 }
899 }
900 }
901 }
902 Err(self.llm_no_answer("rename", "nfs_rename_response"))
903 }
904 Err(e) => Err(self.llm_failure("rename", &e)),
905 }
906 }
907
908 async fn readdir(
909 &self,
910 dirid: fileid3,
911 start_after: fileid3,
912 max_entries: usize,
913 ) -> Result<ReadDirResult, nfsstat3> {
914 let params = serde_json::json!({
915 "dirid": dirid,
916 "start_after": start_after,
917 "max_entries": max_entries,
918 });
919
920 let result = self.consult_llm("readdir", params).await;
921
922 match result {
923 Ok(actions) => {
924 for action in actions {
925 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_readdir_response") {
926 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
927 debug!("NFS readdir failed: {}", error);
928 return Err(nfsstat3::NFS3ERR_NOTDIR);
929 }
930
931 let Some(entries_json) =
937 action.get("entries").and_then(|v| v.as_array()).cloned()
938 else {
939 warn!(
940 "NFS readdir: nfs_readdir_response omitted the required \
941 `entries` array (decision=fail_closed_no_entries); refusing \
942 rather than reporting the directory as empty"
943 );
944 return Err(nfsstat3::NFS3ERR_SERVERFAULT);
945 };
946
947 let mut entries = Vec::new();
948 for entry in entries_json {
949 let fileid = entry.get("fileid").and_then(|v| v.as_u64()).unwrap_or(0);
950
951 let name = entry
952 .get("name")
953 .and_then(|v| v.as_str())
954 .unwrap_or("")
955 .as_bytes()
956 .to_vec();
957
958 let mut attr =
960 match entry.get("attr").and_then(|v| self.build_fattr3(v).ok()) {
961 Some(a) => a,
962 None => {
963 fattr3 {
965 ftype: ftype3::NF3REG,
966 mode: 0o644,
967 nlink: 1,
968 uid: 1000,
969 gid: 1000,
970 size: 0,
971 used: 0,
972 rdev: nfsserve::nfs::specdata3 {
973 specdata1: 0,
974 specdata2: 0,
975 },
976 fsid: 0,
977 fileid,
978 atime: self.parse_nfstime(None),
979 mtime: self.parse_nfstime(None),
980 ctime: self.parse_nfstime(None),
981 }
982 }
983 };
984 attr.fileid = fileid; entries.push(DirEntry {
987 fileid,
988 name: nfsserve::nfs::nfsstring(name),
989 attr,
990 });
991 }
992
993 let end = action
997 .get("eof")
998 .or_else(|| action.get("end"))
999 .and_then(|v| v.as_bool())
1000 .unwrap_or(true);
1001
1002 debug!(
1003 "NFS readdir returned {} entries, end={}",
1004 entries.len(),
1005 end
1006 );
1007 return Ok(ReadDirResult { entries, end });
1008 }
1009 }
1010 Err(self.llm_no_answer("readdir", "nfs_readdir_response"))
1011 }
1012 Err(e) => Err(self.llm_failure("readdir", &e)),
1013 }
1014 }
1015
1016 async fn symlink(
1017 &self,
1018 dirid: fileid3,
1019 linkname: &filename3,
1020 symlink: &nfspath3,
1021 attr: &sattr3,
1022 ) -> Result<(fileid3, fattr3), nfsstat3> {
1023 use nfsserve::nfs::set_mode3;
1024
1025 let linkname_str = String::from_utf8_lossy(linkname).to_string();
1026 let target_str = String::from_utf8_lossy(symlink).to_string();
1027 let mode_val = match attr.mode {
1028 set_mode3::mode(v) => Some(v as u64),
1029 set_mode3::Void => None,
1030 };
1031
1032 let params = serde_json::json!({
1033 "dirid": dirid,
1034 "linkname": linkname_str,
1035 "target": target_str,
1036 "mode": mode_val,
1037 });
1038
1039 let result = self.consult_llm("symlink", params).await;
1040
1041 match result {
1042 Ok(actions) => {
1043 for action in actions {
1044 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_create_response") {
1045 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
1046 debug!("NFS symlink failed: {}", error);
1047 return Err(nfsstat3::NFS3ERR_ACCES);
1048 }
1049
1050 let fileid = action.get("fileid").and_then(|v| v.as_u64()).unwrap_or(0);
1051
1052 match self.build_fattr3(&action) {
1053 Ok(mut attrs) => {
1054 attrs.fileid = fileid;
1055 attrs.ftype = ftype3::NF3LNK; debug!("NFS symlink succeeded: {} -> {}", linkname_str, target_str);
1057 return Ok((fileid, attrs));
1058 }
1059 Err(e) => {
1060 error!("Failed to build fattr3: {}", e);
1061 return Err(nfsstat3::NFS3ERR_IO);
1062 }
1063 }
1064 }
1065 }
1066 Err(self.llm_no_answer("symlink", "nfs_create_response"))
1067 }
1068 Err(e) => Err(self.llm_failure("symlink", &e)),
1069 }
1070 }
1071
1072 async fn readlink(&self, id: fileid3) -> Result<nfspath3, nfsstat3> {
1073 let params = serde_json::json!({
1074 "fileid": id,
1075 });
1076
1077 let result = self.consult_llm("readlink", params).await;
1078
1079 match result {
1080 Ok(actions) => {
1081 for action in actions {
1082 if action.get("type").and_then(|v| v.as_str()) == Some("nfs_read_response") {
1084 if let Some(error) = action.get("error").and_then(|v| v.as_str()) {
1085 debug!("NFS readlink failed: {}", error);
1086 return Err(nfsstat3::NFS3ERR_INVAL);
1087 }
1088
1089 let target_bytes = action
1090 .get("data")
1091 .and_then(|v| v.as_str())
1092 .unwrap_or("")
1093 .as_bytes()
1094 .to_vec();
1095
1096 debug!(
1097 "NFS readlink for fileid {}: {}",
1098 id,
1099 String::from_utf8_lossy(&target_bytes)
1100 );
1101 return Ok(nfsserve::nfs::nfsstring(target_bytes));
1102 }
1103 }
1104 Err(self.llm_no_answer("readlink", "nfs_read_response"))
1105 }
1106 Err(e) => Err(self.llm_failure("readlink", &e)),
1107 }
1108 }
1109}