diff --git a/CHANGELOG.md b/CHANGELOG.md index 717c19a..9221f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ All notable changes to this project will be documented in this file. request limit and explicit malformed-input failures. - Reusable local child-process client for issuing correlated protocol requests and shutting down the server process cleanly. +- Loopback-only TCP JSON-lines endpoint for local development clients, sharing + the same live image as stdio and limiting concurrent clients. - Execution-model decision and Phase 1 runtime documentation. - Lifecycle tests proving failed-message retry from committed state and stop/restart preservation of reserved-first FIFO order. diff --git a/README.md b/README.md index 78d77bd..ee751e2 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,16 @@ experiment using Rune 0.14.2: script execution, native calls, async suspension, instruction budgets, stepping, and compiled-code sharing. It also runs a minimal single-worker actor demonstration with bounded mailboxes and explicit message effects. Source and compiled implementation identities are BLAKE3 content -addresses with an in-memory compile cache. Persistence, canonicalization, the -network development API, graphical world, and VS Code extension are not +addresses with an in-memory compile cache. Persistence, canonicalization, +remote network access, graphical world, and VS Code extension are not implemented yet. The code library now supports an in-memory versioned behavior registry, and the CLI exposes publishing, history, rollback, safe-boundary installation, pause/resume, and cancellation through a trusted in-process image API. A versioned transport-neutral protocol contract is now defined, but no -network listener or authentication is implemented. The dispatcher is available -through a local JSON-lines stdio process, with a bounded request size. The -interactive CLI now uses the versioned protocol and dispatcher rather than -calling the image API directly. +authentication is implemented. The dispatcher is available through local +JSON-lines stdio and an unauthenticated TCP listener restricted to localhost; +both use a bounded request size. The interactive CLI now uses the versioned +protocol and dispatcher rather than calling the image API directly. The prototype uses worker-local invocation stacks and cooperative instruction slices, and actor snapshots report basic active-execution progress. Native @@ -53,13 +53,19 @@ mise run run For a local child-process protocol endpoint, run `mise run serve` and exchange one JSON request per line on stdin; responses are written to stdout. Request -lines are limited to 1 MiB. This is not a network server and is intended for a -trusted parent process. +lines are limited to 1 MiB. -The `runa-client` crate can launch that process and exchange typed protocol -requests with request/response correlation. The VS Code extension is intended -to use an authenticated network connection to this same live image; actors -remain in the image regardless of how clients connect. +For a TCP endpoint, run `mise run serve-network`. It listens on +`127.0.0.1:3737` and accepts the same JSON-lines protocol, with up to 16 +simultaneous clients sharing one live image. The listener rejects non-loopback +addresses and has no authentication; any local process that can connect has +full access to the development API. Use it only on a trusted local machine. + +The `runa-client` crate can launch the stdio process and exchange typed +protocol requests with request/response correlation. The VS Code extension is +intended to connect over the network to this same live image; actors remain in +the image regardless of how clients connect. Remote network access requires +authentication and authorization, which are not implemented. The CLI opens an interactive runtime session using the default counter behavior: diff --git a/crates/runa-server/src/lib.rs b/crates/runa-server/src/lib.rs index 523b65b..40b2345 100644 --- a/crates/runa-server/src/lib.rs +++ b/crates/runa-server/src/lib.rs @@ -1,7 +1,10 @@ //! Transport-neutral dispatch from protocol requests to image API operations. -use std::io::{self, BufRead, Write}; +use std::io::{self, BufRead, Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; use std::num::NonZeroUsize; +use std::thread; +use std::time::Duration; use runa_actor::{ActorId, Event, Message, Status}; use runa_image_api::ImageApi; @@ -12,6 +15,7 @@ use runa_protocol::{ }; pub const MAX_REQUEST_LINE_BYTES: usize = 1024 * 1024; +const MAX_NETWORK_CLIENTS: usize = 16; pub struct Server { image: ImageApi, @@ -23,18 +27,204 @@ pub struct Server { /// oversized input terminates the stream with `InvalidData`. pub fn serve_json_lines( server: &mut Server, - mut reader: R, - mut writer: W, + reader: R, + writer: W, ) -> io::Result<()> { + serve_json_lines_with(reader, writer, |request| Ok(server.dispatch(request))) +} + +/// Bind the protocol listener to an IPv4 or IPv6 loopback address only. +pub fn bind_loopback_listener(address: SocketAddr) -> io::Result { + if !address.ip().is_loopback() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "network listener address must be a loopback address", + )); + } + + TcpListener::bind(address) +} + +/// Serve JSON-lines requests from loopback clients using one shared image. +/// +/// The nonblocking event loop keeps the non-`Send` Rune runtime on one thread. +/// At most `MAX_NETWORK_CLIENTS` are active. +pub fn serve_tcp(listener: TcpListener, mut server: Server) -> io::Result<()> { + let address = listener.local_addr()?; + if !address.ip().is_loopback() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "network listener must be bound to a loopback address", + )); + } + listener.set_nonblocking(true)?; + eprintln!("runa-server: listening on {address}"); + let mut clients = Vec::new(); + loop { + loop { + match listener.accept() { + Ok((stream, peer_address)) => { + if clients.len() >= MAX_NETWORK_CLIENTS { + eprintln!("runa-server: rejecting {peer_address}; client limit reached"); + continue; + } + stream.set_nonblocking(true)?; + clients.push(NetworkClient::new(stream, peer_address)); + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => break, + Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, + Err(error) => return Err(error), + } + } + + let mut index = 0; + while index < clients.len() { + match clients[index].poll(&mut server) { + Ok(true) => index += 1, + Ok(false) => { + clients.swap_remove(index); + } + Err(error) => { + eprintln!("runa-server: client {}: {error}", clients[index].address); + clients.swap_remove(index); + } + } + } + + thread::sleep(Duration::from_millis(5)); + } +} + +struct NetworkClient { + stream: TcpStream, + address: SocketAddr, + input: Vec, + output: Vec, + output_offset: usize, + read_closed: bool, +} + +impl NetworkClient { + fn new(stream: TcpStream, address: SocketAddr) -> Self { + Self { + stream, + address, + input: Vec::new(), + output: Vec::new(), + output_offset: 0, + read_closed: false, + } + } + + fn poll(&mut self, server: &mut Server) -> io::Result { + if self.output_offset == self.output.len() { + self.output.clear(); + self.output_offset = 0; + self.process_pending_requests(server, self.read_closed)?; + + if self.output.is_empty() && !self.read_closed { + let mut buffer = [0; 8192]; + match self.stream.read(&mut buffer) { + Ok(0) => { + self.read_closed = true; + self.process_pending_requests(server, true)?; + } + Ok(length) => { + self.input.extend_from_slice(&buffer[..length]); + self.process_pending_requests(server, false)?; + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => {} + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + } + + if self.output_offset < self.output.len() { + match self.stream.write(&self.output[self.output_offset..]) { + Ok(0) => { + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to write protocol response", + )); + } + Ok(length) => { + self.output_offset += length; + if self.output_offset == self.output.len() { + self.output.clear(); + self.output_offset = 0; + } + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => {} + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + + Ok(!self.read_closed || !self.input.is_empty() || self.output_offset < self.output.len()) + } + + fn process_pending_requests( + &mut self, + server: &mut Server, + final_line: bool, + ) -> io::Result<()> { + let Some(line_end) = self.input.iter().position(|byte| *byte == b'\n') else { + if self.input.len() > MAX_REQUEST_LINE_BYTES { + return Err(invalid_data(format!( + "request line exceeds {MAX_REQUEST_LINE_BYTES} bytes" + ))); + } + if !final_line || self.input.is_empty() { + return Ok(()); + } + let line = self.input.clone(); + self.input.clear(); + self.dispatch_line(server, &line)?; + return Ok(()); + }; + + if line_end > MAX_REQUEST_LINE_BYTES { + return Err(invalid_data(format!( + "request line exceeds {MAX_REQUEST_LINE_BYTES} bytes" + ))); + } + let line = self.input[..line_end].to_vec(); + self.input.drain(..=line_end); + self.dispatch_line(server, &line) + } + + fn dispatch_line(&mut self, server: &mut Server, line: &[u8]) -> io::Result<()> { + let request: Request = serde_json::from_slice(line) + .map_err(|error| invalid_data(format!("invalid protocol request JSON: {error}")))?; + let response = server.dispatch(request); + let mut encoded = serde_json::to_vec(&response).map_err(io::Error::other)?; + if encoded.len() > MAX_REQUEST_LINE_BYTES { + return Err(invalid_data(format!( + "response exceeds {MAX_REQUEST_LINE_BYTES} bytes" + ))); + } + encoded.push(b'\n'); + self.output.extend_from_slice(&encoded); + Ok(()) + } +} + +fn invalid_data(message: String) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} + +fn serve_json_lines_with(mut reader: R, mut writer: W, mut dispatch: F) -> io::Result<()> +where + R: BufRead, + W: Write, + F: FnMut(Request) -> io::Result, +{ let mut line = Vec::new(); while read_bounded_line(&mut reader, &mut line)? { - let request = serde_json::from_slice(&line).map_err(|error| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("invalid protocol request JSON: {error}"), - ) - })?; - let response = server.dispatch(request); + let request: Request = serde_json::from_slice(&line) + .map_err(|error| invalid_data(format!("invalid protocol request JSON: {error}")))?; + let response = dispatch(request)?; serde_json::to_writer(&mut writer, &response).map_err(io::Error::other)?; writer.write_all(b"\n")?; writer.flush()?; @@ -347,7 +537,8 @@ fn invalid_request(message: impl Into) -> ProtocolError { mod tests { use super::*; use runa_protocol::{Command, PROTOCOL_VERSION}; - use std::io::Cursor; + use std::io::{BufRead, BufReader, Cursor}; + use std::net::Shutdown; fn request(command: Command) -> Request { Request { @@ -545,4 +736,50 @@ mod tests { let error = serve_json_lines(&mut server, Cursor::new(oversized), Vec::new()).unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::InvalidData); } + + #[test] + fn loopback_listener_rejects_non_loopback_addresses() { + let error = bind_loopback_listener("0.0.0.0:0".parse().unwrap()).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidInput); + + let listener = bind_loopback_listener("127.0.0.1:0".parse().unwrap()).unwrap(); + assert!(listener.local_addr().unwrap().ip().is_loopback()); + } + + #[test] + fn tcp_transport_dispatches_requests_to_the_shared_server() { + let listener = bind_loopback_listener("127.0.0.1:0".parse().unwrap()).unwrap(); + let address = listener.local_addr().unwrap(); + let mut client = TcpStream::connect(address).unwrap(); + let (server_stream, peer_address) = listener.accept().unwrap(); + let mut network_client = NetworkClient::new(server_stream, peer_address); + + let first_request = request(Command::ListActors); + let mut second_request = request(Command::ListActors); + second_request.request_id = "second-request".to_owned(); + writeln!( + client, + "{}\n{}", + serde_json::to_string(&first_request).unwrap(), + serde_json::to_string(&second_request).unwrap() + ) + .unwrap(); + client.shutdown(Shutdown::Write).unwrap(); + + assert!(network_client.poll(&mut Server::new()).unwrap()); + let mut response_line = String::new(); + let mut client_reader = BufReader::new(client); + client_reader.read_line(&mut response_line).unwrap(); + let first_response: Response = serde_json::from_str(&response_line).unwrap(); + assert_eq!(first_response.request_id, first_request.request_id); + + assert!(network_client.poll(&mut Server::new()).unwrap()); + response_line.clear(); + client_reader.read_line(&mut response_line).unwrap(); + let second_response: Response = serde_json::from_str(&response_line).unwrap(); + assert_eq!(second_response.request_id, second_request.request_id); + assert_eq!(second_response.result, Ok(Reply::Actors(Vec::new()))); + + assert!(!network_client.poll(&mut Server::new()).unwrap()); + } } diff --git a/crates/runa-server/src/main.rs b/crates/runa-server/src/main.rs index 875f8e2..594f9df 100644 --- a/crates/runa-server/src/main.rs +++ b/crates/runa-server/src/main.rs @@ -1,12 +1,11 @@ use std::io; +use std::net::SocketAddr; use std::process::ExitCode; -use runa_server::{Server, serve_json_lines}; +use runa_server::{Server, bind_loopback_listener, serve_json_lines, serve_tcp}; fn main() -> ExitCode { - let stdin = io::stdin(); - let stdout = io::stdout(); - match serve_json_lines(&mut Server::new(), stdin.lock(), stdout.lock()) { + match run() { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("runa-server: {error}"); @@ -14,3 +13,41 @@ fn main() -> ExitCode { } } } + +fn run() -> io::Result<()> { + let mut args = std::env::args().skip(1); + match args.next().as_deref() { + None => serve_stdio(), + Some("--listen") => { + let address = args.next().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "usage: runa-server [--listen ]", + ) + })?; + if args.next().is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "usage: runa-server [--listen ]", + )); + } + let address = address.parse::().map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid listener address: {error}"), + ) + })?; + serve_tcp(bind_loopback_listener(address)?, Server::new()) + } + Some(_) => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "usage: runa-server [--listen ]", + )), + } +} + +fn serve_stdio() -> io::Result<()> { + let stdin = io::stdin(); + let stdout = io::stdout(); + serve_json_lines(&mut Server::new(), stdin.lock(), stdout.lock()) +} diff --git a/crates/runa-server/tests/local_client.rs b/crates/runa-server/tests/local_client.rs index ac735ec..59201be 100644 --- a/crates/runa-server/tests/local_client.rs +++ b/crates/runa-server/tests/local_client.rs @@ -1,5 +1,19 @@ +use std::io::{BufRead, BufReader, Write}; +use std::net::{Shutdown, SocketAddr, TcpStream}; +use std::process::{Child, Command as ProcessCommand, Stdio}; +use std::time::Duration; + use runa_client::LocalClient; -use runa_protocol::{Command, MessageDto, Reply}; +use runa_protocol::{Command, MessageDto, PROTOCOL_VERSION, Reply, Request, Response}; + +struct ServerProcess(Child); + +impl Drop for ServerProcess { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} #[test] fn local_client_launches_server_and_operates_the_same_live_image() { @@ -46,3 +60,41 @@ fn local_client_launches_server_and_operates_the_same_live_image() { )); assert!(client.shutdown().unwrap().success()); } + +#[test] +fn tcp_server_accepts_local_protocol_requests() { + let child = ProcessCommand::new(env!("CARGO_BIN_EXE_runa-server")) + .args(["--listen", "127.0.0.1:0"]) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let mut server = ServerProcess(child); + let stderr = server.0.stderr.take().unwrap(); + let mut stderr = BufReader::new(stderr); + let mut startup = String::new(); + stderr.read_line(&mut startup).unwrap(); + let address = startup + .strip_prefix("runa-server: listening on ") + .expect("server should report its bound address") + .trim() + .parse::() + .unwrap(); + + let mut client = TcpStream::connect_timeout(&address, Duration::from_secs(2)).unwrap(); + let request = Request { + version: PROTOCOL_VERSION, + request_id: "tcp-process-test".to_owned(), + command: Command::ListActors, + }; + writeln!(client, "{}", serde_json::to_string(&request).unwrap()).unwrap(); + client.shutdown(Shutdown::Write).unwrap(); + + let mut response_line = String::new(); + BufReader::new(client) + .read_line(&mut response_line) + .unwrap(); + let response: Response = serde_json::from_str(&response_line).unwrap(); + assert_eq!(response.request_id, "tcp-process-test"); + assert_eq!(response.result, Ok(Reply::Actors(Vec::new()))); +} diff --git a/docs/architecture.md b/docs/architecture.md index 41c945c..af39486 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,7 +5,7 @@ **Initial embedded language:** Rune **Inspirations:** Smalltalk/Squeak/Pharo, Morphic, Erlang/OTP **Primary interfaces:** Self-contained graphical world and VS Code extension -connected to the same live image through an authenticated network API +connected to the same live image through a network API ## 1. Vision @@ -63,7 +63,7 @@ Rust supplies a minimal, reliable substrate; increasingly much of the actor fram +-----------------------------------------------------------+ ``` -The graphical world should use internal service calls to the image API. The VS Code extension connects to the same live image through a versioned network transport. CLI tools, tests, and local automation can use in-process calls or a child-process protocol over stdio. The network boundary must authenticate and authorize clients; it does not create a separate actor runtime or image. +The graphical world should use internal service calls to the image API. The VS Code extension connects to the same live image through a versioned network transport. CLI tools, tests, and local automation can use in-process calls or a child-process protocol over stdio. The initial TCP transport is unauthenticated and must bind only to loopback; a remote-capable network boundary must authenticate and authorize clients. Neither transport creates a separate actor runtime or image. ## 4. Fundamental Actor Model @@ -229,11 +229,11 @@ These are desired capabilities, not a requirement to implement every endpoint im ### 8.1 Security -Normal actors should not automatically receive development privileges. Arbitrary evaluation, state mutation, code replacement, and process control require explicit capabilities. The current stdio bridge is limited to a trusted parent process. Any future network bridge must bind locally by default and use a defensible authentication mechanism; never expose unauthenticated remote code execution endpoints. +Normal actors should not automatically receive development privileges. Arbitrary evaluation, state mutation, code replacement, and process control require explicit capabilities. The stdio bridge is intended for a trusted parent process. The initial TCP bridge accepts unauthenticated connections only on IPv4 or IPv6 loopback and grants each connection the full development API; any local process may connect. It is suitable only for trusted local development. Never bind this endpoint to a non-loopback address or expose it remotely until authentication and authorization are implemented. ## 9. VS Code Extension -VS Code is an external client of the same live image, not a separate runtime. It connects through an authenticated versioned network transport, initially planned as JSON-RPC over WebSocket. The extension does not host actors or create a separate image. The local stdio protocol remains useful for CLI tools, tests, and other child-process clients. Network security, connection lifecycle, and event subscription semantics must be designed before exposing the listener. +VS Code is an external client of the same live image, not a separate runtime. The initial development transport is versioned JSON Lines over TCP, bound to loopback and unauthenticated; the extension must connect only to a trusted local image. Remote connections remain out of scope until authentication and authorization are implemented. The extension does not host actors or create a separate image. The local stdio protocol remains useful for CLI tools, tests, and other child-process clients. Connection lifecycle and event subscription semantics still need design. Required extension UX: @@ -311,7 +311,7 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen | `runa-runtime` | Scheduling, actor execution coordination, state commits, crash recovery, binding actor semantics to Rune execution | `runa-core`, `runa-actor`, `runa-code`, `runa-rune` | | `runa-image-api` | Trusted in-process application services for spawning, inspecting, sending, editing, and upgrading | `runa-core`, `runa-actor`, `runa-code`, `runa-runtime` | | `runa-protocol` | Versioned transport-neutral request/response/event DTOs and validation | `serde` | -| `runa-server` | Protocol dispatch and local JSON-lines stdio transport; authenticated network transport for VS Code clients | `runa-protocol`, `runa-image-api` | +| `runa-server` | Protocol dispatch, JSON-lines stdio, and loopback-only TCP transport | `runa-protocol`, `runa-image-api` | | `runa-client` | Local child-process client for the JSON-lines stdio protocol | `runa-protocol` | | `runa-graphics` | Native window, rendering and input mechanisms; no actor policy | `runa-core` if necessary | | `runa-world` | Morphic-style graphical composition and in-world development UI | `runa-image-api`, `runa-graphics` | @@ -331,14 +331,15 @@ than reaching into the code store and runtime directly. `runa-protocol` now defines versioned, serializable request, response, and event DTOs without depending on a transport or the runtime implementation. `runa-server` now dispatches those commands through the image API and maps service results into -protocol replies and typed errors. It remains an in-process dispatcher; there -is no network listener, session model, authentication, or authorization. The -dispatcher is exposed through local child-process JSON-lines stdio with a 1 -MiB request-line limit. In-process consumers call `runa-image-api`, and +protocol replies and typed errors. The dispatcher is exposed through local +child-process JSON-lines stdio and an unauthenticated TCP listener restricted +to IPv4 or IPv6 loopback; both enforce a 1 MiB request-line limit. The TCP +listener shares one image across at most 16 concurrent clients. Every local +client receives full development API access, so the endpoint is for trusted +local development only. In-process consumers call `runa-image-api`, and external local tools can use `runa-client` to launch the stdio server. Actors -do not depend on network transport to coexist in one live image; the network -boundary exists for external clients such as VS Code. Authenticated network -transport is a planned integration requirement, not a runtime prerequisite. +do not depend on network transport to coexist in one live image. Remote +network access will require authentication and authorization. **Important:** `runa-core` must not contain schedulers, mailbox implementations, persistence engines, Rune VM types, service traits used by only one crate, generic helper collections, or GUI abstractions. Prefer a type defined in its owning crate until two independent consumers demonstrably need a stable shared representation. @@ -447,9 +448,11 @@ The first Phase 5 increments are a trusted in-process application API used by the CLI, a transport-neutral versioned protocol contract, and a dispatcher exposed through bounded local JSON-lines stdio. The interactive CLI uses the same protocol commands and dispatcher in-process. `runa-client` launches the -stdio server for external local tools. The next server increment for the VS -Code integration is an authenticated network transport over the same API and -live image; it is not needed for actors to share that image. +stdio server for external local tools. The server also provides an +unauthenticated TCP JSON-lines endpoint restricted to loopback for local VS +Code development. Remote access will require authentication and +authorization; neither network transport nor client location changes the +single-image actor model. ### Phase 6 — Graphical world diff --git a/docs/protocol.md b/docs/protocol.md index 32a7234..54973bc 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -2,10 +2,10 @@ `runa-protocol` defines versioned request, response, and event data contracts for clients and the `runa-server` dispatcher. It is independent of transport -and runtime implementation. The `runa-server` executable currently accepts -JSON Lines over stdin and emits one response line per valid request to stdout. -It is a local child-process transport, not a network endpoint, and provides no -authentication. +and runtime implementation. `runa-server` accepts JSON Lines over stdio or, +when started with `--listen`, over TCP bound to an IPv4 or IPv6 loopback +address. The TCP listener is unauthenticated and is intended only for trusted +local development. ## Version 1 @@ -46,14 +46,17 @@ rather than claiming a more specific category. `get_events` drains and returns the events currently accumulated by this single-worker image. It is not a durable subscription or replayable event log. -The stdio transport limits each request line to 1 MiB before parsing. Malformed -JSON or an oversized line terminates the stream with an error on stderr; no -protocol response is emitted when a request cannot be decoded. EOF cleanly -terminates the process. The `runa-client` crate launches this server locally -and validates response protocol versions and request IDs. The VS Code -extension is planned to use a separate authenticated network transport to -the same image; any such transport must impose its own message and -request-size limits before dispatch. +Both transports limit each request line to 1 MiB before parsing. Malformed +JSON or an oversized line terminates that stream with an error; no protocol +response is emitted when a request cannot be decoded. Stdio EOF terminates the +process, while TCP EOF closes only that client connection. The TCP listener +allows up to 16 simultaneous clients and serializes each dispatch through the +same image. TCP response payloads are capped at 1 MiB, with at most one +response queued per client. Every local client can use the full development +API; loopback binding is not authentication or authorization. Remote access +must not be enabled until appropriate authentication and authorization exist. +The `runa-client` crate launches the stdio server locally and validates +response protocol versions and request IDs. The current validation checks the protocol version, request ID, positive actor/revision identifiers where required, non-empty behavior names, mailbox diff --git a/mise.toml b/mise.toml index ae9528c..74e2566 100644 --- a/mise.toml +++ b/mise.toml @@ -17,6 +17,10 @@ run = "cargo run -p runa" description = "Run the local JSON-lines protocol server" run = "cargo run -p runa-server" +[tasks.serve-network] +description = "Run the protocol server on localhost" +run = "cargo run -p runa-server -- --listen 127.0.0.1:3737" + [tasks.lint] description = "Lint all workspace targets with Clippy" run = "cargo clippy --workspace --all-targets -- -D warnings"