plugins 2.0 update

This commit is contained in:
KingKDot
2026-06-22 10:52:27 -05:00
parent e1f5e4c800
commit 30dfdda3ed
55 changed files with 2534 additions and 128 deletions
@@ -12,7 +12,7 @@ import { encodeMessage, type PluginSignatureInfo } from "../../protocol";
import { getConfig, updatePluginsConfig } from "../../config";
import { getOrVerifySignature, BUILTIN_TRUSTED_KEYS } from "../plugin-signature";
import type { PluginRuntime } from "../plugin-runtime/runtime";
import { getPluginPull, deletePluginPull, detectPluginIdFromZip } from "../plugin-state-bundle";
import { arePluginNeedsApproved, computePluginNeedsHash, getPluginPull, deletePluginPull, detectPluginIdFromZip } from "../plugin-state-bundle";
import { isAuthorizedAgentRequest } from "../agent-auth";
import { logger } from "../../logger";
@@ -21,6 +21,11 @@ type PluginManifest = {
name: string;
signature?: PluginSignatureInfo;
hasServer?: boolean;
apiVersion?: number;
runtime?: string;
wasm?: string;
needs?: any;
needsHash?: string;
};
type PluginBundle = {
@@ -34,6 +39,7 @@ type PluginState = {
lastError: Record<string, string>;
autoLoad: Record<string, boolean>;
autoStartEvents: Record<string, Array<{ event: string; payload: any }>>;
approvedNeeds: Record<string, string>;
};
type PluginRouteDeps = {
@@ -72,6 +78,34 @@ export async function handlePluginRoutes(
return null;
}
async function loadManifest(pluginId: string): Promise<PluginManifest> {
await deps.ensurePluginExtracted(pluginId);
const raw = await fs.readFile(path.join(deps.PLUGIN_ROOT, pluginId, "manifest.json"), "utf-8");
const manifest = JSON.parse(raw) as PluginManifest;
manifest.id = manifest.id || pluginId;
manifest.name = manifest.name || pluginId;
manifest.needsHash = computePluginNeedsHash(manifest.needs);
return manifest;
}
function needsApproved(manifest: PluginManifest): boolean {
return arePluginNeedsApproved(deps.pluginState, manifest.id, manifest.needs);
}
async function requireNeedsApproval(pluginId: string): Promise<Response | null> {
const manifest = await loadManifest(pluginId);
if (needsApproved(manifest)) return null;
return Response.json(
{
ok: false,
error: "needs_approval_required",
needs: manifest.needs || {},
needsHash: manifest.needsHash || "",
},
{ status: 428 },
);
}
if (req.method === "GET" && url.pathname.startsWith("/api/plugins/pull/")) {
const agentToken = getConfig().auth.agentToken;
if (!isAuthorizedAgentRequest(req, url, agentToken)) {
@@ -137,6 +171,11 @@ export async function handlePluginRoutes(
autoLoad: deps.pluginState.autoLoad[p.id] === true,
autoStartEvents: deps.pluginState.autoStartEvents[p.id] || [],
signature: p.signature || { signed: false, trusted: false, valid: false },
runtime: p.runtime || "native",
apiVersion: p.apiVersion || 1,
needs: p.needs || {},
needsHash: p.needsHash || computePluginNeedsHash(p.needs),
needsApproved: arePluginNeedsApproved(deps.pluginState, p.id, p.needs),
hasServer: p.hasServer === true || deps.pluginRuntime.hasServerCode(p.id),
serverRunning: deps.pluginRuntime.isRunning(p.id),
}));
@@ -229,6 +268,11 @@ export async function handlePluginRoutes(
enabled: deps.pluginState.enabled[manifest.id] !== false,
lastError: deps.pluginState.lastError[manifest.id] || "",
signature: manifest.signature || { signed: false, trusted: false, valid: false },
runtime: manifest.runtime || "native",
apiVersion: manifest.apiVersion || 1,
needs: manifest.needs || {},
needsHash: manifest.needsHash || computePluginNeedsHash(manifest.needs),
needsApproved: arePluginNeedsApproved(deps.pluginState, manifest.id, manifest.needs),
}));
return Response.json({ plugins });
}
@@ -312,8 +356,10 @@ export async function handlePluginRoutes(
const sigInfo = await getOrVerifySignature(deps.PLUGIN_ROOT, pluginId);
const uploadedManifest = await loadManifest(pluginId);
if (deps.pluginState.enabled[pluginId] === undefined) {
deps.pluginState.enabled[pluginId] = sigInfo.trusted === true;
deps.pluginState.enabled[pluginId] = sigInfo.trusted === true && needsApproved(uploadedManifest);
await deps.savePluginState();
}
@@ -364,6 +410,8 @@ export async function handlePluginRoutes(
);
}
}
const needsResponse = await requireNeedsApproval(pluginId);
if (needsResponse) return needsResponse;
}
deps.pluginState.enabled[pluginId] = enabled;
@@ -410,6 +458,10 @@ export async function handlePluginRoutes(
{ status: 400 },
);
}
if (autoLoad) {
const needsResponse = await requireNeedsApproval(pluginId);
if (needsResponse) return needsResponse;
}
deps.pluginState.autoLoad[pluginId] = autoLoad;
@@ -444,6 +496,33 @@ export async function handlePluginRoutes(
});
}
const pluginNeedsApproveMatch = url.pathname.match(/^\/api\/plugins\/(.+)\/needs\/approve$/);
if (req.method === "POST" && pluginNeedsApproveMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "plugins:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let pluginId = "";
try {
pluginId = deps.sanitizePluginId(pluginNeedsApproveMatch[1]);
} catch {
return new Response("Invalid plugin id", { status: 400 });
}
const manifest = await loadManifest(pluginId);
const needsHash = manifest.needsHash || "";
if (!needsHash) {
delete deps.pluginState.approvedNeeds[pluginId];
} else {
deps.pluginState.approvedNeeds[pluginId] = needsHash;
}
await deps.savePluginState();
return Response.json({ ok: true, id: pluginId, needsHash, needsApproved: true });
}
function resolveDataPath(pluginId: string, relPath: string): string | null {
const dataDir = path.join(deps.PLUGIN_ROOT, pluginId, "data");
const target = path.resolve(dataDir, relPath);
@@ -755,6 +834,7 @@ export async function handlePluginRoutes(
delete deps.pluginState.lastError[pluginId];
delete deps.pluginState.autoLoad[pluginId];
delete deps.pluginState.autoStartEvents[pluginId];
delete deps.pluginState.approvedNeeds[pluginId];
await deps.savePluginState();
if (deps.pluginRuntime.isRunning(pluginId)) {
@@ -821,6 +901,8 @@ export async function handlePluginRoutes(
try {
const bundle = await deps.loadPluginBundle(pluginId, target.os, target.arch);
const needsResponse = await requireNeedsApproval(pluginId);
if (needsResponse) return needsResponse;
deps.markPluginLoading(targetId, pluginId);
deps.sendPluginBundle(target, bundle);
metrics.recordCommand("plugin_load");
@@ -902,6 +984,8 @@ export async function handlePluginRoutes(
}
if (!deps.isPluginLoaded(targetId, pluginId)) {
const needsResponse = await requireNeedsApproval(pluginId);
if (needsResponse) return needsResponse;
deps.enqueuePluginEvent(targetId, pluginId, event, payload);
if (!deps.isPluginLoading(targetId, pluginId)) {
try {