1pub mod actions;
3
4pub use actions::SmbClientProtocol;
5
6use crate::llm::actions::client_trait::Client;
7use anyhow::{Context, Result};
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11use tracing::{debug, error, info};
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::client::smb::actions::{
15 SMB_CLIENT_CONNECTED_EVENT, SMB_CLIENT_DIR_LISTED_EVENT, SMB_CLIENT_ERROR_EVENT,
16 SMB_CLIENT_FILE_READ_EVENT, SMB_CLIENT_FILE_WRITTEN_EVENT,
17};
18use crate::llm::ollama_client::OllamaClient;
19use crate::llm::ClientLlmResult;
20use crate::protocol::{Event, StartupParams};
21use crate::state::app_state::AppState;
22use crate::state::{ClientId, ClientStatus};
23
24use pavao::{
25 SmbClient as PavaoSmbClient, SmbCredentials, SmbDirent, SmbDirentType, SmbOpenOptions,
26 SmbOptions,
27};
28use std::io::{Read as IoRead, Write as IoWrite};
29use tokio::sync::Mutex;
30
31type SharedSmb = Arc<Mutex<NativeSmb>>;
37static NATIVE_SESSION: std::sync::LazyLock<Arc<tokio::sync::Semaphore>> =
40 std::sync::LazyLock::new(|| Arc::new(tokio::sync::Semaphore::new(1)));
41struct NativeSmb {
42 client: Option<PavaoSmbClient>,
43 lease: Option<tokio::sync::OwnedSemaphorePermit>,
44}
45impl Drop for NativeSmb {
46 fn drop(&mut self) {
47 let client = self.client.take();
48 let lease = self.lease.take();
49 let cleanup = move || {
50 drop(client);
51 drop(lease);
52 };
53 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
54 runtime.spawn_blocking(cleanup);
55 } else {
56 std::thread::spawn(cleanup);
57 }
58 }
59}
60
61pub const MAX_FILE_BYTES: usize = 8 * 1024 * 1024;
63
64pub fn read_file_bounded(reader: impl IoRead) -> std::io::Result<Vec<u8>> {
66 let mut data = Vec::new();
67 reader
68 .take(MAX_FILE_BYTES as u64 + 1)
69 .read_to_end(&mut data)?;
70 if data.len() > MAX_FILE_BYTES {
71 return Err(std::io::Error::new(
72 std::io::ErrorKind::InvalidData,
73 "SMB file exceeds buffered byte cap",
74 ));
75 }
76 Ok(data)
77}
78
79async fn blocking_smb<T: Send + 'static>(
80 client: &SharedSmb,
81 operation: impl FnOnce(&PavaoSmbClient) -> Result<T> + Send + 'static,
82) -> Result<T> {
83 let client = client.clone().lock_owned().await;
84 tokio::task::spawn_blocking(move || {
85 operation(client.client.as_ref().context("SMB client is closed")?)
86 })
87 .await
88 .context("SMB worker panicked")?
89}
90
91pub enum SmbApplied {
98 Executed(String),
100 Disconnected,
102}
103
104pub struct SmbClient;
106
107impl SmbClient {
108 pub async fn connect_with_llm_actions(
110 remote_addr: String,
111 llm_client: OllamaClient,
112 app_state: Arc<AppState>,
113 status_tx: mpsc::UnboundedSender<String>,
114 client_id: ClientId,
115 startup_params: Option<StartupParams>,
116 ) -> Result<SocketAddr> {
117 info!(
118 "SMB client {} initializing connection to {}",
119 client_id, remote_addr
120 );
121
122 let (username, password, domain, workgroup) = if let Some(params) = startup_params {
124 let username = params
125 .get_optional_string("username")?
126 .unwrap_or_else(|| "guest".to_string());
127 let password = params
128 .get_optional_string("password")?
129 .unwrap_or_else(|| "".to_string());
130 let domain = params.get_optional_string("domain")?;
131 let workgroup = params.get_optional_string("workgroup")?;
132
133 (username, password, domain, workgroup)
134 } else {
135 ("guest".to_string(), "".to_string(), None, None)
136 };
137
138 info!(
139 "SMB client {} using credentials - username: {}, domain: {:?}, workgroup: {:?}",
140 client_id, username, domain, workgroup
141 );
142
143 let mut creds = SmbCredentials::default()
145 .server(&remote_addr)
146 .username(&username)
147 .password(&password);
148
149 if let Some(w) = workgroup.or(domain) {
151 creds = creds.workgroup(&w);
152 }
153
154 let lease = NATIVE_SESSION
156 .clone()
157 .try_acquire_owned()
158 .context("the pavao SMB backend supports one active native session per process")?;
159 let smb_client: SharedSmb = Arc::new(Mutex::new(
161 tokio::task::spawn_blocking(move || {
162 let client = PavaoSmbClient::new(creds, SmbOptions::default())
163 .context("Failed to create SMB client")?;
164 client.set_timeout(std::time::Duration::from_secs(30))?;
165 Ok::<_, anyhow::Error>(NativeSmb {
166 client: Some(client),
167 lease: Some(lease),
168 })
169 })
170 .await
171 .context("SMB initialization worker panicked")??,
172 ));
173
174 let local_addr = "127.0.0.1:0".parse::<SocketAddr>()?;
177
178 app_state
180 .update_client_status(client_id, ClientStatus::Connected)
181 .await;
182 let _ = status_tx.send(format!("[CLIENT] SMB client {} connected", client_id));
183 let _ = status_tx.send("__UPDATE_UI__".to_string());
184
185 info!("SMB client {} connected to {}", client_id, remote_addr);
186
187 let command_rx =
193 crate::client::command_support::register_command_channel(&app_state, client_id).await;
194 let cmd_task = tokio::spawn({
195 let smb_client = smb_client.clone();
196 let llm_client = llm_client.clone();
197 let app_state = app_state.clone();
198 let status_tx = status_tx.clone();
199 async move {
200 Self::command_loop(
201 command_rx, smb_client, client_id, llm_client, app_state, status_tx,
202 )
203 .await;
204 }
205 });
206 app_state.register_client_task(client_id, cmd_task).await;
207
208 let app_state_clone = app_state.clone();
210 let status_tx_clone = status_tx.clone();
211 let remote_addr_clone = remote_addr.clone();
212
213 let task_registrar = app_state.clone();
216 let task_handle = tokio::spawn(async move {
217 if let Some(instruction) = app_state_clone.get_instruction_for_client(client_id).await {
219 let protocol = Arc::new(SmbClientProtocol::new());
220 let event = Event::new(
221 &SMB_CLIENT_CONNECTED_EVENT,
222 serde_json::json!({
223 "share_url": format!("smb://{}", remote_addr_clone),
224 }),
225 );
226
227 let memory = app_state_clone
228 .get_memory_for_client(client_id)
229 .await
230 .unwrap_or_default();
231
232 match call_llm_for_client(
233 &llm_client,
234 &app_state_clone,
235 client_id.to_string(),
236 &instruction,
237 &memory,
238 Some(&event),
239 protocol.as_ref(),
240 &status_tx_clone,
241 )
242 .await
243 {
244 Ok(ClientLlmResult {
245 actions,
246 memory_updates,
247 }) => {
248 if let Some(mem) = memory_updates {
250 app_state_clone.set_memory_for_client(client_id, mem).await;
251 }
252
253 for action in actions {
255 if let Err(e) = Self::execute_smb_action(
256 &smb_client,
257 action,
258 client_id,
259 &protocol,
260 &llm_client,
261 &app_state_clone,
262 &status_tx_clone,
263 )
264 .await
265 {
266 error!("SMB client {} action error: {}", client_id, e);
267
268 let error_event = Event::new(
270 &SMB_CLIENT_ERROR_EVENT,
271 serde_json::json!({
272 "error": e.to_string(),
273 "operation": "action_execution",
274 }),
275 );
276
277 let _ = Self::call_llm_with_event(
278 &error_event,
279 client_id,
280 &protocol,
281 &llm_client,
282 &app_state_clone,
283 &status_tx_clone,
284 &smb_client,
285 )
286 .await;
287 }
288 }
289 }
290 Err(e) => {
291 error!("LLM error for SMB client {}: {}", client_id, e);
292 }
293 }
294 }
295 });
296 task_registrar
297 .register_client_task(client_id, task_handle)
298 .await;
299
300 Ok(local_addr)
301 }
302
303 async fn command_loop(
313 mut command_rx: tokio::sync::mpsc::Receiver<crate::state::client_handles::ClientCommand>,
314 smb_client: SharedSmb,
315 client_id: ClientId,
316 llm_client: OllamaClient,
317 app_state: Arc<AppState>,
318 status_tx: mpsc::UnboundedSender<String>,
319 ) {
320 use crate::llm::actions::protocol_trait::Protocol;
321 use crate::state::client_handles::ClientSendOutcome;
322 use crate::state::AccessLogOwner;
323
324 let protocol = Arc::new(SmbClientProtocol::new());
325
326 while let Some(command) = command_rx.recv().await {
327 let action = command.action.clone();
328
329 let outcome = match protocol.execute_action(action.clone()) {
333 Err(e) => Ok(ClientSendOutcome::Rejected {
334 error: e.to_string(),
335 }),
336 Ok(result) => Self::apply_smb_result(
337 result,
338 &smb_client,
339 client_id,
340 &protocol,
341 &llm_client,
342 &app_state,
343 &status_tx,
344 )
345 .await
346 .map(|applied| match applied {
347 SmbApplied::Executed(detail) => ClientSendOutcome::Executed { detail },
348 SmbApplied::Disconnected => ClientSendOutcome::Disconnected,
349 }),
350 };
351
352 let outcome_json = match &outcome {
353 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
354 Err(e) => serde_json::json!({"error": e.to_string()}),
355 };
356 app_state
357 .record_access_log(
358 AccessLogOwner::Client(client_id.as_u32()),
359 protocol.protocol_name(),
360 None,
361 "injected_action",
362 action,
363 vec![outcome_json],
364 )
365 .await;
366
367 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
368 if let Err(e) = &outcome {
369 error!("SMB client {} injected action failed: {}", client_id, e);
370 let _ = status_tx.send(format!(
371 "[WARN] Client {} injected action failed: {}",
372 client_id, e
373 ));
374 }
375 let _ = status_tx.send("__UPDATE_UI__".to_string());
376 crate::client::command_support::reply(command, outcome);
377
378 if disconnect {
379 break;
380 }
381 }
382
383 app_state.remove_client_handle(client_id).await;
386 let _ = status_tx.send("__UPDATE_UI__".to_string());
387 }
388
389 async fn execute_smb_action(
391 smb_client: &SharedSmb,
392 action: serde_json::Value,
393 client_id: ClientId,
394 protocol: &Arc<SmbClientProtocol>,
395 llm_client: &OllamaClient,
396 app_state: &Arc<AppState>,
397 status_tx: &mpsc::UnboundedSender<String>,
398 ) -> Result<SmbApplied> {
399 let result = protocol.execute_action(action)?;
400 Self::apply_smb_result(
401 result, smb_client, client_id, protocol, llm_client, app_state, status_tx,
402 )
403 .await
404 }
405
406 async fn apply_smb_result(
410 action_result: crate::llm::actions::client_trait::ClientActionResult,
411 smb_client: &SharedSmb,
412 client_id: ClientId,
413 protocol: &Arc<SmbClientProtocol>,
414 llm_client: &OllamaClient,
415 app_state: &Arc<AppState>,
416 status_tx: &mpsc::UnboundedSender<String>,
417 ) -> Result<SmbApplied> {
418 let detail: String;
420 let mut failure = None;
421 match action_result {
422 crate::llm::actions::client_trait::ClientActionResult::Custom { name, data } => {
423 match name.as_str() {
424 "smb_list_dir" => {
425 let path = data
426 .get("path")
427 .and_then(|v| v.as_str())
428 .context("Missing path")?;
429
430 debug!("SMB client {} listing directory: {}", client_id, path);
431
432 let owned_path = path.to_string();
435 let list_result =
436 blocking_smb(smb_client, move |smb| Ok(smb.list_dir(&owned_path)?))
437 .await;
438 match list_result {
439 Ok(entries) => {
440 let entry_list: Vec<serde_json::Value> = entries
441 .iter()
442 .map(|entry| {
443 let dirent: &SmbDirent = entry;
444 serde_json::json!({
445 "name": dirent.name(),
446 "type": match dirent.get_type() {
447 SmbDirentType::Workgroup => "workgroup",
448 SmbDirentType::Server => "server",
449 SmbDirentType::FileShare => "file_share",
450 SmbDirentType::PrinterShare => "printer_share",
451 SmbDirentType::CommsShare => "comms_share",
452 SmbDirentType::IpcShare => "ipc_share",
453 SmbDirentType::Dir => "dir",
454 SmbDirentType::File => "file",
455 SmbDirentType::Link => "link",
456 },
457 "comment": dirent.comment(),
458 })
459 })
460 .collect();
461
462 info!(
463 "SMB client {} listed {} entries in {}",
464 client_id,
465 entry_list.len(),
466 path
467 );
468 detail = format!(
469 "list_directory {:?}: {} entries",
470 path,
471 entry_list.len()
472 );
473
474 let event = Event::new(
475 &SMB_CLIENT_DIR_LISTED_EVENT,
476 serde_json::json!({
477 "path": path,
478 "entries": entry_list,
479 }),
480 );
481
482 Self::call_llm_with_event(
483 &event, client_id, protocol, llm_client, app_state, status_tx,
484 smb_client,
485 )
486 .await?;
487 }
488 Err(e) => {
489 error!("SMB client {} list_dir error: {}", client_id, e);
490 detail = format!("list_directory {path:?} failed: {e}");
491 failure = Some(e.to_string());
492 let error_event = Event::new(
493 &SMB_CLIENT_ERROR_EVENT,
494 serde_json::json!({
495 "error": e.to_string(),
496 "operation": "list_directory",
497 }),
498 );
499
500 Self::call_llm_with_event(
501 &error_event,
502 client_id,
503 protocol,
504 llm_client,
505 app_state,
506 status_tx,
507 smb_client,
508 )
509 .await?;
510 }
511 }
512 }
513 "smb_read_file" => {
514 let path = data
515 .get("path")
516 .and_then(|v| v.as_str())
517 .context("Missing path")?;
518
519 debug!("SMB client {} reading file: {}", client_id, path);
520
521 let owned_path = path.to_string();
524 let read_result = blocking_smb(smb_client, move |smb| {
525 let file =
526 smb.open_with(&owned_path, SmbOpenOptions::default().read(true))?;
527 Ok(read_file_bounded(file)?)
528 })
529 .await;
530
531 match read_result {
532 Ok(content_bytes) => {
533 let size = content_bytes.len();
534
535 let content =
537 if let Ok(text) = String::from_utf8(content_bytes.clone()) {
538 text
539 } else {
540 use base64::{engine::general_purpose, Engine as _};
541 format!(
542 "base64:{}",
543 general_purpose::STANDARD.encode(&content_bytes)
544 )
545 };
546
547 info!("SMB client {} read {} bytes from {}", client_id, size, path);
548 detail = format!("read_file {path:?}: {size} bytes read");
549
550 let event = Event::new(
551 &SMB_CLIENT_FILE_READ_EVENT,
552 serde_json::json!({
553 "path": path,
554 "content": content,
555 "size": size,
556 }),
557 );
558
559 Self::call_llm_with_event(
560 &event, client_id, protocol, llm_client, app_state, status_tx,
561 smb_client,
562 )
563 .await?;
564 }
565 Err(e) => {
566 error!("SMB client {} read_file error: {}", client_id, e);
567 detail = format!("read_file {path:?} failed: {e}");
568 failure = Some(e.to_string());
569 let error_event = Event::new(
570 &SMB_CLIENT_ERROR_EVENT,
571 serde_json::json!({
572 "error": e.to_string(),
573 "operation": "read_file",
574 }),
575 );
576
577 Self::call_llm_with_event(
578 &error_event,
579 client_id,
580 protocol,
581 llm_client,
582 app_state,
583 status_tx,
584 smb_client,
585 )
586 .await?;
587 }
588 }
589 }
590 "smb_write_file" => {
591 let path = data
592 .get("path")
593 .and_then(|v| v.as_str())
594 .context("Missing path")?;
595 let content = data
596 .get("content")
597 .and_then(|v| v.as_str())
598 .context("Missing content")?;
599
600 debug!("SMB client {} writing file: {}", client_id, path);
601
602 let content_bytes = match content.strip_prefix("base64:") {
608 Some(encoded) => {
609 use base64::{engine::general_purpose, Engine as _};
610 general_purpose::STANDARD.decode(encoded.trim()).context(
611 "content began with \"base64:\" but the rest is not valid \
612 base64. Binary content must be base64 exactly as \
613 smb_file_read reports it; text content must not start \
614 with \"base64:\".",
615 )?
616 }
617 None => content.as_bytes().to_vec(),
618 };
619 if content_bytes.len() > MAX_FILE_BYTES {
620 anyhow::bail!("SMB write exceeds buffered byte cap");
621 }
622 let owned_path = path.to_string();
623 let write_result = blocking_smb(smb_client, move |smb| {
624 let mut file = smb.open_with(
625 &owned_path,
626 SmbOpenOptions::default()
627 .write(true)
628 .create(true)
629 .truncate(true),
630 )?;
631 file.write_all(&content_bytes)?;
632 Ok(content_bytes.len())
633 })
634 .await;
635
636 match write_result {
637 Ok(bytes_written) => {
638 info!(
639 "SMB client {} wrote {} bytes to {}",
640 client_id, bytes_written, path
641 );
642 detail =
643 format!("write_file {path:?}: {bytes_written} bytes written");
644
645 let event = Event::new(
646 &SMB_CLIENT_FILE_WRITTEN_EVENT,
647 serde_json::json!({
648 "path": path,
649 "bytes_written": bytes_written,
650 }),
651 );
652
653 Self::call_llm_with_event(
654 &event, client_id, protocol, llm_client, app_state, status_tx,
655 smb_client,
656 )
657 .await?;
658 }
659 Err(e) => {
660 error!("SMB client {} write_file error: {}", client_id, e);
661 detail = format!("write_file {path:?} failed: {e}");
662 failure = Some(e.to_string());
663 let error_event = Event::new(
664 &SMB_CLIENT_ERROR_EVENT,
665 serde_json::json!({
666 "error": e.to_string(),
667 "operation": "write_file",
668 }),
669 );
670
671 Self::call_llm_with_event(
672 &error_event,
673 client_id,
674 protocol,
675 llm_client,
676 app_state,
677 status_tx,
678 smb_client,
679 )
680 .await?;
681 }
682 }
683 }
684 "smb_create_dir" => {
685 let path = data
686 .get("path")
687 .and_then(|v| v.as_str())
688 .context("Missing path")?;
689
690 debug!("SMB client {} creating directory: {}", client_id, path);
691
692 let owned_path = path.to_string();
693 let mkdir_result = blocking_smb(smb_client, move |smb| {
694 Ok(smb.mkdir(&owned_path, pavao::SmbMode::from(0o755))?)
695 })
696 .await;
697 match mkdir_result {
698 Ok(()) => {
699 info!("SMB client {} created directory {}", client_id, path);
700 detail = format!("create_directory {path:?}: created");
701 let _ = status_tx.send(format!(
702 "[CLIENT] SMB client {} created directory: {}",
703 client_id, path
704 ));
705 }
706 Err(e) => {
707 error!("SMB client {} mkdir error: {}", client_id, e);
708 detail = format!("create_directory {path:?} failed: {e}");
709 failure = Some(e.to_string());
710 let error_event = Event::new(
711 &SMB_CLIENT_ERROR_EVENT,
712 serde_json::json!({
713 "error": e.to_string(),
714 "operation": "create_directory",
715 }),
716 );
717
718 Self::call_llm_with_event(
719 &error_event,
720 client_id,
721 protocol,
722 llm_client,
723 app_state,
724 status_tx,
725 smb_client,
726 )
727 .await?;
728 }
729 }
730 }
731 "smb_delete_file" => {
732 let path = data
733 .get("path")
734 .and_then(|v| v.as_str())
735 .context("Missing path")?;
736
737 debug!("SMB client {} deleting file: {}", client_id, path);
738
739 let owned_path = path.to_string();
740 let unlink_result =
741 blocking_smb(smb_client, move |smb| Ok(smb.unlink(&owned_path)?)).await;
742 match unlink_result {
743 Ok(()) => {
744 info!("SMB client {} deleted file {}", client_id, path);
745 detail = format!("delete_file {path:?}: deleted");
746 let _ = status_tx.send(format!(
747 "[CLIENT] SMB client {} deleted file: {}",
748 client_id, path
749 ));
750 }
751 Err(e) => {
752 error!("SMB client {} unlink error: {}", client_id, e);
753 detail = format!("delete_file {path:?} failed: {e}");
754 failure = Some(e.to_string());
755 let error_event = Event::new(
756 &SMB_CLIENT_ERROR_EVENT,
757 serde_json::json!({
758 "error": e.to_string(),
759 "operation": "delete_file",
760 }),
761 );
762
763 Self::call_llm_with_event(
764 &error_event,
765 client_id,
766 protocol,
767 llm_client,
768 app_state,
769 status_tx,
770 smb_client,
771 )
772 .await?;
773 }
774 }
775 }
776 "smb_delete_dir" => {
777 let path = data
778 .get("path")
779 .and_then(|v| v.as_str())
780 .context("Missing path")?;
781
782 debug!("SMB client {} deleting directory: {}", client_id, path);
783
784 let owned_path = path.to_string();
785 let rmdir_result =
786 blocking_smb(smb_client, move |smb| Ok(smb.rmdir(&owned_path)?)).await;
787 match rmdir_result {
788 Ok(()) => {
789 info!("SMB client {} deleted directory {}", client_id, path);
790 detail = format!("delete_directory {path:?}: deleted");
791 let _ = status_tx.send(format!(
792 "[CLIENT] SMB client {} deleted directory: {}",
793 client_id, path
794 ));
795 }
796 Err(e) => {
797 error!("SMB client {} rmdir error: {}", client_id, e);
798 detail = format!("delete_directory {path:?} failed: {e}");
799 failure = Some(e.to_string());
800 let error_event = Event::new(
801 &SMB_CLIENT_ERROR_EVENT,
802 serde_json::json!({
803 "error": e.to_string(),
804 "operation": "delete_directory",
805 }),
806 );
807
808 Self::call_llm_with_event(
809 &error_event,
810 client_id,
811 protocol,
812 llm_client,
813 app_state,
814 status_tx,
815 smb_client,
816 )
817 .await?;
818 }
819 }
820 }
821 _ => {
822 error!("SMB client {} unknown action: {}", client_id, name);
823 detail = format!("custom result '{name}' has no SMB handler");
824 }
825 }
826 }
827 crate::llm::actions::client_trait::ClientActionResult::Disconnect => {
828 info!("SMB client {} disconnecting", client_id);
829 app_state
830 .update_client_status(client_id, ClientStatus::Disconnected)
831 .await;
832 app_state.remove_client_handle(client_id).await;
836 let _ = status_tx.send(format!("[CLIENT] SMB client {} disconnected", client_id));
837 let _ = status_tx.send("__UPDATE_UI__".to_string());
838 return Ok(SmbApplied::Disconnected);
839 }
840 crate::llm::actions::client_trait::ClientActionResult::WaitForMore => {
841 debug!("SMB client {} waiting for more", client_id);
842 detail = "wait_for_more".to_string();
843 }
844 other => {
845 debug!("SMB client {} unhandled action result", client_id);
846 detail = format!("unhandled action result {other:?}");
847 }
848 }
849
850 if let Some(error) = failure {
851 anyhow::bail!("SMB operation failed: {error}");
852 }
853 Ok(SmbApplied::Executed(detail))
854 }
855
856 async fn call_llm_with_event(
858 event: &Event,
859 client_id: ClientId,
860 protocol: &Arc<SmbClientProtocol>,
861 llm_client: &OllamaClient,
862 app_state: &Arc<AppState>,
863 status_tx: &mpsc::UnboundedSender<String>,
864 smb_client: &SharedSmb,
865 ) -> Result<()> {
866 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
867 let memory = app_state
868 .get_memory_for_client(client_id)
869 .await
870 .unwrap_or_default();
871
872 match call_llm_for_client(
873 llm_client,
874 app_state,
875 client_id.to_string(),
876 &instruction,
877 &memory,
878 Some(event),
879 protocol.as_ref(),
880 status_tx,
881 )
882 .await
883 {
884 Ok(ClientLlmResult {
885 actions,
886 memory_updates,
887 }) => {
888 if let Some(mem) = memory_updates {
890 app_state.set_memory_for_client(client_id, mem).await;
891 }
892
893 for action in actions {
895 Box::pin(Self::execute_smb_action(
896 smb_client, action, client_id, protocol, llm_client, app_state,
897 status_tx,
898 ))
899 .await?;
900 }
901 }
902 Err(e) => {
903 error!("LLM error for SMB client {}: {}", client_id, e);
904 }
905 }
906 }
907
908 Ok(())
909 }
910}