From d4a92cff60d907eb2b975d5295a0ab68fc5c0d00 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 04:38:32 +0100 Subject: [PATCH] fix(plugins): avoid plugin sdk alias rewrite races --- CHANGELOG.md | 1 + package.json | 2 +- src/config/schema.base.generated.ts | 2 +- src/plugins/loader.test.ts | 18 ++++++++++++++++++ src/plugins/loader.ts | 10 +++++++++- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26043d9d39e..804ca3d304d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai - Gateway/sessions: extend the webchat session-mutation guard to `sessions.compact` and `sessions.compaction.restore`, so `WEBCHAT_UI` clients are rejected from compaction-side session mutations consistently with the existing patch/delete guards. (#70716) Thanks @drobison00. - QA channel/security: reject non-HTTP(S) inbound attachment URLs before media fetch, and log rejected schemes so suspicious or misconfigured payloads are visible during debugging. (#70708) Thanks @vincentkoc. - Plugins/install: link the host OpenClaw package into external plugins that declare `openclaw` as a peer dependency, so peer-only plugin SDK imports resolve after install without bundling a duplicate host package. (#70462) Thanks @anishesg. +- Plugins/Windows: refresh the packaged plugin SDK alias in place during bundled runtime dependency repair, so gateway and CLI plugin startup no longer race on `ENOTEMPTY`/`EPERM` after same-guest npm updates. - Teams/security: require shared Bot Framework audience tokens to name the configured Teams app via verified `appid` or `azp`, blocking cross-bot token replay on the global audience. (#70724) Thanks @vincentkoc. - Plugins/startup: resolve bundled plugin Jiti loads relative to the target plugin module instead of the central loader, so Bun global installs no longer hang while discovering bundled image providers. (#70073) Thanks @yidianyiko. - Anthropic/CLI security: derive Claude CLI `bypassPermissions` from OpenClaw's existing YOLO exec policy, preserve explicit raw Claude `--permission-mode` overrides, and strip malformed permission-mode args instead of silently falling back to a bypass. (#70723) Thanks @vincentkoc. diff --git a/package.json b/package.json index 1060144332f..58d7ab8ab89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openclaw", - "version": "2026.4.23-beta.2", + "version": "2026.4.23-beta.3", "description": "Multi-channel AI gateway with extensible messaging integrations", "keywords": [], "homepage": "https://github.com/openclaw/openclaw#readme", diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index e689025afd5..30d0a6ba5b1 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -27769,6 +27769,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { tags: ["advanced", "url-secret"], }, }, - version: "2026.4.23-beta.2", + version: "2026.4.23-beta.3", generatedAt: "2026-03-22T21:17:33.302Z", }; diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index b92bd553e8a..3a8d2e56a0e 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -837,6 +837,24 @@ afterAll(() => { }); describe("loadOpenClawPlugins", () => { + it("refreshes bundled plugin-sdk aliases without deleting the shared alias directory", () => { + const distRoot = makeTempDir(); + const pluginSdkDir = path.join(distRoot, "plugin-sdk"); + const aliasDir = path.join(distRoot, "extensions", "node_modules", "openclaw", "plugin-sdk"); + mkdirSafe(pluginSdkDir); + mkdirSafe(aliasDir); + fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const value = 1;\n", "utf8"); + fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 1;\n", "utf8"); + fs.writeFileSync(path.join(aliasDir, "sentinel.txt"), "keep\n", "utf8"); + + __testing.ensureOpenClawPluginSdkAlias(distRoot); + fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 2;\n", "utf8"); + __testing.ensureOpenClawPluginSdkAlias(distRoot); + + expect(fs.existsSync(path.join(aliasDir, "sentinel.txt"))).toBe(true); + expect(fs.readFileSync(path.join(aliasDir, "core.js"), "utf8")).toContain("core.js"); + }); + it("disables bundled plugins by default", () => { const bundledDir = makeTempDir(); writePlugin({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 12ca435abae..829af937ce1 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -689,7 +689,14 @@ function ensureOpenClawPluginSdkAlias(distRoot: string): void { "./plugin-sdk/*": "./plugin-sdk/*.js", }, }); - fs.rmSync(pluginSdkAliasDir, { recursive: true, force: true }); + try { + if (fs.existsSync(pluginSdkAliasDir) && !fs.lstatSync(pluginSdkAliasDir).isDirectory()) { + fs.rmSync(pluginSdkAliasDir, { recursive: true, force: true }); + } + } catch { + // Another process may be creating the alias at the same time; mkdir/write + // below will either converge or surface the real filesystem error. + } fs.mkdirSync(pluginSdkAliasDir, { recursive: true }); for (const entry of fs.readdirSync(pluginSdkDir, { withFileTypes: true })) { if (!entry.isFile() || path.extname(entry.name) !== ".js") { @@ -727,6 +734,7 @@ export const __testing = { resolvePluginSdkAliasCandidateOrder, resolvePluginSdkAliasFile, resolvePluginRuntimeModulePath, + ensureOpenClawPluginSdkAlias, shouldLoadChannelPluginInSetupRuntime, shouldPreferNativeJiti, toSafeImportPath,