Repository navigation
rutis 0.10.0
0.10.0 is a quality release. Following the second quality step, it adds tests across processes and platforms and fixes the defects those tests found. It adds three features: a format version in rutis.json, link states and listening addresses in rutis-host run, and runtime output passed on a whole line at a time. Everything else is fixes. A few behaviours now fail explicitly; see "Compatibility".
Packages
All of these are at 0.10.0:
| Registry | Packages |
|---|---|
| crates.io | rutis (the core), rutis-bridge, rutis-loader, rutis-host; the dylib toolchain rutis-sdk, rutis-dylib, rutis-dylib-meta, rutis-dylib-launcher |
| npm | @arcships/rutis, @arcships/rutis-runtime, @arcships/rutis-bun, @arcships/rutis-host, and the five platform host packages |
| PyPI | rutis, rutis-host |
| Go | github.com/arcships/rutis/go/rutis (tag go/rutis/v0.10.0) |
| GitHub | rutis-host binaries for Linux / macOS (x64 / arm64) and Windows (x64), with SHA-256 files |
rutis-cli is still released on its own.
New
- A format version in
rutis.json(#207): top-level"format": 1. It can be left out, and a file without it reads as format 1, so existing files need no change. When a file's format is newer than the one rutis-host supports, rutis-host refuses it before reading any other field and says to upgrade rutis-host.rutis-host newwrites the field. Every releasedrutis.jsonsince 0.8.0 is kept as a sample, and tests check that each still reads as it did. dsh'scordis.patch.ymlis Cordis's configuration format and gets no version. rutis-host runprints link states and listening addresses (#321): at start, each listener prints the address it actually bound:rutis-host: listening on <name> <address>. When the link of a node row changes state, rutis-host prints one line:link connected,link down (<reason>); retrying in …,link refused: auth-rejected (<reason>), orlink stopped: incompatible (<reason>). You can now tell from the output whether a token was refused, a certificate was not trusted, or the far end is not up. Tokens never appear in the output.- Runtime output is passed on a whole line at a time (#332): each line that a Node, Bun, Python or Go runtime writes to stdout or stderr appears whole and unchanged on the same stream of rutis-host. It no longer runs into rutis-host's own lines, even when a plugin writes a line in several pieces. Runtimes now see pipes, not a terminal, on stdout and stderr, so libraries that choose colours by whether they write to a terminal print without colours. On the Rust side,
Stdio::Linesis new; the library default is stillInherit. - A protocol version mismatch says which end to upgrade (#293): in all five implementations (Rust, Node, Bun, Python, Go), handshake refusals and WebSocket subprotocol refusals name the end to upgrade and its package. The error category is still
incompatible.
Fixes
Values across processes
- A value may nest at most 64 levels deep (#279). Before, a value deeper than serde_json's recursion limit on the receiving side ended the whole session, so every other call on it failed. Now the side that sends the value checks it before sending, and a value nested too deep fails only that call; the session goes on.
- Floating-point numbers reach the Rust side bit for bit (#290). Before, they could change by one or two ULPs.
- On Node 24 and later,
JSON.parsereads some object keys that contain escapes as other keys (#302). The Node runtime now works around it, so plugins no longer receive silently changed values. - The Go runtime ends the session on a frame with invalid UTF-8 (#294), as the other runtimes do. Before, it replaced the invalid bytes with U+FFFD and went on.
- When Python sends an integer outside ±(2^53−1), that call fails explicitly (#356). Before, nothing checked it: integers over 2^64 silently became floats when relayed through Rust, and integers over 2^53 were not exact on the JS and Go sides either. The limit is the same as in JS and Go.
Sessions and links
- When a call chain crosses processes over links, a process no longer takes another process's calls for its own (#325). Session tags are now unique across processes; the wire format is unchanged.
- When a client certificate is invalid, the listener sends its TLS alert before it closes the connection (#300). The dialer now gets
AuthRejectedon Windows too, instead of a connection reset. - On macOS, the system sometimes killed a runtime process (SIGKILL) as it started (#287). This happened when another thread was initialising libnotify during a fork. rutis now finishes that initialisation before its first fork. The problem appeared on macOS 26 and 27.
Boundary rules of Cordis plugins (#281, #282)
- A synchronous
bailon the Cordis side for an event forwarded to rutis now fails explicitly and suggestsserial. Before, it silently got a Promise. - When a
waterfallpasses through the rutis side, rutis's listeners run as one asynchronous step, and the chain goes on to the listeners after it on the Cordis side. - A mount that lists an
internal/*event ineventsoremitsfails, with the error reported before the process starts.
Core and loader
- A failed plugin is retried only when its dependencies actually change (#327). Before, repeated notices about the same dependencies could load it again, and it could stay in Loading.
- The inject and listener lists under one key shrink back once they hold fewer entries (#291).
- Go runtimes: a restart whose caller stopped waiting still finishes, so two processes of the same name no longer run at once (#307). The manifest of a binary that was just replaced is read again when the first read failed, and ETXTBSY on Linux is retried (#306).
rutis-hostreports that it has started only after its runtimes have actually let their rows through (#314).- When the host is killed (
kill -9), its runtimes exit too (#353). Before, on Unix, a Node runtime kept running whenever a plugin left a timer, and a cleanup that never returned made the Node, Bun and Python runtimes hang. Now, once the session ends, all four runtimes unload their rows within a deadline and exit. The deadline isrutis-host's--shutdown-timeout, passed to the runtimes in the environment variableRUTIS_SHUTDOWN_TIMEOUT, 10 seconds by default. Past it, the remaining cleanups are given up and the exit code is 1.
Python
- On CPython 3.13.6,
listen("wss://…")refuses to start (#304). That patch release has an SSL regression (python/cpython#137583) that hangs every TLS connection. Use 3.13.7 or later.ws://is not affected.
Tests and quality
Most of the work in this release went into tests, which bring no direct change for users. This section only lists what they cover:
- End-to-end scenarios: real
rutis-hostprocesses with probe plugins in every language. They cover the plugin development loop, combinations of languages and runtime crashes, moving an implementation, nodes in two separate processes, and running right after installing. They run on Linux, macOS and Windows. - The conformance suites have Windows and loopback columns. Fault injection is wired into sessions and links. There are negative TLS tests, and property tests for values crossing languages and for reference release counts.
- The testing tools (
@arcships/rutis/testing,rutis.testing) and the real runtimes are checked against the same expectations file (#206). - Core guarantees are covered: event order, lifecycle and reclamation. Assertions no longer depend on the real clock, and randomised tests take a seed.
- The guarantee registry records rutis's core promises and contract behaviours, and the tests that verify them. 0.10 publishes it as a baseline; from 0.11 on, it is checked before every release.
Compatibility
- Protocol versions are unchanged: local sessions are still protocol 2, sessions with endpoint identities protocol 3, and
PLUGIN_APIis still 1. 0.9 and 0.10 hosts and runtimes work together. Only between two 0.9 processes is #325 still possible. rutis.json: files from 0.10'srutis-host newhave"format": 1. A 0.9 rutis-host does not know the field and reports it as an unknown field. To read such a file with 0.9, delete that line.- Behaviours that now fail explicitly:
- A synchronous
bailfrom Cordis on an event forwarded to rutis throws (support to come: #358). - A mount that lists
internal/*fails (support to come: #359). - A value nested more than 64 levels deep cannot cross processes.
- A
wss://listener cannot start on CPython 3.13.6. - Python plugins can no longer send integers outside ±(2^53−1), such as the nanosecond timestamps from
time.time_ns(). Send them as strings or in milliseconds.
- A synchronous
- Stricter testing tools:
@arcships/rutis/testingandrutis.testingnow match the real runtimes and refuse values that cannot cross processes, for example Node'sDate,Map,SetandUint8Array, and Python'ssetandbytes. An error thrown across the boundary keeps only its name and message. A plugin test that used to pass in the testing tool but would have failed in a real runtime now fails in the testing tool. These types will be supported in later versions: #308, #360. - Rust API:
rutis-bridge:LinkState::Stoppedgains afailurefield.rutis-bridge:Stdiogains the variantLines(the enum was already#[non_exhaustive]).- New:
session::MAX_DEPTH,write_lineandoutput_ended. - The
rutis-hostlibrary:HostConfiggains aformatfield. - The
rutis-hostlibrary:status::followtakes one more argument. - New:
Host::start_withandEmbedding, which let an embedding program register built-in plugins. rutisandrutis-loaderhave no breaking changes.
- Runtime processes: when the session ends and cleanups pass the deadline, a runtime exits with 1 (the Go runtime used to exit with 0 after 5 seconds).
- dylib: rebuild dylib plugins with the 0.10.0 SDK.
- Minimum versions are unchanged: Rust 1.88, Node 22, Python 3.10, Bun 1.4, Go 1.24.
Known issues
-
Three value inconsistencies between languages remain (#355):
- Python's
datetime,DecimalandUUIDcross as object references, so the far end gets a proxy, not data; time.Time,[]byteand large integers inside Go struct fields are not checked;- Python drops dict keys whose value is undefined, where the other languages send
null.
They will be handled in 0.11, together with support for date and binary values (#308).
- Python's
Getting started
npx @arcships/rutis-host@0.10.0 new greeter --lang nodeuvx --from rutis-host==0.10.0 rutis-host new greeter-py --lang pythonThen read TypeScript plugins, Bun plugins, Python plugins, Go plugins, embedding in Rust or connecting nodes.