From 9da5f9819b729f8bfe88593f7c7b458e657f3c0a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 19:18:50 +0100 Subject: [PATCH] fix(plugins): ignore archived extension dirs during discovery Co-authored-by: chenzhuoms --- CHANGELOG.md | 1 + src/infra/install-package-dir.ts | 4 +++- src/plugins/discovery.test.ts | 32 ++++++++++++++++++++++++++++++++ src/plugins/discovery.ts | 20 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 716c66767..db9df49cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai - Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip `openclaw: workspace:*` from plugin `devDependencies` during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603) - Plugins/Install: strip `workspace:*` devDependency entries from copied plugin manifests before `npm install --omit=dev`, preventing `EUNSUPPORTEDPROTOCOL` install failures for npm-published channel plugins (including Feishu and MS Teams). - Config/Channels: auto-enable built-in channels by writing `channels..enabled=true` (not `plugins.entries.`), and stop adding built-ins to `plugins.allow`, preventing `plugins.entries.telegram: plugin not found` validation failures. +- Plugins/Discovery: ignore scanned extension backup/disabled directory patterns (for example `.backup-*`, `.bak`, `.disabled*`) and move updater backup directories under `.openclaw-install-backups`, preventing duplicate plugin-id collisions from archived copies. - Dev tooling: prevent `CLAUDE.md` symlink target regressions by excluding CLAUDE symlink sentinels from `oxfmt` and marking them `-text` in `.gitattributes`, so formatter/EOL normalization cannot reintroduce trailing-newline targets. Thanks @vincentkoc. - Cron: honor `cron.maxConcurrentRuns` in the timer loop so due jobs can execute up to the configured parallelism instead of always running serially. (#11595) Thanks @Takhoffman. - Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg. diff --git a/src/infra/install-package-dir.ts b/src/infra/install-package-dir.ts index ffbbbf53a..d93131642 100644 --- a/src/infra/install-package-dir.ts +++ b/src/infra/install-package-dir.ts @@ -62,7 +62,9 @@ export async function installPackageDir(params: { params.logger?.info?.(`Installing to ${params.targetDir}…`); let backupDir: string | null = null; if (params.mode === "update" && (await fileExists(params.targetDir))) { - backupDir = `${params.targetDir}.backup-${Date.now()}`; + const backupRoot = path.join(path.dirname(params.targetDir), ".openclaw-install-backups"); + backupDir = path.join(backupRoot, `${path.basename(params.targetDir)}-${Date.now()}`); + await fs.mkdir(backupRoot, { recursive: true }); await fs.rename(params.targetDir, backupDir); } diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index 47dd47916..180ab87cc 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -58,6 +58,38 @@ describe("discoverOpenClawPlugins", () => { expect(ids).toContain("beta"); }); + it("ignores backup and disabled plugin directories in scanned roots", async () => { + const stateDir = makeTempDir(); + const globalExt = path.join(stateDir, "extensions"); + fs.mkdirSync(globalExt, { recursive: true }); + + const backupDir = path.join(globalExt, "feishu.backup-20260222"); + fs.mkdirSync(backupDir, { recursive: true }); + fs.writeFileSync(path.join(backupDir, "index.ts"), "export default function () {}", "utf-8"); + + const disabledDir = path.join(globalExt, "telegram.disabled.20260222"); + fs.mkdirSync(disabledDir, { recursive: true }); + fs.writeFileSync(path.join(disabledDir, "index.ts"), "export default function () {}", "utf-8"); + + const bakDir = path.join(globalExt, "discord.bak"); + fs.mkdirSync(bakDir, { recursive: true }); + fs.writeFileSync(path.join(bakDir, "index.ts"), "export default function () {}", "utf-8"); + + const liveDir = path.join(globalExt, "live"); + fs.mkdirSync(liveDir, { recursive: true }); + fs.writeFileSync(path.join(liveDir, "index.ts"), "export default function () {}", "utf-8"); + + const { candidates } = await withStateDir(stateDir, async () => { + return discoverOpenClawPlugins({}); + }); + + const ids = candidates.map((candidate) => candidate.idHint); + expect(ids).toContain("live"); + expect(ids).not.toContain("feishu.backup-20260222"); + expect(ids).not.toContain("telegram.disabled.20260222"); + expect(ids).not.toContain("discord.bak"); + }); + it("loads package extension packs", async () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions", "pack"); diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index 932602107..1df727fab 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -206,6 +206,23 @@ function isExtensionFile(filePath: string): boolean { return !filePath.endsWith(".d.ts"); } +function shouldIgnoreScannedDirectory(dirName: string): boolean { + const normalized = dirName.trim().toLowerCase(); + if (!normalized) { + return true; + } + if (normalized.endsWith(".bak")) { + return true; + } + if (normalized.includes(".backup-")) { + return true; + } + if (normalized.includes(".disabled")) { + return true; + } + return false; +} + function readPackageManifest(dir: string): PackageManifest | null { const manifestPath = path.join(dir, "package.json"); if (!fs.existsSync(manifestPath)) { @@ -362,6 +379,9 @@ function discoverInDirectory(params: { if (!entry.isDirectory()) { continue; } + if (shouldIgnoreScannedDirectory(entry.name)) { + continue; + } const manifest = readPackageManifest(fullPath); const extensions = manifest ? resolvePackageExtensions(manifest) : [];