1use crate::llm::actions::{
15 protocol_trait::{ActionResult, Protocol, Server},
16 ActionDefinition, Parameter,
17};
18use crate::protocol::log_template::LogTemplate;
19use crate::protocol::EventType;
20use crate::server::connection::ConnectionId;
21use crate::state::app_state::AppState;
22use anyhow::{Context, Result};
23use serde_json::json;
24use std::sync::{Arc, LazyLock};
25use tokio::sync::mpsc;
26use tracing::debug;
27
28pub struct SnowflakeProtocol {
34 _connection_id: ConnectionId,
35 _app_state: Arc<AppState>,
36 status_tx: mpsc::UnboundedSender<String>,
37}
38
39impl SnowflakeProtocol {
40 pub fn new(
41 connection_id: ConnectionId,
42 app_state: Arc<AppState>,
43 status_tx: mpsc::UnboundedSender<String>,
44 ) -> Self {
45 Self {
46 _connection_id: connection_id,
47 _app_state: app_state,
48 status_tx,
49 }
50 }
51}
52
53impl Protocol for SnowflakeProtocol {
54 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
55 vec![]
59 }
60
61 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
62 vec![]
64 }
65
66 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
67 vec![
68 snowflake_login_success_action(),
69 snowflake_query_response_action(),
70 snowflake_session_response_action(),
71 snowflake_error_action(),
72 ]
73 }
74
75 fn protocol_name(&self) -> &'static str {
76 "Snowflake"
77 }
78
79 fn get_event_types(&self) -> Vec<EventType> {
80 get_snowflake_event_types()
81 }
82
83 fn stack_name(&self) -> &'static str {
84 "ETH>IP>TCP>HTTP>Snowflake"
85 }
86
87 fn keywords(&self) -> Vec<&'static str> {
88 vec!["snowflake", "snowflake warehouse", "snowflake data cloud"]
90 }
91
92 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
93 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
94
95 ProtocolMetadataV2::builder()
96 .state(DevelopmentState::Experimental)
97 .implementation("Hand-written Snowflake REST/JSON endpoints over hyper (login, query, session)")
98 .llm_control("Session tokens at login, rowset/rowtype for queries, error envelopes")
99 .e2e_testing("tests/server/snowflake/real_client_test.rs drives the server with snowflake-connector-python 4.8.0, Snowflake's official Python driver (gzipped request bodies): a login the model grants and one it refuses (the driver raises 390100), a six-column rowset typed by the driver's own JSON converters (FIXED with a scale, BOOLEAN, DATE, TIMESTAMP_NTZ, NULL), a query error raised with the model's errno and SQLSTATE, and the driver's POST /session?delete=true logout. e2e_test.rs and llm_failure_test.rs drive the envelopes with reqwest")
100 .notes(
101 "HTTP/JSON only; TLS termination is out of scope (real drivers use HTTPS — front with a \
102 TLS proxy if needed). One real driver so far (snowflake-connector-python over plain \
103 HTTP); JDBC, ODBC and gosnowflake are untested, and results are JSON only (no Arrow, \
104 no chunks). No session store, so tokens are not validated against issued ones. \
105 Fail-closed: an LLM outage on login is a refusal (no token issued), never a \
106 success-shaped empty result.",
107 )
108 .max_inbound_bytes(crate::server::snowflake::MAX_REQUEST_BYTES)
109 .request_only("The Snowflake API is HTTP request/response; a server cannot send a peer anything unprompted")
110 .build()
111 }
112
113 fn description(&self) -> &'static str {
114 "Snowflake data-warehouse server (REST/JSON login + query protocol)"
115 }
116
117 fn example_prompt(&self) -> &'static str {
118 "Start a Snowflake server on port 8085 that logs clients in and answers SELECT queries"
119 }
120
121 fn group_name(&self) -> &'static str {
122 "Database"
123 }
124
125 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
126 use crate::llm::actions::StartupExamples;
127
128 let script = r#"import json, sys
131data = json.load(sys.stdin)
132if data["event_type_id"] == "snowflake_query":
133 actions = [{"type": "snowflake_query_response",
134 "rowtype": [{"name": "STATUS", "type": "text"}],
135 "rowset": [["OK"]]}]
136else:
137 actions = []
138print(json.dumps({"actions": actions}))"#;
139
140 StartupExamples::new(
141 json!({
143 "type": "open_server",
144 "port": 8085,
145 "base_stack": "snowflake",
146 "instruction": "Snowflake warehouse: accept logins and answer SQL queries with plausible rowsets"
147 }),
148 json!({
150 "type": "open_server",
151 "port": 8085,
152 "base_stack": "snowflake",
153 "event_handlers": [{
154 "event_pattern": "snowflake_query",
155 "handler": {
156 "type": "script",
157 "language": "python",
158 "code": script
159 }
160 }]
161 }),
162 json!({
164 "type": "open_server",
165 "port": 8085,
166 "base_stack": "snowflake",
167 "event_handlers": [{
168 "event_pattern": "snowflake_login",
169 "handler": {
170 "type": "static",
171 "actions": [{
172 "type": "snowflake_login_success",
173 "token": "SESSION_TOKEN_ABC",
174 "master_token": "MASTER_TOKEN_ABC",
175 "session_id": 100
176 }]
177 }
178 }]
179 }),
180 )
181 }
182}
183
184impl Server for SnowflakeProtocol {
185 fn spawn(
186 &self,
187 ctx: crate::protocol::SpawnContext,
188 ) -> std::pin::Pin<
189 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
190 > {
191 Box::pin(async move {
192 use crate::server::snowflake::SnowflakeServer;
193 SnowflakeServer::spawn_with_llm_actions(
194 ctx.legacy_listen_addr(),
195 ctx.llm_client,
196 ctx.state,
197 ctx.status_tx,
198 ctx.server_id,
199 )
200 .await
201 })
202 }
203
204 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
205 let action_type = action
206 .get("type")
207 .and_then(|v| v.as_str())
208 .context("Missing 'type' field in action")?;
209
210 match action_type {
211 "snowflake_login_success" => self.execute_login_success(action),
212 "snowflake_query_response" => self.execute_query_response(action),
213 "snowflake_session_response" => self.execute_session_response(action),
214 "snowflake_error" => self.execute_error(action),
215 _ => Err(anyhow::anyhow!("Unknown Snowflake action: {}", action_type)),
216 }
217 }
218}
219
220impl SnowflakeProtocol {
221 fn execute_login_success(&self, action: serde_json::Value) -> Result<ActionResult> {
222 let token = action
223 .get("token")
224 .and_then(|v| v.as_str())
225 .context("Missing 'token' in snowflake_login_success")?
226 .to_string();
227 let master_token = action
228 .get("master_token")
229 .and_then(|v| v.as_str())
230 .map(|s| s.to_string());
231 let session_id = action.get("session_id").cloned();
232 let validity_seconds = action.get("validity_seconds").and_then(|v| v.as_u64());
233
234 debug!("Snowflake login success: session token minted");
235 let _ = self
236 .status_tx
237 .send("[DEBUG] Snowflake → login success (token issued)".to_string());
238
239 Ok(ActionResult::Custom {
240 name: "snowflake_login_success".to_string(),
241 data: json!({
242 "token": token,
243 "master_token": master_token,
244 "session_id": session_id,
245 "validity_seconds": validity_seconds,
246 }),
247 })
248 }
249
250 fn execute_query_response(&self, action: serde_json::Value) -> Result<ActionResult> {
251 let rowtype = action
252 .get("rowtype")
253 .and_then(|v| v.as_array())
254 .context("Missing 'rowtype' array in snowflake_query_response")?;
255 let rowset = action
256 .get("rowset")
257 .and_then(|v| v.as_array())
258 .context("Missing 'rowset' array in snowflake_query_response")?;
259 let query_id = action
260 .get("query_id")
261 .and_then(|v| v.as_str())
262 .map(|s| s.to_string());
263
264 debug!(
265 "Snowflake query response: {} columns, {} rows",
266 rowtype.len(),
267 rowset.len()
268 );
269 let _ = self.status_tx.send(format!(
270 "[DEBUG] Snowflake → rowset: {} columns, {} rows",
271 rowtype.len(),
272 rowset.len()
273 ));
274
275 Ok(ActionResult::Custom {
276 name: "snowflake_query_response".to_string(),
277 data: json!({
278 "rowtype": rowtype,
279 "rowset": rowset,
280 "query_id": query_id,
281 }),
282 })
283 }
284
285 fn execute_session_response(&self, action: serde_json::Value) -> Result<ActionResult> {
286 let data = action.get("data").cloned().unwrap_or(json!({}));
287 debug!("Snowflake session response");
288 let _ = self
289 .status_tx
290 .send("[DEBUG] Snowflake → session response".to_string());
291 Ok(ActionResult::Custom {
292 name: "snowflake_session_response".to_string(),
293 data: json!({ "data": data }),
294 })
295 }
296
297 fn execute_error(&self, action: serde_json::Value) -> Result<ActionResult> {
298 let code = action
299 .get("code")
300 .and_then(|v| v.as_str())
301 .unwrap_or("000603")
302 .to_string();
303 let message = action
304 .get("message")
305 .and_then(|v| v.as_str())
306 .unwrap_or("Unknown error")
307 .to_string();
308 let sql_state = action
309 .get("sql_state")
310 .and_then(|v| v.as_str())
311 .filter(|s| !s.is_empty());
312 debug!("Snowflake error response: {} - {}", code, message);
313 let _ = self
314 .status_tx
315 .send(format!("[DEBUG] Snowflake ✗ error {}: {}", code, message));
316 let mut data = json!({ "code": code, "message": message });
317 if let Some(state) = sql_state {
318 data["sql_state"] = json!(state);
319 }
320 Ok(ActionResult::Custom {
321 name: "snowflake_error".to_string(),
322 data,
323 })
324 }
325}
326
327pub fn snowflake_login_success_action() -> ActionDefinition {
333 ActionDefinition {
334 name: "snowflake_login_success".to_string(),
335 description: "Authenticate the client and issue a Snowflake session token. Only emit this \
336 when the login should succeed; to refuse a login use snowflake_error instead (never \
337 leave the event unanswered — an unanswered login is treated as a refusal)."
338 .to_string(),
339 parameters: vec![
340 Parameter {
341 name: "token".to_string(),
342 type_hint: "string".to_string(),
343 description: "The session token the driver will send back as \
344 Authorization: Snowflake Token=\"...\" on subsequent query requests."
345 .to_string(),
346 required: true,
347 },
348 Parameter {
349 name: "master_token".to_string(),
350 type_hint: "string".to_string(),
351 description: "Master token used by the driver to renew the session (optional)."
352 .to_string(),
353 required: false,
354 },
355 Parameter {
356 name: "session_id".to_string(),
357 type_hint: "number".to_string(),
358 description: "Numeric session id (optional)".to_string(),
359 required: false,
360 },
361 Parameter {
362 name: "validity_seconds".to_string(),
363 type_hint: "number".to_string(),
364 description: "Session token validity in seconds (optional, default 3600)"
365 .to_string(),
366 required: false,
367 },
368 ],
369 example: json!({
370 "type": "snowflake_login_success",
371 "token": "ver:1-hint:abc-ETMsDgAAA...",
372 "master_token": "ver:1-hint:def-ETMsDgAAA...",
373 "session_id": 123456789,
374 "validity_seconds": 3600
375 }),
376 log_template: Some(
377 LogTemplate::new()
378 .with_info("-> Snowflake login OK (session {session_id})")
379 .with_debug("Snowflake snowflake_login_success: session_id={session_id}"),
380 ),
381 }
382}
383
384pub fn snowflake_query_response_action() -> ActionDefinition {
386 ActionDefinition {
387 name: "snowflake_query_response".to_string(),
388 description: "Return a result set for a SQL query. `rowtype` describes the columns and \
389 `rowset` is the rows. Snowflake's JSON result format transmits every value as a \
390 string, so all cell values are stringified on the wire."
391 .to_string(),
392 parameters: vec![
393 Parameter {
394 name: "rowtype".to_string(),
395 type_hint: "array".to_string(),
396 description: "Array of column descriptors. Each is an object with at least \
397 'name' and 'type' (Snowflake logical types: text, fixed, real, boolean, date, \
398 time, timestamp_ntz, variant, ...). Optional fields (nullable, length, \
399 precision, scale) are passed through; sensible defaults are filled in."
400 .to_string(),
401 required: true,
402 },
403 Parameter {
404 name: "rowset".to_string(),
405 type_hint: "array".to_string(),
406 description: "Array of rows; each row is an array of cell values matching the \
407 column order. Values are sent as strings (Snowflake JSON result format)."
408 .to_string(),
409 required: true,
410 },
411 Parameter {
412 name: "query_id".to_string(),
413 type_hint: "string".to_string(),
414 description: "Optional query id (UUID) echoed back to the driver".to_string(),
415 required: false,
416 },
417 ],
418 example: json!({
419 "type": "snowflake_query_response",
420 "rowtype": [
421 {"name": "ID", "type": "fixed"},
422 {"name": "NAME", "type": "text"}
423 ],
424 "rowset": [["1", "Alice"], ["2", "Bob"]],
425 "query_id": "01b2c3d4-0000-0000-0000-000000000001"
426 }),
427 log_template: Some(
428 LogTemplate::new()
429 .with_info("-> Snowflake {rowtype_len} cols, {rowset_len} rows")
430 .with_debug(
431 "Snowflake snowflake_query_response: {rowtype_len} cols, {rowset_len} rows",
432 ),
433 ),
434 }
435}
436
437pub fn snowflake_session_response_action() -> ActionDefinition {
439 ActionDefinition {
440 name: "snowflake_session_response".to_string(),
441 description: "Acknowledge a session-management request (logout or token renewal) with a \
442 success envelope. For logout, omit `data`. For token renewal, put the renewed \
443 {sessionToken, masterToken, validityInSeconds} in `data`."
444 .to_string(),
445 parameters: vec![Parameter {
446 name: "data".to_string(),
447 type_hint: "object".to_string(),
448 description: "Optional data object placed in the response 'data' field (e.g. renewed \
449 tokens for a token-request). Omit for a plain logout acknowledgement."
450 .to_string(),
451 required: false,
452 }],
453 example: json!({
454 "type": "snowflake_session_response",
455 "data": {"sessionToken": "ver:1-...", "validityInSeconds": 3600}
456 }),
457 log_template: Some(
458 LogTemplate::new()
459 .with_info("-> Snowflake session OK")
460 .with_debug("Snowflake snowflake_session_response"),
461 ),
462 }
463}
464
465pub fn snowflake_error_action() -> ActionDefinition {
467 ActionDefinition {
468 name: "snowflake_error".to_string(),
469 description: "Refuse the current request with a Snowflake error envelope (success:false). \
470 This is the only way to deny a login or reject a query. The client receives HTTP 200 \
471 with success:false and the code/message you provide."
472 .to_string(),
473 parameters: vec![
474 Parameter {
475 name: "code".to_string(),
476 type_hint: "string".to_string(),
477 description: "Snowflake error code string, e.g. \"390100\" (incorrect username or \
478 password), \"390114\" (authentication token expired), \"002003\" (object does \
479 not exist), \"001003\" (SQL compilation error)."
480 .to_string(),
481 required: true,
482 },
483 Parameter {
484 name: "message".to_string(),
485 type_hint: "string".to_string(),
486 description: "Human-readable error message shown to the client".to_string(),
487 required: true,
488 },
489 Parameter {
490 name: "sql_state".to_string(),
491 type_hint: "string".to_string(),
492 description: "Five-character SQLSTATE for a refused query, e.g. \"42S02\" \
493 (table not found) or \"42000\" (syntax error); drivers raise it with the \
494 error. Ignored for a refused login."
495 .to_string(),
496 required: false,
497 },
498 ],
499 example: json!({
500 "type": "snowflake_error",
501 "code": "390100",
502 "message": "Incorrect username or password was specified."
503 }),
504 log_template: Some(
505 LogTemplate::new()
506 .with_info("-> Snowflake ERR {code}: {message}")
507 .with_debug("Snowflake snowflake_error: code={code}, message={message}"),
508 ),
509 }
510}
511
512pub static SNOWFLAKE_LOGIN_SUCCESS_ACTION: LazyLock<ActionDefinition> =
517 LazyLock::new(snowflake_login_success_action);
518pub static SNOWFLAKE_QUERY_RESPONSE_ACTION: LazyLock<ActionDefinition> =
519 LazyLock::new(snowflake_query_response_action);
520pub static SNOWFLAKE_SESSION_RESPONSE_ACTION: LazyLock<ActionDefinition> =
521 LazyLock::new(snowflake_session_response_action);
522pub static SNOWFLAKE_ERROR_ACTION: LazyLock<ActionDefinition> =
523 LazyLock::new(snowflake_error_action);
524
525pub static SNOWFLAKE_LOGIN_EVENT: LazyLock<EventType> = LazyLock::new(|| {
531 EventType::new(
532 "snowflake_login",
533 "Snowflake client login request",
534 json!({
535 "type": "snowflake_login_success",
536 "token": "SESSION_TOKEN",
537 "session_id": 100
538 }),
539 )
540 .with_parameters(vec![
541 Parameter {
542 name: "login_name".to_string(),
543 type_hint: "string".to_string(),
544 description: "The username the client is logging in as".to_string(),
545 required: false,
546 },
547 Parameter {
548 name: "account".to_string(),
549 type_hint: "string".to_string(),
550 description: "The Snowflake account name the client is targeting".to_string(),
551 required: false,
552 },
553 Parameter {
554 name: "client_app_id".to_string(),
555 type_hint: "string".to_string(),
556 description: "The driver/app id (e.g. PythonConnector, JDBC)".to_string(),
557 required: false,
558 },
559 Parameter {
560 name: "client_app_version".to_string(),
561 type_hint: "string".to_string(),
562 description: "The driver/app version".to_string(),
563 required: false,
564 },
565 Parameter {
566 name: "has_password".to_string(),
567 type_hint: "boolean".to_string(),
568 description: "True if a password was supplied in the login request".to_string(),
569 required: false,
570 },
571 ])
572 .with_actions(vec![
573 SNOWFLAKE_LOGIN_SUCCESS_ACTION.clone(),
574 SNOWFLAKE_ERROR_ACTION.clone(),
575 ])
576 .with_log_template(
577 LogTemplate::new()
578 .with_info("Snowflake login: {login_name}@{account}")
579 .with_debug("Snowflake login request: {json_pretty(.)}"),
580 )
581});
582
583pub static SNOWFLAKE_QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
585 EventType::new(
586 "snowflake_query",
587 "Snowflake SQL query request",
588 json!({
589 "type": "snowflake_query_response",
590 "rowtype": [{"name": "COL", "type": "text"}],
591 "rowset": [["value"]]
592 }),
593 )
594 .with_parameters(vec![
595 Parameter {
596 name: "sql_text".to_string(),
597 type_hint: "string".to_string(),
598 description: "The SQL statement the client wants to run".to_string(),
599 required: true,
600 },
601 Parameter {
602 name: "has_auth_token".to_string(),
603 type_hint: "boolean".to_string(),
604 description: "True if the client presented an Authorization session token. There is \
605 no session store, so the token is not validated — decide based on this signal."
606 .to_string(),
607 required: false,
608 },
609 Parameter {
610 name: "request_id".to_string(),
611 type_hint: "string".to_string(),
612 description: "The requestId query parameter, if present".to_string(),
613 required: false,
614 },
615 ])
616 .with_actions(vec![
617 SNOWFLAKE_QUERY_RESPONSE_ACTION.clone(),
618 SNOWFLAKE_ERROR_ACTION.clone(),
619 ])
620 .with_log_template(
621 LogTemplate::new()
622 .with_info("Snowflake query: {preview(sql_text,80)}")
623 .with_debug("Snowflake query: {sql_text}"),
624 )
625});
626
627pub static SNOWFLAKE_SESSION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
629 EventType::new(
630 "snowflake_session",
631 "Snowflake session management request (logout or token renewal)",
632 json!({
633 "type": "snowflake_session_response"
634 }),
635 )
636 .with_parameters(vec![
637 Parameter {
638 name: "operation".to_string(),
639 type_hint: "string".to_string(),
640 description: "Either \"logout\" or \"token_renew\"".to_string(),
641 required: true,
642 },
643 Parameter {
644 name: "has_auth_token".to_string(),
645 type_hint: "boolean".to_string(),
646 description: "True if the client presented an Authorization session token".to_string(),
647 required: false,
648 },
649 ])
650 .with_actions(vec![
651 SNOWFLAKE_SESSION_RESPONSE_ACTION.clone(),
652 SNOWFLAKE_ERROR_ACTION.clone(),
653 ])
654 .with_log_template(
655 LogTemplate::new()
656 .with_info("Snowflake session: {operation}")
657 .with_debug("Snowflake session request: {operation}"),
658 )
659});
660
661pub fn get_snowflake_event_types() -> Vec<EventType> {
663 vec![
664 SNOWFLAKE_LOGIN_EVENT.clone(),
665 SNOWFLAKE_QUERY_EVENT.clone(),
666 SNOWFLAKE_SESSION_EVENT.clone(),
667 ]
668}