Skip to main content

netget/client/nats/
mod.rs

1//! NATS client: NetGet joins a real messaging fabric as a peer.
2//!
3//! The connection is two tasks, and the split is the whole design:
4//!
5//! * **Reader** — owns the read half, frames `INFO`/`MSG`/`HMSG`/`+OK`/`-ERR`/`PING`/`PONG`
6//!   ([`parse_server_frame`]), answers `PING` with `PONG` itself, and forwards everything that
7//!   is a decision over a **bounded** channel.
8//! * **Dispatcher** — owns the model. It makes one LLM call at a time, in arrival order, and
9//!   also serves the injected-command channel that backs the dashboard's `[ send ]`.
10//!
11//! Answering `PING` in Rust is not an optimisation. A broker that gets no `PONG` within two
12//! ping intervals declares the connection stale and drops it, and an instance created from the
13//! dashboard defaults to a `*` → manual rule — so routing the keepalive through the model
14//! would mean a human had to answer it, in seconds, forever. The same reasoning is why the
15//! reader never blocks on the dispatcher's LLM call: the two tasks exist so that a parked
16//! event cannot silence the keepalive.
17//!
18//! This is the `Idle → Processing → Accumulating` machine of `src/server/tcp/mod.rs` in a
19//! different shape. The dispatcher being busy *is* `Processing`, the channel *is*
20//! `queued_data`, and there is no `Accumulating` state because NATS frames are self-delimiting
21//! — there is never a partial message to accumulate, so `wait_for_more` here means "say
22//! nothing", not "the response was cut short".
23//!
24//! ## The action → event → action cycle, and why there is no depth bound here
25//!
26//! The chain that matters is: a `MSG` arrives, the model answers with `send_nats_publish`, the
27//! bytes go on the wire, and whatever comes back arrives at the reader as another frame and
28//! raises another event. That cycle passes through the socket and the reader task, so nothing
29//! recurses and no future contains itself — this is the `datalink` case the root `CLAUDE.md`
30//! describes, where "the chain continues by itself" and boxing would be ceremony. What bounds
31//! it is [`crate::client::llm_budget`]'s per-client call budget, which is the backstop for
32//! exactly this shape.
33//!
34//! Both tasks are registered with [`AppState::register_client_task`]: aborting a task does not
35//! abort tasks it spawned, so a reader registered nowhere would keep the socket alive past
36//! `remove_client()`.
37
38pub mod actions;
39
40pub mod jetstream;
41pub use actions::NatsClientProtocol;
42
43use anyhow::{Context, Result};
44use std::collections::BTreeMap;
45use std::net::SocketAddr;
46use std::sync::Arc;
47use tokio::io::{AsyncReadExt, AsyncWriteExt};
48use tokio::net::TcpStream;
49use tokio::sync::{mpsc, Mutex};
50use tracing::{debug, error, info, trace, warn};
51
52use crate::client::llm_budget::call_llm_for_client;
53use crate::client::nats::actions::{
54    NatsClientProtocol as Proto, NATS_CLIENT_CONNECTED_EVENT, NATS_CLIENT_ERROR_RECEIVED_EVENT,
55    NATS_CLIENT_MESSAGE_RECEIVED_EVENT, NATS_CLIENT_PERMISSION_ERROR_EVENT,
56};
57use crate::llm::actions::client_trait::{Client, ClientActionResult};
58use crate::llm::ollama_client::OllamaClient;
59use crate::protocol::Event;
60use crate::state::app_state::AppState;
61use crate::state::{ClientId, ClientStatus};
62
63/// Longest control line accepted before the peer is treated as not speaking NATS.
64///
65/// Deliberately far above the server half's 4096: an `INFO` from a real clustered
66/// `nats-server` carries a `connect_urls` array with one entry per route, and rejecting the
67/// greeting of a large cluster would be a bug that only shows up in production.
68pub const MAX_CONTROL_LINE: usize = 65_536;
69
70/// Payload ceiling used until the broker's own `max_payload` has been read from `INFO`, and
71/// the floor applied to it afterwards. A broker that advertises less than this still cannot
72/// send more than it accepts, so the floor costs nothing and protects the handshake.
73pub const DEFAULT_MAX_PAYLOAD: usize = 1_048_576;
74
75/// Hard ceiling on the broker's advertised `max_payload`. A broker is more trusted than a
76/// random peer, but "trusted" is not "allowed to name any allocation size it likes".
77pub const MAX_ACCEPTED_PAYLOAD: usize = 64 * 1_048_576;
78
79/// Hard ceiling on the reader's frame buffer, over and above whatever any frame declares.
80///
81/// A frame is at most one control line plus `max_payload` plus its terminator, so anything
82/// past that with nothing extractable is a peer that will never complete one.
83const MAX_BUFFERED_SLACK: usize = MAX_CONTROL_LINE + 16;
84
85/// How many decision frames may wait for the dispatcher before the reader stops reading.
86///
87/// Bounded on purpose. If the model is slower than the fabric for long enough to fill this,
88/// the reader blocks and the connection eventually goes stale — which is a visible failure,
89/// where an unbounded queue is an invisible one that ends in the process dying.
90const FRAME_QUEUE_CAPACITY: usize = 256;
91
92/// How long to wait for the broker's `INFO` greeting before giving up on the connection.
93///
94/// A NATS server writes `INFO` immediately on accept, so anything slower is not a NATS server.
95/// Failing here means `connect()` returns `Err` and the client is marked `Error` rather than
96/// sitting in `Connected` having never handshaked.
97const INFO_TIMEOUT_SECS: u64 = 10;
98
99// ============================================================================
100// Wire framing (server -> client)
101// ============================================================================
102
103/// One decodable frame in the broker → client direction.
104///
105/// This is a different grammar from `crate::server::nats::Frame`, which decodes the client →
106/// server direction. They share no verbs except `PING`/`PONG`, so they are two parsers rather
107/// than one with a mode flag.
108#[derive(Debug, Clone, PartialEq)]
109pub enum ServerFrame {
110    /// `INFO {json}` — the greeting, and occasionally re-sent mid-session when a cluster
111    /// changes shape.
112    Info(serde_json::Value),
113    /// `MSG`/`HMSG`. `headers` is empty for a plain `MSG`.
114    Message {
115        subject: String,
116        sid: String,
117        reply_to: Option<String>,
118        headers: BTreeMap<String, String>,
119        payload: Vec<u8>,
120    },
121    /// `+OK`, sent per command when the client asked for `verbose`.
122    Ok,
123    /// `-ERR '<text>'`, carrying the text with its surrounding quotes removed.
124    Err(String),
125    Ping,
126    Pong,
127}
128
129/// A frame the connection cannot continue after.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ServerFrameError {
132    UnknownProtocolOperation,
133    MaximumControlLineExceeded,
134    MaximumPayloadViolation,
135    InvalidInfo,
136}
137
138impl std::fmt::Display for ServerFrameError {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.write_str(match self {
141            Self::UnknownProtocolOperation => "Unknown Protocol Operation",
142            Self::MaximumControlLineExceeded => "Maximum Control Line Exceeded",
143            Self::MaximumPayloadViolation => "Maximum Payload Violation",
144            Self::InvalidInfo => "Invalid INFO document",
145        })
146    }
147}
148
149fn parse_len(token: &str) -> Result<usize, ServerFrameError> {
150    token
151        .parse::<usize>()
152        .map_err(|_| ServerFrameError::UnknownProtocolOperation)
153}
154
155/// Try to decode one broker frame from the front of `buf`.
156///
157/// `Ok(None)` means "only part of a frame has arrived" — read more and try again — and is the
158/// normal case on a stream. `Ok(Some((frame, consumed)))` gives the number of bytes to remove
159/// from the front, which for a `MSG` includes its byte-counted payload and the CRLF after it.
160///
161/// A bare LF is accepted as well as CRLF, so a hand-rolled broker (or `nc` on the other end of
162/// a test) behaves rather than producing an error on every line.
163pub fn parse_server_frame(
164    buf: &[u8],
165    max_payload: usize,
166) -> Result<Option<(ServerFrame, usize)>, ServerFrameError> {
167    let skipped = blank_line_prefix_len(buf);
168    match parse_one_server_frame(&buf[skipped..], max_payload)? {
169        Some((frame, consumed)) => Ok(Some((frame, skipped + consumed))),
170        None => Ok(None),
171    }
172}
173
174/// Length of the leading run of blank lines (`\n`, `\r\n`, or a line of only whitespace).
175///
176/// Skipping them iteratively rather than by recursing into `parse_server_frame` is the same
177/// fix as on the server half, for the same reason: one stack frame per blank line, not in
178/// tail position, so a broker (or anything on that socket) writing 8 KB of newlines — one
179/// `read` — recursed about 8000 levels and overflowed the stack. A Rust stack overflow is
180/// `SIGSEGV`, not a catchable panic, so it aborted the whole NetGet process.
181///
182/// The reader drains this prefix before parsing too: `parse_server_frame` folds it into
183/// `consumed` when a frame follows, but reports `Ok(None)` having consumed nothing when none
184/// does, so without the eager drain the buffer would grow by a chunk on every read.
185pub fn blank_line_prefix_len(buf: &[u8]) -> usize {
186    let mut offset = 0;
187    while let Some(nl) = buf[offset..].iter().position(|&b| b == b'\n') {
188        let line_end = offset + nl;
189        let line = &buf[offset..line_end];
190        let line = line.strip_suffix(b"\r").unwrap_or(line);
191        if !line.iter().all(|b| b.is_ascii_whitespace()) {
192            break;
193        }
194        offset = line_end + 1;
195    }
196    offset
197}
198
199/// Decode one frame from a buffer whose first line is known not to be blank.
200fn parse_one_server_frame(
201    buf: &[u8],
202    max_payload: usize,
203) -> Result<Option<(ServerFrame, usize)>, ServerFrameError> {
204    let Some(nl) = buf.iter().position(|&b| b == b'\n') else {
205        if buf.len() > MAX_CONTROL_LINE {
206            return Err(ServerFrameError::MaximumControlLineExceeded);
207        }
208        return Ok(None);
209    };
210    if nl > MAX_CONTROL_LINE {
211        return Err(ServerFrameError::MaximumControlLineExceeded);
212    }
213
214    let mut line_end = nl;
215    if line_end > 0 && buf[line_end - 1] == b'\r' {
216        line_end -= 1;
217    }
218    let after_line = nl + 1;
219
220    let line = std::str::from_utf8(&buf[..line_end])
221        .map_err(|_| ServerFrameError::UnknownProtocolOperation)?
222        .trim();
223
224    let mut parts = line.split_whitespace();
225    let verb = parts
226        .next()
227        .ok_or(ServerFrameError::UnknownProtocolOperation)?;
228    let args: Vec<&str> = parts.collect();
229
230    if verb.eq_ignore_ascii_case("PING") {
231        return Ok(Some((ServerFrame::Ping, after_line)));
232    }
233    if verb.eq_ignore_ascii_case("PONG") {
234        return Ok(Some((ServerFrame::Pong, after_line)));
235    }
236    if verb.eq_ignore_ascii_case("+OK") {
237        return Ok(Some((ServerFrame::Ok, after_line)));
238    }
239    if verb.eq_ignore_ascii_case("-ERR") {
240        // `-ERR '<text>'`. The quotes are part of the framing, not of the message.
241        let text = line[verb.len()..].trim();
242        let text = text
243            .strip_prefix('\'')
244            .and_then(|t| t.strip_suffix('\''))
245            .unwrap_or(text);
246        return Ok(Some((ServerFrame::Err(text.to_string()), after_line)));
247    }
248    if verb.eq_ignore_ascii_case("INFO") {
249        // The document is the remainder of the line verbatim: splitting on whitespace would
250        // break any JSON string containing a space.
251        let json_start = line
252            .find(char::is_whitespace)
253            .ok_or(ServerFrameError::InvalidInfo)?;
254        let doc: serde_json::Value = serde_json::from_str(line[json_start..].trim())
255            .map_err(|_| ServerFrameError::InvalidInfo)?;
256        if !doc.is_object() {
257            return Err(ServerFrameError::InvalidInfo);
258        }
259        return Ok(Some((ServerFrame::Info(doc), after_line)));
260    }
261
262    let is_hmsg = verb.eq_ignore_ascii_case("HMSG");
263    if is_hmsg || verb.eq_ignore_ascii_case("MSG") {
264        // MSG  <subject> <sid> [reply-to] <#bytes>
265        // HMSG <subject> <sid> [reply-to] <#header bytes> <#total bytes>
266        let (subject, sid, reply_to, header_len, total_len) = if is_hmsg {
267            match args.len() {
268                4 => (
269                    args[0],
270                    args[1],
271                    None,
272                    parse_len(args[2])?,
273                    parse_len(args[3])?,
274                ),
275                5 => (
276                    args[0],
277                    args[1],
278                    Some(args[2].to_string()),
279                    parse_len(args[3])?,
280                    parse_len(args[4])?,
281                ),
282                _ => return Err(ServerFrameError::UnknownProtocolOperation),
283            }
284        } else {
285            match args.len() {
286                3 => (args[0], args[1], None, 0, parse_len(args[2])?),
287                4 => (
288                    args[0],
289                    args[1],
290                    Some(args[2].to_string()),
291                    0,
292                    parse_len(args[3])?,
293                ),
294                _ => return Err(ServerFrameError::UnknownProtocolOperation),
295            }
296        };
297
298        if subject.is_empty() || sid.is_empty() {
299            return Err(ServerFrameError::UnknownProtocolOperation);
300        }
301        if header_len > total_len {
302            return Err(ServerFrameError::UnknownProtocolOperation);
303        }
304        // The limit applies to the whole declared size, header block included — the same fix
305        // as on the server half. Bounding only `total_len - header_len` left `header_len`
306        // unbounded, and `header_len == total_len` gives a zero-length body that passed both
307        // checks, so `HMSG s 1 4000000000 4000000000` made the reader buffer toward 4 GB and
308        // `HMSG s 1 <usize::MAX> <usize::MAX>` overflowed the index below.
309        if total_len > max_payload {
310            return Err(ServerFrameError::MaximumPayloadViolation);
311        }
312
313        let Some(body_end) = after_line.checked_add(total_len) else {
314            return Err(ServerFrameError::MaximumPayloadViolation);
315        };
316        if buf.len() <= body_end {
317            return Ok(None);
318        }
319        let terminator = match buf[body_end] {
320            b'\r' => {
321                if buf.len() < body_end + 2 {
322                    return Ok(None);
323                }
324                if buf[body_end + 1] != b'\n' {
325                    return Err(ServerFrameError::UnknownProtocolOperation);
326                }
327                2
328            }
329            b'\n' => 1,
330            _ => return Err(ServerFrameError::UnknownProtocolOperation),
331        };
332
333        let headers = if header_len > 0 {
334            parse_headers(&buf[after_line..after_line + header_len])
335        } else {
336            BTreeMap::new()
337        };
338        let payload = buf[after_line + header_len..body_end].to_vec();
339
340        return Ok(Some((
341            ServerFrame::Message {
342                subject: subject.to_string(),
343                sid: sid.to_string(),
344                reply_to,
345                headers,
346                payload,
347            },
348            body_end + terminator,
349        )));
350    }
351
352    Err(ServerFrameError::UnknownProtocolOperation)
353}
354
355/// Decode a `NATS/1.0` header block into name/value pairs.
356///
357/// The version line is skipped and malformed lines are dropped rather than failing the frame:
358/// a header a broker invented is not a reason to tear down a session.
359pub fn parse_headers(block: &[u8]) -> BTreeMap<String, String> {
360    let mut out = BTreeMap::new();
361    let text = String::from_utf8_lossy(block);
362    for (index, line) in text.split('\n').enumerate() {
363        let line = line.trim_end_matches('\r');
364        if index == 0 {
365            // `NATS/1.0 404 No Messages`: a status, exposed the way nats.go exposes it.
366            let mut parts = line.splitn(3, ' ');
367            parts.next();
368            if let Some(code) = parts
369                .next()
370                .filter(|c| c.len() == 3 && c.bytes().all(|b| b.is_ascii_digit()))
371            {
372                out.insert("Status".to_string(), code.to_string());
373                if let Some(text) = parts.next().map(str::trim).filter(|t| !t.is_empty()) {
374                    out.insert("Description".to_string(), text.to_string());
375                }
376            }
377            continue;
378        }
379        if line.trim().is_empty() {
380            continue;
381        }
382        if let Some((name, value)) = line.split_once(':') {
383            let name = name.trim();
384            if !name.is_empty() {
385                out.insert(name.to_string(), value.trim().to_string());
386            }
387        }
388    }
389    out
390}
391
392/// Render a payload for an event: the text itself when every byte is printable, hex otherwise.
393///
394/// The companion `payload_encoding` is what makes this reversible — echo both back on
395/// `send_nats_publish` and the exact received bytes go out again. Sniffing on the way back in
396/// would not be reversible, which is why the outbound side takes an explicit `encoding`.
397pub fn payload_for_event(payload: &[u8]) -> (String, &'static str) {
398    if payload
399        .iter()
400        .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
401    {
402        (String::from_utf8_lossy(payload).to_string(), "utf8")
403    } else {
404        (hex::encode(payload), "hex")
405    }
406}
407
408/// Split `-ERR 'Permissions Violation for Publish to "orders.eu"'` into the parts the model
409/// can act on.
410///
411/// Returns `(operation, subject)` where operation is `"publish"`, `"subscription"` or
412/// `"unknown"`. A permissions violation is the one `-ERR` a broker does not hang up after, so
413/// telling the model *which* subject was denied is the difference between it retrying forever
414/// and it choosing another.
415pub fn classify_permission_error(message: &str) -> (&'static str, Option<String>) {
416    let lower = message.to_ascii_lowercase();
417    let operation = if lower.contains("publish") {
418        "publish"
419    } else if lower.contains("subscription") || lower.contains("subscribe") {
420        "subscription"
421    } else {
422        "unknown"
423    };
424
425    // The subject is quoted in the real server's text; fall back to the last token otherwise.
426    let subject = match (message.find('"'), message.rfind('"')) {
427        (Some(start), Some(end)) if end > start + 1 => Some(message[start + 1..end].to_string()),
428        _ => message
429            .rsplit(' ')
430            .next()
431            .filter(|token| !token.is_empty() && *token != "to")
432            .map(|token| token.trim_matches(['"', '\'']).to_string()),
433    };
434    (operation, subject)
435}
436
437/// True for the `-ERR` texts a broker treats as fatal (which is everything except a
438/// permissions violation).
439fn is_permission_error(message: &str) -> bool {
440    message
441        .to_ascii_lowercase()
442        .contains("permissions violation")
443}
444
445// ============================================================================
446// Connection
447// ============================================================================
448
449/// Startup parameters, already validated. See `get_startup_parameters()` in `actions.rs`.
450#[derive(Debug, Clone)]
451pub struct NatsConnectOptions {
452    /// `name` field of the CONNECT document.
453    pub client_name: String,
454    /// Ask the broker to acknowledge every command with `+OK`.
455    pub verbose: bool,
456    /// Subjects subscribed to immediately after CONNECT, before the model is asked anything.
457    pub subscribe_subjects: Vec<String>,
458}
459
460impl Default for NatsConnectOptions {
461    fn default() -> Self {
462        Self {
463            client_name: "netget".to_string(),
464            verbose: false,
465            subscribe_subjects: Vec::new(),
466        }
467    }
468}
469
470/// What a dispatched answer asked the connection to do next.
471enum Flow {
472    Continue,
473    Disconnect,
474}
475
476pub struct NatsClient;
477
478impl NatsClient {
479    /// Connect to a NATS broker and hand the session to the model.
480    ///
481    /// Returns only once `INFO` has been read and `CONNECT` written, so a peer that is not a
482    /// NATS broker is an `Err` the caller turns into `ClientStatus::Error` rather than a client
483    /// that reports `Connected` and never handshaked.
484    pub async fn connect_with_llm_actions(
485        remote_addr: String,
486        llm_client: OllamaClient,
487        app_state: Arc<AppState>,
488        status_tx: mpsc::UnboundedSender<String>,
489        client_id: ClientId,
490        options: NatsConnectOptions,
491    ) -> Result<SocketAddr> {
492        let stream = TcpStream::connect(&remote_addr)
493            .await
494            .with_context(|| format!("Failed to connect to NATS broker at {}", remote_addr))?;
495        // Control lines are small and latency-sensitive; Nagle would hold a SUB back waiting
496        // for company. A failure here is not worth refusing the connection over.
497        let _ = stream.set_nodelay(true);
498
499        let local_addr = stream.local_addr()?;
500        let remote_sock_addr = stream.peer_addr()?;
501
502        let (mut read_half, write_half) = tokio::io::split(stream);
503        let write_half = Arc::new(Mutex::new(write_half));
504
505        // Command channel first, before anything that can park.
506        //
507        // The nats_connected event below goes through the routing table, and a `*` -> manual
508        // rule holds it until a human answers. Registering afterwards would make the
509        // dashboard's [ send ] read "no command channel" for that entire park, which looks
510        // like a protocol limitation and is only a queue that does not exist yet.
511        let command_rx =
512            crate::client::command_support::register_command_channel(&app_state, client_id).await;
513
514        // ---- Handshake: read INFO, write CONNECT ----------------------------------------
515        let mut buffer: Vec<u8> = Vec::with_capacity(8192);
516        let info = tokio::time::timeout(
517            std::time::Duration::from_secs(INFO_TIMEOUT_SECS),
518            Self::read_info(&mut read_half, &mut buffer),
519        )
520        .await
521        .map_err(|_| {
522            anyhow::anyhow!(
523                "No INFO greeting from {} within {}s. A NATS broker writes INFO immediately on \
524                 accept, so this peer is not speaking NATS.",
525                remote_addr,
526                INFO_TIMEOUT_SECS
527            )
528        })??;
529
530        let max_payload = info
531            .get("max_payload")
532            .and_then(|v| v.as_u64())
533            .map(|v| (v as usize).clamp(DEFAULT_MAX_PAYLOAD, MAX_ACCEPTED_PAYLOAD))
534            .unwrap_or(DEFAULT_MAX_PAYLOAD);
535
536        let connect_doc = serde_json::json!({
537            "verbose": options.verbose,
538            "pedantic": false,
539            "tls_required": false,
540            "name": options.client_name,
541            "lang": "rust",
542            "version": env!("CARGO_PKG_VERSION"),
543            "protocol": 1,
544            "headers": info.get("headers").and_then(|v| v.as_bool()).unwrap_or(false),
545            "no_responders": false,
546            "echo": true,
547        });
548
549        // CONNECT, then the startup subscriptions, then PING — one write, in that order. The
550        // PING is what a real client sends to learn that CONNECT was accepted; the PONG comes
551        // back to the reader and is logged, not surfaced.
552        let mut handshake = format!("CONNECT {}\r\n", connect_doc).into_bytes();
553        let mut subscriptions = Vec::new();
554        for (index, subject) in options.subscribe_subjects.iter().enumerate() {
555            let sid = (index + 1).to_string();
556            handshake.extend_from_slice(format!("SUB {} {}\r\n", subject, sid).as_bytes());
557            subscriptions.push(serde_json::json!({"subject": subject, "sid": sid}));
558        }
559        // JetStream replies come back under one inbox, subscribed once.
560        let jetstream = info
561            .get("jetstream")
562            .and_then(|v| v.as_bool())
563            .unwrap_or(false);
564        let js_inbox = jetstream::inbox_for(client_id);
565        if jetstream {
566            handshake.extend_from_slice(
567                format!("SUB {js_inbox}.> {}\r\n", jetstream::INBOX_SID).as_bytes(),
568            );
569        }
570        handshake.extend_from_slice(b"PING\r\n");
571        {
572            let mut guard = write_half.lock().await;
573            guard.write_all(&handshake).await?;
574            guard.flush().await?;
575        }
576
577        info!(
578            "NATS client {} connected to {} (local: {}), broker={} version={} max_payload={} \
579             startup_subscriptions={}",
580            client_id,
581            remote_sock_addr,
582            local_addr,
583            info.get("server_name")
584                .and_then(|v| v.as_str())
585                .unwrap_or("?"),
586            info.get("version").and_then(|v| v.as_str()).unwrap_or("?"),
587            max_payload,
588            subscriptions.len(),
589        );
590        app_state
591            .update_client_status(client_id, ClientStatus::Connected)
592            .await;
593        let _ = status_tx.send(format!(
594            "[CLIENT] NATS client {} connected to {} ({} startup subscription(s))",
595            client_id,
596            remote_sock_addr,
597            subscriptions.len()
598        ));
599        let _ = status_tx.send("__UPDATE_UI__".to_string());
600
601        // ---- Reader task -----------------------------------------------------------------
602        let (frame_tx, frame_rx) = mpsc::channel::<ServerFrame>(FRAME_QUEUE_CAPACITY);
603
604        let reader_state = app_state.clone();
605        let reader_status_tx = status_tx.clone();
606        let reader_write_half = write_half.clone();
607        let reader_handle = tokio::spawn(async move {
608            Self::run_reader(
609                read_half,
610                reader_write_half,
611                buffer,
612                max_payload,
613                frame_tx,
614                client_id,
615                reader_state,
616                reader_status_tx,
617            )
618            .await;
619        });
620        let reader_abort = reader_handle.abort_handle();
621        app_state
622            .register_client_task(client_id, reader_handle)
623            .await;
624
625        // ---- Dispatcher task -------------------------------------------------------------
626        let connected_event = Event::new(
627            &NATS_CLIENT_CONNECTED_EVENT,
628            serde_json::json!({
629                "remote_addr": remote_sock_addr.to_string(),
630                "server_name": info.get("server_name").and_then(|v| v.as_str()).unwrap_or(""),
631                "server_id": info.get("server_id").and_then(|v| v.as_str()).unwrap_or(""),
632                "version": info.get("version").and_then(|v| v.as_str()).unwrap_or(""),
633                "max_payload": max_payload,
634                "headers": info.get("headers").and_then(|v| v.as_bool()).unwrap_or(false),
635                "auth_required": info.get("auth_required").and_then(|v| v.as_bool()).unwrap_or(false),
636                "tls_required": info.get("tls_required").and_then(|v| v.as_bool()).unwrap_or(false),
637                "subscriptions": subscriptions,
638                "jetstream": jetstream,
639                "info": info,
640            }),
641        );
642
643        let dispatcher_state = app_state.clone();
644        let dispatcher_handle = tokio::spawn(async move {
645            Self::run_dispatcher(
646                write_half,
647                frame_rx,
648                command_rx,
649                connected_event,
650                client_id,
651                llm_client,
652                dispatcher_state,
653                status_tx,
654                reader_abort,
655                js_inbox,
656            )
657            .await;
658        });
659        app_state
660            .register_client_task(client_id, dispatcher_handle)
661            .await;
662
663        Ok(local_addr)
664    }
665
666    /// Read until the broker's `INFO` greeting is complete, leaving anything after it in
667    /// `buffer` for the reader task.
668    ///
669    /// A `PING` before `INFO` would be a protocol violation, so nothing else is tolerated
670    /// here: the first frame is the greeting or the peer is not a broker.
671    async fn read_info(
672        read_half: &mut tokio::io::ReadHalf<TcpStream>,
673        buffer: &mut Vec<u8>,
674    ) -> Result<serde_json::Value> {
675        let mut chunk = vec![0u8; 8192];
676        loop {
677            match parse_server_frame(buffer, DEFAULT_MAX_PAYLOAD) {
678                Ok(Some((ServerFrame::Info(doc), consumed))) => {
679                    buffer.drain(..consumed);
680                    return Ok(doc);
681                }
682                Ok(Some((other, _))) => {
683                    return Err(anyhow::anyhow!(
684                        "Expected INFO as the first NATS frame, got {:?}",
685                        other
686                    ))
687                }
688                Ok(None) => {}
689                Err(e) => return Err(anyhow::anyhow!("Malformed NATS greeting: {}", e)),
690            }
691
692            let n = read_half.read(&mut chunk).await?;
693            if n == 0 {
694                return Err(anyhow::anyhow!(
695                    "Broker closed the connection before sending INFO"
696                ));
697            }
698            buffer.extend_from_slice(&chunk[..n]);
699        }
700    }
701
702    /// Frame the stream, answer `PING`, forward decisions.
703    #[allow(clippy::too_many_arguments)]
704    async fn run_reader(
705        mut read_half: tokio::io::ReadHalf<TcpStream>,
706        write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
707        mut buffer: Vec<u8>,
708        max_payload: usize,
709        frame_tx: mpsc::Sender<ServerFrame>,
710        client_id: ClientId,
711        app_state: Arc<AppState>,
712        status_tx: mpsc::UnboundedSender<String>,
713    ) {
714        let mut chunk = vec![0u8; 8192];
715        'outer: loop {
716            // Drain every complete frame currently in the buffer before reading again.
717            loop {
718                // `parse_server_frame` folds a leading run of blank lines into `consumed`
719                // when a frame follows one, but reports `Ok(None)` having consumed nothing
720                // when none does. Draining here is what stops a peer sending only newlines
721                // from growing `buffer` by a chunk on every read.
722                let blank = blank_line_prefix_len(&buffer);
723                if blank > 0 {
724                    buffer.drain(..blank);
725                }
726                if buffer.len() > max_payload.saturating_add(MAX_BUFFERED_SLACK) {
727                    // More buffered than any single frame could need, with none extractable.
728                    error!(
729                        "NATS client {} buffered {} bytes with no complete frame; closing",
730                        client_id,
731                        buffer.len()
732                    );
733                    let _ = status_tx.send(format!(
734                        "[CLIENT] ✖ NATS client {} broker sent an unframeable stream",
735                        client_id
736                    ));
737                    break 'outer;
738                }
739                match parse_server_frame(&buffer, max_payload) {
740                    Ok(Some((frame, consumed))) => {
741                        buffer.drain(..consumed);
742                        match frame {
743                            ServerFrame::Ping => {
744                                // Answered here, never by the model. See the module docs.
745                                let mut guard = write_half.lock().await;
746                                if guard.write_all(b"PONG\r\n").await.is_err()
747                                    || guard.flush().await.is_err()
748                                {
749                                    warn!(
750                                        "NATS client {} could not answer PING; connection is \
751                                         going away",
752                                        client_id
753                                    );
754                                    break 'outer;
755                                }
756                                trace!("NATS client {} PING -> PONG", client_id);
757                            }
758                            ServerFrame::Pong => {
759                                trace!("NATS client {} received PONG", client_id);
760                            }
761                            ServerFrame::Ok => {
762                                trace!("NATS client {} received +OK", client_id);
763                            }
764                            ServerFrame::Info(doc) => {
765                                // A mid-session INFO is a cluster update, not a new session.
766                                // It is logged rather than raised so that "connected" keeps
767                                // meaning "connected once".
768                                debug!("NATS client {} received updated INFO: {}", client_id, doc);
769                            }
770                            decision => {
771                                if frame_tx.send(decision).await.is_err() {
772                                    debug!(
773                                        "NATS client {} dispatcher is gone; reader stopping",
774                                        client_id
775                                    );
776                                    break 'outer;
777                                }
778                            }
779                        }
780                    }
781                    Ok(None) => break,
782                    Err(e) => {
783                        error!(
784                            "NATS client {} could not frame the broker's output ({}); closing",
785                            client_id, e
786                        );
787                        let _ = status_tx.send(format!(
788                            "[CLIENT] ✖ NATS client {} protocol error: {}",
789                            client_id, e
790                        ));
791                        break 'outer;
792                    }
793                }
794            }
795
796            match read_half.read(&mut chunk).await {
797                Ok(0) => {
798                    info!("NATS client {} broker closed the connection", client_id);
799                    break;
800                }
801                Ok(n) => buffer.extend_from_slice(&chunk[..n]),
802                Err(e) => {
803                    error!("NATS client {} read error: {}", client_id, e);
804                    app_state
805                        .update_client_status(client_id, ClientStatus::Error(e.to_string()))
806                        .await;
807                    let _ = status_tx.send("__UPDATE_UI__".to_string());
808                    break;
809                }
810            }
811        }
812
813        // Dropping frame_tx is what tells the dispatcher the session is over.
814        drop(frame_tx);
815        app_state
816            .update_client_status(client_id, ClientStatus::Disconnected)
817            .await;
818        let _ = status_tx.send(format!("[CLIENT] NATS client {} disconnected", client_id));
819        let _ = status_tx.send("__UPDATE_UI__".to_string());
820    }
821
822    /// Own the model: one LLM call at a time, plus the injected-command channel.
823    #[allow(clippy::too_many_arguments)]
824    async fn run_dispatcher(
825        write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
826        mut frame_rx: mpsc::Receiver<ServerFrame>,
827        mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
828        connected_event: Event,
829        client_id: ClientId,
830        llm_client: OllamaClient,
831        app_state: Arc<AppState>,
832        status_tx: mpsc::UnboundedSender<String>,
833        reader_abort: tokio::task::AbortHandle,
834        js_inbox: String,
835    ) {
836        let protocol = Proto::with_inbox(js_inbox.clone());
837        let mut memory = String::new();
838
839        // The connected event runs first, and the reader is already up — so a manual handler
840        // may park this for as long as it likes without the keepalive going unanswered.
841        let mut flow = Self::ask_and_execute(
842            &protocol,
843            &write_half,
844            connected_event,
845            &mut memory,
846            client_id,
847            &llm_client,
848            &app_state,
849            &status_tx,
850        )
851        .await;
852
853        while matches!(flow, Flow::Continue) {
854            tokio::select! {
855                frame = frame_rx.recv() => {
856                    let Some(frame) = frame else { break };
857                    let Some(event) = Self::event_for(frame, client_id, &js_inbox) else { continue };
858                    flow = Self::ask_and_execute(
859                        &protocol,
860                        &write_half,
861                        event,
862                        &mut memory,
863                        client_id,
864                        &llm_client,
865                        &app_state,
866                        &status_tx,
867                    )
868                    .await;
869                }
870                command = command_rx.recv() => {
871                    let Some(command) = command else { break };
872                    let disconnect = crate::client::command_support::handle_stream_client_command(
873                        &protocol,
874                        &write_half,
875                        command,
876                        client_id,
877                        &app_state,
878                        &status_tx,
879                    )
880                    .await;
881                    if disconnect {
882                        flow = Flow::Disconnect;
883                    }
884                }
885            }
886        }
887
888        // Half-close so the broker reads EOF and tears the session down its own way, then stop
889        // the reader: it is parked on a socket the peer may keep open indefinitely, and
890        // dropping a JoinHandle only detaches it.
891        {
892            let mut guard = write_half.lock().await;
893            let _ = guard.shutdown().await;
894        }
895        reader_abort.abort();
896
897        app_state
898            .update_client_status(client_id, ClientStatus::Disconnected)
899            .await;
900        // The dispatcher owns the only receiver; dropping the registered handle makes later
901        // send_to_client calls fail fast instead of timing out.
902        app_state.remove_client_handle(client_id).await;
903        let _ = status_tx.send(format!("[CLIENT] NATS client {} session ended", client_id));
904        let _ = status_tx.send("__UPDATE_UI__".to_string());
905    }
906
907    /// Turn a decision frame into the event the model is asked about.
908    ///
909    /// Returns `None` for frames the reader should have handled; that is a programming error
910    /// rather than a peer error, so it is logged rather than raised.
911    fn event_for(frame: ServerFrame, client_id: ClientId, js_inbox: &str) -> Option<Event> {
912        match frame {
913            ServerFrame::Message {
914                subject,
915                sid,
916                reply_to,
917                headers,
918                payload,
919            } => {
920                if sid == jetstream::INBOX_SID {
921                    if let Some(event) = jetstream::event_for(
922                        js_inbox,
923                        &subject,
924                        reply_to.as_deref(),
925                        &headers,
926                        &payload,
927                    ) {
928                        return Some(event);
929                    }
930                }
931                let (payload, encoding) = payload_for_event(&payload);
932                Some(Event::new(
933                    &NATS_CLIENT_MESSAGE_RECEIVED_EVENT,
934                    serde_json::json!({
935                        "subject": subject,
936                        "sid": sid,
937                        "reply_to": reply_to,
938                        "payload": payload,
939                        "payload_encoding": encoding,
940                        "headers": headers,
941                    }),
942                ))
943            }
944            ServerFrame::Err(message) => {
945                if is_permission_error(&message) {
946                    let (operation, subject) = classify_permission_error(&message);
947                    warn!(
948                        "NATS client {} was denied: {} (operation={})",
949                        client_id, message, operation
950                    );
951                    Some(Event::new(
952                        &NATS_CLIENT_PERMISSION_ERROR_EVENT,
953                        serde_json::json!({
954                            "message": message,
955                            "operation": operation,
956                            "subject": subject,
957                        }),
958                    ))
959                } else {
960                    warn!("NATS client {} received -ERR: {}", client_id, message);
961                    Some(Event::new(
962                        &NATS_CLIENT_ERROR_RECEIVED_EVENT,
963                        serde_json::json!({
964                            "message": message,
965                            // Everything except a permissions violation is fatal in NATS: the
966                            // broker hangs up right after sending it.
967                            "fatal": true,
968                        }),
969                    ))
970                }
971            }
972            other => {
973                debug!(
974                    "NATS client {} dispatcher saw a frame the reader owns: {:?}",
975                    client_id, other
976                );
977                None
978            }
979        }
980    }
981
982    /// Ask the model about one event and run every action it answers with.
983    ///
984    /// The actions are the point. Discarding them — `let _ =`, an `Err`-only arm, a `..` that
985    /// swallows the field, or a `debug!` of `.len()` — is the single most common client defect
986    /// in this repo: the event fires, the round-trip is paid for, the log shows a reply, and
987    /// nothing reaches the wire.
988    #[allow(clippy::too_many_arguments)]
989    async fn ask_and_execute(
990        protocol: &Proto,
991        write_half: &Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
992        event: Event,
993        memory: &mut String,
994        client_id: ClientId,
995        llm_client: &OllamaClient,
996        app_state: &Arc<AppState>,
997        status_tx: &mpsc::UnboundedSender<String>,
998    ) -> Flow {
999        let instruction = app_state
1000            .get_instruction_for_client(client_id)
1001            .await
1002            .unwrap_or_default();
1003
1004        let outcome = call_llm_for_client(
1005            llm_client,
1006            app_state,
1007            client_id.to_string(),
1008            &instruction,
1009            memory,
1010            Some(&event),
1011            protocol,
1012            status_tx,
1013        )
1014        .await;
1015
1016        let result = match outcome {
1017            Ok(result) => result,
1018            Err(e) => {
1019                // Nothing is written to the broker on failure, deliberately: every frame this
1020                // client can send is a positive assertion (a publish, a subscription), and
1021                // inventing one because the backend was down would put fabricated traffic on
1022                // somebody's message bus. The log carries the error; the wire carries nothing.
1023                error!(
1024                    "NATS client {} could not answer '{}': {} (decision=fail_closed_llm_error)",
1025                    client_id,
1026                    event.id(),
1027                    e
1028                );
1029                let _ = status_tx.send(format!(
1030                    "[CLIENT] ✖ NATS client {} could not answer '{}'",
1031                    client_id,
1032                    event.id()
1033                ));
1034                return Flow::Continue;
1035            }
1036        };
1037
1038        if let Some(updated) = result.memory_updates {
1039            *memory = updated;
1040        }
1041
1042        if result.actions.is_empty() {
1043            debug!(
1044                "NATS client {} answered '{}' with no actions (decision=model_no_actions)",
1045                client_id,
1046                event.id()
1047            );
1048        }
1049
1050        for action in result.actions {
1051            match Self::apply_action(protocol, write_half, action, client_id).await {
1052                Flow::Disconnect => return Flow::Disconnect,
1053                Flow::Continue => {}
1054            }
1055        }
1056        Flow::Continue
1057    }
1058
1059    /// Execute one model-authored action and write whatever it produced.
1060    async fn apply_action(
1061        protocol: &Proto,
1062        write_half: &Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
1063        action: serde_json::Value,
1064        client_id: ClientId,
1065    ) -> Flow {
1066        let executed = match protocol.execute_action(action.clone()) {
1067            Ok(executed) => executed,
1068            Err(e) => {
1069                warn!(
1070                    "NATS client {} rejected its own model's action {}: {}",
1071                    client_id, action, e
1072                );
1073                return Flow::Continue;
1074            }
1075        };
1076
1077        // `Multiple` nests one level deep at most — that is all the enum's producers use.
1078        let results = match executed {
1079            ClientActionResult::Multiple(results) => results,
1080            single => vec![single],
1081        };
1082
1083        let mut flow = Flow::Continue;
1084        for result in results {
1085            match result {
1086                ClientActionResult::SendData(bytes) => {
1087                    let mut guard = write_half.lock().await;
1088                    if let Err(e) = guard.write_all(&bytes).await {
1089                        error!("NATS client {} write failed: {}", client_id, e);
1090                        return Flow::Disconnect;
1091                    }
1092                    if let Err(e) = guard.flush().await {
1093                        error!("NATS client {} flush failed: {}", client_id, e);
1094                        return Flow::Disconnect;
1095                    }
1096                    trace!(
1097                        "NATS client {} sent {} bytes: {}",
1098                        client_id,
1099                        bytes.len(),
1100                        String::from_utf8_lossy(&bytes).escape_debug()
1101                    );
1102                }
1103                ClientActionResult::Disconnect => {
1104                    info!(
1105                        "NATS client {} disconnecting on the model's request",
1106                        client_id
1107                    );
1108                    flow = Flow::Disconnect;
1109                }
1110                ClientActionResult::WaitForMore => {
1111                    trace!("NATS client {} is waiting for more traffic", client_id);
1112                }
1113                ClientActionResult::NoAction => {}
1114                ClientActionResult::Custom { name, .. } => {
1115                    warn!(
1116                        "NATS client {} produced an unexpected custom result '{}'; this \
1117                         protocol's actions are all SendData",
1118                        client_id, name
1119                    );
1120                }
1121                ClientActionResult::Multiple(_) => {
1122                    warn!(
1123                        "NATS client {} produced a nested Multiple; ignored",
1124                        client_id
1125                    );
1126                }
1127            }
1128        }
1129        flow
1130    }
1131}