feat: load channel plugins

This commit is contained in:
Peter Steinberger
2026-01-15 02:42:41 +00:00
parent b1e3d79eaa
commit 2b4a68e276
49 changed files with 494 additions and 159 deletions

View File

@@ -136,18 +136,19 @@ export type HookAgentPayload = {
timeoutSeconds?: number;
};
const HOOK_CHANNEL_VALUES = ["last", ...listChannelPlugins().map((plugin) => plugin.id)];
const listHookChannelValues = () => ["last", ...listChannelPlugins().map((plugin) => plugin.id)];
export type HookMessageChannel = ChannelId | "last";
const hookChannelSet = new Set<string>(HOOK_CHANNEL_VALUES);
export const HOOK_CHANNEL_ERROR = `channel must be ${HOOK_CHANNEL_VALUES.join("|")}`;
const getHookChannelSet = () => new Set<string>(listHookChannelValues());
export const getHookChannelError = () =>
`channel must be ${listHookChannelValues().join("|")}`;
export function resolveHookChannel(raw: unknown): HookMessageChannel | null {
if (raw === undefined) return "last";
if (typeof raw !== "string") return null;
const normalized = normalizeMessageChannel(raw);
if (!normalized || !hookChannelSet.has(normalized)) return null;
if (!normalized || !getHookChannelSet().has(normalized)) return null;
return normalized as HookMessageChannel;
}
@@ -176,7 +177,7 @@ export function normalizeAgentPayload(
? sessionKeyRaw.trim()
: `hook:${idFactory()}`;
const channel = resolveHookChannel(payload.channel);
if (!channel) return { ok: false, error: HOOK_CHANNEL_ERROR };
if (!channel) return { ok: false, error: getHookChannelError() };
const toRaw = payload.to;
const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
const modelRaw = payload.model;

View File

@@ -67,6 +67,11 @@ export const handleConfigBridgeMethods: BridgeMethodHandler = async (
description: plugin.description,
configUiHints: plugin.configUiHints,
})),
channels: pluginRegistry.channels.map((entry) => ({
id: entry.plugin.id,
label: entry.plugin.meta.label,
description: entry.plugin.meta.blurb,
})),
});
return { ok: true, payloadJSON: JSON.stringify(schema) };
}

View File

@@ -11,7 +11,7 @@ import type { createSubsystemLogger } from "../logging.js";
import { handleControlUiHttpRequest } from "./control-ui.js";
import {
extractHookToken,
HOOK_CHANNEL_ERROR,
getHookChannelError,
type HookMessageChannel,
type HooksConfigResolved,
normalizeAgentPayload,
@@ -152,7 +152,7 @@ export function createHooksRequestHandler(
}
const channel = resolveHookChannel(mapped.action.channel);
if (!channel) {
sendJson(res, 400, { ok: false, error: HOOK_CHANNEL_ERROR });
sendJson(res, 400, { ok: false, error: getHookChannelError() });
return true;
}
const runId = dispatchAgentHook({

View File

@@ -59,9 +59,10 @@ const BASE_METHODS = [
"chat.send",
];
const CHANNEL_METHODS = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
export const GATEWAY_METHODS = Array.from(new Set([...BASE_METHODS, ...CHANNEL_METHODS]));
export function listGatewayMethods(): string[] {
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
return Array.from(new Set([...BASE_METHODS, ...channelMethods]));
}
export const GATEWAY_EVENTS = [
"agent",

View File

@@ -73,6 +73,11 @@ export const configHandlers: GatewayRequestHandlers = {
description: plugin.description,
configUiHints: plugin.configUiHints,
})),
channels: pluginRegistry.channels.map((entry) => ({
id: entry.plugin.id,
label: entry.plugin.meta.label,
description: entry.plugin.meta.blurb,
})),
});
respond(true, schema, undefined);
},

View File

@@ -38,7 +38,7 @@ import { buildGatewayCronService } from "./server-cron.js";
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
import { coreGatewayHandlers } from "./server-methods.js";
import { GATEWAY_EVENTS, GATEWAY_METHODS } from "./server-methods-list.js";
import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js";
import { hasConnectedMobileNode as hasConnectedMobileNodeFromBridge } from "./server-mobile-nodes.js";
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
import { loadGatewayPlugins } from "./server-plugins.js";
@@ -69,14 +69,6 @@ const logReload = log.child("reload");
const logHooks = log.child("hooks");
const logWsControl = log.child("ws");
const canvasRuntime = runtimeForLogger(logCanvas);
const channelLogs = Object.fromEntries(
listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]),
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
const channelRuntimeEnvs = Object.fromEntries(
Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]),
) as Record<ChannelId, RuntimeEnv>;
const METHODS = GATEWAY_METHODS;
export type GatewayServer = {
close: (opts?: { reason?: string; restartExpectedMs?: number | null }) => Promise<void>;
@@ -163,13 +155,22 @@ export async function startGatewayServer(
await autoMigrateLegacyState({ cfg: cfgAtStart, log });
const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
const { pluginRegistry, gatewayMethods } = loadGatewayPlugins({
const baseMethods = listGatewayMethods();
const { pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayPlugins({
cfg: cfgAtStart,
workspaceDir: defaultWorkspaceDir,
log,
coreGatewayHandlers,
baseMethods: METHODS,
baseMethods,
});
const channelLogs = Object.fromEntries(
listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]),
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
const channelRuntimeEnvs = Object.fromEntries(
Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]),
) as Record<ChannelId, RuntimeEnv>;
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
const gatewayMethods = Array.from(new Set([...baseGatewayMethods, ...channelMethods]));
let pluginServices: PluginServicesHandle | null = null;
const runtimeConfig = await resolveGatewayRuntimeConfig({
cfg: cfgAtStart,