add a little more plugin options for people who make plugins that need to be auto loaded.
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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`:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user