Skip to content

Out-of-process plugins

Hosihikari.PluginLoader.Rpc drives a child process with line-delimited JSON-RPC 2.0 over stdio. The RPC bridge reads only the command from the manifest and the protocol. Unload means the process exits, taking the child process and its state with it.

Pick the HostingShim variant for the server (see Choosing a carrier); the examples below use LeviLamina:

Terminal window
lip install github.com/HosihikariFramework/HostingShim#levilamina
lip install github.com/HosihikariFramework/RpcLoader

The RPC templates cover Node.js/TypeScript, Python, and Kotlin/JVM.

The plugin package also declares a dependency on hosihikari-rpc. The operator provides the executable: put it on PATH or use an absolute path.

plugins/hello-node/
manifest.json
main.mjs
hosihikari.mjs ← copied from the loader package's protocol/node
hello-node.conf
{
"name": "hello-node",
"entry": "main.mjs",
"type": "hosihikari",
"version": "1.0.0",
"dependencies": [
{ "name": "hosihikari" },
{ "name": "hosihikari-rpc" }
],
"extraInfo": {
"api_version": "2",
"manifest_version": "1",
"loader": "rpc",
"command": "[\"node\", \"./hosihikari.mjs\", \"./main.mjs\"]"
}
}

command is a JSON array encoded as a string; the working directory is the package directory, and no shell is involved.

Copy the binding from the installed loader package:

Terminal window
cp <server>/plugins/hosihikari-rpc/protocol/node/hosihikari.mjs .
import fs from 'node:fs';
import path from 'node:path';
export default {
onLoad(ctx) {
fs.writeFileSync(path.join(ctx.dataDirectory, 'loaded.txt'), 'loaded');
ctx.logInfo(`loaded, package ${ctx.packageDirectory}`);
},
onEnable(ctx) {
const greeting = ctx.config('greeting');
fs.writeFileSync(path.join(ctx.dataDirectory, 'enabled.txt'), greeting ?? '');
ctx.logInfo(`enabled, greeting=${greeting}`);
},
onDisable(ctx) {
return true;
},
onUnload(ctx) {},
};

Hooks may be async. Return values: undefined or true allows; false or { allowed: false, reason } refuses; a thrown exception is a fault.

ctx provides:

Member Meaning
name / packageDirectory / dataDirectory Plugin name, package directory, data directory.
config(key) / configObject The layered configuration.
logInfo / logWarn / logError Structured logs over the protocol.
writeData(file, text) Write a file under the data directory.
publish(topic, json) Publish an event.
subscribe(topic, fn, { sync }) Subscribe; with sync: true the publisher waits for an ack.
scheduleOnce(seconds, fn) / schedulePeriodic(seconds, fn) Timers.

Events are asynchronous by default. Disabling closes the gate; subscription callbacks and timers stop, and a refused disable reopens it. Unload ends the child process.

stdout carries the protocol; stderr carries text logs. The Node binding redirects console.log to stderr; do not write to stdout.

The loader package ships reference bindings:

  • protocol/node/hosihikari.mjs and hosihikari.d.ts (TypeScript declarations)
  • protocol/quickjs/hosihikari.js (requires qjs; JSDoc types)
  • protocol/python/hosihikari.py and hosihikari.pyi (module-level on_load and friends; type stubs)
  • protocol/java/hosihikari (shared by Java and Kotlin; command points at hosihikari.Binding)

Host requests from the Node and QuickJS bindings time out after 30 seconds; a timed-out request rejects.

The command forms and context APIs are in the RpcLoader README.

The Endstone CLI replaces the child process PATH with Python's sys.path; if the executable cannot be found, use an absolute path in command.

A custom binding implements the same protocol:

  • Host → plugin: hosihikari/hello, hosihikari/enable, hosihikari/disable, hosihikari/unload, hosihikari/events/deliver
  • Plugin → host: hosihikari/events/publish, hosihikari/events/subscribe, hosihikari/events/unsubscribe, hosihikari/log
  • Unload: unload request → close stdin → wait 5 seconds → kill the process tree.