By Eric Cheng · Arcships · First published in Chinese on October 23, 2026
TL;DR: rutis is a Rust-based plugin runtime for agents and other long-running software. Plugins provide named services, so a caller can keep using calendar while its implementation changes language or moves to another machine. rutis tracks dependencies, cleanup, and the restart of affected plugins so those changes can happen without restarting the host.
Agents can write integration code quickly. The harder question is how to try that code, replace it, and roll back failures while the system keeps running. This post explains rutis's lifecycle model, how it crosses process and machine boundaries, and where the design is headed.
1. When changing code is no longer expensive
In the past, we designed software on the assumption that changes were costly. We tried to settle requirements first, stabilize interfaces, lock down dependencies, and then change as little as possible.
When changes become cheap, that assumption no longer holds. For software that can iterate on itself, the everyday activity is not simply running. It is trying things out:
- Today it needs to connect to an internal calendar system, so it writes a first version and tries it.
- Tomorrow it finds a better interface for that system and swaps in a new version.
- The next day it needs a model on a GPU machine, so it moves part of its capability there.
- Later, a heavily used component needs to be faster and more reliable, so it is rewritten in another language.
Each change is small. But a running system may go through dozens of these small changes in a week.
The hard part shifts from how to write it to how to keep replacing it. When a component is replaced, are old connections, timers, and listeners left behind? Do the components that depend on it break? Can a bad replacement be rolled back? If a service moves to another machine or is rewritten in another language, must all of its callers change too?
These questions determine whether software can really grow.
2. Wait for others to adapt, or connect to them ourselves?
Today, the usual way for software to gain a new capability is to wait for someone else to make it available.
The maintainers of an environment provide an API, an SDK, an open platform, or, more recently, an MCP server. Software calls what they provide. This is a good approach, but it assumes that what software can do depends on what someone outside it has prepared. The connection sits outside the software. It belongs to the environment, not to the software.
Once software can write its own code, that assumption becomes unnecessary. It can reverse the direction of the work:
It can spend its own effort connecting to and adapting to other systems.
Whatever it needs to reach, it can write a plugin to connect to it—in the language that fits best, perhaps with an agent writing the plugin. It can try, revise, and replace that plugin. The connection becomes part of the software, with its own lifecycle. It can be observed and cleanly removed.
Eventually, software could even replace an external interface or system. That may sound like a slogan, but in rutis it follows from a simple mechanism:
A consumer depends on a service's name, not on its implementation.
Suppose a scheduling feature depends on a service named calendar.
- At first, a plugin provides
calendarby wrapping the company's existing calendar system. - After real use, the team understands which methods
calendarneeds and what they should return. - Later, the provider is replaced with an in-house implementation that still exposes the name
calendar.
The scheduling feature does not change a line of code across those three stages, and the host program does not restart. Adapting to a system and replacing it are two stages of the same process. There is no separate migration project between them.
Both approaches can coexist. An MCP client can be an ordinary plugin in rutis. But the environment should not determine the limits of what software can do.
3. The foundation: replace one piece while the system keeps running
For software to iterate this way, it first needs to be split into pieces that can be replaced individually at runtime. Those pieces are plugins.
Plugin systems are not new. But replacing plugins frequently while a system is running demands stronger guarantees. The rutis core focuses on a few of them, each stated as a promise and backed by tests:
- A plugin does not start before its dependencies are ready. It declares the services it needs and runs only when all of them are available.
- Everything it registers is cleaned up exactly once when it stops. Services, listeners, background tasks, and child plugins are released in the reverse order of registration—no more and no less.
- A failed startup rolls back. Anything already registered is withdrawn, leaving no half-finished state.
- Replacing one piece restarts only the parts that use it. When a service provider changes, its dependent plugins stop and wait for the new provider to become ready. Unrelated parts keep running.
With those guarantees, replacing a component once and replacing it a thousand times should be equally safe. Software can iterate in smaller steps: replace one part instead of releasing the whole system, test it in a real running environment, and switch back if it does not work.
The term hot update can mean different things. Some systems replace machine code in place. rutis replaces running components. This approach is in the same tradition as Erlang/OTP and Java's OSGi. Rust's ownership model gives clean unloading a firmer foundation: in a garbage-collected environment, one stray reference can keep an old module alive indefinitely.
But plugins are only the foundation. What rutis really aims to do is let this kind of iteration spread outward.
4. Let iteration cross processes, languages, and devices
If iteration can happen only within one process and one language, the environments software can reach remain limited to that ecosystem. To use Python's model ecosystem, it has to be brought in. To use a device on another machine, someone has to build a separate remote-call layer. Every boundary forces the system to solve startup, dependencies, and cleanup again.
rutis makes those boundaries invisible to a plugin: a plugin does not need to know which process provides a service, which language implements it, or which machine runs it. It knows the service by name. rutis handles where the service comes from, what happens when the connection drops, and how dependents follow a replacement.
The following three examples cross three different kinds of boundary.
Example 1: an agent whose shell runs on another machine
Imagine an agent built with rutis. Its model calls, conversations, and decisions happen locally, but it needs to execute commands somewhere else—perhaps in an isolated sandbox or on a GPU machine with CUDA installed.
The shell can be a plugin running in a Python runtime on that machine:
import asyncio from rutis import define_plugin class Shell: async def run(self, command): proc = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) out, _ = await proc.communicate() return out.decode() def apply(ctx, config): ctx.provide("shell", Shell()) plugin = define_plugin(apply, provides={"shell": Shell}) That machine runs a listening Python runtime with shell_tool installed. The rutis host where the agent lives decides how to configure the plugin and when it starts and stops:
{ "id": "agent", "rows": [ { "id": "sandbox", "name": "rutis-host/dial", "config": { "address": "wss://sandbox.example.com:7443/rutis" } }, { "id": "shell", "name": "sandbox:shell_tool" } ] } This is an architecture example, not a complete deployment recipe. The remote runtime also needs TLS and a shared token; the node connection guide covers those steps.
The agent's side depends only on the name shell. As a result:
- Moving to another execution environment requires changing one address in the configuration.
- If the remote machine disconnects, the parts that depend on
shellstop and wait instead of crashing. They load again when the connection returns. - The execution environment can evolve too. The shell plugin may begin in Python and later be replaced with a Go version without changing the agent's code.
The agent's "hand" has reached another machine, yet it uses that hand much as it would a local one.
Example 2: a server with components spread across processes and machines
Consider a more familiar case: a backend service.
Its main logic is written in Rust, but different supporting components belong in different places:
Server's main logic (Rust) ├─ Database client Same process ├─ Third-party API integration Python process, using the official SDK ├─ Inference service Runtime on a GPU machine └─ Adapter for a legacy system To be replaced with an in-house implementation later Each component is a plugin that provides a named service. The main logic uses those services by name. Where they run and which language implements them live in configuration, not in the callers' code.
This has several consequences:
- Failures have boundaries. If the Python process crashes, the services it provided are withdrawn and their dependents stop and wait. Other parts of the same host keep running. rutis can also restart a failed runtime according to a backoff policy.
- A configuration change does not leave the service half-broken. Reloading configuration at runtime is all-or-nothing. If the new configuration is invalid, the old one keeps running unchanged.
- Adapt first, replace later happens here. A plugin initially wraps the legacy system. Once the service contract has settled under real traffic, it is replaced with an in-house implementation. The main logic does not need to know this happened.
Example 3: multiple rutis hosts sharing capabilities
The final boundary is the device. Each rutis host can be a node in a network.
Office node ── exports calendar ──▶ Cloud node running the agent ◀── exports device ── Home node ◀── shared services · remote plugins · forwarded events ──▶ Nodes are joined by an encrypted connection. Over that connection, they can:
- Share services: export a local service to another node or import a remote service as a local one.
- Run plugins on another node: a plugin uses that machine's services while its configuration and lifecycle are controlled from here.
- Forward events: an event on one side can be received on the other.
A dropped connection reconnects automatically. While the other node is offline, the parts that depend on it wait and then resume when it returns.
This is the clearest picture of iteration spreading outward: each node contributes the capabilities of its own environment. The office node can reach internal systems, the home node can reach devices at home, and the cloud node has compute capacity. When one node learns to connect to a system, software across the network gains access to that capability.
Further ahead, phone and desktop apps could join in the same way. An app could expose contacts, location, or notifications as plugins, connect to rutis, and become part of the network. This part is still at the design stage, but the direction is the same.
5. Give the stable parts to Rust and the changing parts to the ecosystems
This works because rutis makes a basic division:
Give what is stable and unlikely to change to the systems layer. Give what changes and needs an ecosystem to other languages.
- The stable part is the lifecycle: what starts first, what happens when a dependency disappears, how cleanup works, and what cannot be done across a process boundary. rutis implements those rules once, in Rust. Its core depends on tokio and only a handful of other libraries. Each guarantee is recorded and tied to specific tests.
- The changing part is connection and adaptation. Python has an ecosystem for AI and data processing; Node has npm and existing Cordis plugins; Go has a large set of infrastructure libraries; and another machine has its own processes and devices. On these language-specific sides, there is only a small SDK, not another implementation of the framework.
This division also describes a progression over time. A capability often starts in Python or TypeScript so it can be tried quickly. Real use gradually clarifies its shape. The parts that have stood the test of time can then be rewritten in Rust for reliability and performance. An early design note put it this way:
TypeScript is the lab; Rust takes the graduates.
Which parts deserve to "graduate"? Parts whose meaning has settled; parts that are mechanisms rather than wording; parts that are performance-sensitive or sit on a trust boundary. For an agent, for example, the mechanism for assembling prompts can be made permanent, while the prompt content should remain changeable.
Because consumers depend only on service names, the path from experiment to stable implementation can be taken one step at a time. There is no need to stop one day for a large migration. Each additional runtime connects the software's ability to evolve to another ecosystem.
6. A new container for growth
Placing rutis next to an operating system helps clarify what it is.
| Traditional operating system | rutis | |
|---|---|---|
| Assumption | The program has already been written and runs inside it | The program keeps growing and changing while it runs |
| What it manages | CPU, memory, and processes | Connections and capabilities: who provides what, and who depends on whom |
| Boundary | One machine | Across processes, languages, and devices |
| How a program gains a new capability | Install software or a driver | Write a plugin to connect to something, try it, and replace it |
| Main concern | Isolation and scheduling | Whether the whole system remains correct and clean after one piece is replaced |
rutis does not replace the operating system; it runs on top of it. An operating system lets programs run. rutis aims to let programs grow. From the perspective of rutis, plugins are like processes, services are like system calls, and language runtimes and remote nodes are like drivers. All of them follow the same lifecycle rules; none receives special treatment.
Closing
For the first time, software has a chance to understand and engage with the world around it on its own. Instead of waiting for someone else to prepare an interface, it can go out, connect, try, improve, and, when needed, replace what is there.
That requires a container that remains reliable through frequent change, one that is not confined to a process, a language, or a device. That is what rutis aims to become.
The rutis lifecycle model comes from Cordis by Shigma. rutis implements it in Rust and carries it across more languages, processes, and machines.
Try it with Node 22+:
npx @arcships/rutis-host new weather --lang node cd weather && npm install npx rutis-host dev # Run the plugin and reload it automatically when files change Python, Bun, and Go can be starting points as well.
- GitHub: https://github.com/arcships/rutis
- Design philosophy: How rutis is designed
English translation prepared with AI assistance. Original article by Eric Cheng.





