mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-22 18:35:21 -06:00
549e6349f8
* fix(workboard): make ownership updates atomic Persist launch intent before worker acceptance, reconcile accepted identities, fence cross-host card mutations, and deduplicate session capture in SQLite. Closes #125932 * test(workboard): keep race fixtures plugin-local Use a plugin-local synchronization helper and update the sessions-page capture expectation for the new store-owned RPC. * fix(workboard): fence lifecycle and restore races Apply terminal lifecycle state only while the matched association is current, and converge concurrent archived session restores on the active winner.
646 lines
24 KiB
TypeScript
646 lines
24 KiB
TypeScript
// Gateway plugin runtime adapter.
|
|
// Loads plugin registries and builds fallback request context for non-WS paths.
|
|
import { randomUUID } from "node:crypto";
|
|
import { performance } from "node:perf_hooks";
|
|
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
|
import type { AmbientEnvTriggerPolicy } from "../channels/config-presence.js";
|
|
import { allowsProcessHomeSessionScan } from "../config/paths.js";
|
|
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
|
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "../plugins/installed-plugin-index-install-records.js";
|
|
import { activatePluginRegistry } from "../plugins/loader-shared.js";
|
|
import type { ChannelPluginLoadIntent } from "../plugins/loader-types.js";
|
|
import { loadAndActivateRootPluginRegistry } from "../plugins/loader.js";
|
|
import { loadPluginLookUpTable, type PluginLookUpTable } from "../plugins/plugin-lookup-table.js";
|
|
import { getPluginModuleLoaderStats } from "../plugins/plugin-module-loader-cache.js";
|
|
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
|
import type { PluginRegistryParams } from "../plugins/registry-types.js";
|
|
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
|
import { getPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js";
|
|
import { createPluginRuntimeLoaderLogger } from "../plugins/runtime/load-context.js";
|
|
import { resolvePluginSubagentCompletionRequester } from "../plugins/runtime/subagent-requester-context.js";
|
|
import type {
|
|
CreatePluginRuntimeOptions,
|
|
PluginRuntime,
|
|
RuntimeGatewayRequestOptions,
|
|
} from "../plugins/runtime/types.js";
|
|
import type { PluginLogger, PluginOrigin } from "../plugins/types.js";
|
|
import { ADMIN_SCOPE } from "./method-scopes.js";
|
|
import { normalizeOperatorScopeList, type OperatorScope } from "./operator-scopes.js";
|
|
import type { GatewayRequestHandler, GatewayRequestOptions } from "./server-methods/types.js";
|
|
import {
|
|
dispatchGatewayMethodInProcess,
|
|
dispatchGatewayMethodInProcessRaw,
|
|
getInProcessGatewayRequestContext,
|
|
type GatewayContextResolver,
|
|
} from "./server-plugin-in-process-dispatch.js";
|
|
import { resolvePluginSubagentToolsAlsoAllow } from "./server-plugin-runtime-client.js";
|
|
import {
|
|
normalizePluginSubagentAllowedModelRef,
|
|
normalizePluginSubagentRunRuntime,
|
|
resolvePluginSubagentRequestedModelRef,
|
|
} from "./server-plugin-subagent-runtime.js";
|
|
import {
|
|
hasInProcessGatewayContext,
|
|
projectGatewayRuntimeNodes,
|
|
} from "./server-plugins-node-runtime.js";
|
|
|
|
export {
|
|
dispatchGatewayMethodInProcess,
|
|
dispatchGatewayMethodInProcessRaw,
|
|
getInProcessGatewayRequestContext,
|
|
};
|
|
export type { GatewayMethodDispatchResponse } from "./server-plugin-in-process-dispatch.js";
|
|
export { hasInProcessGatewayContext } from "./server-plugins-node-runtime.js";
|
|
|
|
type PluginSubagentOverridePolicy = {
|
|
allowModelOverride: boolean;
|
|
|
|
allowAnyModel: boolean;
|
|
hasConfiguredAllowlist: boolean;
|
|
allowedModels: Set<string>;
|
|
};
|
|
|
|
type PluginSubagentOverridePolicies = Record<string, PluginSubagentOverridePolicy>;
|
|
|
|
function resolvePluginSubagentOverridePolicies(
|
|
cfg: OpenClawConfig,
|
|
): PluginSubagentOverridePolicies {
|
|
const normalized = normalizePluginsConfig(cfg.plugins);
|
|
const policies: PluginSubagentOverridePolicies = {};
|
|
for (const [pluginId, entry] of Object.entries(normalized.entries)) {
|
|
const allowModelOverride = entry.subagent?.allowModelOverride === true;
|
|
const hasConfiguredAllowlist = entry.subagent?.hasAllowedModelsConfig === true;
|
|
const configuredAllowedModels = entry.subagent?.allowedModels ?? [];
|
|
const allowedModels = new Set<string>();
|
|
let allowAnyModel = false;
|
|
for (const modelRef of configuredAllowedModels) {
|
|
const normalizedModelRef = normalizePluginSubagentAllowedModelRef(modelRef);
|
|
if (!normalizedModelRef) {
|
|
continue;
|
|
}
|
|
if (normalizedModelRef === "*") {
|
|
allowAnyModel = true;
|
|
continue;
|
|
}
|
|
allowedModels.add(normalizedModelRef);
|
|
}
|
|
if (
|
|
!allowModelOverride &&
|
|
!hasConfiguredAllowlist &&
|
|
allowedModels.size === 0 &&
|
|
!allowAnyModel
|
|
) {
|
|
continue;
|
|
}
|
|
policies[pluginId] = {
|
|
allowModelOverride,
|
|
allowAnyModel,
|
|
hasConfiguredAllowlist,
|
|
allowedModels,
|
|
};
|
|
}
|
|
return policies;
|
|
}
|
|
|
|
function authorizeFallbackModelOverride(params: {
|
|
policies: PluginSubagentOverridePolicies;
|
|
pluginId?: string;
|
|
provider?: string;
|
|
model?: string;
|
|
}): { allowed: true } | { allowed: false; reason: string } {
|
|
const pluginId = params.pluginId?.trim();
|
|
if (!pluginId) {
|
|
return {
|
|
allowed: false,
|
|
reason: "provider/model override requires plugin identity in fallback subagent runs.",
|
|
};
|
|
}
|
|
const policy = params.policies[pluginId];
|
|
if (!policy?.allowModelOverride) {
|
|
return {
|
|
allowed: false,
|
|
reason:
|
|
`plugin "${pluginId}" is not trusted for fallback provider/model override requests. ` +
|
|
"See https://docs.openclaw.ai/plugins/sdk-runtime#api-runtime-subagent and search for: " +
|
|
"plugins.entries.<id>.subagent.allowModelOverride",
|
|
};
|
|
}
|
|
if (policy.allowAnyModel) {
|
|
return { allowed: true };
|
|
}
|
|
if (policy.hasConfiguredAllowlist && policy.allowedModels.size === 0) {
|
|
return {
|
|
allowed: false,
|
|
reason: `plugin "${pluginId}" configured subagent.allowedModels, but none of the entries normalized to a valid provider/model target.`,
|
|
};
|
|
}
|
|
if (policy.allowedModels.size === 0) {
|
|
return { allowed: true };
|
|
}
|
|
const requestedModelRef = resolvePluginSubagentRequestedModelRef(params);
|
|
if (!requestedModelRef) {
|
|
return {
|
|
allowed: false,
|
|
reason:
|
|
"fallback provider/model overrides that use an allowlist must resolve to a canonical provider/model target.",
|
|
};
|
|
}
|
|
if (policy.allowedModels.has(requestedModelRef)) {
|
|
return { allowed: true };
|
|
}
|
|
return {
|
|
allowed: false,
|
|
reason: `model override "${requestedModelRef}" is not allowlisted for plugin "${pluginId}".`,
|
|
};
|
|
}
|
|
|
|
// ── Internal gateway dispatch for plugin runtime ────────────────────
|
|
|
|
function hasAdminScope(client: GatewayRequestOptions["client"] | undefined): boolean {
|
|
const scopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : [];
|
|
return scopes.includes(ADMIN_SCOPE);
|
|
}
|
|
|
|
function canClientUseModelOverride(client: GatewayRequestOptions["client"]): boolean {
|
|
return hasAdminScope(client) || client?.internal?.allowModelOverride === true;
|
|
}
|
|
|
|
function canTrustedOfficialPluginRequestScopes(params: {
|
|
pluginId?: string;
|
|
pluginOrigin?: PluginOrigin;
|
|
pluginTrustedOfficialInstall?: boolean;
|
|
}): boolean {
|
|
if (!params.pluginId) {
|
|
return false;
|
|
}
|
|
if (params.pluginOrigin === "bundled" || params.pluginTrustedOfficialInstall === true) {
|
|
return true;
|
|
}
|
|
const registry = getActivePluginRegistry();
|
|
const record = registry?.plugins.find((entry) => entry.id === params.pluginId);
|
|
return record?.origin === "bundled" || record?.trustedOfficialInstall === true;
|
|
}
|
|
|
|
function resolveRuntimeNodeInvokeSyntheticScopes(params: {
|
|
pluginId?: string;
|
|
pluginOrigin?: PluginOrigin;
|
|
pluginTrustedOfficialInstall?: boolean;
|
|
requestedScopes?: OperatorScope[];
|
|
}): OperatorScope[] | undefined {
|
|
// Requested scopes may replace caller scopes, so only bundled or trusted official plugins qualify.
|
|
return params.requestedScopes && canTrustedOfficialPluginRequestScopes(params)
|
|
? params.requestedScopes
|
|
: undefined;
|
|
}
|
|
|
|
export async function dispatchTrustedPluginGatewayMethod<T>(
|
|
method: string,
|
|
params: Record<string, unknown> = {},
|
|
options?: RuntimeGatewayRequestOptions,
|
|
resolveGatewayContext?: GatewayContextResolver,
|
|
): Promise<T> {
|
|
const scope = getPluginRuntimeGatewayRequestScope();
|
|
const pluginId = scope?.pluginId?.trim();
|
|
if (!canTrustedOfficialPluginRequestScopes(scope ?? {})) {
|
|
throw new Error("Gateway requests are only available to bundled or trusted official plugins.");
|
|
}
|
|
const syntheticScopes = normalizeOperatorScopeList(options?.scopes);
|
|
return await dispatchGatewayMethodInProcess<T>(method, params, {
|
|
forceSyntheticClient: true,
|
|
pluginRuntimeOwnerId: pluginId,
|
|
resolveGatewayContext,
|
|
...(syntheticScopes ? { syntheticScopes } : {}),
|
|
...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
});
|
|
}
|
|
|
|
const PLUGIN_SUBAGENT_SESSION_MESSAGES_MAX_LIMIT = 1_000;
|
|
|
|
export function createGatewaySubagentRuntime(
|
|
resolveGatewayContext?: GatewayContextResolver,
|
|
overridePolicies: PluginSubagentOverridePolicies = {},
|
|
): PluginRuntime["subagent"] {
|
|
const getSessionMessages: PluginRuntime["subagent"]["getSessionMessages"] = async (params) => {
|
|
const limit =
|
|
params.limit == null || !Number.isFinite(params.limit)
|
|
? undefined
|
|
: Math.min(
|
|
PLUGIN_SUBAGENT_SESSION_MESSAGES_MAX_LIMIT,
|
|
Math.max(1, Math.floor(params.limit)),
|
|
);
|
|
const payload = await dispatchGatewayMethodInProcess<{ messages?: unknown[] }>(
|
|
"sessions.get",
|
|
{
|
|
key: params.sessionKey,
|
|
...(limit != null && { limit }),
|
|
},
|
|
{ resolveGatewayContext },
|
|
);
|
|
return { messages: Array.isArray(payload?.messages) ? payload.messages : [] };
|
|
};
|
|
|
|
return {
|
|
async run(params) {
|
|
const pluginSubagentRequester = resolvePluginSubagentCompletionRequester(
|
|
params.completionDelivery,
|
|
);
|
|
const scope = getPluginRuntimeGatewayRequestScope();
|
|
const pluginId =
|
|
typeof scope?.pluginId === "string" && scope.pluginId.trim()
|
|
? scope.pluginId.trim()
|
|
: undefined;
|
|
const runtimePluginToolGrant = resolvePluginSubagentToolsAlsoAllow({
|
|
pluginId,
|
|
toolsAlsoAllow: params.toolsAlsoAllow,
|
|
});
|
|
const overrideRequested = Boolean(params.provider || params.model);
|
|
const hasRequestScopeClient = Boolean(scope?.client);
|
|
let allowOverride = hasRequestScopeClient && canClientUseModelOverride(scope?.client ?? null);
|
|
let allowSyntheticModelOverride = false;
|
|
if (overrideRequested && !allowOverride && !hasRequestScopeClient) {
|
|
const fallbackAuth = authorizeFallbackModelOverride({
|
|
policies: overridePolicies,
|
|
pluginId: scope?.pluginId,
|
|
provider: params.provider,
|
|
model: params.model,
|
|
});
|
|
if (!fallbackAuth.allowed) {
|
|
throw new Error(fallbackAuth.reason);
|
|
}
|
|
allowOverride = true;
|
|
allowSyntheticModelOverride = true;
|
|
}
|
|
if (overrideRequested && !allowOverride) {
|
|
throw new Error("provider/model override is not authorized for this plugin subagent run.");
|
|
}
|
|
const payload = await dispatchGatewayMethodInProcess<{
|
|
runId?: string;
|
|
sessionKey?: string;
|
|
runtime?: unknown;
|
|
}>(
|
|
"agent",
|
|
{
|
|
sessionKey: params.sessionKey,
|
|
message: params.message,
|
|
deliver: params.deliver ?? false,
|
|
...(allowOverride && params.provider && { provider: params.provider }),
|
|
...(allowOverride && params.model && { model: params.model }),
|
|
...(params.extraSystemPrompt && { extraSystemPrompt: params.extraSystemPrompt }),
|
|
...(params.lane && { lane: params.lane }),
|
|
...(params.cwd && { cwd: params.cwd }),
|
|
...(params.lightContext === true && { bootstrapContextMode: "lightweight" }),
|
|
// The gateway `agent` schema requires `idempotencyKey: NonEmptyString`,
|
|
// so fall back to a generated UUID when the caller omits it. Without
|
|
// this, plugin subagent runs (for example memory-core dreaming
|
|
// narrative) silently fail schema validation at the gateway.
|
|
idempotencyKey: params.idempotencyKey || randomUUID(),
|
|
},
|
|
{
|
|
allowSyntheticModelOverride,
|
|
agentRunTracking: "plugin_subagent",
|
|
...(pluginId ? { pluginRuntimeOwnerId: pluginId } : {}),
|
|
...(pluginSubagentRequester ? { pluginSubagentRequester } : {}),
|
|
...(runtimePluginToolGrant ? { runtimePluginToolGrant } : {}),
|
|
resolveGatewayContext,
|
|
},
|
|
);
|
|
const runId = payload?.runId;
|
|
if (typeof runId !== "string" || !runId) {
|
|
throw new Error("Gateway agent method returned an invalid runId.");
|
|
}
|
|
const sessionKey = payload?.sessionKey?.trim() || params.sessionKey;
|
|
const runtime = normalizePluginSubagentRunRuntime(payload?.runtime);
|
|
return { runId, sessionKey, ...(runtime ? { runtime } : {}) };
|
|
},
|
|
async waitForRun(params) {
|
|
const payload = await dispatchGatewayMethodInProcess<{ status?: string; error?: string }>(
|
|
"agent.wait",
|
|
{
|
|
runId: params.runId,
|
|
...(params.timeoutMs != null && { timeoutMs: params.timeoutMs }),
|
|
},
|
|
{ resolveGatewayContext },
|
|
);
|
|
let status = payload?.status;
|
|
if (status === "completed" || status === "succeeded") {
|
|
status = "ok";
|
|
} else if (status === "error" && payload?.error?.trim().toLowerCase() === "completed") {
|
|
status = "ok";
|
|
}
|
|
if (status !== "ok" && status !== "error" && status !== "timeout") {
|
|
throw new Error(`Gateway agent.wait returned unexpected status: ${payload?.status}`);
|
|
}
|
|
return {
|
|
status,
|
|
...(status !== "ok" &&
|
|
typeof payload?.error === "string" &&
|
|
payload.error && { error: payload.error }),
|
|
};
|
|
},
|
|
getSessionMessages,
|
|
async deleteSession(params) {
|
|
const scope = getPluginRuntimeGatewayRequestScope();
|
|
const pluginId =
|
|
typeof scope?.pluginId === "string" && scope.pluginId.trim()
|
|
? scope.pluginId.trim()
|
|
: undefined;
|
|
const pluginOwnedCleanupOptions = pluginId
|
|
? {
|
|
pluginRuntimeOwnerId: pluginId,
|
|
...(!hasAdminScope(scope?.client)
|
|
? {
|
|
forceSyntheticClient: true,
|
|
syntheticScopes: [ADMIN_SCOPE],
|
|
}
|
|
: {}),
|
|
}
|
|
: undefined;
|
|
await dispatchGatewayMethodInProcess(
|
|
"sessions.delete",
|
|
{
|
|
key: params.sessionKey,
|
|
deleteTranscript: params.deleteTranscript ?? true,
|
|
},
|
|
{ ...pluginOwnedCleanupOptions, resolveGatewayContext },
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
type GatewayRuntimeNodes = Awaited<ReturnType<PluginRuntime["nodes"]["list"]>>["nodes"];
|
|
|
|
export function createGatewayNodesRuntime(
|
|
resolveGatewayContext?: GatewayContextResolver,
|
|
): PluginRuntime["nodes"] {
|
|
return {
|
|
async list(params) {
|
|
const context = getInProcessGatewayRequestContext(resolveGatewayContext);
|
|
const payload = await dispatchGatewayMethodInProcess<{ nodes?: unknown[] }>(
|
|
"node.list",
|
|
{},
|
|
{
|
|
resolveGatewayContext: () => context,
|
|
},
|
|
);
|
|
const nodes = Array.isArray(payload?.nodes) ? payload.nodes : [];
|
|
const filteredNodes =
|
|
params?.connected === true
|
|
? nodes.filter(
|
|
(node) =>
|
|
typeof node === "object" &&
|
|
(node as { connected?: unknown } | null)?.connected === true,
|
|
)
|
|
: nodes;
|
|
return {
|
|
nodes: projectGatewayRuntimeNodes(filteredNodes, context) as GatewayRuntimeNodes,
|
|
};
|
|
},
|
|
async invoke(params) {
|
|
const scope = getPluginRuntimeGatewayRequestScope();
|
|
const pluginId =
|
|
typeof scope?.pluginId === "string" && scope.pluginId.trim()
|
|
? scope.pluginId.trim()
|
|
: undefined;
|
|
const syntheticScopes = resolveRuntimeNodeInvokeSyntheticScopes({
|
|
pluginId,
|
|
pluginOrigin: scope?.pluginOrigin,
|
|
pluginTrustedOfficialInstall: scope?.pluginTrustedOfficialInstall,
|
|
requestedScopes: normalizeOperatorScopeList(params.scopes),
|
|
});
|
|
return await dispatchGatewayMethodInProcess<unknown>(
|
|
"node.invoke",
|
|
{
|
|
nodeId: params.nodeId,
|
|
command: params.command,
|
|
...(params.params !== undefined && { params: params.params }),
|
|
timeoutMs: params.timeoutMs,
|
|
idempotencyKey: params.idempotencyKey || randomUUID(),
|
|
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
},
|
|
{
|
|
...(pluginId ? { pluginRuntimeOwnerId: pluginId } : {}),
|
|
...(syntheticScopes ? { forceSyntheticClient: true, syntheticScopes } : {}),
|
|
...(params.signal ? { signal: params.signal } : {}),
|
|
resolveGatewayContext,
|
|
},
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createGatewayPluginRuntimeBindings(
|
|
resolveGatewayContext: GatewayContextResolver,
|
|
overridePolicies: PluginSubagentOverridePolicies,
|
|
): Pick<PluginRuntime, "gateway" | "nodes" | "subagent"> &
|
|
Pick<CreatePluginRuntimeOptions, "dispatchReplyFromConfig"> {
|
|
return {
|
|
dispatchReplyFromConfig: async (params) => {
|
|
const { dispatchReplyFromConfig } =
|
|
await import("../auto-reply/reply/dispatch-from-config.js");
|
|
const sessionWorkerPlacementContext =
|
|
getInProcessGatewayRequestContext(resolveGatewayContext);
|
|
return await dispatchReplyFromConfig({
|
|
...params,
|
|
...(sessionWorkerPlacementContext ? { sessionWorkerPlacementContext } : {}),
|
|
});
|
|
},
|
|
gateway: {
|
|
isAvailable: async () => hasInProcessGatewayContext(resolveGatewayContext),
|
|
request: (method, params, options) =>
|
|
dispatchTrustedPluginGatewayMethod(method, params, options, resolveGatewayContext),
|
|
},
|
|
nodes: createGatewayNodesRuntime(resolveGatewayContext),
|
|
subagent: createGatewaySubagentRuntime(resolveGatewayContext, overridePolicies),
|
|
};
|
|
}
|
|
|
|
// ── Plugin loading ──────────────────────────────────────────────────
|
|
|
|
function createGatewayPluginRegistrationLogger(params?: {
|
|
suppressInfoLogs?: boolean;
|
|
}): PluginLogger {
|
|
const logger = createPluginRuntimeLoaderLogger();
|
|
if (params?.suppressInfoLogs !== true) {
|
|
return logger;
|
|
}
|
|
return {
|
|
...logger,
|
|
info: (_message: string) => undefined,
|
|
};
|
|
}
|
|
|
|
export function loadGatewayPlugins(params: {
|
|
cfg: OpenClawConfig;
|
|
activationSourceConfig?: OpenClawConfig;
|
|
autoEnabledReasons?: Readonly<Record<string, string[]>>;
|
|
workspaceDir?: string;
|
|
log: {
|
|
info: (msg: string) => void;
|
|
warn: (msg: string) => void;
|
|
error: (msg: string) => void;
|
|
debug: (msg: string) => void;
|
|
};
|
|
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
|
|
coreGatewayMethodNames?: readonly string[];
|
|
hostServices?: PluginRegistryParams["hostServices"];
|
|
baseMethods: string[];
|
|
pluginIds?: string[];
|
|
pluginLookUpTable?: PluginLookUpTable;
|
|
channelPluginLoadIntent?: ChannelPluginLoadIntent;
|
|
suppressPluginInfoLogs?: boolean;
|
|
startupTrace?: {
|
|
detail: (name: string, metrics: ReadonlyArray<readonly [string, number | string]>) => void;
|
|
};
|
|
ambientEnvTriggers?: AmbientEnvTriggerPolicy;
|
|
resolveGatewayContext?: GatewayContextResolver;
|
|
}) {
|
|
const started = performance.now();
|
|
const allowProcessHomeSessionCatalogs = allowsProcessHomeSessionScan();
|
|
const activationAutoEnabled =
|
|
params.activationSourceConfig !== undefined && params.autoEnabledReasons === undefined
|
|
? applyPluginAutoEnable({
|
|
config: params.activationSourceConfig,
|
|
env: process.env,
|
|
...(params.pluginLookUpTable?.manifestRegistry
|
|
? { manifestRegistry: params.pluginLookUpTable.manifestRegistry }
|
|
: {}),
|
|
discovery: params.pluginLookUpTable?.discovery,
|
|
ambientEnvTriggers: params.ambientEnvTriggers,
|
|
})
|
|
: undefined;
|
|
const autoEnableMs = performance.now() - started;
|
|
const autoEnabled =
|
|
params.activationSourceConfig !== undefined
|
|
? {
|
|
config: params.cfg,
|
|
changes: activationAutoEnabled?.changes ?? [],
|
|
autoEnabledReasons:
|
|
params.autoEnabledReasons ?? activationAutoEnabled?.autoEnabledReasons ?? {},
|
|
}
|
|
: params.autoEnabledReasons !== undefined
|
|
? {
|
|
config: params.cfg,
|
|
changes: [],
|
|
autoEnabledReasons: params.autoEnabledReasons,
|
|
}
|
|
: applyPluginAutoEnable({
|
|
config: params.cfg,
|
|
env: process.env,
|
|
...(params.pluginLookUpTable?.manifestRegistry
|
|
? { manifestRegistry: params.pluginLookUpTable.manifestRegistry }
|
|
: {}),
|
|
discovery: params.pluginLookUpTable?.discovery,
|
|
ambientEnvTriggers: params.ambientEnvTriggers,
|
|
});
|
|
const resolvedConfigMs = performance.now() - started;
|
|
const resolvedConfig = autoEnabled.config;
|
|
const pluginIds = params.pluginIds ?? [
|
|
...(
|
|
params.pluginLookUpTable ??
|
|
loadPluginLookUpTable({
|
|
config: resolvedConfig,
|
|
activationSourceConfig: params.activationSourceConfig,
|
|
workspaceDir: params.workspaceDir,
|
|
env: process.env,
|
|
ambientEnvTriggers: params.ambientEnvTriggers,
|
|
})
|
|
).startup.pluginIds,
|
|
];
|
|
const pluginIdsMs = performance.now() - started;
|
|
if (pluginIds.length === 0) {
|
|
const pluginRegistry = createEmptyPluginRegistry();
|
|
activatePluginRegistry(pluginRegistry, null, "gateway-bindable", params.workspaceDir);
|
|
params.startupTrace?.detail("plugins.gateway-load", [
|
|
["autoEnableMs", autoEnableMs],
|
|
["resolvedConfigMs", resolvedConfigMs],
|
|
["pluginIdsMs", pluginIdsMs],
|
|
["loadMs", 0],
|
|
["pluginIds", "0"],
|
|
["pluginCount", 0],
|
|
["gatewayHandlerCount", 0],
|
|
]);
|
|
return {
|
|
pluginRegistry,
|
|
gatewayMethods: [...params.baseMethods],
|
|
};
|
|
}
|
|
const beforeLoad = performance.now();
|
|
const loaderStatsBefore = getPluginModuleLoaderStats();
|
|
const gatewayRuntimeBindings = createGatewayPluginRuntimeBindings(
|
|
params.resolveGatewayContext ?? (() => undefined),
|
|
resolvePluginSubagentOverridePolicies(resolvedConfig),
|
|
);
|
|
const pluginRegistry = loadAndActivateRootPluginRegistry({
|
|
config: resolvedConfig,
|
|
allowProcessHomeSessionCatalogs,
|
|
activationSourceConfig: params.activationSourceConfig ?? params.cfg,
|
|
autoEnabledReasons: autoEnabled.autoEnabledReasons,
|
|
workspaceDir: params.workspaceDir,
|
|
onlyPluginIds: pluginIds,
|
|
logger: createGatewayPluginRegistrationLogger({
|
|
suppressInfoLogs: params.suppressPluginInfoLogs,
|
|
}),
|
|
...(params.coreGatewayHandlers !== undefined && {
|
|
coreGatewayHandlers: params.coreGatewayHandlers,
|
|
}),
|
|
...(params.coreGatewayMethodNames !== undefined && {
|
|
coreGatewayMethodNames: params.coreGatewayMethodNames,
|
|
}),
|
|
...(params.hostServices !== undefined && {
|
|
hostServices: params.hostServices,
|
|
}),
|
|
runtimeOptions: {
|
|
allowGatewaySubagentBinding: true,
|
|
...gatewayRuntimeBindings,
|
|
},
|
|
channelPluginLoadIntent: params.channelPluginLoadIntent,
|
|
preferBuiltPluginArtifacts: true,
|
|
...(params.startupTrace !== undefined && {
|
|
startupTrace: params.startupTrace,
|
|
}),
|
|
...(params.pluginLookUpTable
|
|
? {
|
|
manifestRegistry: params.pluginLookUpTable.manifestRegistry,
|
|
installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(
|
|
params.pluginLookUpTable.index,
|
|
),
|
|
}
|
|
: {}),
|
|
});
|
|
const loadMs = performance.now() - beforeLoad;
|
|
const loaderStatsAfter = getPluginModuleLoaderStats();
|
|
const pluginMethods = Object.keys(pluginRegistry.gatewayHandlers);
|
|
const gatewayMethods = uniqueStrings([...params.baseMethods, ...pluginMethods]);
|
|
params.startupTrace?.detail("plugins.gateway-load", [
|
|
["autoEnableMs", autoEnableMs],
|
|
["resolvedConfigMs", resolvedConfigMs],
|
|
["pluginIdsMs", pluginIdsMs],
|
|
["loadMs", loadMs],
|
|
["pluginIds", String(pluginIds.length)],
|
|
["pluginCount", pluginIds.length],
|
|
["gatewayHandlers", String(pluginMethods.length)],
|
|
["gatewayHandlerCount", pluginMethods.length],
|
|
["loaderCallsCount", loaderStatsAfter.calls - loaderStatsBefore.calls],
|
|
["loaderNativeHitsCount", loaderStatsAfter.nativeHits - loaderStatsBefore.nativeHits],
|
|
["loaderNativeMissesCount", loaderStatsAfter.nativeMisses - loaderStatsBefore.nativeMisses],
|
|
[
|
|
"loaderSourceTransformForcedCount",
|
|
loaderStatsAfter.sourceTransformForced - loaderStatsBefore.sourceTransformForced,
|
|
],
|
|
[
|
|
"loaderSourceTransformFallbacksCount",
|
|
loaderStatsAfter.sourceTransformFallbacks - loaderStatsBefore.sourceTransformFallbacks,
|
|
],
|
|
[
|
|
"loaderTopSourceTransformTargets",
|
|
loaderStatsAfter.topSourceTransformTargets
|
|
.slice(0, 3)
|
|
.map((entry) => `${entry.count}:${entry.target}`)
|
|
.join(","),
|
|
],
|
|
]);
|
|
return { pluginRegistry, gatewayMethods };
|
|
}
|