From e11be576fbbbbdeae405de2890dee2e1572e5c55 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 11 Mar 2026 23:53:43 +0000 Subject: [PATCH] fix: repair bundled plugin dirs after npm install --- src/plugins/discovery.test.ts | 29 +++++++++++++++++++++++++++++ src/plugins/discovery.ts | 25 +++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index aa33803c2ab..4c8933f1d1a 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -328,6 +328,35 @@ describe("discoverOpenClawPlugins", () => { ); }); + it.runIf(process.platform !== "win32")( + "repairs world-writable bundled plugin dirs before loading them", + async () => { + const stateDir = makeTempDir(); + const bundledDir = path.join(stateDir, "bundled"); + const packDir = path.join(bundledDir, "demo-pack"); + fs.mkdirSync(packDir, { recursive: true }); + fs.writeFileSync(path.join(packDir, "index.ts"), "export default function () {}", "utf-8"); + fs.chmodSync(packDir, 0o777); + + const result = await withEnvAsync( + { + OPENCLAW_STATE_DIR: stateDir, + CLAWDBOT_STATE_DIR: undefined, + OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir, + }, + async () => discoverOpenClawPlugins({}), + ); + + expect(result.candidates.some((candidate) => candidate.idHint === "demo-pack")).toBe(true); + expect( + result.diagnostics.some( + (diag) => diag.source === packDir && diag.message.includes("world-writable path"), + ), + ).toBe(false); + expect(fs.statSync(packDir).mode & 0o777).toBe(0o755); + }, + ); + it.runIf(process.platform !== "win32" && typeof process.getuid === "function")( "blocks suspicious ownership when uid mismatch is detected", async () => { diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index c03b0fe01bf..36d7d4792f4 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -153,7 +153,7 @@ function checkPathStatAndPermissions(params: { continue; } seen.add(normalized); - const stat = safeStatSync(targetPath); + let stat = safeStatSync(targetPath); if (!stat) { return { reason: "path_stat_failed", @@ -162,7 +162,28 @@ function checkPathStatAndPermissions(params: { targetPath, }; } - const modeBits = stat.mode & 0o777; + let modeBits = stat.mode & 0o777; + if ((modeBits & 0o002) !== 0 && params.origin === "bundled") { + // npm/global installs can create package-managed extension dirs without + // directory entries in the tarball, which may widen them to 0777. + // Tighten bundled dirs in place before applying the normal safety gate. + try { + fs.chmodSync(targetPath, modeBits & ~0o022); + const repairedStat = safeStatSync(targetPath); + if (!repairedStat) { + return { + reason: "path_stat_failed", + sourcePath: params.source, + rootPath: params.rootDir, + targetPath, + }; + } + stat = repairedStat; + modeBits = repairedStat.mode & 0o777; + } catch { + // Fall through to the normal block path below when repair is not possible. + } + } if ((modeBits & 0o002) !== 0) { return { reason: "path_world_writable",