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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 226 additions and 19 deletions

View File

@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/security default response headers: add `Permissions-Policy: camera=(), microphone=(), geolocation=()` to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan.
- Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into `openclaw/plugin-sdk/core` and `openclaw/plugin-sdk/telegram`, and preserve `api.runtime` reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy.
- Security/auth labels: remove token and API-key snippets from user-facing auth status labels so `/status` and `/models` do not expose credential fragments. (#33262) thanks @cu1ch3n.
- Security/audit denyCommands guidance: suggest likely exact node command IDs for unknown `gateway.nodes.denyCommands` entries so ineffective denylist entries are easier to correct. (#29713) thanks @liquidhorizon88-bot.
- Docs/security hardening guidance: document Docker `DOCKER-USER` + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan.

View File

@ -1,12 +1,12 @@
import os from "node:os";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import {
approveDevicePairing,
listDevicePairing,
resolveGatewayBindUrl,
runPluginCommandWithTimeout,
resolveTailnetHostWithRunner,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/core";
import qrcode from "qrcode-terminal";
import {
armPairNotifyOnce,

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
const memoryCorePlugin = {
id: "memory-core",

View File

@ -1,6 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { OpenClawPluginApi, OpenClawPluginService } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi, OpenClawPluginService } from "openclaw/plugin-sdk/core";
type ArmGroup = "camera" | "screen" | "writes" | "all";

View File

@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
type ElevenLabsVoice = {
voice_id: string;

View File

@ -1,5 +1,5 @@
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { telegramPlugin } from "./src/channel.js";
import { setTelegramRuntime } from "./src/runtime.js";

View File

@ -31,7 +31,7 @@ import {
type OpenClawConfig,
type ResolvedTelegramAccount,
type TelegramProbe,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/telegram";
import { getTelegramRuntime } from "./runtime.js";
const meta = getChatChannelMeta("telegram");

View File

@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
let runtime: PluginRuntime | null = null;

View File

@ -40,6 +40,14 @@
"types": "./dist/plugin-sdk/index.d.ts",
"default": "./dist/plugin-sdk/index.js"
},
"./plugin-sdk/core": {
"types": "./dist/plugin-sdk/core.d.ts",
"default": "./dist/plugin-sdk/core.js"
},
"./plugin-sdk/telegram": {
"types": "./dist/plugin-sdk/telegram.d.ts",
"default": "./dist/plugin-sdk/telegram.js"
},
"./plugin-sdk/account-id": {
"types": "./dist/plugin-sdk/account-id.d.ts",
"default": "./dist/plugin-sdk/account-id.js"

View File

@ -6,7 +6,7 @@ import path from "node:path";
//
// Our package export map points subpath `types` at `dist/plugin-sdk/<entry>.d.ts`, so we
// generate stable entry d.ts files that re-export the real declarations.
const entrypoints = ["index", "account-id"] as const;
const entrypoints = ["index", "core", "telegram", "account-id"] as const;
for (const entry of entrypoints) {
const out = path.join(process.cwd(), `dist/plugin-sdk/${entry}.d.ts`);
fs.mkdirSync(path.dirname(out), { recursive: true });

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,
}
: {}),
});

View File

@ -12,6 +12,8 @@
},
"include": [
"src/plugin-sdk/index.ts",
"src/plugin-sdk/core.ts",
"src/plugin-sdk/telegram.ts",
"src/plugin-sdk/account-id.ts",
"src/plugin-sdk/keyed-async-queue.ts",
"src/types/**/*.d.ts"

View File

@ -30,6 +30,24 @@ export default defineConfig([
fixedExtension: false,
platform: "node",
},
{
// Keep sync lazy-runtime channel modules as concrete dist files.
entry: {
"channels/plugins/agent-tools/whatsapp-login":
"src/channels/plugins/agent-tools/whatsapp-login.ts",
"channels/plugins/actions/discord": "src/channels/plugins/actions/discord.ts",
"channels/plugins/actions/signal": "src/channels/plugins/actions/signal.ts",
"channels/plugins/actions/telegram": "src/channels/plugins/actions/telegram.ts",
"telegram/audit": "src/telegram/audit.ts",
"telegram/token": "src/telegram/token.ts",
"line/accounts": "src/line/accounts.ts",
"line/send": "src/line/send.ts",
"line/template-messages": "src/line/template-messages.ts",
},
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/index.ts",
outDir: "dist/plugin-sdk",
@ -37,6 +55,20 @@ export default defineConfig([
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/core.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/telegram.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/account-id.ts",
outDir: "dist/plugin-sdk",

View File

@ -17,6 +17,14 @@ export default defineConfig({
find: "openclaw/plugin-sdk/account-id",
replacement: path.join(repoRoot, "src", "plugin-sdk", "account-id.ts"),
},
{
find: "openclaw/plugin-sdk/core",
replacement: path.join(repoRoot, "src", "plugin-sdk", "core.ts"),
},
{
find: "openclaw/plugin-sdk/telegram",
replacement: path.join(repoRoot, "src", "plugin-sdk", "telegram.ts"),
},
{
find: "openclaw/plugin-sdk/keyed-async-queue",
replacement: path.join(repoRoot, "src", "plugin-sdk", "keyed-async-queue.ts"),