netget/server/nats/mod.rs
1//! NATS server implementation.
2//!
3//! The NATS client protocol is a line protocol: CRLF-terminated control lines, with
4//! `PUB`/`HPUB` followed by a byte-counted payload. A server must send `INFO {json}` before
5//! the client says anything, and must answer `PING` with `PONG` or the client tears the
6//! connection down.
7//!
8//! # Two tasks per connection, and why
9//!
10//! * The **reader** frames the stream and answers everything that is not a decision: `PING`
11//! with `PONG`, and the per-command `+OK` a client that connected with `"verbose": true`
12//! expects. Neither costs an LLM call - `async-nats` writes `CONNECT` and `PING` in one
13//! flush and waits for the reply, so parking the keepalive behind a model round-trip (or
14//! behind a manual handler waiting for a human) would break connect itself.
15//! * The **dispatcher** owns the subscription table and makes exactly one LLM call at a
16//! time, in arrival order. It is the state machine the root CLAUDE.md describes: busy is
17//! `Processing`, the bounded channel between the two tasks is the queue, and there is no
18//! `Accumulating` state because NATS frames are self-delimiting - there is never a partial
19//! message to accumulate, so `wait_for_more` would mean nothing here and is not offered.
20//!
21//! The channel is **bounded**. A publisher faster than the model backs pressure up through
22//! the reader into TCP, which is the honest outcome; an unbounded queue would grow until the
23//! process died.
24
25pub mod actions;
26pub mod jetstream;
27
28use anyhow::Result;
29use std::collections::{BTreeMap, HashMap};
30use std::net::SocketAddr;
31use std::sync::Arc;
32use tokio::io::{AsyncReadExt, AsyncWriteExt};
33use tokio::net::TcpStream;
34use tokio::sync::{mpsc, oneshot, Mutex};
35
36use crate::llm::action_helper::call_llm;
37use crate::llm::actions::protocol_trait::ActionResult;
38use crate::llm::ollama_client::OllamaClient;
39use crate::logging::emit::Log;
40use crate::protocol::Event;
41use crate::server::connection::ConnectionId;
42use crate::state::app_state::AppState;
43use actions::{
44 NatsProtocol, NATS_CONNECT_EVENT, NATS_PUBLISH_EVENT, NATS_SUBSCRIBE_EVENT,
45 NATS_UNSUBSCRIBE_EVENT,
46};
47
48/// NATS' own default control-line limit. A line longer than this without a CRLF is a peer
49/// that is not speaking NATS.
50///
51/// This bounds one *line*. It does not bound the read buffer, which the previous wording
52/// claimed it did: a `PUB` declares its own length, and the reader buffers toward it.
53/// `MAX_BUFFERED_BYTES` below is what bounds the buffer.
54pub const MAX_CONTROL_LINE: usize = 4096;
55
56/// Hard ceiling on the reader's frame buffer, whatever any frame declares.
57///
58/// A frame is at most one control line plus `max_payload` plus its terminator, so anything
59/// past that with no frame extractable is a peer that will never produce one. Without this,
60/// `max_payload` was the only bound in the system and it was applied to a *declared* size,
61/// not to what had actually been buffered.
62const MAX_BUFFERED_SLACK: usize = MAX_CONTROL_LINE + 16;
63
64/// How many `accept()` failures in a row before the listener is treated as dead.
65///
66/// Transient failures - descriptor exhaustion, a peer hanging up between SYN and accept -
67/// clear on their own, so one of them must not end the accept loop. A listener that is
68/// genuinely broken fails every time, and this bounds how long that takes to notice.
69const MAX_CONSECUTIVE_ACCEPT_ERRORS: u32 = 64;
70
71/// How many parsed frames may wait for the dispatcher before the reader stops reading.
72const FRAME_QUEUE_CAPACITY: usize = 256;
73
74/// How long to wait for a peer's first frame after the `INFO` greeting has gone out.
75///
76/// NATS is server-speaks-first — `INFO` before the client says anything — and every client
77/// answers with `CONNECT` immediately, inside its own connect path. So a peer that has been
78/// greeted and has sent nothing has begun no session, which is the state an unauthenticated
79/// flood lives in.
80const FIRST_FRAME_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
81
82/// How long a peer that has already sent a frame may send nothing further.
83///
84/// **A NATS subscriber is legitimately silent**, sometimes for hours: it sends `SUB` once and
85/// then only receives. What keeps this bound from closing one is that NATS has a keepalive and
86/// both ends use it — `nats-server`'s `ping_interval` defaults to **2 minutes** (two missed
87/// PONGs and it drops the client), and `async-nats` sends its own `PING` on a **60-second**
88/// interval. Ten minutes therefore sits above *both* intervals with five times the margin on
89/// the longer one, so any client that is still there has spoken several times over.
90///
91/// It is also comfortably above a model round-trip and above the 300-second default a `manual`
92/// rule gives a human to answer — which matters here in a way it does not for a
93/// request/response protocol: the reader task keeps reading while the dispatcher answers, so a
94/// peer waiting for its own reply is silent on this socket for the whole of that work.
95const IDLE_BETWEEN_FRAMES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
96
97/// Concurrent connections this server admits.
98///
99/// Each admitted connection buffers toward `max_payload` plus a control line, and carries a
100/// reader task, a dispatcher task and a 256-frame queue, so the cap is what turns that
101/// per-connection bound into a total one.
102const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
103
104/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
105///
106/// `-ERR` is NATS's own error line, and this is the exact text `nats-server` itself sends when a
107/// connection arrives over `max_connections`, so a client reports the real reason rather than an
108/// unexplained reset. The peer has not sent `CONNECT`, so nothing has been negotiated and there
109/// is nothing else this server could usefully say.
110const CONNECTION_CAP_REFUSAL: &[u8] = b"-ERR 'Maximum Connections Exceeded'\r\n";
111
112pub struct NatsServer;
113
114// ============================================================================
115// Wire framing
116// ============================================================================
117
118/// One decodable client frame.
119///
120/// `Ping`/`Pong` are here rather than being handled inside the parser so that the parser
121/// stays a pure function of the buffer - it is the part worth testing directly
122/// (`tests/server/nats/e2e_test.rs` does, with no LLM calls).
123#[derive(Debug, Clone, PartialEq)]
124pub enum Frame {
125 /// `CONNECT {json}`
126 Connect(serde_json::Value),
127 /// `PUB`/`HPUB`. `headers` is empty for a plain `PUB`.
128 Publish {
129 subject: String,
130 reply_to: Option<String>,
131 headers: BTreeMap<String, String>,
132 payload: Vec<u8>,
133 },
134 /// `SUB <subject> [queue] <sid>`
135 Subscribe {
136 subject: String,
137 queue_group: Option<String>,
138 sid: String,
139 },
140 /// `UNSUB <sid> [max]`
141 Unsubscribe {
142 sid: String,
143 max_msgs: Option<u64>,
144 },
145 Ping,
146 Pong,
147}
148
149/// A frame the peer cannot be allowed to continue after.
150///
151/// Each carries the text a real NATS server uses, because clients match on some of them
152/// (`Maximum Payload Violation` in particular) and a novel string would be reported as an
153/// unknown error.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum FrameError {
156 UnknownProtocolOperation,
157 MaximumControlLineExceeded,
158 MaximumPayloadViolation,
159 InvalidConnectConfig,
160 InvalidSubject,
161}
162
163impl FrameError {
164 pub fn wire_text(self) -> &'static str {
165 match self {
166 Self::UnknownProtocolOperation => "Unknown Protocol Operation",
167 Self::MaximumControlLineExceeded => "Maximum Control Line Exceeded",
168 Self::MaximumPayloadViolation => "Maximum Payload Violation",
169 Self::InvalidConnectConfig => "Invalid Connect Config",
170 Self::InvalidSubject => "Invalid Subject",
171 }
172 }
173}
174
175impl std::fmt::Display for FrameError {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.write_str(self.wire_text())
178 }
179}
180
181/// Try to decode one frame from the front of `buf`.
182///
183/// Returns `Ok(None)` when the buffer holds only part of a frame - the caller reads more and
184/// tries again - and `Ok(Some((frame, consumed)))` when one is complete. `consumed` is the
185/// number of bytes to remove from the front.
186///
187/// Line endings: the protocol says CRLF, and every real client sends CRLF. A bare LF is
188/// accepted too, so that a human poking at the port with `nc` gets sensible behaviour rather
189/// than `Unknown Protocol Operation` on every line.
190pub fn parse_frame(buf: &[u8], max_payload: u64) -> Result<Option<(Frame, usize)>, FrameError> {
191 let skipped = blank_line_prefix_len(buf);
192 match parse_one_frame(&buf[skipped..], max_payload)? {
193 Some((frame, consumed)) => Ok(Some((frame, skipped + consumed))),
194 None => Ok(None),
195 }
196}
197
198/// Length of the leading run of blank lines (`\n`, `\r\n`, or a line of only whitespace).
199///
200/// Real servers ignore blank lines between frames. Skipping them iteratively is not a style
201/// choice. The version this replaced recursed into `parse_frame` once per blank line — one
202/// stack frame deeper each time, and not in tail position, since the returned offset had to
203/// be adjusted after the call. A peer writing 8 KB of newlines, which is one `read`, recursed
204/// about 8000 levels and overflowed the stack. A Rust stack overflow is `SIGSEGV`, not a
205/// catchable panic, so `tokio::spawn` could not contain it and the whole NetGet process
206/// aborted — every other server with it.
207///
208/// The reader drains this prefix before parsing as well. `parse_frame` folds it into
209/// `consumed` when a frame follows, but when nothing follows it reports `Ok(None)` having
210/// consumed nothing — so without the eager drain a peer sending only newlines would still
211/// grow the read buffer by a chunk on every read, without bound.
212pub fn blank_line_prefix_len(buf: &[u8]) -> usize {
213 let mut offset = 0;
214 while let Some(nl) = buf[offset..].iter().position(|&b| b == b'\n') {
215 let line_end = offset + nl;
216 let line = &buf[offset..line_end];
217 let line = line.strip_suffix(b"\r").unwrap_or(line);
218 if !line.iter().all(|b| b.is_ascii_whitespace()) {
219 break;
220 }
221 offset = line_end + 1;
222 }
223 offset
224}
225
226/// Decode one frame from a buffer whose first line is known not to be blank.
227fn parse_one_frame(buf: &[u8], max_payload: u64) -> Result<Option<(Frame, usize)>, FrameError> {
228 let Some(nl) = buf.iter().position(|&b| b == b'\n') else {
229 if buf.len() > MAX_CONTROL_LINE {
230 return Err(FrameError::MaximumControlLineExceeded);
231 }
232 return Ok(None);
233 };
234 if nl > MAX_CONTROL_LINE {
235 return Err(FrameError::MaximumControlLineExceeded);
236 }
237
238 let mut line_end = nl;
239 if line_end > 0 && buf[line_end - 1] == b'\r' {
240 line_end -= 1;
241 }
242 let after_line = nl + 1;
243
244 let line = std::str::from_utf8(&buf[..line_end])
245 .map_err(|_| FrameError::UnknownProtocolOperation)?
246 .trim();
247
248 let mut parts = line.split_whitespace();
249 let verb = parts.next().ok_or(FrameError::UnknownProtocolOperation)?;
250 let args: Vec<&str> = parts.collect();
251
252 if verb.eq_ignore_ascii_case("PING") {
253 return Ok(Some((Frame::Ping, after_line)));
254 }
255 if verb.eq_ignore_ascii_case("PONG") {
256 return Ok(Some((Frame::Pong, after_line)));
257 }
258 if verb.eq_ignore_ascii_case("CONNECT") {
259 // The document is the remainder of the line verbatim: splitting on whitespace would
260 // break any JSON string containing a space.
261 let json_start = line
262 .find(char::is_whitespace)
263 .ok_or(FrameError::InvalidConnectConfig)?;
264 let doc: serde_json::Value = serde_json::from_str(line[json_start..].trim())
265 .map_err(|_| FrameError::InvalidConnectConfig)?;
266 if !doc.is_object() {
267 return Err(FrameError::InvalidConnectConfig);
268 }
269 return Ok(Some((Frame::Connect(doc), after_line)));
270 }
271 if verb.eq_ignore_ascii_case("SUB") {
272 // SUB <subject> [queue group] <sid>
273 let (subject, queue_group, sid) = match args.len() {
274 2 => (args[0], None, args[1]),
275 3 => (args[0], Some(args[1].to_string()), args[2]),
276 _ => return Err(FrameError::UnknownProtocolOperation),
277 };
278 if subject.is_empty() {
279 return Err(FrameError::InvalidSubject);
280 }
281 return Ok(Some((
282 Frame::Subscribe {
283 subject: subject.to_string(),
284 queue_group,
285 sid: sid.to_string(),
286 },
287 after_line,
288 )));
289 }
290 if verb.eq_ignore_ascii_case("UNSUB") {
291 // UNSUB <sid> [max_msgs]
292 let sid = args.first().ok_or(FrameError::UnknownProtocolOperation)?;
293 let max_msgs = match args.get(1) {
294 Some(v) => Some(
295 v.parse::<u64>()
296 .map_err(|_| FrameError::UnknownProtocolOperation)?,
297 ),
298 None => None,
299 };
300 if args.len() > 2 {
301 return Err(FrameError::UnknownProtocolOperation);
302 }
303 return Ok(Some((
304 Frame::Unsubscribe {
305 sid: sid.to_string(),
306 max_msgs,
307 },
308 after_line,
309 )));
310 }
311
312 let is_hpub = verb.eq_ignore_ascii_case("HPUB");
313 if is_hpub || verb.eq_ignore_ascii_case("PUB") {
314 // PUB <subject> [reply-to] <#bytes>
315 // HPUB <subject> [reply-to] <#header bytes> <#total bytes>
316 let (subject, reply_to, header_len, total_len) = if is_hpub {
317 match args.len() {
318 3 => (args[0], None, parse_len(args[1])?, parse_len(args[2])?),
319 4 => (
320 args[0],
321 Some(args[1].to_string()),
322 parse_len(args[2])?,
323 parse_len(args[3])?,
324 ),
325 _ => return Err(FrameError::UnknownProtocolOperation),
326 }
327 } else {
328 match args.len() {
329 2 => (args[0], None, 0, parse_len(args[1])?),
330 3 => (args[0], Some(args[1].to_string()), 0, parse_len(args[2])?),
331 _ => return Err(FrameError::UnknownProtocolOperation),
332 }
333 };
334
335 if subject.is_empty() {
336 return Err(FrameError::InvalidSubject);
337 }
338 if header_len > total_len {
339 return Err(FrameError::UnknownProtocolOperation);
340 }
341 // The limit applies to the whole declared size, header block included, which is what
342 // nats-server's processHeaderPub does (`c.pa.size > c.mpay`, where `size` is the
343 // total). Bounding only `total_len - header_len` left `header_len` itself unbounded,
344 // and because `header_len == total_len` yields a zero-length body it passed both
345 // checks: `HPUB x 4000000000 4000000000` is 30 bytes on the wire and made the reader
346 // buffer its way toward 4 GB, while `HPUB x <usize::MAX> <usize::MAX>` overflowed the
347 // `after_line + total_len` index below — a panic in debug, and in release a wrap to a
348 // `start > end` slice that panicked one line further on. `PUB` was safe only by
349 // accident, its header length being a literal `0`.
350 if total_len as u64 > max_payload {
351 return Err(FrameError::MaximumPayloadViolation);
352 }
353
354 // `max_payload` is caller-supplied and may itself be enormous; a declared size must
355 // never be able to wrap the index that reads it.
356 let Some(body_end) = after_line.checked_add(total_len) else {
357 return Err(FrameError::MaximumPayloadViolation);
358 };
359 if buf.len() <= body_end {
360 return Ok(None);
361 }
362 let terminator = match buf[body_end] {
363 b'\r' => {
364 if buf.len() < body_end + 2 {
365 return Ok(None);
366 }
367 if buf[body_end + 1] != b'\n' {
368 return Err(FrameError::UnknownProtocolOperation);
369 }
370 2
371 }
372 b'\n' => 1,
373 _ => return Err(FrameError::UnknownProtocolOperation),
374 };
375
376 let headers = if header_len > 0 {
377 parse_headers(&buf[after_line..after_line + header_len])
378 } else {
379 BTreeMap::new()
380 };
381 let payload = buf[after_line + header_len..body_end].to_vec();
382
383 return Ok(Some((
384 Frame::Publish {
385 subject: subject.to_string(),
386 reply_to,
387 headers,
388 payload,
389 },
390 body_end + terminator,
391 )));
392 }
393
394 Err(FrameError::UnknownProtocolOperation)
395}
396
397fn parse_len(token: &str) -> Result<usize, FrameError> {
398 token
399 .parse::<usize>()
400 .map_err(|_| FrameError::UnknownProtocolOperation)
401}
402
403/// Decode a `NATS/1.0` header block into name/value pairs.
404///
405/// The version line is skipped, malformed lines are dropped rather than failing the frame -
406/// a header a client invented is not a reason to kill its connection - and names are
407/// reported as sent.
408pub fn parse_headers(block: &[u8]) -> BTreeMap<String, String> {
409 let mut out = BTreeMap::new();
410 let text = String::from_utf8_lossy(block);
411 for (index, line) in text.split('\n').enumerate() {
412 let line = line.trim_end_matches('\r');
413 if index == 0 || line.trim().is_empty() {
414 continue;
415 }
416 if let Some((name, value)) = line.split_once(':') {
417 let name = name.trim();
418 if !name.is_empty() {
419 out.insert(name.to_string(), value.trim().to_string());
420 }
421 }
422 }
423 out
424}
425
426/// NATS subject matching: `*` matches exactly one token, `>` matches one or more remaining
427/// tokens and may only appear last.
428pub fn subject_matches(pattern: &str, subject: &str) -> bool {
429 let pattern_tokens: Vec<&str> = pattern.split('.').collect();
430 let subject_tokens: Vec<&str> = subject.split('.').collect();
431
432 for (i, token) in pattern_tokens.iter().enumerate() {
433 if *token == ">" {
434 return i + 1 == pattern_tokens.len() && subject_tokens.len() > i;
435 }
436 if i >= subject_tokens.len() {
437 return false;
438 }
439 if *token != "*" && *token != subject_tokens[i] {
440 return false;
441 }
442 }
443 pattern_tokens.len() == subject_tokens.len()
444}
445
446// ============================================================================
447// Server
448// ============================================================================
449
450/// One subscription as the client declared it.
451///
452/// This is live connection state, not storage: it exists only to tell the model which
453/// subscriptions a publish could plausibly go to, and it dies with the connection. Nothing
454/// is delivered because of it.
455#[derive(Debug, Clone)]
456struct Subscription {
457 subject: String,
458 queue_group: Option<String>,
459}
460
461impl NatsServer {
462 /// Bind, and return only once the listener is accepting.
463 ///
464 /// The bind is awaited before returning, so a port that is taken (or privileged) is an
465 /// `Err` the caller turns into `ServerStatus::Error` rather than a server that reports
466 /// `Running` with no socket.
467 #[allow(clippy::too_many_arguments)]
468 pub async fn spawn_with_llm_actions(
469 listen_addr: SocketAddr,
470 llm_client: OllamaClient,
471 app_state: Arc<AppState>,
472 status_tx: mpsc::UnboundedSender<String>,
473 server_id: crate::state::ServerId,
474 server_name: String,
475 max_payload: u64,
476 jetstream: Option<Arc<jetstream::Shared>>,
477 ) -> Result<SocketAddr> {
478 let listener =
479 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
480 let local_addr = listener.local_addr()?;
481
482 Log::new(Some(&status_tx)).info(format!(
483 "NATS server listening on {} (server_name={}, max_payload={})",
484 local_addr, server_name, max_payload
485 ));
486
487 let protocol = Arc::new(NatsProtocol::new());
488 let task_registrar = app_state.clone();
489
490 let accept_handle = tokio::spawn(async move {
491 // `accept()` fails transiently when the process is briefly out of descriptors
492 // (EMFILE/ENFILE) or a peer hangs up between SYN and accept (ECONNABORTED).
493 // Breaking on those stopped the listener for good while `AppState` still reported
494 // the server `Running` - a server that lies about being up, arrived at from the
495 // other direction. Retry with a short backoff, and give up only when the failures
496 // are relentless enough that the listener really is dead.
497 let mut consecutive_accept_errors = 0u32;
498 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
499 loop {
500 match crate::server::accept_bounded::accept_bounded(
501 &listener,
502 &limiter,
503 CONNECTION_CAP_REFUSAL,
504 "NATS",
505 Some(&status_tx),
506 )
507 .await
508 {
509 Ok((stream, peer_addr, permit)) => {
510 consecutive_accept_errors = 0;
511 let connection_id =
512 ConnectionId::new(app_state.get_next_unified_id().await);
513 let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
514
515 if let Err(e) = accept_connection(
516 stream,
517 peer_addr,
518 local_addr_conn,
519 connection_id,
520 server_id,
521 &llm_client,
522 &app_state,
523 &status_tx,
524 &protocol,
525 &server_name,
526 max_payload,
527 jetstream.clone(),
528 permit,
529 )
530 .await
531 {
532 Log::new(Some(&status_tx)).error(format!(
533 "NATS could not set up connection from {}: {}",
534 peer_addr, e
535 ));
536 }
537 }
538 Err(e) => {
539 consecutive_accept_errors += 1;
540 if consecutive_accept_errors >= MAX_CONSECUTIVE_ACCEPT_ERRORS {
541 Log::new(Some(&status_tx)).error(format!(
542 "NATS accept failed {} times in a row ({}); listener stopping",
543 consecutive_accept_errors, e
544 ));
545 break;
546 }
547 Log::new(Some(&status_tx))
548 .warn(format!("NATS accept error: {} - retrying", e));
549 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
550 }
551 }
552 }
553 });
554
555 task_registrar
556 .register_server_task(server_id, accept_handle)
557 .await;
558
559 Ok(local_addr)
560 }
561}
562
563/// Register one accepted connection, greet it with `INFO`, and start its two tasks.
564#[allow(clippy::too_many_arguments)]
565async fn accept_connection(
566 stream: TcpStream,
567 peer_addr: SocketAddr,
568 local_addr: SocketAddr,
569 connection_id: ConnectionId,
570 server_id: crate::state::ServerId,
571 llm_client: &OllamaClient,
572 app_state: &Arc<AppState>,
573 status_tx: &mpsc::UnboundedSender<String>,
574 protocol: &Arc<NatsProtocol>,
575 server_name: &str,
576 max_payload: u64,
577 jetstream: Option<Arc<jetstream::Shared>>,
578 permit: crate::server::accept_bounded::ConnectionPermit,
579) -> Result<()> {
580 // One connection, two tasks: the slot must stay taken until *both* are gone, so the permit
581 // is shared rather than parked in whichever one happens to end last. The dispatcher outlives
582 // the reader by however long its last frame takes.
583 let permit = Arc::new(permit);
584 let (read_half, write_half) = tokio::io::split(stream);
585 let write_half = Arc::new(Mutex::new(write_half));
586
587 use crate::state::server::{
588 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
589 };
590 let now = crate::utils::clock::Instant::now();
591 app_state
592 .add_connection_to_server(
593 server_id,
594 ServerConnectionState {
595 id: connection_id,
596 remote_addr: peer_addr,
597 local_addr,
598 bytes_sent: 0,
599 bytes_received: 0,
600 packets_sent: 0,
601 packets_received: 0,
602 last_activity: now,
603 status: ConnectionStatus::Active,
604 status_changed_at: now,
605 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
606 "state": "Idle"
607 })),
608 },
609 )
610 .await;
611 let _ = status_tx.send("__UPDATE_UI__".to_string());
612
613 let log = Log::new(Some(status_tx));
614 log.info(format!("NATS client connected from {}", peer_addr));
615
616 // Peer messaging, registered before the greeting: a NATS connection can sit for minutes
617 // with a parked event, and the operator must be able to reach it meanwhile.
618 let peer_rx = crate::server::peer_support::register_peer_channel(
619 app_state,
620 server_id,
621 connection_id.as_u32(),
622 )
623 .await;
624 crate::server::peer_support::spawn_peer_command_task(
625 peer_rx,
626 protocol.clone(),
627 app_state.clone(),
628 server_id,
629 connection_id.as_u32(),
630 write_half.clone(),
631 status_tx.clone(),
632 );
633
634 // The server speaks first, always, and before the client will say anything at all.
635 let info = actions::build_info_json(
636 server_name,
637 max_payload,
638 &local_addr.ip().to_string(),
639 local_addr.port(),
640 connection_id.as_u32() as u64,
641 &peer_addr.ip().to_string(),
642 jetstream.is_some(),
643 );
644 let greeting = format!("INFO {}\r\n", info);
645 log.debug(format!("NATS -> {} INFO greeting", peer_addr));
646 log.trace(format!("NATS INFO: {}", info));
647 write_counted(
648 &write_half,
649 greeting.as_bytes(),
650 app_state,
651 server_id,
652 connection_id,
653 )
654 .await?;
655
656 let (frame_tx, frame_rx) = mpsc::channel::<Frame>(FRAME_QUEUE_CAPACITY);
657 let (close_tx, close_rx) = oneshot::channel::<()>();
658
659 let reader_permit = Arc::clone(&permit);
660 let reader = run_reader(
661 read_half,
662 write_half.clone(),
663 frame_tx,
664 close_rx,
665 app_state.clone(),
666 status_tx.clone(),
667 server_id,
668 connection_id,
669 peer_addr,
670 max_payload,
671 );
672 let reader_handle = tokio::spawn(async move {
673 let _permit = reader_permit;
674 reader.await
675 });
676 let dispatcher = run_dispatcher(
677 frame_rx,
678 close_tx,
679 write_half,
680 llm_client.clone(),
681 app_state.clone(),
682 status_tx.clone(),
683 server_id,
684 connection_id,
685 peer_addr,
686 protocol.clone(),
687 jetstream,
688 );
689 let dispatcher_handle = tokio::spawn(async move {
690 let _permit = permit;
691 dispatcher.await
692 });
693
694 // Both, not just the accept loop: aborting a task does not abort the tasks it spawned,
695 // so a connection whose reader was left registered nowhere would keep the socket alive
696 // after stop_server.
697 app_state
698 .register_server_task(server_id, reader_handle)
699 .await;
700 app_state
701 .register_server_task(server_id, dispatcher_handle)
702 .await;
703
704 Ok(())
705}
706
707/// Write one frame and count it against the connection.
708///
709/// The guard is dropped before the stats update, so nothing awaits an `AppState` lock while
710/// holding the write half.
711async fn write_counted<W>(
712 write_half: &Arc<Mutex<W>>,
713 data: &[u8],
714 app_state: &AppState,
715 server_id: crate::state::ServerId,
716 connection_id: ConnectionId,
717) -> std::io::Result<()>
718where
719 W: tokio::io::AsyncWrite + Unpin,
720{
721 {
722 let mut writer = write_half.lock().await;
723 writer.write_all(data).await?;
724 writer.flush().await?;
725 }
726 app_state
727 .update_connection_stats(
728 server_id,
729 connection_id,
730 None,
731 Some(data.len() as u64),
732 None,
733 Some(1),
734 )
735 .await;
736 Ok(())
737}
738
739/// Frame the inbound stream, answer the non-decisions, hand everything else to the
740/// dispatcher.
741#[allow(clippy::too_many_arguments)]
742async fn run_reader(
743 mut read_half: tokio::io::ReadHalf<TcpStream>,
744 write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
745 frame_tx: mpsc::Sender<Frame>,
746 mut close_rx: oneshot::Receiver<()>,
747 app_state: Arc<AppState>,
748 status_tx: mpsc::UnboundedSender<String>,
749 server_id: crate::state::ServerId,
750 connection_id: ConnectionId,
751 peer_addr: SocketAddr,
752 max_payload: u64,
753) {
754 let log = Log::new(Some(&status_tx));
755 let mut buf: Vec<u8> = Vec::with_capacity(8192);
756 let mut chunk = vec![0u8; 8192];
757 // Set from the client's CONNECT. In verbose mode a real server acknowledges every
758 // command with +OK, and clients that asked for it wait for those acknowledgements.
759 let mut verbose = false;
760 // "Has said nothing at all" and "has gone quiet mid-session" are different claims and get
761 // different deadlines. This flips on the first byte read: unlike a message-framed protocol,
762 // any inbound byte here is the peer being alive, which is exactly what the bound measures.
763 let mut spoke_once = false;
764
765 'session: loop {
766 let read_deadline = if spoke_once {
767 IDLE_BETWEEN_FRAMES_TIMEOUT
768 } else {
769 FIRST_FRAME_READ_TIMEOUT
770 };
771 let n = tokio::select! {
772 // Resolves when the dispatcher closed the connection, and also (as an Err) when
773 // the dispatcher task is gone. Either way there is nothing left to read for.
774 _ = &mut close_rx => break 'session,
775 // The deadline covers this read and nothing else. The dispatcher answers on its
776 // own task, so an LLM round-trip — or a `manual` rule parked for a human — never
777 // sits inside it; what it measures is the peer's own silence, which is why the
778 // idle value has to be above the interval at which NATS clients PING.
779 _ = tokio::time::sleep(read_deadline) => {
780 log.info(format!(
781 "NATS client {} sent nothing for {}s; closing \
782 decision=fail_closed_idle_timeout",
783 peer_addr,
784 read_deadline.as_secs()
785 ));
786 let _ = write_counted(
787 &write_half,
788 b"-ERR 'Stale Connection'\r\n",
789 &app_state,
790 server_id,
791 connection_id,
792 )
793 .await;
794 let _ = write_half.lock().await.shutdown().await;
795 break 'session;
796 }
797 result = read_half.read(&mut chunk) => match result {
798 Ok(0) => {
799 log.info(format!("NATS client {} disconnected", peer_addr));
800 break 'session;
801 }
802 Ok(n) => n,
803 Err(e) => {
804 log.error(format!("NATS read error from {}: {}", peer_addr, e));
805 break 'session;
806 }
807 }
808 };
809
810 spoke_once = true;
811 buf.extend_from_slice(&chunk[..n]);
812 app_state
813 .update_connection_stats(
814 server_id,
815 connection_id,
816 Some(n as u64),
817 None,
818 Some(1),
819 None,
820 )
821 .await;
822 log.debug(format!("NATS received {} bytes from {}", n, peer_addr));
823
824 loop {
825 // `parse_frame` folds a leading run of blank lines into `consumed` when a frame
826 // follows one, but reports `Ok(None)` having consumed nothing when none does.
827 // Draining here is what stops a peer that sends only newlines from growing `buf`
828 // by a chunk on every read for as long as it cares to keep typing.
829 let blank = blank_line_prefix_len(&buf);
830 if blank > 0 {
831 buf.drain(..blank);
832 }
833 match parse_frame(&buf, max_payload) {
834 Ok(None) => {
835 // Nothing extractable, and more buffered than any single frame could
836 // ever need. The peer is not going to complete one.
837 if buf.len() as u64 > max_payload.saturating_add(MAX_BUFFERED_SLACK as u64) {
838 log.warn(format!(
839 "NATS buffered {} bytes from {} with no complete frame \
840 decision=fail_closed_buffer_too_large - closing connection",
841 buf.len(),
842 peer_addr
843 ));
844 let _ = write_counted(
845 &write_half,
846 format!(
847 "-ERR '{}'\r\n",
848 FrameError::MaximumPayloadViolation.wire_text()
849 )
850 .as_bytes(),
851 &app_state,
852 server_id,
853 connection_id,
854 )
855 .await;
856 let _ = write_half.lock().await.shutdown().await;
857 break 'session;
858 }
859 break;
860 }
861 Ok(Some((frame, consumed))) => {
862 buf.drain(..consumed);
863
864 // PING/PONG and the verbose acknowledgements are protocol bookkeeping,
865 // never a decision, and are answered here without an LLM call.
866 match &frame {
867 Frame::Ping => {
868 log.debug(format!("NATS PING from {} -> PONG", peer_addr));
869 if write_counted(
870 &write_half,
871 b"PONG\r\n",
872 &app_state,
873 server_id,
874 connection_id,
875 )
876 .await
877 .is_err()
878 {
879 break 'session;
880 }
881 continue;
882 }
883 Frame::Pong => {
884 log.trace(format!("NATS PONG from {}", peer_addr));
885 continue;
886 }
887 Frame::Connect(doc) => {
888 verbose = doc
889 .get("verbose")
890 .and_then(|v| v.as_bool())
891 .unwrap_or(false);
892 }
893 _ => {}
894 }
895
896 if verbose
897 && write_counted(
898 &write_half,
899 b"+OK\r\n",
900 &app_state,
901 server_id,
902 connection_id,
903 )
904 .await
905 .is_err()
906 {
907 break 'session;
908 }
909
910 if frame_tx.send(frame).await.is_err() {
911 // The dispatcher is gone; nothing can answer any further frame.
912 break 'session;
913 }
914 }
915 Err(e) => {
916 // A malformed frame is unrecoverable: the byte stream is no longer
917 // aligned to a frame boundary. A real server answers -ERR and hangs up,
918 // and so does this one. Nothing here is derived from an internal error.
919 // A payload over `max_payload` is refused on the size the PUB *declared*,
920 // before any of it is buffered; that refusal is the inbound bound and is
921 // tagged apart from a malformed frame.
922 let decision = if e == FrameError::MaximumPayloadViolation {
923 "fail_closed_payload_too_large"
924 } else {
925 "fail_closed_protocol_error"
926 };
927 log.warn(format!(
928 "NATS protocol error from {}: {} decision={} - closing connection",
929 peer_addr, e, decision
930 ));
931 let _ = write_counted(
932 &write_half,
933 format!("-ERR '{}'\r\n", e.wire_text()).as_bytes(),
934 &app_state,
935 server_id,
936 connection_id,
937 )
938 .await;
939 let _ = write_half.lock().await.shutdown().await;
940 break 'session;
941 }
942 }
943 }
944 }
945
946 app_state
947 .remove_peer_handle(server_id, connection_id.as_u32())
948 .await;
949 app_state
950 .close_connection_on_server(server_id, connection_id)
951 .await;
952 let _ = status_tx.send("__UPDATE_UI__".to_string());
953}
954
955/// Ask the model about each decision-bearing frame, in order, one at a time.
956#[allow(clippy::too_many_arguments)]
957async fn run_dispatcher(
958 mut frame_rx: mpsc::Receiver<Frame>,
959 close_tx: oneshot::Sender<()>,
960 write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
961 llm_client: OllamaClient,
962 app_state: Arc<AppState>,
963 status_tx: mpsc::UnboundedSender<String>,
964 server_id: crate::state::ServerId,
965 connection_id: ConnectionId,
966 peer_addr: SocketAddr,
967 protocol: Arc<NatsProtocol>,
968 jetstream: Option<Arc<jetstream::Shared>>,
969) {
970 let log = Log::new(Some(&status_tx));
971 // Owned by this task alone, so there is no lock to hold across the LLM call.
972 let mut subscriptions: HashMap<String, Subscription> = HashMap::new();
973 let mut close_tx = Some(close_tx);
974
975 while let Some(frame) = frame_rx.recv().await {
976 // JetStream's subjects are answered by the JetStream module, never as plain publishes.
977 if let (
978 Some(js),
979 Frame::Publish {
980 subject,
981 reply_to,
982 headers,
983 payload,
984 },
985 ) = (&jetstream, &frame)
986 {
987 if let Some(kind) = js.classify(subject) {
988 let sid_for = |inbox: &str| {
989 subscriptions
990 .iter()
991 .find(|(_, sub)| subject_matches(&sub.subject, inbox))
992 .map(|(sid, _)| sid.clone())
993 };
994 let ask = async |event: Event| {
995 call_llm(
996 &llm_client,
997 &app_state,
998 server_id,
999 Some(connection_id),
1000 &event,
1001 protocol.as_ref(),
1002 )
1003 .await
1004 };
1005 let bytes = jetstream::answer(
1006 js,
1007 kind,
1008 jetstream::Frame {
1009 subject,
1010 reply_to: reply_to.as_deref(),
1011 headers,
1012 payload,
1013 },
1014 sid_for,
1015 ask,
1016 &log,
1017 )
1018 .await;
1019 if !bytes.is_empty()
1020 && write_counted(&write_half, &bytes, &app_state, server_id, connection_id)
1021 .await
1022 .is_err()
1023 {
1024 break;
1025 }
1026 continue;
1027 }
1028 }
1029 let event = match &frame {
1030 Frame::Connect(doc) => Event::new(
1031 &NATS_CONNECT_EVENT,
1032 serde_json::json!({
1033 "options": doc,
1034 "client_name": doc.get("name").and_then(|v| v.as_str()).unwrap_or(""),
1035 "lang": doc.get("lang").and_then(|v| v.as_str()).unwrap_or(""),
1036 "verbose": doc.get("verbose").and_then(|v| v.as_bool()).unwrap_or(false),
1037 }),
1038 ),
1039 Frame::Publish {
1040 subject,
1041 reply_to,
1042 headers,
1043 payload,
1044 } => {
1045 // Same convention as tcp_data_received: printable text stays text, anything
1046 // else is hex, and `payload_encoding` says which so the model can echo it
1047 // back symmetrically.
1048 let (payload_str, encoding) = if payload
1049 .iter()
1050 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
1051 {
1052 (String::from_utf8_lossy(payload).to_string(), "utf8")
1053 } else {
1054 (hex::encode(payload), "hex")
1055 };
1056
1057 let matching: Vec<serde_json::Value> = subscriptions
1058 .iter()
1059 .filter(|(_, sub)| subject_matches(&sub.subject, subject))
1060 .map(|(sid, sub)| {
1061 serde_json::json!({
1062 "sid": sid,
1063 "subject": sub.subject,
1064 "queue_group": sub.queue_group,
1065 })
1066 })
1067 .collect();
1068
1069 Event::new(
1070 &NATS_PUBLISH_EVENT,
1071 serde_json::json!({
1072 "subject": subject,
1073 "reply_to": reply_to,
1074 "payload": payload_str,
1075 "payload_encoding": encoding,
1076 "headers": headers,
1077 "matching_subscriptions": matching,
1078 }),
1079 )
1080 }
1081 Frame::Subscribe {
1082 subject,
1083 queue_group,
1084 sid,
1085 } => {
1086 subscriptions.insert(
1087 sid.clone(),
1088 Subscription {
1089 subject: subject.clone(),
1090 queue_group: queue_group.clone(),
1091 },
1092 );
1093 Event::new(
1094 &NATS_SUBSCRIBE_EVENT,
1095 serde_json::json!({
1096 "subject": subject,
1097 "queue_group": queue_group,
1098 "sid": sid,
1099 }),
1100 )
1101 }
1102 Frame::Unsubscribe { sid, max_msgs } => {
1103 // An UNSUB with a max is an auto-unsubscribe *after* that many more
1104 // messages. Deliveries are not counted here (see CLAUDE.md), so the
1105 // subscription stays targetable and the model is told the threshold; a
1106 // plain UNSUB removes it.
1107 let subject = subscriptions.get(sid).map(|s| s.subject.clone());
1108 if max_msgs.is_none() {
1109 subscriptions.remove(sid);
1110 }
1111 Event::new(
1112 &NATS_UNSUBSCRIBE_EVENT,
1113 serde_json::json!({
1114 "sid": sid,
1115 "subject": subject,
1116 "max_msgs": max_msgs,
1117 }),
1118 )
1119 }
1120 // Answered by the reader; never forwarded.
1121 Frame::Ping | Frame::Pong => continue,
1122 };
1123
1124 let outcome = call_llm(
1125 &llm_client,
1126 &app_state,
1127 server_id,
1128 Some(connection_id),
1129 &event,
1130 protocol.as_ref(),
1131 )
1132 .await;
1133
1134 let mut should_close = false;
1135 match outcome {
1136 Ok(result) => {
1137 for message in &result.messages {
1138 log.info(message);
1139 }
1140
1141 let mut wrote_output = false;
1142 let mut stack: Vec<ActionResult> = result.protocol_results;
1143 stack.reverse();
1144 while let Some(item) = stack.pop() {
1145 match item {
1146 ActionResult::Output(bytes) => {
1147 log.debug(format!("NATS sent {} bytes to {}", bytes.len(), peer_addr));
1148 log.trace(format!(
1149 "NATS sent: {}",
1150 crate::utils::truncate_for_log(
1151 &String::from_utf8_lossy(&bytes),
1152 512
1153 )
1154 ));
1155 if write_counted(
1156 &write_half,
1157 &bytes,
1158 &app_state,
1159 server_id,
1160 connection_id,
1161 )
1162 .await
1163 .is_err()
1164 {
1165 should_close = true;
1166 break;
1167 }
1168 wrote_output = true;
1169 }
1170 ActionResult::Multiple(items) => {
1171 for inner in items.into_iter().rev() {
1172 stack.push(inner);
1173 }
1174 }
1175 ActionResult::CloseConnection => should_close = true,
1176 _ => {}
1177 }
1178 }
1179
1180 // Saying nothing is a legitimate answer in NATS - most publishes to a
1181 // subject nobody subscribed to produce no frame at all - so this is a debug
1182 // line, distinguishable in the log from a backend failure below.
1183 if !wrote_output && !should_close {
1184 log.debug(format!(
1185 "No NATS frame for {} on {}: decision=model_no_actions",
1186 event.id(),
1187 connection_id
1188 ));
1189 }
1190 }
1191 Err(e) => {
1192 let failure = crate::utils::WireFailure::classify(&e);
1193 let class = if failure.is_overloaded() {
1194 "overloaded"
1195 } else {
1196 "unavailable"
1197 };
1198 // The peer gets a category; the operator's log gets the error.
1199 log.warn(format!(
1200 "NATS LLM error for {} on {}: decision=fail_closed_llm_error class={class} error={e}",
1201 event.id(),
1202 connection_id
1203 ));
1204 let _ = write_counted(
1205 &write_half,
1206 format!("-ERR '{}'\r\n", failure.prefixed_text()).as_bytes(),
1207 &app_state,
1208 server_id,
1209 connection_id,
1210 )
1211 .await;
1212 // NATS clients treat -ERR as fatal, and this server has nothing further to
1213 // say on a connection it cannot answer for: hang up so the peer learns now
1214 // rather than at its own timeout.
1215 should_close = true;
1216 }
1217 }
1218
1219 if should_close {
1220 let _ = write_half.lock().await.shutdown().await;
1221 app_state
1222 .remove_peer_handle(server_id, connection_id.as_u32())
1223 .await;
1224 app_state
1225 .close_connection_on_server(server_id, connection_id)
1226 .await;
1227 let _ = status_tx.send("__UPDATE_UI__".to_string());
1228 log.info(format!(
1229 "NATS connection {} closed by the server",
1230 connection_id
1231 ));
1232 break;
1233 }
1234 }
1235
1236 // Wakes the reader out of its read(), whether we closed the connection or simply ran
1237 // out of frames.
1238 if let Some(tx) = close_tx.take() {
1239 let _ = tx.send(());
1240 }
1241}