From ae7e2fa2a4754364f9373ade1788bde4747e1513 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 11:23:27 +0100 Subject: [PATCH] Serve TeX plugin ABI over CultNet --- package-lock.json | 34 ++++++++++- package.json | 4 ++ plugins/eve-plugin-tex/advertisement.json | 6 +- plugins/eve-plugin-tex/plugin.json | 6 +- .../eve-plugin-tex/src/cultnet-sidecar.mjs | 61 +++++++++++++++++++ scripts/run-tex-witness.mjs | 31 +++++++--- 6 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 plugins/eve-plugin-tex/src/cultnet-sidecar.mjs diff --git a/package-lock.json b/package-lock.json index 99f9e86..7293ae0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,40 @@ "license": "MPL-2.0", "workspaces": [ "plugins/*" - ] + ], + "dependencies": { + "@msgpack/msgpack": "^3.1.3", + "cultnet-ts": "file:../CultLib/packages/cultnet-ts" + } + }, + "../CultLib/packages/cultnet-ts": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "^3.1.3", + "ajv": "^8.17.1", + "cultcache-ts": "file:../cultcache-ts" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "json-schema-to-typescript": "^15.0.4", + "typescript": "^5.9.0", + "zod": "^3.25.76" + } }, "node_modules/@gamecult/eve-plugin-tex": { "resolved": "plugins/eve-plugin-tex", "link": true }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -25,6 +53,10 @@ "node": ">= 12" } }, + "node_modules/cultnet-ts": { + "resolved": "../CultLib/packages/cultnet-ts", + "link": true + }, "node_modules/katex": { "version": "0.16.47", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", diff --git a/package.json b/package.json index cef38c9..e80530c 100644 --- a/package.json +++ b/package.json @@ -7,5 +7,9 @@ "scripts": { "test": "C:\\Progra~1\\nodejs\\node.exe --test plugins/*/test/*.test.mjs", "witness:tex": "C:\\Progra~1\\nodejs\\node.exe scripts/run-tex-witness.mjs" + }, + "dependencies": { + "@msgpack/msgpack": "^3.1.3", + "cultnet-ts": "file:../CultLib/packages/cultnet-ts" } } diff --git a/plugins/eve-plugin-tex/advertisement.json b/plugins/eve-plugin-tex/advertisement.json index 38e4532..2fcb2a8 100644 --- a/plugins/eve-plugin-tex/advertisement.json +++ b/plugins/eve-plugin-tex/advertisement.json @@ -7,11 +7,12 @@ "runtime": { "invocationModel": "executable-sidecar", "contract": "gamecult.eve.plugin_abi.v1", - "transports": ["stdio"], + "transports": ["cultnet-rudp", "stdio"], "authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"], "sidecar": { "processKind": "long-running-daemon", - "protocol": "stdio-ndjson", + "protocol": "cultnet-operation-v0", + "debugProtocol": "stdio-ndjson", "requestSchema": "gamecult.eve.plugin_abi.request.v1", "responseSchema": "gamecult.eve.plugin_abi.response.v1", "operations": ["describe", "validate", "project", "lower", "measure", "apply"], @@ -25,4 +26,3 @@ "commands": [], "fixtures": ["tex-math-basic"] } - diff --git a/plugins/eve-plugin-tex/plugin.json b/plugins/eve-plugin-tex/plugin.json index 9f82dc1..a1cec4e 100644 --- a/plugins/eve-plugin-tex/plugin.json +++ b/plugins/eve-plugin-tex/plugin.json @@ -14,11 +14,12 @@ "runtime": { "invocationModel": "executable-sidecar", "contract": "gamecult.eve.plugin_abi.v1", - "transports": ["stdio"], + "transports": ["cultnet-rudp", "stdio"], "authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"], "sidecar": { "processKind": "long-running-daemon", - "protocol": "stdio-ndjson", + "protocol": "cultnet-operation-v0", + "debugProtocol": "stdio-ndjson", "requestSchema": "gamecult.eve.plugin_abi.request.v1", "responseSchema": "gamecult.eve.plugin_abi.response.v1", "operations": ["describe", "validate", "project", "lower", "measure", "apply"], @@ -37,4 +38,3 @@ "optionalCapabilities": ["tex.inline", "tex.block", "tex.scene-placement"] } } - diff --git a/plugins/eve-plugin-tex/src/cultnet-sidecar.mjs b/plugins/eve-plugin-tex/src/cultnet-sidecar.mjs new file mode 100644 index 0000000..d59d979 --- /dev/null +++ b/plugins/eve-plugin-tex/src/cultnet-sidecar.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +import { Buffer } from "node:buffer"; +import { decode, encode } from "@msgpack/msgpack"; +import { startCultNetOperationServer } from "cultnet-ts"; +import { dispatch, pluginId } from "./plugin.mjs"; + +const host = process.env.EVE_PLUGIN_HOST || "127.0.0.1"; +const port = Number(process.env.EVE_PLUGIN_PORT || 0); +const server = await startCultNetOperationServer({ + runtimeId: "eve-plugin-tex", + host, + port, + handler: request => { + if (request.serviceId !== pluginId) throw new Error(`Expected service ${pluginId}.`); + if (request.payloadSchema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported plugin payload schema."); + const abiRequest = decode(Buffer.from(request.payload, "base64")); + let abiResponse; + try { + abiResponse = { + schema: "gamecult.eve.plugin_abi.response.v1", + pluginId, + operation: abiRequest.operation, + requestId: abiRequest.requestId, + status: "accepted", + output: dispatch(abiRequest), + }; + } catch (error) { + abiResponse = { + schema: "gamecult.eve.plugin_abi.response.v1", + pluginId, + operation: abiRequest?.operation || request.operation, + requestId: abiRequest?.requestId || request.messageId, + status: "rejected", + output: {}, + diagnostics: [{ message: error instanceof Error ? error.message : String(error) }], + }; + } + return { + schemaVersion: "cultnet.operation_response.v0", + messageId: request.messageId, + serviceId: pluginId, + operation: request.operation, + status: abiResponse.status, + payloadSchema: abiResponse.schema, + payloadEncoding: "messagepack-base64", + payload: Buffer.from(encode(abiResponse)).toString("base64"), + diagnostics: (abiResponse.diagnostics || []).map(item => item.message), + sourceRuntimeId: "eve-plugin-tex", + }; + }, +}); + +process.stdout.write(`${JSON.stringify({ schema: "gamecult.eve.plugin_endpoint.v1", pluginId, endpoint: server.endpoint })}\n`); +const stop = async () => { + await server.close(); + process.exit(0); +}; +process.once("SIGINT", stop); +process.once("SIGTERM", stop); +await new Promise(() => {}); diff --git a/scripts/run-tex-witness.mjs b/scripts/run-tex-witness.mjs index 305b5ff..ebb9126 100644 --- a/scripts/run-tex-witness.mjs +++ b/scripts/run-tex-witness.mjs @@ -3,11 +3,19 @@ import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; +import { Buffer } from "node:buffer"; +import { decode, encode } from "@msgpack/msgpack"; +import { invokeCultNetOperation } from "cultnet-ts"; const root = path.resolve(import.meta.dirname, ".."); const fixture = JSON.parse(fs.readFileSync(path.join(root, "plugins/eve-plugin-tex/plugin-abi-fixture.json"), "utf8")); -const child = spawn(process.execPath, [path.join(root, "plugins/eve-plugin-tex/src/sidecar.mjs")], { cwd: root, stdio: ["pipe", "pipe", "inherit"] }); +const child = spawn(process.execPath, [path.join(root, "plugins/eve-plugin-tex/src/cultnet-sidecar.mjs")], { cwd: root, stdio: ["ignore", "pipe", "inherit"] }); const responses = readline.createInterface({ input: child.stdout })[Symbol.asyncIterator](); +const endpointLine = await responses.next(); +assert.equal(endpointLine.done, false); +const endpointAdvertisement = JSON.parse(endpointLine.value); +assert.equal(endpointAdvertisement.pluginId, fixture.pluginId); +const endpoint = endpointAdvertisement.endpoint; const startedAt = performance.now(); const operationWitnesses = []; @@ -16,10 +24,18 @@ try { const operation = fixture.operations[index]; const requestId = `tex-witness-${index}`; const operationStartedAt = performance.now(); - child.stdin.write(`${JSON.stringify({ schema: fixture.requestSchema, pluginId: fixture.pluginId, operation: operation.operation, requestId, input: operation.input })}\n`); - const next = await responses.next(); - assert.equal(next.done, false); - const response = JSON.parse(next.value); + const abiRequest = { schema: fixture.requestSchema, pluginId: fixture.pluginId, operation: operation.operation, requestId, input: operation.input }; + const envelope = await invokeCultNetOperation(endpoint, { + schemaVersion: "cultnet.operation_request.v0", + messageId: requestId, + serviceId: fixture.pluginId, + operation: operation.operation, + payloadSchema: fixture.requestSchema, + payloadEncoding: "messagepack-base64", + payload: Buffer.from(encode(abiRequest)).toString("base64"), + sourceRuntimeId: "eve-conformance", + }, { runtimeId: "eve-conformance" }); + const response = decode(Buffer.from(envelope.payload, "base64")); assert.equal(response.schema, fixture.responseSchema); assert.equal(response.pluginId, fixture.pluginId); assert.equal(response.operation, operation.operation); @@ -35,7 +51,6 @@ try { }); } } finally { - child.stdin.end(); child.kill(); } @@ -47,13 +62,13 @@ const witness = { pluginId: "tex.math", ownerRepo: "EvePlugins", status: "pass", - transport: "stdio-ndjson", + transport: "cultnet-operation-v0+rudp", generatedAtUtc: new Date().toISOString(), durationMs: Number((performance.now() - startedAt).toFixed(3)), operations: operationWitnesses, advertisementPath: "plugins/eve-plugin-tex/advertisement.json", fixturePath: "plugins/eve-plugin-tex/plugin-abi-fixture.json", - executable: { artifact: "plugins/eve-plugin-tex/src/sidecar.mjs", command: "node" }, + executable: { artifact: "plugins/eve-plugin-tex/src/cultnet-sidecar.mjs", command: "node" }, diagnostics: [], authority: "plugin-sidecar-owns-tex-semantics-provider-retains-source-and-command-acceptance", };