Skip to content

rutis v0.10.0

Latest

Choose a tag to compare

@github-actions github-actions released this 13 Oct 01:07
· 18 commits to main since this release
ea487b2

rutis 0.10.0

中文 · 0.9.1 release notes

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 new writes the field. Every released rutis.json since 0.8.0 is kept as a sample, and tests check that each still reads as it did. dsh's cordis.patch.yml is Cordis's configuration format and gets no version.
  • rutis-host run prints 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>), or link 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::Lines is new; the library default is still Inherit.
  • 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.parse reads 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 AuthRejected on 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 bail on the Cordis side for an event forwarded to rutis now fails explicitly and suggests serial. Before, it silently got a Promise.
  • When a waterfall passes 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 in events or emits fails, 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-host reports 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 is rutis-host's --shutdown-timeout, passed to the runtimes in the environment variable RUTIS_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-host processes 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_API is 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's rutis-host new have "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 bail from 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.
  • Stricter testing tools: @arcships/rutis/testing and rutis.testing now match the real runtimes and refuse values that cannot cross processes, for example Node's Date, Map, Set and Uint8Array, and Python's set and bytes. 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::Stopped gains a failure field.
    • rutis-bridge: Stdio gains the variant Lines (the enum was already #[non_exhaustive]).
    • New: session::MAX_DEPTH, write_line and output_ended.
    • The rutis-host library: HostConfig gains a format field.
    • The rutis-host library: status::follow takes one more argument.
    • New: Host::start_with and Embedding, which let an embedding program register built-in plugins.
    • rutis and rutis-loader have 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, Decimal and UUID cross as object references, so the far end gets a proxy, not data;
    • time.Time, []byte and 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).

Getting started

npx @arcships/rutis-host@0.10.0 new greeter --lang node
uvx --from rutis-host==0.10.0 rutis-host new greeter-py --lang python

Then read TypeScript plugins, Bun plugins, Python plugins, Go plugins, embedding in Rust or connecting nodes.