feat: add plugin loader with manifest validation and crash restart

Validates an unpacked plugin, spawns a worker_threads worker and manages
activate/deactivate/crash-restart with backoff, disabling after three
tries.

The crash counter clears only after a plugin stays up past a stability
window, not on activation. Clearing it on activation let a plugin that
activates cleanly and then dies restart forever, because every attempt
looked like the first.

loader.ts documents what the worker boundary does and does not buy you.
Node's permission model is process-wide, not per-worker, so it cannot be
enabled for plugin workers alone and is not claimed here.
This commit is contained in:
LukeGus committed 2026-09-21 07:19:01 -05:00
1 parent 9f4a2d3951
commit 0a771c9600
6 files changed
+1341

No files matched your search

@@ -0,0 +1,82 @@
/**
* Builds a real unpacked plugin on disk for the loader/broker tests.
*
* The backend entry has to be a genuine .mjs file the worker can import, and
* tsc only emits .ts, so these are written at test time rather than committed.
* Same mkdtemp convention the other disk-touching backend tests use.
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { SUPPORTED_PLUGIN_API_VERSION } from "../../plugins/manifest.js";
export interface FixtureOptions {
id?: string;
permissions?: string[];
/** Body of backend/index.mjs. Must export activate(ctx). */
backendSource?: string;
/** Omit the backend entry file even though the manifest declares one. */
omitBackendEntry?: boolean;
manifestOverrides?: Record<string, unknown>;
}
export interface Fixture {
root: string;
dir: string;
id: string;
cleanup: () => void;
}
const DEFAULT_BACKEND = `
export async function activate(ctx) {
ctx.__ready = true;
}
`;
export function createFixturePlugin(options: FixtureOptions = {}): Fixture {
const id = options.id ?? "sample-plugin";
const root = fs.mkdtempSync(path.join(os.tmpdir(), "termix-plugins-"));
const dir = path.join(root, id);
fs.mkdirSync(path.join(dir, "backend"), { recursive: true });
const manifest = {
id,
name: "Sample Plugin",
version: "1.0.0",
description: "Fixture plugin for runtime tests.",
author: { name: "Termix Tests" },
license: "MIT",
category: "Productivity",
engine: { termix: ">=2.9.0", api: SUPPORTED_PLUGIN_API_VERSION },
capabilities: {
backend: true,
frontend: false,
electron: false,
platforms: ["linux", "win32", "darwin"],
},
permissions: options.permissions ?? ["hosts.read", "storage.own"],
sidecars: [],
...options.manifestOverrides,
};
fs.writeFileSync(
path.join(dir, "manifest.json"),
JSON.stringify(manifest, null, 2),
);
if (!options.omitBackendEntry) {
fs.writeFileSync(
path.join(dir, "backend", "index.mjs"),
options.backendSource ?? DEFAULT_BACKEND,
);
}
return {
root,
dir,
id,
cleanup: () => fs.rmSync(root, { recursive: true, force: true }),
};
}