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) {