Skip to main content

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}