1pub mod actions;
3
4pub use actions::MssqlClientProtocol;
5
6use crate::client::llm_budget::call_llm_for_client;
7use crate::client::mssql::actions::{
8 MSSQL_CLIENT_CONNECTED_EVENT, MSSQL_CLIENT_ERROR_EVENT, MSSQL_CLIENT_QUERY_RESULT_EVENT,
9};
10use crate::llm::actions::client_trait::{Client, ClientActionResult};
11use crate::llm::ollama_client::OllamaClient;
12use crate::llm::ClientLlmResult;
13use crate::protocol::Event;
14use crate::state::app_state::AppState;
15use crate::state::{ClientId, ClientStatus};
16use anyhow::{Context, Result};
17use futures::StreamExt;
18use serde_json::json;
19use std::net::SocketAddr;
20use std::sync::Arc;
21use tiberius::{AuthMethod, Client as TiberiusClient, Config, QueryItem, Row};
22use tokio::net::TcpStream;
23use tokio::sync::{mpsc, Mutex};
24use tokio_util::compat::TokioAsyncWriteCompatExt;
25use tracing::{debug, error, info, trace, warn};
26
27type MssqlConn = TiberiusClient<tokio_util::compat::Compat<TcpStream>>;
29
30enum MssqlApplied {
36 Query {
38 query: String,
39 result: std::result::Result<QueryResult, String>,
40 },
41 Disconnect,
43 Nothing(String),
45}
46
47pub struct MssqlClient;
49
50impl MssqlClient {
51 pub async fn connect_with_llm_actions(
53 remote_addr: String,
54 llm_client: OllamaClient,
55 app_state: Arc<AppState>,
56 status_tx: mpsc::UnboundedSender<String>,
57 client_id: ClientId,
58 ) -> Result<SocketAddr> {
59 let (host_port, config_params) = if remote_addr.contains(';') {
61 let parts: Vec<&str> = remote_addr.splitn(2, ';').collect();
62 (parts[0].to_string(), Some(parts[1].to_string()))
63 } else {
64 (remote_addr.clone(), None)
65 };
66
67 let (host, port) = if host_port.contains(':') {
69 let parts: Vec<&str> = host_port.split(':').collect();
70 (
71 parts[0].to_string(),
72 parts
73 .get(1)
74 .and_then(|p| p.parse::<u16>().ok())
75 .unwrap_or(1433),
76 )
77 } else {
78 (host_port, 1433)
79 };
80
81 let mut config = Config::new();
83 config.host(&host);
84 config.port(port);
85 config.trust_cert(); config.encryption(tiberius::EncryptionLevel::NotSupported);
92
93 if let Some(params_str) = config_params {
95 for param in params_str.split(';') {
96 let kv: Vec<&str> = param.split('=').collect();
97 if kv.len() == 2 {
98 match kv[0].to_lowercase().as_str() {
99 "database" => config.database(kv[1]),
100 "user" => config.authentication(AuthMethod::sql_server(kv[1], "")),
101 _ => {}
102 };
103 }
104 }
105 }
106
107 config.authentication(AuthMethod::None);
110
111 let tcp = TcpStream::connect((host.as_str(), port))
113 .await
114 .context(format!("Failed to connect to MSSQL at {}:{}", host, port))?;
115
116 let local_addr = tcp.local_addr()?;
117 let remote_sock_addr = tcp.peer_addr()?;
118
119 info!(
120 "MSSQL client {} connected to {} (local: {})",
121 client_id, remote_sock_addr, local_addr
122 );
123
124 let client = TiberiusClient::connect(config, tcp.compat_write())
126 .await
127 .context("Failed to create tiberius client")?;
128
129 app_state
131 .update_client_status(client_id, ClientStatus::Connected)
132 .await;
133 let _ = status_tx.send(format!("[CLIENT] MSSQL client {} connected", client_id));
134 let _ = status_tx.send("__UPDATE_UI__".to_string());
135
136 let client_arc = Arc::new(Mutex::new(client));
137 let client_for_connected = client_arc.clone();
138
139 let command_rx =
144 crate::client::command_support::register_command_channel(&app_state, client_id).await;
145 let command_task = tokio::spawn(Self::command_loop(
146 client_arc.clone(),
147 command_rx,
148 client_id,
149 llm_client.clone(),
150 app_state.clone(),
151 status_tx.clone(),
152 ));
153 app_state
154 .register_client_task(client_id, command_task)
155 .await;
156
157 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
159 let event = Event::new(
160 &MSSQL_CLIENT_CONNECTED_EVENT,
161 json!({
162 "remote_addr": remote_sock_addr.to_string(),
163 }),
164 );
165
166 match call_llm_for_client(
167 &llm_client,
168 &app_state,
169 client_id.to_string(),
170 &instruction,
171 &String::new(), Some(&event),
173 &MssqlClientProtocol::new(),
174 &status_tx,
175 )
176 .await
177 {
178 Ok(result) => {
179 for action in result.actions {
183 if let Err(e) = Self::execute_action_internal(
184 client_for_connected.clone(),
185 action,
186 client_id,
187 &llm_client,
188 &app_state,
189 &status_tx,
190 0,
191 )
192 .await
193 {
194 error!("Failed to execute action after connect: {}", e);
195 }
196 }
197 }
198 Err(e) => {
199 error!("LLM error on mssql_connected event: {}", e);
200 }
201 }
202 }
203
204 info!("MSSQL client {} ready for queries", client_id);
209
210 Ok(local_addr)
211 }
212
213 const MAX_FOLLOWUP_DEPTH: u8 = 4;
223
224 #[allow(clippy::too_many_arguments)]
230 fn execute_action_internal<'a>(
231 client: Arc<Mutex<MssqlConn>>,
232 action: serde_json::Value,
233 client_id: ClientId,
234 llm_client: &'a OllamaClient,
235 app_state: &'a Arc<AppState>,
236 status_tx: &'a mpsc::UnboundedSender<String>,
237 depth: u8,
238 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
239 Box::pin(async move {
240 let applied = Self::apply_action(&client, action, client_id).await?;
241 Self::follow_up(
242 applied, client, client_id, llm_client, app_state, status_tx, depth,
243 )
244 .await;
245 Ok(())
246 })
247 }
248
249 async fn apply_action(
256 client: &Arc<Mutex<MssqlConn>>,
257 action: serde_json::Value,
258 client_id: ClientId,
259 ) -> Result<MssqlApplied> {
260 let protocol = MssqlClientProtocol::new();
261 match protocol.execute_action(action)? {
262 ClientActionResult::Custom { name, data } if name == "mssql_query" => {
263 let query = data
264 .get("query")
265 .and_then(|v| v.as_str())
266 .context("mssql_query carried no 'query' string")?
267 .to_string();
268 debug!("MSSQL client {} executing query: {}", client_id, query);
269
270 let outcome = {
272 let mut client_guard = client.lock().await;
273 Self::execute_and_collect_query(&mut client_guard, &query).await
274 };
275 Ok(MssqlApplied::Query {
276 query,
277 result: outcome.map_err(|e| e.to_string()),
278 })
279 }
280 ClientActionResult::Disconnect => Ok(MssqlApplied::Disconnect),
281 ClientActionResult::WaitForMore => {
282 trace!("MSSQL client {} waiting for more data", client_id);
283 Ok(MssqlApplied::Nothing("wait_for_more".to_string()))
284 }
285 other => Ok(MssqlApplied::Nothing(format!(
286 "unsupported action result {other:?}"
287 ))),
288 }
289 }
290
291 #[allow(clippy::too_many_arguments)]
296 async fn follow_up(
297 applied: MssqlApplied,
298 client: Arc<Mutex<MssqlConn>>,
299 client_id: ClientId,
300 llm_client: &OllamaClient,
301 app_state: &Arc<AppState>,
302 status_tx: &mpsc::UnboundedSender<String>,
303 depth: u8,
304 ) {
305 let protocol = Arc::new(MssqlClientProtocol::new());
306
307 let event = match applied {
308 MssqlApplied::Disconnect => {
309 info!("MSSQL client {} disconnecting", client_id);
310 app_state.remove_client_handle(client_id).await;
312 app_state
313 .update_client_status(client_id, ClientStatus::Disconnected)
314 .await;
315 let _ = status_tx.send("__UPDATE_UI__".to_string());
316 return;
317 }
318 MssqlApplied::Nothing(detail) => {
319 trace!(
320 "MSSQL client {} action produced nothing: {}",
321 client_id,
322 detail
323 );
324 return;
325 }
326 MssqlApplied::Query {
327 result: Ok(result), ..
328 } => {
329 debug!(
330 "MSSQL client {} received {} columns, {} rows",
331 client_id,
332 result.columns.len(),
333 result.rows.len()
334 );
335 Event::new(
336 &MSSQL_CLIENT_QUERY_RESULT_EVENT,
337 json!({
338 "columns": result.columns,
339 "rows": result.rows,
340 "rows_affected": result.rows_affected,
341 }),
342 )
343 }
344 MssqlApplied::Query {
345 result: Err(e),
346 query,
347 } => {
348 error!("MSSQL query error on '{}': {}", query, e);
349 Event::new(
350 &MSSQL_CLIENT_ERROR_EVENT,
351 json!({
352 "error_number": 50000,
353 "message": e,
354 }),
355 )
356 }
357 };
358
359 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
360 return;
361 };
362 let memory = app_state
363 .get_memory_for_client(client_id)
364 .await
365 .unwrap_or_default();
366
367 match call_llm_for_client(
368 llm_client,
369 app_state,
370 client_id.to_string(),
371 &instruction,
372 &memory,
373 Some(&event),
374 protocol.as_ref(),
375 status_tx,
376 )
377 .await
378 {
379 Ok(ClientLlmResult {
380 actions,
381 memory_updates,
382 }) => {
383 if let Some(mem) = memory_updates {
384 app_state.set_memory_for_client(client_id, mem).await;
385 }
386 if depth + 1 >= Self::MAX_FOLLOWUP_DEPTH {
390 if !actions.is_empty() {
391 warn!(
392 "MSSQL client {} reached the follow-up depth limit of {}; dropping \
393 {} further action(s)",
394 client_id,
395 Self::MAX_FOLLOWUP_DEPTH,
396 actions.len()
397 );
398 }
399 return;
400 }
401
402 for follow_action in actions {
403 if let Err(e) = Self::execute_action_internal(
404 client.clone(),
405 follow_action,
406 client_id,
407 llm_client,
408 app_state,
409 status_tx,
410 depth + 1,
411 )
412 .await
413 {
414 error!("Failed to execute follow-up action: {}", e);
415 }
416 }
417 }
418 Err(e) => {
419 error!("LLM error for MSSQL client {}: {}", client_id, e);
420 }
421 }
422 }
423
424 async fn command_loop(
435 client: Arc<Mutex<MssqlConn>>,
436 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
437 client_id: ClientId,
438 llm_client: OllamaClient,
439 app_state: Arc<AppState>,
440 status_tx: mpsc::UnboundedSender<String>,
441 ) {
442 use crate::llm::actions::protocol_trait::Protocol;
443 use crate::state::client_handles::ClientSendOutcome;
444 use crate::state::AccessLogOwner;
445
446 let protocol = MssqlClientProtocol::new();
447
448 while let Some(command) = command_rx.recv().await {
449 let action = command.action.clone();
450 let mut applied_for_follow_up = None;
451 let mut disconnect = false;
452
453 let outcome: Result<ClientSendOutcome> =
454 match Self::apply_action(&client, action.clone(), client_id).await {
455 Err(e) => Ok(ClientSendOutcome::Rejected {
457 error: e.to_string(),
458 }),
459 Ok(MssqlApplied::Disconnect) => {
460 disconnect = true;
461 Ok(ClientSendOutcome::Disconnected)
462 }
463 Ok(MssqlApplied::Nothing(detail)) => Ok(ClientSendOutcome::Executed { detail }),
464 Ok(MssqlApplied::Query {
465 query,
466 result: Err(e),
467 }) => Err(anyhow::anyhow!("query '{query}' failed: {e}")),
468 Ok(MssqlApplied::Query {
469 query,
470 result: Ok(result),
471 }) => {
472 let detail = format!(
473 "execute_query '{}': {} row(s), {} column(s)",
474 query,
475 result.rows.len(),
476 result.columns.len()
477 );
478 applied_for_follow_up = Some(MssqlApplied::Query {
479 query,
480 result: Ok(result),
481 });
482 Ok(ClientSendOutcome::Executed { detail })
483 }
484 };
485
486 let outcome_json = match &outcome {
487 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
488 Err(e) => serde_json::json!({"error": e.to_string()}),
489 };
490 app_state
491 .record_access_log(
492 AccessLogOwner::Client(client_id.as_u32()),
493 protocol.protocol_name(),
494 None,
495 "injected_action",
496 action,
497 vec![outcome_json],
498 )
499 .await;
500
501 if let Err(e) = &outcome {
502 error!("MSSQL client {} injected action failed: {}", client_id, e);
503 let _ = status_tx.send(format!(
504 "[WARN] Client {} injected action failed: {}",
505 client_id, e
506 ));
507 }
508 let _ = status_tx.send("__UPDATE_UI__".to_string());
509 crate::client::command_support::reply(command, outcome);
510
511 if disconnect {
512 app_state.remove_client_handle(client_id).await;
513 app_state
514 .update_client_status(client_id, ClientStatus::Disconnected)
515 .await;
516 let _ = status_tx.send("__UPDATE_UI__".to_string());
517 break;
518 }
519
520 if let Some(applied) = applied_for_follow_up {
523 let client = client.clone();
524 let llm_client = llm_client.clone();
525 let state = app_state.clone();
526 let tx = status_tx.clone();
527 let handle = tokio::spawn(async move {
528 Self::follow_up(applied, client, client_id, &llm_client, &state, &tx, 0).await;
531 });
532 app_state.register_client_task(client_id, handle).await;
533 }
534 }
535
536 app_state.remove_client_handle(client_id).await;
537 let _ = status_tx.send("__UPDATE_UI__".to_string());
538 }
539
540 async fn execute_and_collect_query(
542 client: &mut TiberiusClient<tokio_util::compat::Compat<TcpStream>>,
543 query: &str,
544 ) -> Result<QueryResult> {
545 let mut stream = client.query(query, &[]).await?;
546
547 let mut columns = Vec::new();
548 let mut rows = Vec::new();
549 let rows_affected: u64;
550
551 if let Some(cols) = stream.columns().await? {
553 for col in cols {
554 columns.push(json!({
555 "name": col.name(),
556 "type": format!("{:?}", col.column_type()),
557 }));
558 }
559 }
560
561 while let Some(item) = stream.next().await {
563 match item {
564 Ok(QueryItem::Row(row)) => {
565 let row_values = Self::row_to_json(&row)?;
566 rows.push(row_values);
567 }
568 Ok(QueryItem::Metadata(_)) => {
569 }
571 Err(e) => {
572 return Err(e.into());
573 }
574 }
575 }
576
577 rows_affected = rows.len() as u64;
579
580 Ok(QueryResult {
581 columns,
582 rows,
583 rows_affected,
584 })
585 }
586
587 #[allow(dead_code)]
589 async fn collect_query_results(mut stream: tiberius::QueryStream<'_>) -> Result<QueryResult> {
590 let mut columns = Vec::new();
591 let mut rows = Vec::new();
592
593 if let Some(cols) = stream.columns().await? {
595 for col in cols {
596 columns.push(json!({
597 "name": col.name(),
598 "type": format!("{:?}", col.column_type()),
599 }));
600 }
601 }
602
603 while let Some(item) = stream.next().await {
605 match item {
606 Ok(QueryItem::Row(row)) => {
607 let row_values = Self::row_to_json(&row)?;
608 rows.push(row_values);
609 }
610 Ok(QueryItem::Metadata(_)) => {
611 }
613 Err(e) => {
614 return Err(e.into());
615 }
616 }
617 }
618
619 let rows_affected = rows.len() as u64;
623
624 Ok(QueryResult {
625 columns,
626 rows,
627 rows_affected,
628 })
629 }
630
631 fn row_to_json(row: &Row) -> Result<serde_json::Value> {
633 let mut values = Vec::new();
634
635 for i in 0..row.len() {
636 let value = if let Ok(Some(s)) = row.try_get::<&str, _>(i) {
637 json!(s)
638 } else if let Ok(Some(n)) = row.try_get::<i32, _>(i) {
639 json!(n)
640 } else if let Ok(Some(n)) = row.try_get::<i64, _>(i) {
641 json!(n)
642 } else if let Ok(Some(b)) = row.try_get::<bool, _>(i) {
643 json!(b)
644 } else if let Ok(Some(f)) = row.try_get::<f32, _>(i) {
645 json!(f)
646 } else if let Ok(Some(f)) = row.try_get::<f64, _>(i) {
647 json!(f)
648 } else {
649 json!(null)
651 };
652
653 values.push(value);
654 }
655
656 Ok(json!(values))
657 }
658}
659
660struct QueryResult {
662 columns: Vec<serde_json::Value>,
663 rows: Vec<serde_json::Value>,
664 rows_affected: u64,
665}