add a little more plugin options for people who make plugins that need to be auto loaded.

This commit is contained in:
KingKDot
2026-07-31 01:47:07 -05:00
parent 486944b111
commit 76e0bc77c0
8 changed files with 59 additions and 24 deletions
+1
View File
@@ -345,6 +345,7 @@ export type PluginManifest = {
apiVersion?: number; apiVersion?: number;
runtime?: "native" | "wasm" | "server" | string; runtime?: "native" | "wasm" | "server" | string;
nativeLoader?: "memory" | "os" | string; nativeLoader?: "memory" | "os" | string;
autoLoadByDefault?: boolean;
nativeEntrypoints?: PluginNativeEntrypoints; nativeEntrypoints?: PluginNativeEntrypoints;
version?: string; version?: string;
description?: string; description?: string;
@@ -217,6 +217,7 @@ export function createPluginRuntime(opts: PluginRuntimeOptions): PluginRuntime {
} }
} }
function rpc( function rpc(
pluginId: string, pluginId: string,
method: string, method: string,
@@ -105,6 +105,7 @@ describe("loadPluginBundle", () => {
apiVersion: 2, apiVersion: 2,
runtime: "native", runtime: "native",
nativeLoader: "loadlibraryex", nativeLoader: "loadlibraryex",
autoLoadByDefault: true,
nativeEntrypoints: { nativeEntrypoints: {
onLoad: "StartPlugin", onLoad: "StartPlugin",
onEvent: "HandlePluginEvent", onEvent: "HandlePluginEvent",
@@ -123,6 +124,7 @@ describe("loadPluginBundle", () => {
const manifest = JSON.parse(await readFile(join(root, "native-loader-demo", "manifest.json"), "utf-8")); const manifest = JSON.parse(await readFile(join(root, "native-loader-demo", "manifest.json"), "utf-8"));
expect(manifest.nativeLoader).toBe("os"); expect(manifest.nativeLoader).toBe("os");
expect(manifest.autoLoadByDefault).toBe(true);
expect(manifest.nativeEntrypoints.onLoad).toBe("StartPlugin"); expect(manifest.nativeEntrypoints.onLoad).toBe("StartPlugin");
expect(manifest.nativeEntrypoints.onEvent).toBe("HandlePluginEvent"); expect(manifest.nativeEntrypoints.onEvent).toBe("HandlePluginEvent");
expect(manifest.nativeEntrypoints.onUnload).toBe("StopPlugin"); expect(manifest.nativeEntrypoints.onUnload).toBe("StopPlugin");
@@ -210,6 +210,7 @@ export async function ensurePluginExtracted(
apiVersion: Number(extraConfig.apiVersion) === 2 || runtime === "wasm" ? 2 : 1, apiVersion: Number(extraConfig.apiVersion) === 2 || runtime === "wasm" ? 2 : 1,
runtime, runtime,
...(nativeLoader && { nativeLoader }), ...(nativeLoader && { nativeLoader }),
...(extraConfig.autoLoadByDefault === true && { autoLoadByDefault: true }),
...(nativeEntrypoints && { nativeEntrypoints }), ...(nativeEntrypoints && { nativeEntrypoints }),
version: extraConfig.version || "1.0.0", version: extraConfig.version || "1.0.0",
description: extraConfig.description, description: extraConfig.description,
@@ -8,7 +8,11 @@ import { requireAnyPermission, requireClientAccess, requirePermission, requirePl
import { canUserAccessClient, canUserAccessPlugin } from "../../users"; import { canUserAccessClient, canUserAccessPlugin } from "../../users";
import * as clientManager from "../../clientManager"; import * as clientManager from "../../clientManager";
import { metrics } from "../../metrics"; import { metrics } from "../../metrics";
import { encodeMessage, type PluginSignatureInfo } from "../../protocol"; import {
encodeMessage,
type PluginManifest as ProtocolPluginManifest,
type PluginSignatureInfo,
} from "../../protocol";
import { getConfig, updatePluginsConfig } from "../../config"; import { getConfig, updatePluginsConfig } from "../../config";
import { getOrVerifySignature, BUILTIN_TRUSTED_KEYS } from "../plugin-signature"; import { getOrVerifySignature, BUILTIN_TRUSTED_KEYS } from "../plugin-signature";
import type { PluginRuntime } from "../plugin-runtime/runtime"; import type { PluginRuntime } from "../plugin-runtime/runtime";
@@ -16,17 +20,9 @@ import { arePluginNeedsApproved, computePluginNeedsHash, getPluginPull, deletePl
import { isAuthorizedAgentRequest } from "../agent-auth"; import { isAuthorizedAgentRequest } from "../agent-auth";
import { logger } from "../../logger"; import { logger } from "../../logger";
type PluginManifest = { type PluginManifest = ProtocolPluginManifest & {
id: string;
name: string;
signature?: PluginSignatureInfo; signature?: PluginSignatureInfo;
hasServer?: boolean;
apiVersion?: number;
runtime?: string;
wasm?: string;
needs?: any;
needsHash?: string; needsHash?: string;
dashboard?: any;
}; };
type PluginBundle = { type PluginBundle = {
@@ -118,6 +114,20 @@ export async function handlePluginRoutes(
); );
} }
async function loadPluginOnConnectedClients(pluginId: string): Promise<void> {
const allClients = clientManager.getAllClients();
for (const [clientId, client] of allClients) {
if (deps.isPluginLoaded(clientId, pluginId) || deps.isPluginLoading(clientId, pluginId)) continue;
try {
const bundle = await deps.loadPluginBundle(pluginId, client.os, client.arch);
deps.markPluginLoading(clientId, pluginId);
deps.sendPluginBundle(client, bundle);
metrics.recordCommand("plugin_load");
} catch {
}
}
}
if (req.method === "GET" && url.pathname.startsWith("/api/plugins/pull/")) { if (req.method === "GET" && url.pathname.startsWith("/api/plugins/pull/")) {
const agentToken = getConfig().auth.agentToken; const agentToken = getConfig().auth.agentToken;
if (!isAuthorizedAgentRequest(req, url, agentToken)) { if (!isAuthorizedAgentRequest(req, url, agentToken)) {
@@ -531,7 +541,15 @@ export async function handlePluginRoutes(
const uploadedManifest = await loadManifest(pluginId); const uploadedManifest = await loadManifest(pluginId);
if (deps.pluginState.enabled[pluginId] === undefined) { if (deps.pluginState.enabled[pluginId] === undefined) {
deps.pluginState.enabled[pluginId] = sigInfo.trusted === true && needsApproved(uploadedManifest); const enabledByDefault = sigInfo.trusted === true && needsApproved(uploadedManifest);
deps.pluginState.enabled[pluginId] = enabledByDefault;
if (
enabledByDefault &&
uploadedManifest.runtime !== "server" &&
uploadedManifest.autoLoadByDefault === true
) {
deps.pluginState.autoLoad[pluginId] = true;
}
await deps.savePluginState(); await deps.savePluginState();
} }
@@ -544,6 +562,9 @@ export async function handlePluginRoutes(
} else if (deps.pluginRuntime.isRunning(pluginId)) { } else if (deps.pluginRuntime.isRunning(pluginId)) {
await deps.pluginRuntime.stopPlugin(pluginId); await deps.pluginRuntime.stopPlugin(pluginId);
} }
if (deps.pluginState.enabled[pluginId] && deps.pluginState.autoLoad[pluginId]) {
await loadPluginOnConnectedClients(pluginId);
}
return Response.json({ ok: true, id: pluginId, enabled: deps.pluginState.enabled[pluginId], signature: sigInfo }); return Response.json({ ok: true, id: pluginId, enabled: deps.pluginState.enabled[pluginId], signature: sigInfo });
} }
@@ -587,6 +608,12 @@ export async function handlePluginRoutes(
} }
deps.pluginState.enabled[pluginId] = enabled; deps.pluginState.enabled[pluginId] = enabled;
if (enabled && deps.pluginState.autoLoad[pluginId] === undefined) {
const manifest = await loadManifest(pluginId);
if (manifest.runtime !== "server" && manifest.autoLoadByDefault === true) {
deps.pluginState.autoLoad[pluginId] = true;
}
}
await deps.savePluginState(); await deps.savePluginState();
if (enabled) { if (enabled) {
@@ -596,6 +623,9 @@ export async function handlePluginRoutes(
} else if (deps.pluginRuntime.isRunning(pluginId)) { } else if (deps.pluginRuntime.isRunning(pluginId)) {
await deps.pluginRuntime.stopPlugin(pluginId); await deps.pluginRuntime.stopPlugin(pluginId);
} }
if (enabled && deps.pluginState.autoLoad[pluginId]) {
await loadPluginOnConnectedClients(pluginId);
}
return Response.json({ ok: true, id: pluginId, enabled }); return Response.json({ ok: true, id: pluginId, enabled });
} }
@@ -658,17 +688,7 @@ export async function handlePluginRoutes(
await deps.savePluginState(); await deps.savePluginState();
if (autoLoad) { if (autoLoad) {
const allClients = clientManager.getAllClients(); await loadPluginOnConnectedClients(pluginId);
for (const [cid, client] of allClients) {
if (deps.isPluginLoaded(cid, pluginId) || deps.isPluginLoading(cid, pluginId)) continue;
try {
const bundle = await deps.loadPluginBundle(pluginId, client.os, client.arch);
deps.markPluginLoading(cid, pluginId);
deps.sendPluginBundle(client, bundle);
metrics.recordCommand("plugin_load");
} catch {
}
}
} }
return Response.json({ return Response.json({
+7
View File
@@ -85,6 +85,13 @@ Native plugins can choose the Windows loader mode and exported ABI names:
`navbar.icon` accepts a Font Awesome 6 solid icon class such as `fa-cube`, `fa-key`, or `fa-network-wired`. If the plugin is enabled, navbar plugins appear in the Plugin Apps group and open at `/plugins/<id>`. `navbar.icon` accepts a Font Awesome 6 solid icon class such as `fa-cube`, `fa-key`, or `fa-network-wired`. If the plugin is enabled, navbar plugins appear in the Plugin Apps group and open at `/plugins/<id>`.
Client-side bundles can set `autoLoadByDefault: true`. The default is applied only
after a trusted plugin is enabled automatically or an administrator explicitly
confirms and enables an untrusted plugin. An existing administrator auto-load
choice is preserved across reinstalls.
When the default is applied, compatible connected clients are loaded immediately;
future clients receive the plugin as they connect.
## Dashboard Badges ## Dashboard Badges
Plugins can contribute compact badges to client cards on the main dashboard. Declare the dashboard integration in `config.json`: Plugins can contribute compact badges to client cards on the main dashboard. Declare the dashboard integration in `config.json`:
+1
View File
@@ -53,6 +53,7 @@ Auto-load notes:
| WASM needs | WASM plugins with unapproved needs are blocked. | | WASM needs | WASM plugins with unapproved needs are blocked. |
| Persistence | Auto-load state is saved in `.plugin-state.json`. | | Persistence | Auto-load state is saved in `.plugin-state.json`. |
| Platform matching | WASM is universal; legacy native requires a matching binary. | | Platform matching | WASM is universal; legacy native requires a matching binary. |
| Bundle default | `autoLoadByDefault: true` enables auto-load only after the plugin passes trusted enablement or an administrator explicitly confirms enablement. |
## Useful APIs ## Useful APIs
+4 -2
View File
@@ -37,8 +37,8 @@ export default {
ctx.log.info("server plugin started"); ctx.log.info("server plugin started");
}, },
async onEvent(ctx, event) { async onEvent(ctx, clientId, event, payload) {
ctx.log.info(`plugin event from ${event.clientId}`); ctx.log.info(`${event} from ${clientId}: ${JSON.stringify(payload)}`);
}, },
rpc: { rpc: {
@@ -53,6 +53,8 @@ export default {
}; };
``` ```
`onEvent` receives events emitted by that plugin's client module.
## The `ctx` Object ## The `ctx` Object
| Field | Description | | Field | Description |