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:
parent
e5d839f419
commit
d96d603286
|
|
@ -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.
|
||||
|
|
|
|||
30
README.md
30
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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue