diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b8b6fbd6f..a0ce842f8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index d9b31fe8a4b..1a002447711 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -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(); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index e06f4322782..6bbdaacd5e0 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -405,10 +405,34 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi // 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) { - resolvedRuntime ??= createPluginRuntime(); - return Reflect.get(resolvedRuntime, 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({