1use super::values;
10use crate::llm::actions::{
11 protocol_trait::{ActionResult, Protocol, Server},
12 ActionDefinition, Parameter,
13};
14use crate::protocol::log_template::LogTemplate;
15use crate::protocol::EventType;
16use crate::state::app_state::AppState;
17use anyhow::{anyhow, Context, Result};
18use serde_json::{json, Value};
19use std::sync::LazyLock;
20
21pub const LOGIN_RESULT: &str = "bolt_login";
23pub const RECORDS_RESULT: &str = "bolt_records";
24pub const FAILURE_RESULT: &str = "bolt_failure";
25
26pub const UNAUTHORIZED: &str = "Neo.ClientError.Security.Unauthorized";
28
29pub struct BoltProtocol;
30
31impl BoltProtocol {
32 pub fn new() -> Self {
33 Self
34 }
35}
36
37impl Default for BoltProtocol {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl Protocol for BoltProtocol {
44 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
45 use crate::llm::actions::ParameterDefinition;
46 vec![
47 ParameterDefinition {
48 name: "password".to_string(),
49 type_hint: "string".to_string(),
50 description: "Password every basic-auth login must present. NetGet compares it \
51 itself (constant time) and refuses a mismatch with \
52 Neo.ClientError.Security.Unauthorized without asking anyone; the \
53 credential never reaches an event. Unset: any password passes to \
54 the bolt_authenticate event, which decides."
55 .to_string(),
56 required: false,
57 example: json!("s3cret"),
58 default: None,
59 },
60 ParameterDefinition {
61 name: "neo4j_version".to_string(),
62 type_hint: "string".to_string(),
63 description: format!(
64 "Neo4j version the server claims (default {}): the HELLO agent is \
65 'Neo4j/<version>' - drivers refuse any agent not starting 'Neo4j/' - and \
66 CALL dbms.components() reports it.",
67 super::DEFAULT_NEO4J_VERSION
68 ),
69 required: false,
70 example: json!("5.26.0"),
71 default: Some(json!(super::DEFAULT_NEO4J_VERSION)),
72 },
73 ParameterDefinition {
74 name: "first_byte_timeout_secs".to_string(),
75 type_hint: "number".to_string(),
76 description: "Seconds a new connection may take to send the 20-byte Bolt \
77 handshake (default 30). Bolt is client-speaks-first and every \
78 driver sends the handshake the moment it connects."
79 .to_string(),
80 required: false,
81 example: json!(30),
82 default: Some(json!(super::FIRST_BYTE_TIMEOUT.as_secs())),
83 },
84 ParameterDefinition {
85 name: "idle_timeout_secs".to_string(),
86 type_hint: "number".to_string(),
87 description: "Seconds the server waits for the next message once the handshake \
88 is done (default 300). Drivers pool connections and leave them \
89 idle between queries, so this is deliberately long. It never runs \
90 while a query is waiting on the model or on a human."
91 .to_string(),
92 required: false,
93 example: json!(300),
94 default: Some(json!(super::IDLE_TIMEOUT.as_secs())),
95 },
96 ]
97 }
98
99 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
100 Vec::new()
101 }
102
103 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
104 vec![
105 accept_bolt_login_action(),
106 reject_bolt_login_action(),
107 send_bolt_records_action(),
108 send_bolt_failure_action(),
109 close_connection_action(),
110 ]
111 }
112
113 fn protocol_name(&self) -> &'static str {
114 "Bolt"
115 }
116
117 fn get_event_types(&self) -> Vec<EventType> {
118 vec![BOLT_AUTHENTICATE_EVENT.clone(), BOLT_QUERY_EVENT.clone()]
119 }
120
121 fn stack_name(&self) -> &'static str {
122 "ETH>IP>TCP>BOLT"
123 }
124
125 fn keywords(&self) -> Vec<&'static str> {
126 vec!["bolt", "neo4j", "cypher", "graph database", "cypher-shell"]
127 }
128
129 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
130 use crate::protocol::metadata::{
131 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
132 };
133
134 ProtocolMetadataV2::builder()
135 .state(DevelopmentState::Beta)
136 .well_known_port(7687)
137 .privilege_requirement(PrivilegeRequirement::None)
139 .implementation(
140 "Neo4j's Bolt protocol, hand-written over tokio TCP: the 60 60 B0 17 handshake \
141 negotiating Bolt 5.0-5.8, a depth-bounded PackStream codec and message \
142 chunking, and the server state machine (HELLO/LOGON, RUN/PULL/DISCARD, \
143 BEGIN/COMMIT/ROLLBACK, RESET, ROUTE, LOGOFF, TELEMETRY, GOODBYE) owned by NetGet",
144 )
145 .llm_control(
146 "The graph database: whether a login is accepted, and what each Cypher query \
147 returns - columns, rows (plain values, nodes, relationships, paths), update \
148 statistics - or which Neo4j error it fails with. NetGet stores no graph.",
149 )
150 .e2e_testing(
151 "tests/server/bolt/real_client_test.rs drives the real cypher-shell (Neo4j's \
152 Java client, neo4j-java-driver 6.2) against a script handler and a mocked \
153 model: a query printed row for row, nodes and paths rendered as graph values, \
154 stats, a model-chosen error printed as that error, a wrong password refused, \
155 an explicit transaction and neo4j:// routing. It fails, never skips, when \
156 cypher-shell is absent. tests/server/bolt/e2e_test.rs covers the mocked-model \
157 path on a raw socket.",
158 )
159 .notes(
160 "Negotiates the highest of Bolt 5.0-5.8 the client offers (cypher-shell \
161 2026.09 offers 5.8; the 5.7+ handshake manifest is declined). 5.0 \
162 authenticates in HELLO, 5.1+ in LOGON. FAILURE carries {code, message} before \
163 5.7 and the GQL error object (gql_status 50N42, neo4j_code) from 5.7. \
164 Answered by NetGet without a model call: HELLO, ROUTE (a single-server \
165 routing table naming the address the client used), BEGIN/COMMIT/ROLLBACK, \
166 RESET, TELEMETRY, and three admin queries cypher-shell sends on connect - CALL \
167 db.ping(), CALL dbms.licenseAgreementDetails() and CALL dbms.components(). \
168 PULL/DISCARD require explicit integer n and typed qid over the model's rows; \
169 known messages require exact arity. Owned model/injected actions are preflighted \
170 before copies at JSON depth36/nodes65536/retained8MiB; emitted RECORDs include \
171 their structure/list envelopes in the strict wire-depth32 check. Messages are capped at 1 \
172 MiB summed over chunks and PackStream nesting at 32; either refusal is a \
173 Neo.ClientError.Request.Invalid FAILURE and a close. Not implemented: TLS \
174 (bolt+s), the handshake manifest (Bolt 6), notifications, temporal and \
175 spatial values in answers. No pcap oracle: this Wireshark build has no Bolt \
176 dissector.",
177 )
178 .max_inbound_bytes(super::packstream::MAX_MESSAGE_BYTES)
179 .answers_on_failure()
182 .build()
183 }
184
185 fn description(&self) -> &'static str {
186 "Neo4j graph database over Bolt - the model answers every Cypher query"
187 }
188
189 fn example_prompt(&self) -> &'static str {
190 "Neo4j Bolt server on port 7687 - a small movie graph: people who ACTED_IN and DIRECTED \
191 movies"
192 }
193
194 fn group_name(&self) -> &'static str {
195 "Database"
196 }
197
198 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
199 use crate::llm::actions::StartupExamples;
200 StartupExamples::new(
201 json!({
202 "type": "open_server",
203 "port": 7687,
204 "base_stack": "bolt",
205 "instruction": "Neo4j graph database of a film club: Person nodes (name, born) \
206 who ACTED_IN or DIRECTED Movie nodes (title, released). Accept \
207 any login. Answer Cypher queries with plausible rows."
208 }),
209 json!({
210 "type": "open_server",
211 "port": 7687,
212 "base_stack": "bolt",
213 "event_handlers": [{
214 "event_pattern": "bolt_authenticate",
215 "handler": {"type": "static", "actions": [{"type": "accept_bolt_login"}]}
216 }, {
217 "event_pattern": "bolt_query",
218 "handler": {
219 "type": "script",
220 "language": "python",
221 "code": "import json, sys\nq = json.load(sys.stdin)['event'].get('query', '')\nif 'Person' in q:\n a = [{'type': 'send_bolt_records', 'fields': ['name'], 'records': [['Alice'], ['Bob']]}]\nelse:\n a = [{'type': 'send_bolt_failure', 'code': 'Neo.ClientError.Statement.SyntaxError', 'message': 'This graph only knows Person nodes'}]\nprint(json.dumps({'actions': a}))"
222 }
223 }]
224 }),
225 json!({
226 "type": "open_server",
227 "port": 7687,
228 "base_stack": "bolt",
229 "event_handlers": [{
230 "event_pattern": "bolt_authenticate",
231 "handler": {"type": "static", "actions": [{"type": "accept_bolt_login"}]}
232 }, {
233 "event_pattern": "bolt_query",
234 "handler": {"type": "static", "actions": [{
235 "type": "send_bolt_records",
236 "fields": ["n"],
237 "records": [[{"$node": {"id": 1, "labels": ["Person"],
238 "properties": {"name": "Alice"}}}]]
239 }]}
240 }]
241 }),
242 )
243 }
244}
245
246impl Server for BoltProtocol {
247 fn spawn(
248 &self,
249 ctx: crate::protocol::SpawnContext,
250 ) -> std::pin::Pin<
251 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
252 > {
253 Box::pin(async move {
254 let params = ctx.startup_params.as_ref();
255 let secs = |name: &str| -> anyhow::Result<Option<u64>> {
256 Ok(params
257 .map(|p| p.get_optional_u64(name))
258 .transpose()?
259 .flatten())
260 };
261 let string = |name: &str| -> anyhow::Result<Option<String>> {
262 Ok(params
263 .map(|p| p.get_optional_string(name))
264 .transpose()?
265 .flatten())
266 };
267 let config = super::BoltConfig {
268 password: string("password")?,
269 neo4j_version: string("neo4j_version")?
270 .unwrap_or_else(|| super::DEFAULT_NEO4J_VERSION.to_string()),
271 first_byte_timeout: secs("first_byte_timeout_secs")?
272 .map(std::time::Duration::from_secs)
273 .unwrap_or(super::FIRST_BYTE_TIMEOUT),
274 idle_timeout: secs("idle_timeout_secs")?
275 .map(std::time::Duration::from_secs)
276 .unwrap_or(super::IDLE_TIMEOUT),
277 };
278 crate::server::bolt::BoltServer::spawn_with_llm_actions(
279 ctx.legacy_listen_addr(),
280 ctx.llm_client,
281 ctx.state,
282 ctx.status_tx,
283 ctx.server_id,
284 config,
285 )
286 .await
287 })
288 }
289
290 fn execute_action(&self, action: Value) -> Result<ActionResult> {
291 if !values::action_within_budget(&action) {
292 crate::utils::json_budget::drop_iteratively(action);
293 return Err(anyhow!(
294 "Bolt action exceeds the in-memory depth/node/retained-content budget"
295 ));
296 }
297 let action_type = action
298 .get("type")
299 .and_then(|v| v.as_str())
300 .context("Missing 'type' field in action")?;
301
302 match action_type {
303 "accept_bolt_login" => Ok(ActionResult::Custom {
304 name: LOGIN_RESULT.to_string(),
305 data: json!({"accept": true}),
306 }),
307 "reject_bolt_login" => {
308 let code = action
309 .get("code")
310 .and_then(Value::as_str)
311 .unwrap_or(UNAUTHORIZED);
312 if !code.starts_with("Neo.ClientError.Security.")
313 || !values::valid_failure_code(code)
314 {
315 return Err(anyhow!(
316 "reject_bolt_login code must be a Neo.ClientError.Security.* status \
317 such as {UNAUTHORIZED}, got '{code}'"
318 ));
319 }
320 let message = action
321 .get("message")
322 .and_then(Value::as_str)
323 .filter(|m| !m.trim().is_empty())
324 .unwrap_or("The client is unauthorized due to authentication failure.");
325 Ok(ActionResult::Custom {
326 name: LOGIN_RESULT.to_string(),
327 data: json!({"accept": false, "code": code, "message": message}),
328 })
329 }
330 "send_bolt_records" => {
331 values::query_answer(&action).map_err(|e| anyhow!("send_bolt_records: {e}"))?;
332 Ok(ActionResult::Custom {
333 name: RECORDS_RESULT.to_string(),
334 data: action.clone(),
335 })
336 }
337 "send_bolt_failure" => {
338 let code = action
339 .get("code")
340 .and_then(Value::as_str)
341 .context("send_bolt_failure needs a 'code'")?;
342 if !values::valid_failure_code(code) {
343 return Err(anyhow!(
344 "send_bolt_failure code must look like Neo.ClientError.<Category>.<Title>, \
345 Neo.TransientError.<Category>.<Title> or \
346 Neo.DatabaseError.<Category>.<Title>, got '{code}'"
347 ));
348 }
349 let message = action
350 .get("message")
351 .and_then(Value::as_str)
352 .filter(|m| !m.trim().is_empty())
353 .context("send_bolt_failure needs a non-empty 'message'")?;
354 Ok(ActionResult::Custom {
355 name: FAILURE_RESULT.to_string(),
356 data: json!({"code": code, "message": message}),
357 })
358 }
359 "close_connection" => Ok(ActionResult::CloseConnection),
360 _ => Err(anyhow!("Unknown Bolt action: {}", action_type)),
361 }
362 }
363}
364
365fn accept_bolt_login_action() -> ActionDefinition {
366 ActionDefinition {
367 name: "accept_bolt_login".to_string(),
368 description: "Let this login in. NetGet answers the client's LOGON with SUCCESS and the \
369 connection can run queries."
370 .to_string(),
371 parameters: vec![],
372 example: json!({"type": "accept_bolt_login"}),
373 log_template: Some(
374 LogTemplate::new()
375 .with_info("-> Bolt login accepted")
376 .with_debug("Bolt accept_bolt_login"),
377 ),
378 }
379}
380
381fn reject_bolt_login_action() -> ActionDefinition {
382 ActionDefinition {
383 name: "reject_bolt_login".to_string(),
384 description: "Refuse this login. The client receives a FAILURE with the code (default \
385 Neo.ClientError.Security.Unauthorized) and the connection closes."
386 .to_string(),
387 parameters: vec![
388 Parameter {
389 name: "code".to_string(),
390 type_hint: "string".to_string(),
391 description: "A Neo.ClientError.Security.* status, e.g. \
392 Neo.ClientError.Security.Unauthorized or \
393 Neo.ClientError.Security.AuthenticationRateLimit"
394 .to_string(),
395 required: false,
396 },
397 Parameter {
398 name: "message".to_string(),
399 type_hint: "string".to_string(),
400 description: "The message the client prints".to_string(),
401 required: false,
402 },
403 ],
404 example: json!({
405 "type": "reject_bolt_login",
406 "code": "Neo.ClientError.Security.Unauthorized",
407 "message": "The client is unauthorized due to authentication failure."
408 }),
409 log_template: Some(
410 LogTemplate::new()
411 .with_info("-> Bolt login rejected: {code}")
412 .with_debug("Bolt reject_bolt_login: code={code}"),
413 ),
414 }
415}
416
417fn send_bolt_records_action() -> ActionDefinition {
418 ActionDefinition {
419 name: "send_bolt_records".to_string(),
420 description: "Answer the Cypher query with a result: column names and rows. NetGet sends \
421 the column list, streams the rows as the client pulls them, and ends with \
422 the summary. A query that returns nothing (a CREATE without RETURN) has an \
423 empty 'fields' and 'records'. A value may be any JSON; a graph value is \
424 {\"$node\": {id, labels, properties}}, {\"$relationship\": {id, type, \
425 start, end, properties}} or {\"$path\": {nodes: [...], relationships: \
426 [...]}} where relationships[i] joins nodes[i] and nodes[i+1]."
427 .to_string(),
428 parameters: vec![
429 Parameter {
430 name: "fields".to_string(),
431 type_hint: "array".to_string(),
432 description: "Column names, in order - the names after RETURN / AS".to_string(),
433 required: true,
434 },
435 Parameter {
436 name: "records".to_string(),
437 type_hint: "array".to_string(),
438 description: "Rows; each row is an array with one value per field, in field \
439 order"
440 .to_string(),
441 required: true,
442 },
443 Parameter {
444 name: "stats".to_string(),
445 type_hint: "object".to_string(),
446 description: "Update counts for a write, e.g. {\"nodes_created\": 1, \
447 \"properties_set\": 2}. Keys: nodes_created, nodes_deleted, \
448 relationships_created, relationships_deleted, properties_set, \
449 labels_added, labels_removed, indexes_added, indexes_removed, \
450 constraints_added, constraints_removed, system_updates"
451 .to_string(),
452 required: false,
453 },
454 Parameter {
455 name: "query_type".to_string(),
456 type_hint: "string".to_string(),
457 description: "r (read, default), w (write), rw (read and write) or s (schema)"
458 .to_string(),
459 required: false,
460 }
461 .with_choices(["r", "w", "rw", "s"]),
462 ],
463 example: json!({
464 "type": "send_bolt_records",
465 "fields": ["name", "born"],
466 "records": [["Keanu Reeves", 1964], ["Carrie-Anne Moss", 1967]]
467 }),
468 log_template: Some(
469 LogTemplate::new()
470 .with_info("-> Bolt result")
471 .with_debug("Bolt send_bolt_records"),
472 ),
473 }
474}
475
476fn send_bolt_failure_action() -> ActionDefinition {
477 ActionDefinition {
478 name: "send_bolt_failure".to_string(),
479 description: "Fail the query with a Neo4j error, which the client prints. Common codes: \
480 Neo.ClientError.Statement.SyntaxError (bad Cypher), \
481 Neo.ClientError.Statement.EntityNotFound, \
482 Neo.ClientError.Schema.ConstraintValidationFailed, \
483 Neo.ClientError.Security.Forbidden, \
484 Neo.ClientError.Database.DatabaseNotFound, \
485 Neo.TransientError.Transaction.DeadlockDetected."
486 .to_string(),
487 parameters: vec![
488 Parameter {
489 name: "code".to_string(),
490 type_hint: "string".to_string(),
491 description: "Neo.ClientError.*, Neo.TransientError.* or Neo.DatabaseError.* \
492 status code"
493 .to_string(),
494 required: true,
495 },
496 Parameter {
497 name: "message".to_string(),
498 type_hint: "string".to_string(),
499 description: "The error message".to_string(),
500 required: true,
501 },
502 ],
503 example: json!({
504 "type": "send_bolt_failure",
505 "code": "Neo.ClientError.Statement.SyntaxError",
506 "message": "Invalid input 'RETRUN': expected 'RETURN' (line 1, column 1 (offset: 0))"
507 }),
508 log_template: Some(
509 LogTemplate::new()
510 .with_info("-> Bolt FAILURE {code}")
511 .with_debug("Bolt send_bolt_failure: code={code}"),
512 ),
513 }
514}
515
516fn close_connection_action() -> ActionDefinition {
517 ActionDefinition {
518 name: "close_connection".to_string(),
519 description: "Close the Bolt connection without answering".to_string(),
520 parameters: vec![],
521 example: json!({"type": "close_connection"}),
522 log_template: Some(
523 LogTemplate::new()
524 .with_info("Bolt connection closed")
525 .with_debug("Bolt close_connection"),
526 ),
527 }
528}
529
530pub fn login_answer_with(principal: &str, scheme: &str, credentials_present: bool) -> String {
538 let user = if principal.is_empty() {
539 "no user name".to_string()
540 } else {
541 format!("user \"{principal}\"")
542 };
543 let credential = if credentials_present {
544 "a credential"
545 } else {
546 "no credential"
547 };
548 format!(
549 "decide only whether {user} (scheme {scheme}, {credential}) may log in. \
550 accept_bolt_login unless your instructions refuse this user, this scheme, or a login \
551 without a credential. No query has been sent yet: rules about queries - rejecting \
552 invalid Cypher, refusing writes - are applied to each query when it arrives, never \
553 to the login"
554 )
555}
556
557pub static BOLT_AUTHENTICATE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
559 EventType::new(
560 "bolt_authenticate",
561 "A client is logging in to the graph database - this decision is about the login \
562 only, before any query has been sent. Decide whether this user may connect. Rules \
563 your instructions give about queries (reject invalid Cypher, refuse writes, ...) do \
564 not apply here: queries arrive later as bolt_query. The credential itself is never \
565 shown; when the server has a configured password NetGet has already checked it.",
566 json!({"type": "accept_bolt_login"}),
567 )
568 .with_parameters(vec![
569 Parameter {
570 name: "user_agent".to_string(),
571 type_hint: "string".to_string(),
572 description: "The client's own name, e.g. neo4j-cypher-shell/v2026.09.0".to_string(),
573 required: true,
574 },
575 Parameter {
576 name: "scheme".to_string(),
577 type_hint: "string".to_string(),
578 description: "Auth scheme: basic, none, bearer, kerberos or a custom one".to_string(),
579 required: true,
580 },
581 Parameter {
582 name: "principal".to_string(),
583 type_hint: "string".to_string(),
584 description: "The user name (empty for scheme none)".to_string(),
585 required: true,
586 },
587 Parameter {
588 name: "credentials_present".to_string(),
589 type_hint: "boolean".to_string(),
590 description: "Whether the client sent a credential at all".to_string(),
591 required: true,
592 },
593 Parameter {
594 name: "password_configured".to_string(),
595 type_hint: "boolean".to_string(),
596 description: "True when the server has a password and the client's matched it"
597 .to_string(),
598 required: true,
599 },
600 Parameter {
601 name: "answer_with".to_string(),
602 type_hint: "string".to_string(),
603 description: "What this login is decided on: the user, the scheme and whether a \
604 credential was sent - never a query, which has not been sent yet"
605 .to_string(),
606 required: true,
607 },
608 ])
609 .with_log_template(
610 LogTemplate::new()
611 .with_info("Bolt login {principal} ({scheme})")
612 .with_debug("Bolt bolt_authenticate: principal={principal} scheme={scheme}"),
613 )
614 .with_actions(vec![
615 accept_bolt_login_action(),
616 reject_bolt_login_action(),
617 close_connection_action(),
618 ])
619 .with_alternative_example(json!({
620 "type": "reject_bolt_login",
621 "code": "Neo.ClientError.Security.Unauthorized",
622 "message": "The client is unauthorized due to authentication failure."
623 }))
624});
625
626pub static BOLT_QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
628 EventType::new(
629 "bolt_query",
630 "A client ran a Cypher query against the graph database. Answer with the rows it \
631 returns (send_bolt_records) or the Neo4j error it fails with (send_bolt_failure).",
632 json!({
633 "type": "send_bolt_records",
634 "fields": ["name"],
635 "records": [["Alice"], ["Bob"]]
636 }),
637 )
638 .with_parameters(vec![
639 Parameter {
640 name: "query".to_string(),
641 type_hint: "string".to_string(),
642 description: "The Cypher text".to_string(),
643 required: true,
644 },
645 Parameter {
646 name: "parameters".to_string(),
647 type_hint: "object".to_string(),
648 description: "The query's $parameters by name".to_string(),
649 required: true,
650 },
651 Parameter {
652 name: "database".to_string(),
653 type_hint: "string".to_string(),
654 description: "The database the client named, when it named one".to_string(),
655 required: false,
656 },
657 Parameter {
658 name: "mode".to_string(),
659 type_hint: "string".to_string(),
660 description: "read or write: the access mode the client asked for".to_string(),
661 required: true,
662 }
663 .with_choices(["read", "write"]),
664 Parameter {
665 name: "in_transaction".to_string(),
666 type_hint: "boolean".to_string(),
667 description: "True inside an explicit BEGIN ... COMMIT transaction".to_string(),
668 required: true,
669 },
670 ])
671 .with_log_template(
672 LogTemplate::new()
673 .with_info("Bolt query: {query}")
674 .with_debug("Bolt bolt_query: query={query} mode={mode}"),
675 )
676 .with_actions(vec![
677 send_bolt_records_action(),
678 send_bolt_failure_action(),
679 close_connection_action(),
680 ])
681 .with_alternative_example(json!({
682 "type": "send_bolt_failure",
683 "code": "Neo.ClientError.Statement.SyntaxError",
684 "message": "Invalid input 'RETRUN': expected 'RETURN' (line 1, column 1 (offset: 0))"
685 }))
686});