Agent Skills › arcships/rutis › rutis-remote-plugin

rutis-remote-plugin

GitHub

指导在 rutis 框架中开发远程插件,涵盖选择节点链接或远程运行时、编写插件代码、部署配置及验证流程,确保插件在非本地环境正常运行。

.agents/skills/rutis-remote-plugin/SKILL.md arcships/rutis

Trigger Scenarios

需要在另一台机器或设备上运行 rutis 插件 配置 rutis 远程连接或分布式部署 将外部应用能力集成到 rutis

Install

npx skills add arcships/rutis --skill rutis-remote-plugin -g -y
More Options

Non-standard path

npx skills add https://github.com/arcships/rutis/tree/main/.agents/skills/rutis-remote-plugin -g -y

Use without installing

npx skills use arcships/rutis@rutis-remote-plugin

指定 Agent (Claude Code)

npx skills add arcships/rutis --skill rutis-remote-plugin -a claude-code -g -y

安装 repo 全部 skill

npx skills add arcships/rutis --all -g -y

预览 repo 内 skill

npx skills add arcships/rutis --list

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; in apply(ctx, config) use ctx.use / ctx.provide / ctx.effect. What ctx.provide provides reaches rutis by itself (its public methods, all async); provides is optional, to mark methods sync and let rutis-host check know 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 async methods 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:

  1. Install the plugin there: Python pip install into the interpreter the runtime uses; Node npm install in the project; Bun bun add in the project; Go, build the binary. A remote runtime loads only plugins already deployed there — there is no code upload.

  2. Prepare TLS: non-loopback addresses must be wss://. Point RUTIS_CERT / RUTIS_KEY at the certificate and key; with a self-signed certificate, give the issuing CA to RUTIS_CA on the rutis side.

  3. Set the token: RUTIS_TOKEN on the remote side, equal to RUTIS_TOKEN_<RUNTIME NAME> on the rutis side.

  4. Start it listening. --id is this runtime's name, --peer is 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 as heartbeat timeout, connection lost, closed (1001): …), or rutis: 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 .
  • --id is this connection's own id ([a-z0-9-]+, unique per process: two processes with one id keep taking over from each other); --peer is the rutis id. RUTIS_CA is 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 listen entry, and on the runtimes.remote entry listen plus instances naming an instanced group whose rows are <name>:<plugin>; no link row. The token is RUTIS_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-rejected means the token or certificate is wrong; link stopped: incompatible means the protocol or address form is wrong, or runtime is 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

Version History

  • b6e4351 Current 2026-10-18 15:27

Same Skill Collection

.agents/skills/rutis-new-runtime/SKILL.md

Metadata

Files
0
Version
b6e4351
Hash
a8e1941f
Indexed
2026-10-18 15:27

Home - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-19 03:46
浙ICP备14020137号-1