fix: repair bundled plugin dirs after npm install

This commit is contained in:
Peter Steinberger
2026-03-11 23:53:43 +00:00
parent b6d83749c8
commit e11be576fb
2 changed files with 52 additions and 2 deletions

View File

@@ -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 () => {

View File

@@ -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",