Skip to content

Out-of-process bridge (exception first-class provider)

The exception first-class provider: a cross-process bridge over line-delimited JSON-RPC.

Hosihikari.PluginProvider.Rpc drives a child process with line-delimited JSON-RPC 2.0 over stdio. The 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 shim variant for the server (see Quick start); the examples below use LeviLamina:

Terminal window
lip install github.com/HosihikariFramework/Shim#levilamina
lip install github.com/HosihikariFramework/RpcProvider

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 provider package's bindings/node
hello-node.conf
{
"name": "hello-node",
"entry": "main.mjs",
"type": "hosihikari",
"version": "1.0.0",
"dependencies": [
{ "name": "hosihikari" },
{ "name": "hosihikari-rpc" }
],
"extraInfo": {
"api": "2",
"format_version": "1",
"provider": "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 provider package:

Terminal window
Copy-Item <server>/plugins/hosihikari-rpc/bindings/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.
runLater(seconds, fn) / runEvery(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 provider package ships reference bindings:

  • bindings/node/hosihikari.mjs
  • bindings/quickjs/hosihikari.js (requires qjs)
  • bindings/python/hosihikari.py (module-level on_load and friends)
  • bindings/java/hosihikari (shared by Java and Kotlin; command points at hosihikari.Binding)

The command forms and context APIs are in the RpcProvider 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.