rutis-remote-plugin
GitHub指导在 rutis 框架中开发远程插件,涵盖选择节点链接或远程运行时、编写插件代码、部署配置及验证流程,确保插件在非本地环境正常运行。
Trigger Scenarios
Install
npx skills add arcships/rutis --skill rutis-remote-plugin -g -y
SKILL.md
Frontmatter
{
"name": "rutis-remote-plugin",
"description": "Use when a rutis plugin must run on another machine, device or app instead of next to the host — \"remote plugin\", running a Python\/Node\/Go plugin on a GPU box, connecting two rutis hosts, exposing a phone app's capabilities to rutis. Walks through choosing a node link vs a remote runtime vs app hosting, writing the plugin in the rutis paradigm, deploying, configuring rutis.json, verifying, and the rules that keep it inside the rutis design. Self-contained; reference material is in references\/."
}
Adding a remote plugin to rutis
rutis (https://github.com/arcships/rutis) is a plugin host with a Rust core that takes plugins in several languages. This skill does not depend on the rutis repository; everything it refers to is in references/:
| File | Contents |
|---|---|
references/plugin-api.md |
The plugin API: declarations, ctx, events, lifecycle, how values cross, sync calls and reentrancy, cancellation, versions |
references/nodes.md |
Links, running plugins on another node, remote language runtimes, security |
references/rutis-json.md |
rutis.json: runtimes (restart, listen / instances), listen, rows; exit status, live reload, when a runtime ends, credentials, deployment |
Keep one sentence in mind: rutis has no such thing as a "remote plugin" — only an ordinary plugin plus a runtime or node that runs somewhere else. The plugin code is exactly what it would be locally; what changes is where its runtime runs and how rutis reaches it. Do not write a separate plugin API, protocol or lifecycle for remote use.
1. Decide which kind it is
| What the other side is | Use | Row on the rutis side |
|---|---|---|
| A language runtime on another machine; this rutis loads, configures, starts and stops its plugins | Remote runtime (link + runtime) |
<runtime name>:<plugin> |
| Another full rutis host that manages its own plugins and only shares services | Node link (export / import) |
No new row; imported services are available by name |
| Another full rutis host on which this side wants to run rows it writes | Node link (host: true there, rows: true here) |
peer:<peer>/<plugin> |
| A phone or desktop app offering its own capabilities as plugins | App hosting (designed, not implemented) | <runtime name>:<plugin>, one instance per connection |
When unsure, ask one question: who decides whether the plugin is loaded and with what configuration? This rutis → remote runtime / app hosting. The other side → node link.
2. What works today (2026-10-15; check the repository before relying on it)
| Language | Remote listening listen: (rutis dials in) |
Dialing out dial: (runtime connects to rutis) |
|---|---|---|
| Python | ✅ | ✅ (#383) |
| Node / TypeScript | ✅ | ✅ (#384) |
| Go | ✅ | ✅ (#384) |
| Bun | ✅ (#384) | ✅ (#384) |
| Swift / Kotlin apps | Designed, not implemented (#389, #391) | Same |
"One access point accepting many connections, one instance per connection" (many devices, autoscaled workers) works on the rutis side (#382: listen and instances on a runtimes.remote entry); of the language runtimes Python, Go, Node and Bun can connect to it so far (step 4b). Tracking issue: #392.
3. Write the plugin: the paradigm, same as local
Details in references/plugin-api.md. The essentials:
- Write a leaf plugin with the language's SDK: declare
inject(service names it uses) and a config schema; inapply(ctx, config)usectx.use/ctx.provide/ctx.effect. Whatctx.provideprovides reaches rutis by itself (its public methods, allasync);providesis optional, to mark methodssyncand letrutis-host checkknow before running what the plugin provides. The current plugin API version is 2 (adds leaf events,ctx.on/ctx.emit). - The plugin does not decide when it runs, does not reconnect, does not probe whether the other side is up: it is applied when all its services are ready, stops when any is withdrawn, and starts again when it returns. A remote disconnect is, to the plugin, an ordinary stop.
- Values follow the cross-process rules: JSON-compatible data is copied; dates and times, binary data, maps, sets, big integers, decimals and UUIDs are copied and arrive as each language's own types (integers beyond ±(2^53−1) cross as big integers automatically); functions and objects go by reference; nesting at most 64 levels. Never assume the same process.
- Do not hold locks while calling other services: your service may be called again in the middle of your own synchronous call. Prefer
asyncmethods for calls that cross runtimes and may cross each other. - Leaf events are dispatched only within one runtime process; do not use events to talk across machines — use services.
- Dependency declarations contain only service names, never languages, machines or addresses.
4. Deploy a remote runtime (listening, as of today)
On the remote machine:
-
Install the plugin there: Python
pip installinto the interpreter the runtime uses; Nodenpm installin the project; Bunbun addin the project; Go, build the binary. A remote runtime loads only plugins already deployed there — there is no code upload. -
Prepare TLS: non-loopback addresses must be
wss://. PointRUTIS_CERT/RUTIS_KEYat the certificate and key; with a self-signed certificate, give the issuing CA toRUTIS_CAon the rutis side. -
Set the token:
RUTIS_TOKENon the remote side, equal toRUTIS_TOKEN_<RUNTIME NAME>on the rutis side. -
Start it listening.
--idis this runtime's name,--peeris the name of the rutis that controls it:# Python RUTIS_TOKEN=… RUTIS_CERT=server.pem RUTIS_KEY=server.key \ python -m rutis listen:wss://0.0.0.0:7443/rutis --id gpu --peer main /srv/plugins # Node (inside that machine's Node project) node node_modules/@arcships/rutis-runtime/src/runner.mjs listen:wss://0.0.0.0:7443/rutis --id gpu --peer main ./package.json # Bun (inside that machine's Bun project; rows `<runtime>:<module>`, language `bun`) bun --no-install --no-env-file node_modules/@arcships/rutis-bun/src/main.ts listen:wss://0.0.0.0:7443/rutis --id gpu --peer main .A Go plugin binary accepts
listen:wss://…as its channel the same way.The Python runtime reports on stderr:
rutis: listening on …; when a session ends,rutis: session ended (<reason>); waiting for a controller(reason such asheartbeat timeout,connection lost,closed (1001): …), orrutis: session ended (replaced by a new connection)when a new connection takes over.
4b. Or let the runtime connect in (dialing out)
When the remote machines are behind NAT, change address, or come and go in numbers, the runtime connects to rutis instead. On the remote machine, with the plugins installed:
# Python
RUTIS_TOKEN=… RUTIS_CA=ca.pem \
python -m rutis dial:wss://main.example.com:7443/rutis --id worker-1 --peer main /srv/plugins
# Node (inside that machine's Node project)
RUTIS_TOKEN=… RUTIS_CA=ca.pem \
node node_modules/@arcships/rutis-runtime/src/runner.mjs dial:wss://main.example.com:7443/rutis --id worker-1 --peer main ./package.json
# Bun (inside that machine's Bun project)
RUTIS_TOKEN=… RUTIS_CA=ca.pem \
bun --no-install --no-env-file node_modules/@arcships/rutis-bun/src/main.ts dial:wss://main.example.com:7443/rutis --id worker-1 --peer main .
# Go: the plugin binary takes the same channel, flags and environment
RUTIS_TOKEN=… RUTIS_CA=ca.pem \
./weather dial:wss://main.example.com:7443/rutis --id worker-1 --peer main .
-
--idis this connection's own id ([a-z0-9-]+, unique per process: two processes with one id keep taking over from each other);--peeris the rutisid.RUTIS_CAis needed only for a self-signed certificate on the rutis side. -
It reconnects by itself: backoff 0.5 s doubling to 30 s; refused token or id every 30 s; an incompatible protocol stops it (exit status 1). Each disconnect unloads its rows; each reconnection is a new session. SIGTERM / Ctrl-C stops it for good. It reports on stderr:
rutis: connected to …,rutis: connection lost (…); retrying in …,rutis: refused: auth-rejected (…),rutis: refused: incompatible (…); stopping. -
The rutis side: a
listenentry, and on theruntimes.remoteentrylistenplusinstancesnaming aninstancedgroup whose rows are<name>:<plugin>; no link row. The token isRUTIS_TOKEN_<NAME>there:{ "id": "main", "listen": [{ "name": "public", "address": "0.0.0.0:7443", "cert": "server.pem", "key": "server.key" }], "runtimes": { "remote": [{ "name": "worker", "language": "python", "listen": "public", "instances": "worker" }] }, "rows": [{ "id": "worker", "group": true, "instanced": true, "config": [{ "id": "embedder", "name": "worker:embedder" }] }] }Every connection is one instance of the group: its rows run on it, and the services they provide are seen only in that instance.
5. Configure the rutis side
Details in references/rutis-json.md and references/nodes.md.
{
"runtimes": { "remote": [{ "name": "gpu", "language": "python" }] },
"rows": [
{ "id": "gpu", "name": "rutis-bridge/peer",
"config": { "peer": "gpu", "dial": "wss://gpu.example.com:7443/rutis", "runtime": "gpu" } },
{ "id": "embedder", "name": "gpu:embedder", "config": { "model": "small" } }
]
}
language:python/py/go/node/bun.- The link row must have
"runtime": "<name>"; without it the other side is not treated as a runtime and<name>:*rows never become ready. - Rows are named
<runtime name>:<plugin>; the plugin is resolved on the remote machine.
6. Verify
rutis-host check: remote runtimes are not contacted during checking; services that might come from them are reported as such, not as failures.rutis-host run: expect<link row id>: link connected, then<row id>: running.link refused: auth-rejectedmeans the token or certificate is wrong;link stopped: incompatiblemeans the protocol or address form is wrong, orruntimeis missing.- Stop the remote process: those rows stop, and so do the rows using their services; when it comes back rutis reconnects and loads them again.
7. What not to do
| Don't | Because |
|---|---|
| Implement reconnects, heartbeats or offline caches in the plugin to "keep the remote available" | Connections and lifecycle belong to rutis; a plugin only sees whether services are there |
| Upload code to a remote runtime or load plugins by path | A remote runtime loads only deployed plugins |
Use ws:// to a non-loopback address |
Non-loopback needs TLS; the rutis dialer refuses it outright |
| Model "devices" or "identities" in the protocol or the plugin | Device information is a service some plugin provides; identity belongs to a future management service |
| Alias same-named services to tell machines apart | Same-named plugins on different connections are told apart by instances (#382) |
| Write a separate plugin API for remote use, or make an SDK depend on Rust | The runtime side is implemented entirely in its platform language |
| Handle phone foreground/background or keep-alive in an SDK | That is the app's business logic; the SDK offers only start / stop |
Further reading
- Remote plugins design: https://github.com/arcships/rutis/blob/main/docs/design-remote-plugins-2026-10-03.en.md
- Remote runtimes — dialing out and dynamic connections: https://github.com/arcships/rutis/blob/main/docs/design-remote-runtime-dial-2026-10-14.en.md
- App integration: https://github.com/arcships/rutis/blob/main/docs/design-app-integration-2026-10-14.en.md
Version History
- b6e4351 Current 2026-10-18 15:27


