1use super::actions::SFTP_OPERATION_EVENT;
8use crate::llm::action_helper::call_llm;
9use crate::llm::ollama_client::OllamaClient;
10use crate::protocol::Event;
11use crate::server::connection::ConnectionId;
12use crate::server::SshProtocol;
13use crate::state::app_state::AppState;
14use crate::utils::WireFailure;
15use anyhow::Result;
16use russh_sftp::protocol::*;
17use std::collections::HashMap;
18use std::sync::Arc;
19use tokio::sync::{mpsc, Mutex};
20use tokio_util::sync::CancellationToken;
21use tracing::{debug, error, info, trace};
22
23pub struct LlmSftpHandler {
29 connection_id: ConnectionId,
30 server_id: crate::state::ServerId,
31 llm_client: OllamaClient,
32 app_state: Arc<AppState>,
33 protocol: Arc<SshProtocol>,
34 status_tx: mpsc::UnboundedSender<String>,
35 handles: Arc<Mutex<HashMap<String, HandleInfo>>>,
37 version: Option<u32>,
39 owner: CancellationToken,
41}
42
43fn is_sftp_reply_action(name: &str) -> bool {
45 matches!(
46 name,
47 "sftp_handle"
48 | "sftp_directory_listing"
49 | "sftp_file_content"
50 | "sftp_file_attributes"
51 | "sftp_error"
52 )
53}
54
55fn sftp_error_status(response: &serde_json::Value) -> Option<StatusCode> {
59 if response.get("type").and_then(|v| v.as_str()) != Some("sftp_error") {
60 return None;
61 }
62 let code = response
63 .get("code")
64 .and_then(|v| v.as_str())
65 .unwrap_or("no_such_file");
66 Some(match code {
67 "no_such_file" => StatusCode::NoSuchFile,
68 "permission_denied" => StatusCode::PermissionDenied,
69 "op_unsupported" => StatusCode::OpUnsupported,
70 "eof" => StatusCode::Eof,
71 _ => StatusCode::Failure,
72 })
73}
74
75const BACKEND_FAILURE_STATUS: StatusCode = StatusCode::Failure;
87
88fn sftp_no_answer(response: &serde_json::Value) -> bool {
102 let Some(obj) = response.as_object() else {
103 return true;
104 };
105 if obj.is_empty() {
106 return true;
107 }
108 match obj.get("type").and_then(|v| v.as_str()) {
109 Some(name) => !is_sftp_reply_action(name),
110 None => false,
111 }
112}
113
114#[derive(Debug, Clone)]
116struct HandleInfo {
117 path: String,
118 #[allow(dead_code)] is_directory: bool,
120 dir_read_done: bool,
122}
123
124impl LlmSftpHandler {
125 pub fn new(
127 connection_id: ConnectionId,
128 server_id: crate::state::ServerId,
129 llm_client: OllamaClient,
130 app_state: Arc<AppState>,
131 protocol: Arc<SshProtocol>,
132 status_tx: mpsc::UnboundedSender<String>,
133 owner: CancellationToken,
134 ) -> Self {
135 Self {
136 connection_id,
137 server_id,
138 llm_client,
139 app_state,
140 protocol,
141 status_tx,
142 handles: Arc::new(Mutex::new(HashMap::new())),
143 version: None,
144 owner,
145 }
146 }
147
148 async fn llm_sftp_operation(
155 &self,
156 operation: &str,
157 params: serde_json::Value,
158 ) -> Result<serde_json::Value> {
159 debug!(
161 "SFTP LLM request: operation={}, params={}",
162 operation, params
163 );
164 let _ = self.status_tx.send(format!(
165 "[DEBUG] SFTP LLM request: operation={}, params={}",
166 operation, params
167 ));
168
169 let mut event_data = serde_json::json!({ "operation": operation });
171 if let Some(fields) = params.as_object() {
172 for (key, value) in fields {
173 event_data[key] = value.clone();
174 }
175 }
176 let event = Event::new(&SFTP_OPERATION_EVENT, event_data);
177
178 trace!("SFTP calling LLM for operation: {}", operation);
180 let _ = self.status_tx.send(format!(
181 "[TRACE] SFTP calling LLM for operation: {}",
182 operation
183 ));
184
185 match super::until_owner_drops(
187 &self.owner,
188 call_llm(
189 &self.llm_client,
190 &self.app_state,
191 self.server_id,
192 Some(self.connection_id),
193 &event,
194 self.protocol.as_ref(),
195 ),
196 )
197 .await
198 {
199 Ok(execution_result) => {
200 for message in &execution_result.messages {
202 info!("{}", message);
203 let _ = self.status_tx.send(format!("[INFO] {}", message));
204 }
205
206 debug!(
208 "SFTP LLM returned {} actions for operation: {}",
209 execution_result.raw_actions.len(),
210 operation
211 );
212 let _ = self.status_tx.send(format!(
213 "[DEBUG] SFTP LLM returned {} actions for operation: {}",
214 execution_result.raw_actions.len(),
215 operation
216 ));
217
218 if !execution_result.raw_actions.is_empty() {
220 let pretty = serde_json::to_string_pretty(&execution_result.raw_actions[0])
221 .unwrap_or_else(|_| format!("{:?}", execution_result.raw_actions[0]));
222 trace!("SFTP LLM response ({}) JSON:\n{}", operation, pretty);
223 let _ = self.status_tx.send(format!(
224 "[TRACE] SFTP LLM response ({}) JSON:\r\n{}",
225 operation,
226 pretty.replace('\n', "\r\n")
227 ));
228 }
229
230 let reply = execution_result
235 .raw_actions
236 .iter()
237 .find(|a| {
238 a.get("type")
239 .and_then(|v| v.as_str())
240 .is_some_and(is_sftp_reply_action)
241 })
242 .or_else(|| execution_result.raw_actions.first())
243 .cloned()
244 .unwrap_or_else(|| serde_json::json!({}));
245
246 Ok(reply)
247 }
248 Err(e) => {
249 let failure = WireFailure::classify(&e);
255 error!(
256 "SFTP {}: decision=fail_closed_backend_error category={:?}: {}",
257 operation, failure, e
258 );
259 let _ = self.status_tx.send(format!(
260 "[ERROR] SFTP {}: decision=fail_closed_backend_error category={:?}",
261 operation, failure
262 ));
263 Err(e)
264 }
265 }
266 }
267}
268
269impl russh_sftp::server::Handler for LlmSftpHandler {
270 type Error = StatusCode;
271
272 async fn init(
273 &mut self,
274 version: u32,
275 extensions: HashMap<String, String>,
276 ) -> Result<Version, Self::Error> {
277 info!(
278 "SFTP init: version={}, extensions={:?}",
279 version, extensions
280 );
281 self.version = Some(version);
282
283 let _ = self
285 .status_tx
286 .send(format!("SFTP session initialized (v{})", version));
287
288 Ok(Version::new())
290 }
291
292 async fn opendir(&mut self, id: u32, path: String) -> Result<Handle, Self::Error> {
293 debug!("SFTP request: SSH_FXP_OPENDIR id={}, path={}", id, path);
295 let _ = self.status_tx.send(format!(
296 "[DEBUG] SFTP request: SSH_FXP_OPENDIR id={}, path={}",
297 id, path
298 ));
299
300 trace!("SFTP SSH_FXP_OPENDIR request: id={}, path='{}'", id, path);
302 let _ = self.status_tx.send(format!(
303 "[TRACE] SFTP SSH_FXP_OPENDIR request: id={}, path='{}'",
304 id, path
305 ));
306
307 let params = serde_json::json!({ "path": path });
308 match self.llm_sftp_operation("opendir", params).await {
309 Ok(response) => {
310 if let Some(status) = sftp_error_status(&response) {
311 debug!("SFTP opendir refused by handler for {}: {:?}", path, status);
312 let _ = self.status_tx.send(format!(
313 "[DEBUG] SFTP opendir for {path}: decision=model_reject"
314 ));
315 return Err(status);
316 }
317
318 if sftp_no_answer(&response) {
319 error!("SFTP opendir for {}: decision=fail_closed_no_answer", path);
320 let _ = self.status_tx.send(format!(
321 "[ERROR] SFTP opendir for {path}: decision=fail_closed_no_answer"
322 ));
323 return Err(BACKEND_FAILURE_STATUS);
324 }
325
326 let handle_str = response
328 .get("handle")
329 .and_then(|v| v.as_str())
330 .unwrap_or(&path)
331 .to_string();
332
333 self.handles.lock().await.insert(
335 handle_str.clone(),
336 HandleInfo {
337 path: path.clone(),
338 is_directory: true,
339 dir_read_done: false,
340 },
341 );
342
343 let _ = self
344 .status_tx
345 .send(format!("→ SFTP opened directory: {}", path));
346
347 debug!(
349 "SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
350 id,
351 handle_str.len()
352 );
353 let _ = self.status_tx.send(format!(
354 "[DEBUG] SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
355 id,
356 handle_str.len()
357 ));
358
359 trace!(
361 "SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
362 id,
363 handle_str
364 );
365 let _ = self.status_tx.send(format!(
366 "[TRACE] SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
367 id, handle_str
368 ));
369
370 Ok(Handle {
371 id,
372 handle: handle_str,
373 })
374 }
375 Err(_) => {
376 debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
378 let _ = self.status_tx.send(format!(
379 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (opendir '{}')",
380 id, path
381 ));
382
383 Err(BACKEND_FAILURE_STATUS)
384 }
385 }
386 }
387
388 async fn readdir(&mut self, id: u32, handle: String) -> Result<Name, Self::Error> {
389 debug!(
391 "SFTP request: SSH_FXP_READDIR id={}, handle_len={} bytes",
392 id,
393 handle.len()
394 );
395 let _ = self.status_tx.send(format!(
396 "[DEBUG] SFTP request: SSH_FXP_READDIR id={}, handle_len={} bytes",
397 id,
398 handle.len()
399 ));
400
401 trace!(
403 "SFTP SSH_FXP_READDIR request: id={}, handle='{}'",
404 id,
405 handle
406 );
407 let _ = self.status_tx.send(format!(
408 "[TRACE] SFTP SSH_FXP_READDIR request: id={}, handle='{}'",
409 id, handle
410 ));
411
412 let mut handles = self.handles.lock().await;
414 if let Some(handle_info) = handles.get_mut(&handle) {
415 if handle_info.dir_read_done {
416 debug!(
418 "SFTP response: SSH_FXP_STATUS id={}, status=EOF (directory already read)",
419 id
420 );
421 let _ = self.status_tx.send(format!("[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=EOF (directory already read)", id));
422
423 return Err(StatusCode::Eof);
424 }
425
426 let path = handle_info.path.clone();
427 drop(handles); let params = serde_json::json!({ "path": path, "handle": handle });
430 match self.llm_sftp_operation("readdir", params).await {
431 Ok(response) => {
432 if let Some(status) = sftp_error_status(&response) {
433 debug!(
434 "SFTP readdir for {}: decision=model_reject ({:?})",
435 path, status
436 );
437 return Err(status);
438 }
439
440 if sftp_no_answer(&response) {
441 error!("SFTP readdir for {}: decision=fail_closed_no_answer", path);
445 let _ = self.status_tx.send(format!(
446 "[ERROR] SFTP readdir for {path}: decision=fail_closed_no_answer"
447 ));
448 return Err(BACKEND_FAILURE_STATUS);
449 }
450
451 let files = response
453 .get("entries")
454 .and_then(|v| v.as_array())
455 .map(|arr| {
456 arr.iter()
457 .filter_map(|entry| {
458 let name = entry.get("name")?.as_str()?.to_string();
462 let is_dir = entry
463 .get("is_dir")
464 .and_then(|v| v.as_bool())
465 .unwrap_or(false);
466 let size =
467 entry.get("size").and_then(|v| v.as_u64()).unwrap_or(0);
468
469 let attrs = FileAttributes {
471 size: Some(size),
472 permissions: Some(if is_dir { 0o40755 } else { 0o100644 }),
473 ..Default::default()
474 };
475
476 Some(File::new(name, attrs))
477 })
478 .collect::<Vec<_>>()
479 })
480 .unwrap_or_default();
481
482 if let Some(h) = self.handles.lock().await.get_mut(&handle) {
484 h.dir_read_done = true;
485 }
486
487 let _ = self.status_tx.send(format!(
488 "→ SFTP listed {} items in {}",
489 files.len(),
490 path
491 ));
492
493 debug!(
495 "SFTP response: SSH_FXP_NAME id={}, file_count={}",
496 id,
497 files.len()
498 );
499 let _ = self.status_tx.send(format!(
500 "[DEBUG] SFTP response: SSH_FXP_NAME id={}, file_count={}",
501 id,
502 files.len()
503 ));
504
505 let file_names: Vec<&str> = files.iter().map(|f| f.filename.as_str()).collect();
507 trace!(
508 "SFTP SSH_FXP_NAME response: id={}, files={:?}",
509 id,
510 file_names
511 );
512 let _ = self.status_tx.send(format!(
513 "[TRACE] SFTP SSH_FXP_NAME response: id={}, files={:?}",
514 id, file_names
515 ));
516
517 Ok(Name { id, files })
518 }
519 Err(_) => {
520 debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
522 let _ = self.status_tx.send(format!(
523 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (readdir '{}')",
524 id, handle
525 ));
526
527 Err(BACKEND_FAILURE_STATUS)
528 }
529 }
530 } else {
531 error!("SFTP readdir: invalid handle {}", handle);
532 let _ = self
533 .status_tx
534 .send(format!("[ERROR] SFTP readdir: invalid handle {}", handle));
535
536 debug!(
538 "SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
539 id
540 );
541 let _ = self.status_tx.send(format!(
542 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
543 id
544 ));
545
546 Err(StatusCode::BadMessage)
547 }
548 }
549
550 async fn open(
551 &mut self,
552 id: u32,
553 path: String,
554 pflags: OpenFlags,
555 _attrs: FileAttributes,
556 ) -> Result<Handle, Self::Error> {
557 debug!(
559 "SFTP request: SSH_FXP_OPEN id={}, path={}, flags={:?}",
560 id, path, pflags
561 );
562 let _ = self.status_tx.send(format!(
563 "[DEBUG] SFTP request: SSH_FXP_OPEN id={}, path={}, flags={:?}",
564 id, path, pflags
565 ));
566
567 trace!(
569 "SFTP SSH_FXP_OPEN request: id={}, path='{}', flags={:?}",
570 id,
571 path,
572 pflags
573 );
574 let _ = self.status_tx.send(format!(
575 "[TRACE] SFTP SSH_FXP_OPEN request: id={}, path='{}', flags={:?}",
576 id, path, pflags
577 ));
578
579 let params = serde_json::json!({ "path": path });
580 match self.llm_sftp_operation("open", params).await {
581 Ok(response) => {
582 if let Some(status) = sftp_error_status(&response) {
583 debug!("SFTP open refused by handler for {}: {:?}", path, status);
584 let _ = self.status_tx.send(format!(
585 "[DEBUG] SFTP open for {path}: decision=model_reject"
586 ));
587 return Err(status);
588 }
589
590 if sftp_no_answer(&response) {
591 error!("SFTP open for {}: decision=fail_closed_no_answer", path);
592 let _ = self.status_tx.send(format!(
593 "[ERROR] SFTP open for {path}: decision=fail_closed_no_answer"
594 ));
595 return Err(BACKEND_FAILURE_STATUS);
596 }
597
598 let handle_str = response
599 .get("handle")
600 .and_then(|v| v.as_str())
601 .unwrap_or(&path)
602 .to_string();
603
604 self.handles.lock().await.insert(
606 handle_str.clone(),
607 HandleInfo {
608 path: path.clone(),
609 is_directory: false,
610 dir_read_done: false,
611 },
612 );
613
614 let _ = self.status_tx.send(format!("→ SFTP opened file: {}", path));
615
616 debug!(
618 "SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
619 id,
620 handle_str.len()
621 );
622 let _ = self.status_tx.send(format!(
623 "[DEBUG] SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
624 id,
625 handle_str.len()
626 ));
627
628 trace!(
630 "SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
631 id,
632 handle_str
633 );
634 let _ = self.status_tx.send(format!(
635 "[TRACE] SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
636 id, handle_str
637 ));
638
639 Ok(Handle {
640 id,
641 handle: handle_str,
642 })
643 }
644 Err(_) => {
645 debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
647 let _ = self.status_tx.send(format!(
648 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (open '{}')",
649 id, path
650 ));
651
652 Err(BACKEND_FAILURE_STATUS)
653 }
654 }
655 }
656
657 async fn read(
658 &mut self,
659 id: u32,
660 handle: String,
661 offset: u64,
662 len: u32,
663 ) -> Result<Data, Self::Error> {
664 debug!(
666 "SFTP request: SSH_FXP_READ id={}, offset={}, len={} bytes",
667 id, offset, len
668 );
669 let _ = self.status_tx.send(format!(
670 "[DEBUG] SFTP request: SSH_FXP_READ id={}, offset={}, len={} bytes",
671 id, offset, len
672 ));
673
674 trace!(
676 "SFTP SSH_FXP_READ request: id={}, handle='{}', offset={}, len={}",
677 id,
678 handle,
679 offset,
680 len
681 );
682 let _ = self.status_tx.send(format!(
683 "[TRACE] SFTP SSH_FXP_READ request: id={}, handle='{}', offset={}, len={}",
684 id, handle, offset, len
685 ));
686
687 let handles = self.handles.lock().await;
688 if let Some(handle_info) = handles.get(&handle) {
689 let path = handle_info.path.clone();
690 drop(handles);
691
692 let params = serde_json::json!({
693 "path": path,
694 "handle": handle,
695 "offset": offset,
696 "length": len,
697 });
698 match self.llm_sftp_operation("read", params).await {
699 Ok(response) => {
700 if let Some(status) = sftp_error_status(&response) {
701 debug!(
702 "SFTP read for {}: decision=model_reject ({:?})",
703 path, status
704 );
705 return Err(status);
706 }
707
708 if sftp_no_answer(&response) {
709 error!("SFTP read for {}: decision=fail_closed_no_answer", path);
713 let _ = self.status_tx.send(format!(
714 "[ERROR] SFTP read for {path}: decision=fail_closed_no_answer"
715 ));
716 return Err(BACKEND_FAILURE_STATUS);
717 }
718
719 let content = response
721 .get("content")
722 .and_then(|v| v.as_str())
723 .unwrap_or("");
724
725 let full = content.as_bytes();
729 let start = usize::try_from(offset).unwrap_or(usize::MAX);
730 if start >= full.len() {
731 debug!(
732 "SFTP response: SSH_FXP_STATUS id={}, status=EOF (offset {} >= size {})",
733 id,
734 offset,
735 full.len()
736 );
737 return Err(StatusCode::Eof);
738 }
739 let end = start.saturating_add(len as usize).min(full.len());
740 let data = full[start..end].to_vec();
741 let actual_len = data.len();
742
743 let _ = self
744 .status_tx
745 .send(format!("→ SFTP read {} bytes from {}", actual_len, path));
746
747 debug!(
749 "SFTP response: SSH_FXP_DATA id={}, data_len={} bytes",
750 id, actual_len
751 );
752 let _ = self.status_tx.send(format!(
753 "[DEBUG] SFTP response: SSH_FXP_DATA id={}, data_len={} bytes",
754 id, actual_len
755 ));
756
757 if actual_len <= 256 {
759 trace!(
760 "SFTP SSH_FXP_DATA response: id={}, data={:?}",
761 id,
762 String::from_utf8_lossy(&data)
763 );
764 let _ = self.status_tx.send(format!(
765 "[TRACE] SFTP SSH_FXP_DATA response: id={}, data={:?}",
766 id,
767 String::from_utf8_lossy(&data)
768 ));
769 } else {
770 trace!(
771 "SFTP SSH_FXP_DATA response: id={}, data_len={} bytes (truncated)",
772 id,
773 actual_len
774 );
775 let _ = self.status_tx.send(format!("[TRACE] SFTP SSH_FXP_DATA response: id={}, data_len={} bytes (truncated)", id, actual_len));
776 }
777
778 Ok(Data { id, data })
779 }
780 Err(_) => {
781 debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
786 let _ = self.status_tx.send(format!(
787 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (read '{}')",
788 id, handle
789 ));
790
791 Err(BACKEND_FAILURE_STATUS)
792 }
793 }
794 } else {
795 error!("SFTP read: invalid handle {}", handle);
796 let _ = self
797 .status_tx
798 .send(format!("[ERROR] SFTP read: invalid handle {}", handle));
799
800 debug!(
802 "SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
803 id
804 );
805 let _ = self.status_tx.send(format!(
806 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
807 id
808 ));
809
810 Err(StatusCode::BadMessage)
811 }
812 }
813
814 async fn close(&mut self, id: u32, handle: String) -> Result<Status, Self::Error> {
815 debug!(
817 "SFTP request: SSH_FXP_CLOSE id={}, handle_len={} bytes",
818 id,
819 handle.len()
820 );
821 let _ = self.status_tx.send(format!(
822 "[DEBUG] SFTP request: SSH_FXP_CLOSE id={}, handle_len={} bytes",
823 id,
824 handle.len()
825 ));
826
827 trace!("SFTP SSH_FXP_CLOSE request: id={}, handle='{}'", id, handle);
829 let _ = self.status_tx.send(format!(
830 "[TRACE] SFTP SSH_FXP_CLOSE request: id={}, handle='{}'",
831 id, handle
832 ));
833
834 if let Some(handle_info) = self.handles.lock().await.remove(&handle) {
836 let _ = self
837 .status_tx
838 .send(format!("→ SFTP closed: {}", handle_info.path));
839
840 debug!(
842 "SFTP response: SSH_FXP_STATUS id={}, status=OK (closed '{}')",
843 id, handle_info.path
844 );
845 let _ = self.status_tx.send(format!(
846 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=OK (closed '{}')",
847 id, handle_info.path
848 ));
849
850 trace!(
852 "SFTP SSH_FXP_STATUS response: id={}, status=OK, path='{}'",
853 id,
854 handle_info.path
855 );
856 let _ = self.status_tx.send(format!(
857 "[TRACE] SFTP SSH_FXP_STATUS response: id={}, status=OK, path='{}'",
858 id, handle_info.path
859 ));
860 } else {
861 debug!(
863 "SFTP response: SSH_FXP_STATUS id={}, status=OK (unknown handle)",
864 id
865 );
866 let _ = self.status_tx.send(format!(
867 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=OK (unknown handle)",
868 id
869 ));
870 }
871
872 Ok(Status {
873 id,
874 status_code: StatusCode::Ok,
875 error_message: "".to_string(),
876 language_tag: "en-US".to_string(),
877 })
878 }
879
880 async fn lstat(&mut self, id: u32, path: String) -> Result<Attrs, Self::Error> {
881 debug!("SFTP request: SSH_FXP_LSTAT id={}, path={}", id, path);
883 let _ = self.status_tx.send(format!(
884 "[DEBUG] SFTP request: SSH_FXP_LSTAT id={}, path={}",
885 id, path
886 ));
887
888 trace!("SFTP SSH_FXP_LSTAT request: id={}, path='{}'", id, path);
890 let _ = self.status_tx.send(format!(
891 "[TRACE] SFTP SSH_FXP_LSTAT request: id={}, path='{}'",
892 id, path
893 ));
894
895 let params = serde_json::json!({ "path": path });
896 match self.llm_sftp_operation("lstat", params).await {
897 Ok(response) => {
898 if let Some(status) = sftp_error_status(&response) {
899 debug!("SFTP lstat refused by handler for {}: {:?}", path, status);
900 let _ = self.status_tx.send(format!(
901 "[DEBUG] SFTP lstat for {path}: decision=model_reject"
902 ));
903 return Err(status);
904 }
905
906 if sftp_no_answer(&response) {
907 error!("SFTP lstat for {}: decision=fail_closed_no_answer", path);
913 let _ = self.status_tx.send(format!(
914 "[ERROR] SFTP lstat for {path}: decision=fail_closed_no_answer"
915 ));
916 return Err(BACKEND_FAILURE_STATUS);
917 }
918
919 let mut attrs = FileAttributes::default();
920
921 if let Some(size) = response.get("size").and_then(|v| v.as_u64()) {
923 attrs.size = Some(size);
924 }
925
926 let is_dir = response
937 .get("is_dir")
938 .and_then(|v| v.as_bool())
939 .unwrap_or(false);
940 let type_bit = if is_dir { 0o40000 } else { 0o100000 };
941 attrs.permissions =
942 Some(match response.get("permissions").and_then(|v| v.as_u64()) {
943 Some(perms) if perms & 0o170000 != 0 => perms as u32,
945 Some(perms) => perms as u32 | type_bit,
946 None if is_dir => 0o40755,
947 None => 0o100644,
948 });
949
950 debug!(
952 "SFTP response: SSH_FXP_ATTRS id={}, size={:?}, perms={:?}",
953 id, attrs.size, attrs.permissions
954 );
955 let _ = self.status_tx.send(format!(
956 "[DEBUG] SFTP response: SSH_FXP_ATTRS id={}, size={:?}, perms={:?}",
957 id, attrs.size, attrs.permissions
958 ));
959
960 trace!(
962 "SFTP SSH_FXP_ATTRS response: id={}, path='{}', attrs={:?}",
963 id,
964 path,
965 attrs
966 );
967 let _ = self.status_tx.send(format!(
968 "[TRACE] SFTP SSH_FXP_ATTRS response: id={}, path='{}', attrs={:?}",
969 id, path, attrs
970 ));
971
972 Ok(Attrs { id, attrs })
973 }
974 Err(_) => {
975 debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
977 let _ = self.status_tx.send(format!(
978 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (lstat '{}')",
979 id, path
980 ));
981
982 Err(BACKEND_FAILURE_STATUS)
983 }
984 }
985 }
986
987 async fn fstat(&mut self, id: u32, handle: String) -> Result<Attrs, Self::Error> {
988 debug!(
990 "SFTP request: SSH_FXP_FSTAT id={}, handle_len={} bytes",
991 id,
992 handle.len()
993 );
994 let _ = self.status_tx.send(format!(
995 "[DEBUG] SFTP request: SSH_FXP_FSTAT id={}, handle_len={} bytes",
996 id,
997 handle.len()
998 ));
999
1000 trace!("SFTP SSH_FXP_FSTAT request: id={}, handle='{}'", id, handle);
1002 let _ = self.status_tx.send(format!(
1003 "[TRACE] SFTP SSH_FXP_FSTAT request: id={}, handle='{}'",
1004 id, handle
1005 ));
1006
1007 let handles = self.handles.lock().await;
1008 if let Some(handle_info) = handles.get(&handle) {
1009 let path = handle_info.path.clone();
1010 drop(handles);
1011
1012 trace!("SFTP fstat delegating to lstat for path: '{}'", path);
1014 let _ = self.status_tx.send(format!(
1015 "[TRACE] SFTP fstat delegating to lstat for path: '{}'",
1016 path
1017 ));
1018 self.lstat(id, path).await
1019 } else {
1020 error!("SFTP fstat: invalid handle {}", handle);
1021 let _ = self
1022 .status_tx
1023 .send(format!("[ERROR] SFTP fstat: invalid handle {}", handle));
1024
1025 debug!(
1027 "SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
1028 id
1029 );
1030 let _ = self.status_tx.send(format!(
1031 "[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
1032 id
1033 ));
1034
1035 Err(StatusCode::BadMessage)
1036 }
1037 }
1038
1039 async fn stat(&mut self, id: u32, path: String) -> Result<Attrs, Self::Error> {
1048 debug!("SFTP request: SSH_FXP_STAT id={}, path={}", id, path);
1049 let _ = self.status_tx.send(format!(
1050 "[DEBUG] SFTP request: SSH_FXP_STAT id={}, path={}",
1051 id, path
1052 ));
1053 trace!("SFTP stat delegating to lstat for path: '{}'", path);
1054 self.lstat(id, path).await
1055 }
1056
1057 async fn realpath(&mut self, id: u32, path: String) -> Result<Name, Self::Error> {
1058 debug!("SFTP request: SSH_FXP_REALPATH id={}, path={}", id, path);
1060 let _ = self.status_tx.send(format!(
1061 "[DEBUG] SFTP request: SSH_FXP_REALPATH id={}, path={}",
1062 id, path
1063 ));
1064
1065 trace!("SFTP SSH_FXP_REALPATH request: id={}, path='{}'", id, path);
1067 let _ = self.status_tx.send(format!(
1068 "[TRACE] SFTP SSH_FXP_REALPATH request: id={}, path='{}'",
1069 id, path
1070 ));
1071
1072 let path = if path.is_empty() || path == "." {
1076 "/".to_string()
1077 } else {
1078 path
1079 };
1080
1081 let attrs = FileAttributes::default();
1082 let file = File::new(path.clone(), attrs);
1083
1084 debug!(
1086 "SFTP response: SSH_FXP_NAME id={}, resolved_path={}",
1087 id, path
1088 );
1089 let _ = self.status_tx.send(format!(
1090 "[DEBUG] SFTP response: SSH_FXP_NAME id={}, resolved_path={}",
1091 id, path
1092 ));
1093
1094 trace!("SFTP SSH_FXP_NAME response: id={}, path='{}'", id, path);
1096 let _ = self.status_tx.send(format!(
1097 "[TRACE] SFTP SSH_FXP_NAME response: id={}, path='{}'",
1098 id, path
1099 ));
1100
1101 Ok(Name {
1102 id,
1103 files: vec![file],
1104 })
1105 }
1106
1107 fn unimplemented(&self) -> Self::Error {
1108 error!("SFTP unimplemented packet received");
1109 let _ = self
1110 .status_tx
1111 .send("[ERROR] SFTP unimplemented packet received".to_string());
1112 StatusCode::OpUnsupported
1113 }
1114}