feat: add localhost TCP protocol listener

Expose the existing versioned JSON-lines dispatcher over TCP, with an enforced IPv4/IPv6 loopback-only bind, bounded nonblocking client handling, request and response size limits, and a shared single-threaded image. Add a serve-network mise task, subprocess coverage, and document the unauthenticated trusted-local-development boundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Biedermann 2026-11-02 14:50:30 +01:00
parent e5d839f419
commit d96d603286
8 changed files with 399 additions and 55 deletions

View File

@ -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.

View File

@ -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:

View File

@ -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<R: BufRead, W: Write>(
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<TcpListener> {
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<u8>,
output: Vec<u8>,
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<bool> {
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<R, W, F>(mut reader: R, mut writer: W, mut dispatch: F) -> io::Result<()>
where
R: BufRead,
W: Write,
F: FnMut(Request) -> io::Result<Response>,
{
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<String>) -> 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());
}
}

View File

@ -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 <loopback-address>]",
)
})?;
if args.next().is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"usage: runa-server [--listen <loopback-address>]",
));
}
let address = address.parse::<SocketAddr>().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 <loopback-address>]",
)),
}
}
fn serve_stdio() -> io::Result<()> {
let stdin = io::stdin();
let stdout = io::stdout();
serve_json_lines(&mut Server::new(), stdin.lock(), stdout.lock())
}

View File

@ -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::<SocketAddr>()
.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())));
}

View File

@ -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

View File

@ -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

View File

@ -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"