Plug-ins for RivetHub, and the SDKs and templates for writing them. A plug-in is a program that runs in its own process on
one node and reaches the runtime only over its published contract (row B17 and notes 50 to 60 of the
rivethub-runtime CONTRACT): rh starts it, proxies clients to its
views, delivers its events and offers its tools to agents through rh mcp. A plug-in that proves valuable for everyone
gets promoted into the platform, with its domain moving into a runtime context and its payloads into the contract.
packages/contract:@rivethub/contract, the runtime's schemas and the types generated from them.packages/plugin-sdk:@rivethub/plugin-sdk, for plug-ins in TypeScript, with therivethub-plugin-devharness.rust/rivethub-plugin: therivethub-plugincrate, for plug-ins in Rust, in a Cargo workspace of its own.templates/typescript,templates/rust: a checklist plug-in in each language to start from: a page with a form and a list (and a canvas board in TypeScript), anadd_itemagent tool and asession.placedsubscription.templates/javascript: the same checklist with its canvas board, in JavaScript that Node runs as it is, and aGUIDE.mdfor an agent building from it. It is the template rh's plug-in creator scaffolds from.plugins/voice: speech to text and text to speech for the composer, onhttp/transcribeandhttp/speak, through the STT and TTS servers named in its settings. It replaces rh's/api/voiceroutes.plugins/files: the Files page's and the workflow editor's file calls onhttp/list,http/download,http/upload,http/mkdir,http/renameandhttp/delete, with the queries, bodies and errors of rh's/api/filesroutes, which it replaces. Every path stays inside the folder itsrootsetting names, links included.
contract.json pins a runtime commit. npm run contract fetches that commit's contract/sent.schema.json and
contract/accepted.schema.json, vendors them under packages/contract/schema/, and generates:
- the
@rivethub/contracttypes:Sent, what the runtime sends (responses, events), andAccepted, what it accepts (requests, and a plug-in's answers); rust/rivethub-plugin/src/contract.rs, the plug-in types (the manifest, views, canvas, events, jobs, tools, the plug-in API andGatewayError) as Rust, from the accepted schema.
To move to a newer runtime, set ref in contract.json to a commit on the runtime's main, run npm run contract, and
commit the schema and type diff in its own PR. If a plug-in needs something the contract doesn't offer, propose the
change to the runtime first.
A plug-in is a directory holding rivethub-plugin.json and the program its run.command starts. Both SDKs answer what rh
asks on the plug-in socket (GET /health, views, actions, canvas events, tools, events and jobs), drop an event
delivery they have already handled (by seq), pass settings.changed and the starting settings to the settings
handler, and call rh on the plug-in API socket with the per-start token: the plug-in's own record, viewsChanged,
notify, and any route its permissions name.
await serve({
views: { main: () => ({ title: 'Checklist', blocks: [] }) },
actions: { add: ({ values }) => [{ type: 'refresh' }] },
tools: { add_item: ({ title }) => `Added ${String(title)}` },
events: { 'session.placed': ({ sessionId }) => {} },
})Plugin::new(state)
.view("main", |state, request| async move { Ok(view(&state)) })
.tool("add_item", |state, call| async move { add(&state, call.parse()?) })
.events(|state, delivery| async move { record(&state, delivery.event) })
.serve()
.awaitThe TypeScript SDK also serves the http/ routes: http: { speak: (request) => response } gets each
/http/speak/… call as a web Request, body unread, and streams the Response it returns back through rh.
On Linux rh confines a plug-in with Landlock to its own directory, its data and run folders and the program it runs, so
a TypeScript plug-in runs a bundle (npm run build writes dist/plugin.js with esbuild) rather than sources that
import from outside its directory, and a Rust plug-in runs its release binary.
rivethub-plugin-dev <dir> starts a plug-in the way rh does, standing in for its own /api/plugin routes, and reads
commands from stdin (view main, action add {"values":{"title":"x"}}, tool add_item {"title":"x"},
event session.placed {...}, job <id>), printing each answer. To run one on a node, POST /api/plugins with
{"path": "<absolute directory>"} and enable it from a paired device.
rh's plug-in creator (note 70 of the runtime's CONTRACT) lets an agent on a node scaffold a plug-in from a template,
edit it, install it and read its log through rh mcp. rh carries no templates; it reads them from the node's
<state>/plugin-templates/ (~/.rivethub/plugin-templates/ unless the state folder moved), one folder each, and never
fetches one. npm run templates:install writes the templates that need no build there: the javascript folder with
sdk/, the SDK's own TypeScript sources, which Node 22.18 and later runs without a build. Pass another state folder as
npm run templates:install -- <folder>. Run it again after pulling a newer SDK; plug-ins already scaffolded keep the
SDK they were made with.
- Talk to the runtime only over its HTTP and WebSocket API and the plug-in sockets. Never read its database or files.
- Types come from
@rivethub/contractand the generatedcontractmodule. Never hand-write a DTO. - No code comments: names and types carry the meaning.
- Conventional Commits, authored by people, with no AI attribution trailers or text.
npm run check runs what CI runs: Biome lint and format, tsc in strict mode, the tests, the contract freshness check,
the privacy scan, and npm run rust (rustfmt, Clippy with warnings as errors, and the tests, for the crate and the Rust
template). Run it before every push.