Skip to main content

netget/server/snowflake/
mod.rs

1//! Snowflake server implementation
2//!
3//! Serves the subset of Snowflake's REST/JSON client protocol that a driver needs
4//! to log in and run a query:
5//!
6//! | Endpoint | Event | Model answers with |
7//! |---|---|---|
8//! | `POST /session/v1/login-request` | `snowflake_login` | a session token (or a refusal) |
9//! | `POST /queries/v1/query-request` | `snowflake_query` | a rowset (or an error) |
10//! | `POST /session/logout-request` | `snowflake_session` (logout) | an ack |
11//! | `POST /session/token-request` | `snowflake_session` (token_renew) | renewed tokens |
12//!
13//! hyper owns HTTP; the LLM owns the JSON. **No storage**: there is no session
14//! table and no row store — the model answers every request.
15//!
16//! **Fail closed.** When the model gives no usable answer, or the LLM call fails,
17//! every endpoint replies with a Snowflake error envelope (`success:false`), never
18//! a success-shaped empty result. On the login endpoint that means no token is ever
19//! issued on an outage — an LLM failure is a refusal, and it is distinguishable in
20//! the logs (and by message) from a deliberate model denial via `snowflake_error`.
21
22pub mod actions;
23
24use std::convert::Infallible;
25use std::net::SocketAddr;
26use std::sync::Arc;
27
28use bytes::Bytes;
29use http_body_util::{BodyExt, Full, Limited};
30use hyper::body::{Body, Incoming};
31use hyper::server::conn::http1;
32use hyper::service::service_fn;
33use hyper::{Method, Request, Response};
34use hyper_util::rt::TokioIo;
35use serde_json::{json, Value};
36use tokio::sync::mpsc;
37use tracing::{debug, error};
38
39use crate::llm::action_helper::call_llm;
40use crate::llm::actions::protocol_trait::ActionResult;
41use crate::llm::ollama_client::OllamaClient;
42use crate::logging::emit::Log;
43use crate::protocol::Event;
44use crate::server::accept_bounded::{
45    accept_bounded, watch_idle, ConnectionActivity, ConnectionLimiter,
46};
47use crate::server::connection::ConnectionId;
48use crate::server::snowflake::actions::{
49    SnowflakeProtocol, SNOWFLAKE_LOGIN_EVENT, SNOWFLAKE_QUERY_EVENT, SNOWFLAKE_SESSION_EVENT,
50};
51use crate::state::app_state::AppState;
52
53/// Largest request body this server will buffer.
54///
55/// `read_json_body` used an unbounded `req.into_body().collect()`, so an unauthenticated
56/// POST to `/session/v1/login-request` could grow the process without limit. A login body
57/// is a few hundred bytes and a query body is one SQL statement; 4 MiB is far beyond what
58/// any driver sends.
59pub const MAX_REQUEST_BYTES: usize = 4 * 1024 * 1024;
60
61/// Snowflake error code sent when the authentication backend (the LLM) is
62/// unavailable. 390100 is Snowflake's "incorrect username or password" — a
63/// refusal a driver understands as an auth failure. Using it on an outage keeps
64/// the login path fail-closed: no token is issued.
65const CODE_AUTH_UNAVAILABLE: &str = "390100";
66/// Generic internal error code used for a failed query/session request.
67const CODE_INTERNAL: &str = "000603";
68/// Server-busy style code used when the failure is LLM overload (retryable).
69const CODE_REQUEST_TIMEOUT: &str = "000629";
70
71/// How long a peer that has produced nothing at all may hold this connection.
72///
73/// HTTP is client-speaks-first: the request line is the first thing on the wire and every
74/// driver sends it inside its dial path, so a peer that has connected and sent no byte
75/// has begun no request. Thirty seconds sits between nginx's `client_header_timeout` default of
76/// 60s and Apache's `RequestReadTimeout header=20`, both of which bound the same thing.
77///
78/// Enforced with `TcpStream::peek` **before** hyper sees the socket. hyper owns every read once
79/// `serve_connection` starts, and a deadline on its reads would be wrong here rather than merely
80/// awkward: hyper keeps polling the connection for new frames while a request is being answered,
81/// so such a deadline would fire in the middle of an LLM round-trip. `peek` waits for data
82/// without consuming it, so the request line is still there for hyper afterwards.
83const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
84
85/// How long an established connection may sit **silent** between requests.
86///
87/// This bounds the silence and never a transfer. A request being answered holds
88/// `ConnectionActivity` busy for the whole of it - the model round-trip included, and a `manual`
89/// rule parking the event for a human at the dashboard (`src/state/intercepts.rs`, 300s by
90/// default) - and `watch_idle` reports a busy connection as not idle at all, so the clock only
91/// ever runs on a connection with nothing in flight.
92///
93/// Five minutes. A Snowflake driver logs in, submits a query and then fetches the result on the
94/// same pooled connection, with gaps while the application consumes a result set - that gap is
95/// the only thing measured here, because a query being answered is a request in flight and is
96/// held busy for the whole of the model round-trip. Five minutes is four times nginx's
97/// `keepalive_timeout` default of 75s; a driver whose idle connection is closed reopens it and
98/// carries on with the session token it already holds, which is what RFC 9112 requires of it.
99const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
100
101/// Concurrent connections this server admits.
102///
103/// Each admitted connection may hold one whole in-memory response body - a result set - so the
104/// cap is what turns that per-connection bound into a total one, alongside
105/// [`MAX_REQUEST_BYTES`]. A driver pool is tens of connections at most, so 256 is far above any
106/// real client and far below what socket exhaustion needs.
107const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
108
109/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
110///
111/// `503 Service Unavailable` with `Retry-After`, in Snowflake's own vocabulary:
112/// a 503 with `Retry-After` is read by the drivers' HTTP layer as a
113/// retryable server condition, and a human with `curl` reads it directly.
114/// Nothing of netget's is interpolated into it - the peer gets a category and the log gets the
115/// reason, under `decision=fail_closed_connection_cap`.
116const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
117    Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
118
119/// Snowflake server.
120pub struct SnowflakeServer;
121
122impl SnowflakeServer {
123    /// Spawn the Snowflake server. Binds with `?` so a bind failure surfaces as
124    /// `ServerStatus::Error`, and registers the accept-loop `JoinHandle` so
125    /// `stop_server` can release the socket.
126    pub async fn spawn_with_llm_actions(
127        listen_addr: SocketAddr,
128        llm_client: OllamaClient,
129        app_state: Arc<AppState>,
130        status_tx: mpsc::UnboundedSender<String>,
131        server_id: crate::state::ServerId,
132    ) -> anyhow::Result<SocketAddr> {
133        let listener =
134            crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
135        let local_addr = listener.local_addr()?;
136
137        Log::new(Some(&status_tx)).info(format!("Snowflake server listening on {}", local_addr));
138
139        let protocol = Arc::new(SnowflakeProtocol::new(
140            ConnectionId::new(0),
141            app_state.clone(),
142            status_tx.clone(),
143        ));
144
145        let task_registrar = app_state.clone();
146        let limiter = ConnectionLimiter::new(MAX_CONNECTIONS);
147        let accept_handle = tokio::spawn(async move {
148            loop {
149                match accept_bounded(
150                    &listener,
151                    &limiter,
152                    CONNECTION_CAP_REFUSAL,
153                    "SNOWFLAKE",
154                    Some(&status_tx),
155                )
156                .await
157                {
158                    Ok((stream, remote_addr, permit)) => {
159                        let connection_id =
160                            ConnectionId::new(app_state.get_next_unified_id().await);
161                        let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
162                        debug!(
163                            "Snowflake connection {} from {}",
164                            connection_id, remote_addr
165                        );
166
167                        use crate::state::server::{
168                            ConnectionState as ServerConnectionState, ConnectionStatus,
169                            ProtocolConnectionInfo,
170                        };
171                        let now = crate::utils::clock::Instant::now();
172                        let conn_state = ServerConnectionState {
173                            id: connection_id,
174                            remote_addr,
175                            local_addr: local_addr_conn,
176                            bytes_sent: 0,
177                            bytes_received: 0,
178                            packets_sent: 0,
179                            packets_received: 0,
180                            last_activity: now,
181                            status: ConnectionStatus::Active,
182                            status_changed_at: now,
183                            protocol_info: ProtocolConnectionInfo::empty(),
184                        };
185                        app_state
186                            .add_connection_to_server(server_id, conn_state)
187                            .await;
188                        let _ = status_tx.send("__UPDATE_UI__".to_string());
189
190                        let llm_client_clone = llm_client.clone();
191                        let app_state_clone = app_state.clone();
192                        let status_tx_clone = status_tx.clone();
193                        let protocol_clone = protocol.clone();
194
195                        let task_owner = app_state.clone();
196                        task_owner
197                            .spawn_server_task(server_id, async move {
198                                // Held for the life of the connection, so the
199                                // cap counts live clients, not accepts.
200                                let _permit = permit;
201
202                                // The first-byte bound, before hyper sees the
203                                // socket. `peek` waits for data without
204                                // consuming it, so the request line is still
205                                // there for hyper afterwards, and it bounds
206                                // exactly the case that needs bounding: a peer
207                                // that connected and said nothing at all.
208                                let spoke = matches!(
209                                    tokio::time::timeout(
210                                        FIRST_BYTE_READ_TIMEOUT,
211                                        stream.peek(&mut [0u8; 1]),
212                                    )
213                                    .await,
214                                    Ok(Ok(n)) if n > 0
215                                );
216
217                                if spoke {
218                                    let io = TokioIo::new(stream);
219                                    let status_for_service = status_tx_clone.clone();
220                                    let app_state_for_service = app_state_clone.clone();
221
222                                    // Whether this connection is answering
223                                    // anything. The watchdog below reads it, so
224                                    // only genuine silence - never work in
225                                    // flight - can close a connection.
226                                    let activity =
227                                        Arc::new(ConnectionActivity::new());
228                                    let activity_for_service = Arc::clone(&activity);
229                                    let service = service_fn(move |req: Request<Incoming>| {
230                                        let llm_clone = llm_client_clone.clone();
231                                        let state_clone = app_state_for_service.clone();
232                                        let status_clone = status_for_service.clone();
233                                        let protocol_clone = protocol_clone.clone();
234                                        // Busy for the whole of this request - the
235                                        // model round-trip included, and a `manual`
236                                        // rule parked for a human - so the idle
237                                        // watchdog can never close the connection an
238                                        // answer belongs to.
239                                        let activity = Arc::clone(&activity_for_service);
240                                        async move {
241                                            let _busy = activity.busy();
242                                            handle_snowflake_request(
243                                                req,
244                                                connection_id,
245                                                server_id,
246                                                llm_clone,
247                                                state_clone,
248                                                status_clone,
249                                                protocol_clone,
250                                            )
251                                            .await
252                                        }
253                                    });
254
255                                    let conn = http1::Builder::new().serve_connection(io, service);
256                                    tokio::pin!(conn);
257                                    tokio::select! {
258                                        result = &mut conn => {
259                                            if let Err(err) = result {
260                                                error!("Error serving Snowflake connection: {:?}", err);
261                                            }
262                                        }
263                                        // The idle bound, over `ConnectionActivity`
264                                        // rather than over a read: hyper owns every
265                                        // read once `serve_connection` starts, and
266                                        // keeps polling for frames while a request is
267                                        // being answered, so a deadline on those reads
268                                        // would fire mid-answer. A connection with work
269                                        // in flight is not idle at all.
270                                        _ = watch_idle(
271                                            Arc::clone(&activity),
272                                            IDLE_BETWEEN_REQUESTS_TIMEOUT,
273                                        ) => {
274                                            Log::new(Some(&status_tx_clone)).debug(format!(
275                                                "Snowflake connection {} idle for {}s; closing",
276                                                connection_id,
277                                                IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
278                                            ));
279                                        }
280                                    }
281                                } else {
282                                    Log::new(Some(&status_tx_clone)).debug(format!(
283                                        "Snowflake peer {} sent no request within {}s; closing \
284                                         decision=fail_closed_first_byte_timeout",
285                                        remote_addr,
286                                        FIRST_BYTE_READ_TIMEOUT.as_secs()
287                                    ));
288                                }
289
290                                app_state_clone
291                                    .close_connection_on_server(server_id, connection_id)
292                                    .await;
293                                Log::new(Some(&status_tx_clone))
294                                    .info(format!("Snowflake connection {connection_id} closed"));
295                                let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
296                            })
297                            .await;
298                    }
299                    Err(e) => {
300                        Log::new(Some(&status_tx))
301                            .error(format!("Failed to accept Snowflake connection: {}", e));
302                        break;
303                    }
304                }
305            }
306        });
307
308        task_registrar
309            .register_server_task(server_id, accept_handle)
310            .await;
311
312        Ok(local_addr)
313    }
314}
315
316/// Build a `200 OK application/json` response from a server-generated JSON body.
317///
318/// The body is our own JSON (the model influences only values inside it, which are
319/// serialized safely), the status is a constant, and the headers are constant, so
320/// this cannot fail — but it still falls back to a bare 500 rather than `unwrap()`
321/// on the builder, per the codebase's no-panic-on-response rule.
322fn json_200(body: String) -> Response<Full<Bytes>> {
323    match Response::builder()
324        .status(200)
325        .header(hyper::header::CONTENT_TYPE, "application/json")
326        .body(Full::new(Bytes::from(body)))
327    {
328        Ok(resp) => resp,
329        Err(e) => {
330            error!("Snowflake: failed to build response ({e}), sending bare 500");
331            let mut fallback = Response::new(Full::new(Bytes::from_static(b"{\"success\":false}")));
332            *fallback.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
333            fallback
334        }
335    }
336}
337
338/// The `decision=` token for a `call_llm` that returned `Err`.
339///
340/// Both variants refuse and both carry the `fail_closed_` prefix the documented
341/// `grep decision=fail_closed` matches; only the saturated one is worth retrying.
342fn llm_error_decision(err: &anyhow::Error) -> &'static str {
343    match crate::utils::WireFailure::classify(err) {
344        crate::utils::WireFailure::Overloaded => "fail_closed_llm_overloaded",
345        crate::utils::WireFailure::Unavailable => "fail_closed_llm_error",
346    }
347}
348
349/// A Snowflake error envelope: `{"data":null,"code":..,"message":..,"success":false}`.
350///
351/// Used where no session exists yet (a refused login, an unknown endpoint), so `data` carries
352/// nothing at all — in particular never a token.
353fn error_envelope(code: &str, message: &str) -> String {
354    json!({
355        "data": Value::Null,
356        "code": code,
357        "message": message,
358        "success": false,
359    })
360    .to_string()
361}
362
363/// The error envelope for a refused query or session request, which carries
364/// `data.{errorCode, sqlState}` as Snowflake's does.
365///
366/// The drivers read a refused statement's SQLSTATE from `data.sqlState` — with `data: null`
367/// snowflake-connector-python raises the model's error with SQLSTATE `n/a` — so the model's
368/// `sql_state` travels here.
369fn statement_error_envelope(code: &str, message: &str, sql_state: Option<&str>) -> String {
370    let mut data = json!({ "errorCode": code });
371    if let Some(state) = sql_state {
372        data["sqlState"] = json!(state);
373    }
374    json!({
375        "data": data,
376        "code": code,
377        "message": message,
378        "success": false,
379    })
380    .to_string()
381}
382
383async fn handle_snowflake_request(
384    req: Request<Incoming>,
385    connection_id: ConnectionId,
386    server_id: crate::state::ServerId,
387    llm_client: OllamaClient,
388    app_state: Arc<AppState>,
389    status_tx: mpsc::UnboundedSender<String>,
390    protocol: Arc<SnowflakeProtocol>,
391) -> Result<Response<Full<Bytes>>, Infallible> {
392    let method = req.method().clone();
393    let uri = req.uri().clone();
394    let path = uri.path().to_string();
395    let query = uri.query().unwrap_or("").to_string();
396
397    Log::new(Some(&status_tx)).debug(format!("Snowflake request: {} {}", method, path));
398
399    app_state
400        .update_connection_stats(server_id, connection_id, None, None, Some(1), None)
401        .await;
402
403    // Whether the client presented an Authorization session token.
404    let has_auth_token = req
405        .headers()
406        .get(hyper::header::AUTHORIZATION)
407        .and_then(|v| v.to_str().ok())
408        .map(|s| s.contains("Token=") && !s.contains("Token=\"\""))
409        .unwrap_or(false);
410
411    let response = match (method.clone(), path.as_str()) {
412        (Method::POST, "/session/v1/login-request")
413        | (Method::POST, "/session/authenticator-request") => {
414            handle_login(
415                req,
416                connection_id,
417                server_id,
418                llm_client,
419                app_state.clone(),
420                status_tx.clone(),
421                protocol,
422            )
423            .await
424        }
425        (Method::POST, "/queries/v1/query-request") => {
426            handle_query(
427                req,
428                connection_id,
429                server_id,
430                llm_client,
431                app_state.clone(),
432                status_tx.clone(),
433                protocol,
434                has_auth_token,
435                &query,
436            )
437            .await
438        }
439        // The drivers end a session with `POST /session?delete=true` (snowflake-connector-
440        // python's `delete_session`, gosnowflake's close); `/session/logout-request` is kept
441        // for anything that used it.
442        (Method::POST, "/session/logout-request") | (Method::POST, "/session")
443            if path != "/session" || query.split('&').any(|kv| kv == "delete=true") =>
444        {
445            handle_session(
446                connection_id,
447                server_id,
448                llm_client,
449                app_state.clone(),
450                status_tx.clone(),
451                protocol,
452                "logout",
453                has_auth_token,
454            )
455            .await
456        }
457        (Method::POST, "/session/token-request") => {
458            handle_session(
459                connection_id,
460                server_id,
461                llm_client,
462                app_state.clone(),
463                status_tx.clone(),
464                protocol,
465                "token_renew",
466                has_auth_token,
467            )
468            .await
469        }
470        _ => {
471            debug!(
472                "Snowflake conn={} {} {} decision=unknown_endpoint: no LLM call",
473                connection_id, method, path
474            );
475            Ok(json_200(error_envelope(
476                "390318",
477                "Unknown Snowflake endpoint",
478            )))
479        }
480    };
481
482    let body_size = response
483        .as_ref()
484        .ok()
485        .and_then(|resp| resp.body().size_hint().exact())
486        .unwrap_or(0);
487    app_state
488        .update_connection_stats(
489            server_id,
490            connection_id,
491            None,
492            Some(body_size),
493            None,
494            Some(1),
495        )
496        .await;
497    let _ = status_tx.send("__UPDATE_UI__".to_string());
498    response
499}
500
501/// Read a request body into a JSON value, capped at [`MAX_REQUEST_BYTES`].
502///
503/// `Value::Null` on failure, which every caller treats as "no fields present" — and since
504/// each endpoint fails closed when the model cannot answer from those fields, an oversized
505/// or unreadable body can only ever lose a login, never grant one.
506async fn read_json_body(req: Request<Incoming>) -> Value {
507    let gzip = req
508        .headers()
509        .get(hyper::header::CONTENT_ENCODING)
510        .and_then(|v| v.to_str().ok())
511        .is_some_and(|v| v.trim().eq_ignore_ascii_case("gzip"));
512    match Limited::new(req.into_body(), MAX_REQUEST_BYTES)
513        .collect()
514        .await
515    {
516        Ok(collected) => {
517            let bytes = collected.to_bytes();
518            if gzip {
519                return match gunzip_bounded(&bytes) {
520                    Ok(plain) => serde_json::from_slice(&plain).unwrap_or(Value::Null),
521                    Err(e) => {
522                        error!("Snowflake: gzip request body rejected: {e}");
523                        Value::Null
524                    }
525                };
526            }
527            serde_json::from_slice(&bytes).unwrap_or(Value::Null)
528        }
529        Err(e) => {
530            error!(
531                "Snowflake: request body rejected (limit {} bytes): {}",
532                MAX_REQUEST_BYTES, e
533            );
534            Value::Null
535        }
536    }
537}
538
539/// Inflate a `Content-Encoding: gzip` request body, refusing past [`MAX_REQUEST_BYTES`].
540///
541/// Every official connector compresses its request bodies — snowflake-connector-python gzips
542/// each POST — and until a real driver was pointed at this server it read the compressed bytes
543/// as JSON, found none, and refused every login. The bound applies to the **inflated** size,
544/// since a few KiB of gzip can expand to gigabytes.
545fn gunzip_bounded(compressed: &[u8]) -> std::io::Result<Vec<u8>> {
546    use std::io::Read;
547    let mut plain = Vec::new();
548    flate2::read::GzDecoder::new(compressed)
549        .take(MAX_REQUEST_BYTES as u64 + 1)
550        .read_to_end(&mut plain)?;
551    if plain.len() > MAX_REQUEST_BYTES {
552        return Err(std::io::Error::other(format!(
553            "inflates past {MAX_REQUEST_BYTES} bytes"
554        )));
555    }
556    Ok(plain)
557}
558
559/// Find the first `snowflake_*` custom payload the executor produced.
560fn first_custom(results: Vec<ActionResult>) -> Option<(String, Value)> {
561    for r in results {
562        if let ActionResult::Custom { name, data } = r {
563            return Some((name, data));
564        }
565    }
566    None
567}
568
569#[allow(clippy::too_many_arguments)]
570async fn handle_login(
571    req: Request<Incoming>,
572    connection_id: ConnectionId,
573    server_id: crate::state::ServerId,
574    llm_client: OllamaClient,
575    app_state: Arc<AppState>,
576    status_tx: mpsc::UnboundedSender<String>,
577    protocol: Arc<SnowflakeProtocol>,
578) -> Result<Response<Full<Bytes>>, Infallible> {
579    let body = read_json_body(req).await;
580    // Login body shape: {"data": {"LOGIN_NAME":..,"PASSWORD":..,"ACCOUNT_NAME":..,
581    //                             "CLIENT_APP_ID":..,"CLIENT_APP_VERSION":..}}
582    let data = body.get("data").cloned().unwrap_or(Value::Null);
583    let login_name = data
584        .get("LOGIN_NAME")
585        .and_then(|v| v.as_str())
586        .unwrap_or("")
587        .to_string();
588    let account = data
589        .get("ACCOUNT_NAME")
590        .and_then(|v| v.as_str())
591        .unwrap_or("")
592        .to_string();
593    let has_password = data
594        .get("PASSWORD")
595        .and_then(|v| v.as_str())
596        .map(|s| !s.is_empty())
597        .unwrap_or(false);
598
599    let event = Event::new(
600        &SNOWFLAKE_LOGIN_EVENT,
601        json!({
602            "login_name": login_name,
603            "account": account,
604            "client_app_id": data.get("CLIENT_APP_ID").and_then(|v| v.as_str()).unwrap_or(""),
605            "client_app_version": data.get("CLIENT_APP_VERSION").and_then(|v| v.as_str()).unwrap_or(""),
606            "has_password": has_password,
607        }),
608    );
609
610    match call_llm(
611        &llm_client,
612        &app_state,
613        server_id,
614        Some(connection_id),
615        &event,
616        &*protocol,
617    )
618    .await
619    {
620        Ok(execution_result) => match first_custom(execution_result.protocol_results) {
621            Some((name, payload)) if name == "snowflake_login_success" => {
622                let token = payload
623                    .get("token")
624                    .and_then(|v| v.as_str())
625                    .unwrap_or("")
626                    .to_string();
627                if token.is_empty() {
628                    // Fail closed: a "success" with no token is unusable — refuse.
629                    Log::new(Some(&status_tx)).error(format!(
630                        "Snowflake login conn={} user={} decision=fail_closed_bad_action: \
631                         login_success carried no token; refusing the login",
632                        connection_id, login_name
633                    ));
634                    return Ok(json_200(error_envelope(
635                        CODE_AUTH_UNAVAILABLE,
636                        "netget: login backend returned no token",
637                    )));
638                }
639                let validity = payload
640                    .get("validity_seconds")
641                    .and_then(|v| v.as_u64())
642                    .unwrap_or(3600);
643                let master = payload
644                    .get("master_token")
645                    .and_then(|v| v.as_str())
646                    .map(|s| s.to_string())
647                    .unwrap_or_else(|| token.clone());
648                let session_id = payload.get("session_id").cloned().unwrap_or(Value::Null);
649
650                let out = json!({
651                    "data": {
652                        "token": token,
653                        "masterToken": master,
654                        "sessionId": session_id,
655                        "validityInSeconds": validity,
656                        "masterValidityInSeconds": validity * 4,
657                        "displayUserName": login_name,
658                        "serverVersion": "8.0.0",
659                        "firstLogin": false,
660                        "healthCheckInterval": 45,
661                        "sessionInfo": {
662                            "databaseName": Value::Null,
663                            "schemaName": Value::Null,
664                            "warehouseName": Value::Null,
665                            "roleName": "PUBLIC"
666                        },
667                        "parameters": []
668                    },
669                    "message": Value::Null,
670                    "code": Value::Null,
671                    "success": true
672                });
673                Log::new(Some(&status_tx)).info(format!(
674                    "Snowflake login conn={} user={} decision=model_answer: session token issued",
675                    connection_id, login_name
676                ));
677                Ok(json_200(out.to_string()))
678            }
679            Some((name, payload)) if name == "snowflake_error" => {
680                // Deliberate model denial — carries the model's code/message.
681                let code = payload
682                    .get("code")
683                    .and_then(|v| v.as_str())
684                    .unwrap_or(CODE_AUTH_UNAVAILABLE);
685                let message = payload
686                    .get("message")
687                    .and_then(|v| v.as_str())
688                    .unwrap_or("Login refused");
689                Log::new(Some(&status_tx)).info(format!(
690                    "Snowflake login conn={} user={} decision=model_reject: {}",
691                    connection_id, login_name, code
692                ));
693                Ok(json_200(error_envelope(code, message)))
694            }
695            _ => {
696                // No usable answer → refuse (fail closed), do not fabricate a token.
697                Log::new(Some(&status_tx)).warn(format!(
698                    "Snowflake login conn={} user={} decision=model_silent: no login action \
699                     produced; refusing the login",
700                    connection_id, login_name
701                ));
702                Ok(json_200(error_envelope(
703                    CODE_AUTH_UNAVAILABLE,
704                    "netget: no login decision produced",
705                )))
706            }
707        },
708        Err(e) => {
709            // LLM outage: refuse. No token is ever issued on failure, and the
710            // message marks this as a backend outage, distinct from a model denial.
711            Log::new(Some(&status_tx)).error(format!(
712                "Snowflake login conn={} user={} decision={}: refusing, no token issued: {}",
713                connection_id,
714                login_name,
715                llm_error_decision(&e),
716                e
717            ));
718            Ok(json_200(error_envelope(
719                CODE_AUTH_UNAVAILABLE,
720                crate::utils::WireFailure::classify(&e).prefixed_text(),
721            )))
722        }
723    }
724}
725
726#[allow(clippy::too_many_arguments)]
727async fn handle_query(
728    req: Request<Incoming>,
729    connection_id: ConnectionId,
730    server_id: crate::state::ServerId,
731    llm_client: OllamaClient,
732    app_state: Arc<AppState>,
733    status_tx: mpsc::UnboundedSender<String>,
734    protocol: Arc<SnowflakeProtocol>,
735    has_auth_token: bool,
736    query_string: &str,
737) -> Result<Response<Full<Bytes>>, Infallible> {
738    let body = read_json_body(req).await;
739    let sql_text = body
740        .get("sqlText")
741        .and_then(|v| v.as_str())
742        .unwrap_or("")
743        .to_string();
744    let request_id = query_string
745        .split('&')
746        .find_map(|kv| kv.strip_prefix("requestId="))
747        .unwrap_or("")
748        .to_string();
749
750    let event = Event::new(
751        &SNOWFLAKE_QUERY_EVENT,
752        json!({
753            "sql_text": sql_text,
754            "has_auth_token": has_auth_token,
755            "request_id": request_id,
756        }),
757    );
758
759    match call_llm(
760        &llm_client,
761        &app_state,
762        server_id,
763        Some(connection_id),
764        &event,
765        &*protocol,
766    )
767    .await
768    {
769        Ok(execution_result) => match first_custom(execution_result.protocol_results) {
770            Some((name, payload)) if name == "snowflake_query_response" => {
771                let rowtype = normalize_rowtype(payload.get("rowtype"));
772                let rowset = normalize_rowset(payload.get("rowset"));
773                let returned = rowset.len();
774                let query_id = payload
775                    .get("query_id")
776                    .and_then(|v| v.as_str())
777                    .map(|s| s.to_string())
778                    .unwrap_or_else(|| "00000000-0000-0000-0000-000000000000".to_string());
779
780                let out = json!({
781                    "data": {
782                        "parameters": [],
783                        "rowtype": rowtype,
784                        "rowsetBase64": Value::Null,
785                        "rowset": rowset,
786                        "total": returned,
787                        "returned": returned,
788                        "queryId": query_id,
789                        "queryResultFormat": "json",
790                        "finalDatabaseName": Value::Null,
791                        "finalSchemaName": Value::Null,
792                    },
793                    "message": Value::Null,
794                    "code": Value::Null,
795                    "success": true
796                });
797                Log::new(Some(&status_tx)).info(format!(
798                    "Snowflake query conn={} auth={} decision=model_answer: {} rows",
799                    connection_id, has_auth_token, returned
800                ));
801                Ok(json_200(out.to_string()))
802            }
803            Some((name, payload)) if name == "snowflake_error" => {
804                let code = payload
805                    .get("code")
806                    .and_then(|v| v.as_str())
807                    .unwrap_or(CODE_INTERNAL);
808                let message = payload
809                    .get("message")
810                    .and_then(|v| v.as_str())
811                    .unwrap_or("Query failed");
812                Log::new(Some(&status_tx)).info(format!(
813                    "Snowflake query conn={} decision=model_reject: {}",
814                    connection_id, code
815                ));
816                Ok(json_200(statement_error_envelope(
817                    code,
818                    message,
819                    payload.get("sql_state").and_then(|v| v.as_str()),
820                )))
821            }
822            _ => {
823                Log::new(Some(&status_tx)).warn(format!(
824                    "Snowflake query conn={} decision=model_silent: no query action produced; \
825                     returning an error envelope",
826                    connection_id
827                ));
828                Ok(json_200(statement_error_envelope(
829                    CODE_INTERNAL,
830                    "netget: no query result produced",
831                    None,
832                )))
833            }
834        },
835        Err(e) => {
836            let overloaded = crate::llm::is_overload_error(&e);
837            Log::new(Some(&status_tx)).error(format!(
838                "Snowflake query conn={} decision={}: {}",
839                connection_id,
840                llm_error_decision(&e),
841                e
842            ));
843            let code = if overloaded {
844                CODE_REQUEST_TIMEOUT
845            } else {
846                CODE_INTERNAL
847            };
848            Ok(json_200(statement_error_envelope(
849                code,
850                crate::utils::WireFailure::classify(&e).prefixed_text(),
851                None,
852            )))
853        }
854    }
855}
856
857#[allow(clippy::too_many_arguments)]
858async fn handle_session(
859    connection_id: ConnectionId,
860    server_id: crate::state::ServerId,
861    llm_client: OllamaClient,
862    app_state: Arc<AppState>,
863    status_tx: mpsc::UnboundedSender<String>,
864    protocol: Arc<SnowflakeProtocol>,
865    operation: &str,
866    has_auth_token: bool,
867) -> Result<Response<Full<Bytes>>, Infallible> {
868    let event = Event::new(
869        &SNOWFLAKE_SESSION_EVENT,
870        json!({
871            "operation": operation,
872            "has_auth_token": has_auth_token,
873        }),
874    );
875
876    match call_llm(
877        &llm_client,
878        &app_state,
879        server_id,
880        Some(connection_id),
881        &event,
882        &*protocol,
883    )
884    .await
885    {
886        Ok(execution_result) => match first_custom(execution_result.protocol_results) {
887            Some((name, payload)) if name == "snowflake_session_response" => {
888                let data = payload.get("data").cloned().unwrap_or(json!({}));
889                let out = json!({
890                    "data": data,
891                    "message": Value::Null,
892                    "code": Value::Null,
893                    "success": true
894                });
895                Log::new(Some(&status_tx)).info(format!(
896                    "Snowflake session conn={} op={} decision=model_answer",
897                    connection_id, operation
898                ));
899                Ok(json_200(out.to_string()))
900            }
901            Some((name, payload)) if name == "snowflake_error" => {
902                let code = payload
903                    .get("code")
904                    .and_then(|v| v.as_str())
905                    .unwrap_or(CODE_INTERNAL);
906                let message = payload
907                    .get("message")
908                    .and_then(|v| v.as_str())
909                    .unwrap_or("Session request failed");
910                Log::new(Some(&status_tx)).info(format!(
911                    "Snowflake session conn={} op={} decision=model_reject: {}",
912                    connection_id, operation, code
913                ));
914                Ok(json_200(statement_error_envelope(
915                    code,
916                    message,
917                    payload.get("sql_state").and_then(|v| v.as_str()),
918                )))
919            }
920            _ => {
921                // A logout with no answer is harmless to ack; a token renewal with no
922                // answer must fail closed (no renewed token). Distinguish by operation.
923                if operation == "logout" {
924                    // **This arm answers `success: true` when the model said nothing.** It is
925                    // the one affirmative default in this protocol, so it is deliberately NOT
926                    // tagged `fail_closed_*`: that prefix is what an operator greps for, and a
927                    // line claiming it while the peer got an OK would be a lie. The server
928                    // keeps no sessions, so there is nothing a logout could fail to do — but
929                    // the ack is still the *server's* assertion, not the model's.
930                    Log::new(Some(&status_tx)).warn(format!(
931                        "Snowflake session conn={} op=logout decision=default_logout_ack: no \
932                         model answer, acking anyway (success:true was not decided by the model)",
933                        connection_id
934                    ));
935                    Ok(json_200(
936                        json!({"data": {}, "message": Value::Null, "code": Value::Null, "success": true})
937                            .to_string(),
938                    ))
939                } else {
940                    Log::new(Some(&status_tx)).warn(format!(
941                        "Snowflake session conn={} op={} decision=model_silent: no session \
942                         action produced; refusing the renewal",
943                        connection_id, operation
944                    ));
945                    Ok(json_200(statement_error_envelope(
946                        CODE_INTERNAL,
947                        "netget: no session decision produced",
948                        None,
949                    )))
950                }
951            }
952        },
953        Err(e) => {
954            Log::new(Some(&status_tx)).error(format!(
955                "Snowflake session conn={} op={} decision={}: {}",
956                connection_id,
957                operation,
958                llm_error_decision(&e),
959                e
960            ));
961            Ok(json_200(statement_error_envelope(
962                CODE_INTERNAL,
963                crate::utils::WireFailure::classify(&e).prefixed_text(),
964                None,
965            )))
966        }
967    }
968}
969
970/// Ensure each rowtype descriptor has the fields a driver expects. The model
971/// supplies at least `name`/`type`; sensible defaults fill in the rest.
972fn normalize_rowtype(rowtype: Option<&Value>) -> Vec<Value> {
973    let arr = rowtype
974        .and_then(|v| v.as_array())
975        .cloned()
976        .unwrap_or_default();
977    arr.into_iter()
978        .map(|col| {
979            let name = col.get("name").and_then(|v| v.as_str()).unwrap_or("COL");
980            let ty = col.get("type").and_then(|v| v.as_str()).unwrap_or("text");
981            let nullable = col
982                .get("nullable")
983                .and_then(|v| v.as_bool())
984                .unwrap_or(true);
985            json!({
986                "name": name,
987                "type": ty,
988                "nullable": nullable,
989                "length": col.get("length").cloned().unwrap_or(Value::Null),
990                "precision": col.get("precision").cloned().unwrap_or(Value::Null),
991                "scale": col.get("scale").cloned().unwrap_or(Value::Null),
992                "byteLength": Value::Null,
993                "collation": Value::Null
994            })
995        })
996        .collect()
997}
998
999/// Snowflake's JSON result format transmits every cell as a string (or null).
1000/// Convert whatever the model produced into that shape.
1001fn normalize_rowset(rowset: Option<&Value>) -> Vec<Value> {
1002    let arr = rowset
1003        .and_then(|v| v.as_array())
1004        .cloned()
1005        .unwrap_or_default();
1006    arr.into_iter()
1007        .map(|row| {
1008            let cells = row.as_array().cloned().unwrap_or_default();
1009            Value::Array(
1010                cells
1011                    .into_iter()
1012                    .map(|c| match c {
1013                        Value::Null => Value::Null,
1014                        Value::String(s) => Value::String(s),
1015                        other => Value::String(other.to_string()),
1016                    })
1017                    .collect(),
1018            )
1019        })
1020        .collect()
1021}