fix(plugins): lazily initialize runtime and split plugin-sdk startup imports (#28620)

Merged via squash.

Prepared head SHA: 8bd7d6c13b
Co-authored-by: hmemcpy <601206+hmemcpy@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Igal Tabachnik
2026-03-04 02:58:48 +02:00
committed by GitHub
parent 4b17d6d882
commit a4850b1b8f
17 changed files with 226 additions and 19 deletions

26
src/plugin-sdk/core.ts Normal file
View File

@@ -0,0 +1,26 @@
export type { OpenClawPluginApi, OpenClawPluginService } from "../plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export {
approveDevicePairing,
listDevicePairing,
rejectDevicePairing,
} from "../infra/device-pairing.js";
export {
runPluginCommandWithTimeout,
type PluginCommandRunOptions,
type PluginCommandRunResult,
} from "./run-command.js";
export { resolveGatewayBindUrl } from "../shared/gateway-bind-url.js";
export type { GatewayBindUrlResult } from "../shared/gateway-bind-url.js";
export { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
export type {
TailscaleStatusCommandResult,
TailscaleStatusCommandRunner,
} from "../shared/tailscale-status.js";

View File

@@ -0,0 +1,53 @@
export type { ChannelMessageActionAdapter } from "../channels/plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { OpenClawConfig } from "../config/config.js";
export type { ResolvedTelegramAccount } from "../telegram/accounts.js";
export type { TelegramProbe } from "../telegram/probe.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
} from "../channels/plugins/setup-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export { getChatChannelMeta } from "../channels/registry.js";
export {
listTelegramAccountIds,
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
} from "../telegram/accounts.js";
export {
listTelegramDirectoryGroupsFromConfig,
listTelegramDirectoryPeersFromConfig,
} from "../channels/plugins/directory-config.js";
export {
looksLikeTelegramTargetId,
normalizeTelegramMessagingTarget,
} from "../channels/plugins/normalize/telegram.js";
export {
parseTelegramReplyToMessageId,
parseTelegramThreadId,
} from "../telegram/outbound-params.js";
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
export {
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
} from "../config/runtime-group-policy.js";
export {
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
export { telegramOnboardingAdapter } from "../channels/plugins/onboarding/telegram.js";
export { TelegramConfigSchema } from "../config/zod-schema.providers-core.js";
export { buildTokenChannelStatusSummary } from "./status-helpers.js";

View File

@@ -974,6 +974,37 @@ describe("loadOpenClawPlugins", () => {
);
});
it("preserves runtime reflection semantics when runtime is lazily initialized", () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "runtime-introspection",
filename: "runtime-introspection.cjs",
body: `module.exports = { id: "runtime-introspection", register(api) {
const runtime = api.runtime ?? {};
const keys = Object.keys(runtime);
if (!keys.includes("channel")) {
throw new Error("runtime channel key missing");
}
if (!("channel" in runtime)) {
throw new Error("runtime channel missing from has check");
}
if (!Object.getOwnPropertyDescriptor(runtime, "channel")) {
throw new Error("runtime channel descriptor missing");
}
} };`,
});
const registry = loadRegistryFromSinglePlugin({
plugin,
pluginConfig: {
allow: ["runtime-introspection"],
},
});
const record = registry.plugins.find((entry) => entry.id === "runtime-introspection");
expect(record?.status).toBe("loaded");
});
it("prefers dist plugin-sdk alias when loader runs from dist", () => {
const { root, distFile } = createPluginSdkAliasFixture();

View File

@@ -22,6 +22,7 @@ import { isPathInside, safeStatSync } from "./path-safety.js";
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
import { setActivePluginRegistry } from "./runtime.js";
import { createPluginRuntime } from "./runtime/index.js";
import type { PluginRuntime } from "./runtime/types.js";
import { validateJsonSchemaValue } from "./schema-validator.js";
import type {
OpenClawPluginDefinition,
@@ -91,6 +92,14 @@ const resolvePluginSdkAccountIdAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "account-id.ts", distFile: "account-id.js" });
};
const resolvePluginSdkCoreAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "core.ts", distFile: "core.js" });
};
const resolvePluginSdkTelegramAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "telegram.ts", distFile: "telegram.js" });
};
export const __testing = {
resolvePluginSdkAliasFile,
};
@@ -393,7 +402,39 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
// Clear previously registered plugin commands before reloading
clearPluginCommands();
const runtime = createPluginRuntime();
// Lazily initialize the runtime so startup paths that discover/skip plugins do
// not eagerly load every channel runtime dependency.
let resolvedRuntime: PluginRuntime | null = null;
const resolveRuntime = (): PluginRuntime => {
resolvedRuntime ??= createPluginRuntime();
return resolvedRuntime;
};
const runtime = new Proxy({} as PluginRuntime, {
get(_target, prop, receiver) {
return Reflect.get(resolveRuntime(), prop, receiver);
},
set(_target, prop, value, receiver) {
return Reflect.set(resolveRuntime(), prop, value, receiver);
},
has(_target, prop) {
return Reflect.has(resolveRuntime(), prop);
},
ownKeys() {
return Reflect.ownKeys(resolveRuntime() as object);
},
getOwnPropertyDescriptor(_target, prop) {
return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop);
},
defineProperty(_target, prop, attributes) {
return Reflect.defineProperty(resolveRuntime() as object, prop, attributes);
},
deleteProperty(_target, prop) {
return Reflect.deleteProperty(resolveRuntime() as object, prop);
},
getPrototypeOf() {
return Reflect.getPrototypeOf(resolveRuntime() as object);
},
});
const { registry, createApi } = createPluginRegistry({
logger,
runtime,
@@ -435,17 +476,22 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
const pluginSdkAlias = resolvePluginSdkAlias();
const pluginSdkAccountIdAlias = resolvePluginSdkAccountIdAlias();
const pluginSdkCoreAlias = resolvePluginSdkCoreAlias();
const pluginSdkTelegramAlias = resolvePluginSdkTelegramAlias();
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...(pluginSdkCoreAlias ? { "openclaw/plugin-sdk/core": pluginSdkCoreAlias } : {}),
...(pluginSdkTelegramAlias ? { "openclaw/plugin-sdk/telegram": pluginSdkTelegramAlias } : {}),
...(pluginSdkAccountIdAlias
? { "openclaw/plugin-sdk/account-id": pluginSdkAccountIdAlias }
: {}),
};
jitiLoader = createJiti(import.meta.url, {
interopDefault: true,
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
...(pluginSdkAlias || pluginSdkAccountIdAlias
...(Object.keys(aliasMap).length > 0
? {
alias: {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...(pluginSdkAccountIdAlias
? { "openclaw/plugin-sdk/account-id": pluginSdkAccountIdAlias }
: {}),
},
alias: aliasMap,
}
: {}),
});