From 6dca427187e527f668b67fd5339187e1109235e9 Mon Sep 17 00:00:00 2001 From: hzhang Date: Mon, 25 May 2026 10:31:18 +0100 Subject: [PATCH] fix: loadRouters preserves API-registered routers; install.mjs chowns root Two related fixes: 1. core/router-loader.loadRouters() previously called map.clear() before scanning routersDir, which wiped routers registered via __prismFacet.addRouter (the cross-plugin API). Now: track which entries in the map came from a file vs API (filePath sentinel), only delete file-based ones that disappeared between loads. External routers are never touched. 2. scripts/install.mjs: chown installed plugin files to root when the installer is running as root. openclaw 2026.5+ blocks plugins whose files are owned by non-root; rsync/tar from a developer laptop silently broke prism-facet on the next gateway restart. Matches the Meridian + ClawPrompts install.mjs fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/core/router-loader.ts | 20 +++++++++++++++++++- scripts/install.mjs | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/plugin/core/router-loader.ts b/plugin/core/router-loader.ts index 4dd4279..1a515f3 100644 --- a/plugin/core/router-loader.ts +++ b/plugin/core/router-loader.ts @@ -35,7 +35,16 @@ export async function loadRouters( log: { info(msg: string): void; warn(msg: string): void } ): Promise { const map = getRouterMap(); - map.clear(); + // Don't clear() — that would wipe externally-registered routers added + // by other plugins via __prismFacet.addRouter. Instead, track which + // routers came from the filesystem this run and remove only those + // that disappeared. External routers (filePath sentinel) are never + // touched here. + const FILE_SENTINEL_PREFIX = "(); + for (const [name, r] of map.entries()) { + if (!r.filePath.startsWith(FILE_SENTINEL_PREFIX)) fileRoutersBefore.add(name); + } if (typeof _G[LOAD_COUNTER_KEY] !== "number") _G[LOAD_COUNTER_KEY] = 0; (_G[LOAD_COUNTER_KEY] as number)++; @@ -48,9 +57,12 @@ export async function loadRouters( ); } catch { log.warn(`[prism-facet] routers directory not found: ${routersDir}`); + // No file-based routers this run — drop any leftover file routers. + for (const name of fileRoutersBefore) map.delete(name); return; } + const seenThisRun = new Set(); for (const file of files) { const name = routerNameFromFile(file); const filePath = path.resolve(routersDir, file); @@ -61,11 +73,17 @@ export async function loadRouters( continue; } map.set(name, { name, filePath, module: mod }); + seenThisRun.add(name); log.info(`[prism-facet] router loaded: ${name}`); } catch (err) { log.warn(`[prism-facet] router ${name}: failed to load — ${String(err)}`); } } + // Drop file-based routers that disappeared between loads. Untouched + // routers stay. + for (const stale of fileRoutersBefore) { + if (!seenThisRun.has(stale)) map.delete(stale); + } } export function getRouters(): LoadedRouter[] { diff --git a/scripts/install.mjs b/scripts/install.mjs index 3723d91..e402a5d 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -130,6 +130,24 @@ function copyDirRecursive(src, dest, excludeExts = []) { fs.copyFileSync(srcPath, destPath); } } + // openclaw 2026.5+ refuses to load plugins whose files are owned by + // non-root ("suspicious ownership"). fs.copyFileSync preserves source + // ownership, so rsync/tar from a developer laptop (uid 1000) breaks + // the plugin on next gateway restart. Force chown to root when we + // can (root-only); silently skip otherwise (dev mode under + // unprivileged user — uid checks don't apply there). + try { + if (process.getuid && process.getuid() === 0) chownRecursive(dest); + } catch {} +} + +function chownRecursive(dir) { + fs.chownSync(dir, 0, 0); + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) chownRecursive(p); + else fs.chownSync(p, 0, 0); + } } switch (action) {