mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:30:57 +00:00
fix(plugins): harden bundled runtime dep staging
This commit is contained in:
@@ -183,6 +183,12 @@ function assertSafeInstalledDistPath(relativePath, params) {
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
function isStagedRuntimeNodeModulesPath(relativePath) {
|
||||
return /^dist\/extensions\/[^/]+\/node_modules(?:\/|$)/u.test(
|
||||
normalizeRelativePath(relativePath),
|
||||
);
|
||||
}
|
||||
|
||||
function listInstalledDistFiles(params = {}) {
|
||||
const readDir = params.readdirSync ?? readdirSync;
|
||||
const distRoot = resolveInstalledDistRoot(params);
|
||||
@@ -197,6 +203,10 @@ function listInstalledDistFiles(params = {}) {
|
||||
if (!currentDir) {
|
||||
continue;
|
||||
}
|
||||
const relativeCurrentDir = normalizeRelativePath(relative(packageRoot, currentDir));
|
||||
if (isStagedRuntimeNodeModulesPath(relativeCurrentDir)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readDir(currentDir, { withFileTypes: true })) {
|
||||
const entryPath = join(currentDir, entry.name);
|
||||
if (entry.isSymbolicLink()) {
|
||||
@@ -232,6 +242,10 @@ function pruneEmptyDistDirectories(params = {}) {
|
||||
const pathLstat = params.lstatSync ?? lstatSync;
|
||||
|
||||
function prune(currentDir) {
|
||||
const relativeCurrentDir = normalizeRelativePath(relative(packageRoot, currentDir));
|
||||
if (isStagedRuntimeNodeModulesPath(relativeCurrentDir)) {
|
||||
return;
|
||||
}
|
||||
for (const entry of readDir(currentDir, { withFileTypes: true })) {
|
||||
if (entry.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
createBundledRuntimeDepsInstallEnv,
|
||||
ensureBundledPluginRuntimeDeps,
|
||||
installBundledRuntimeDeps,
|
||||
isWritableDirectory,
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
resolveBundledRuntimeDepsNpmRunner,
|
||||
type BundledRuntimeDepsInstallParams,
|
||||
@@ -145,6 +146,21 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => {
|
||||
});
|
||||
|
||||
describe("installBundledRuntimeDeps", () => {
|
||||
it("uses a real write probe for runtime dependency roots", () => {
|
||||
const accessSpy = vi.spyOn(fs, "accessSync").mockImplementation(() => undefined);
|
||||
const mkdirSpy = vi.spyOn(fs, "mkdtempSync").mockImplementation(() => {
|
||||
const error = new Error("read-only file system") as NodeJS.ErrnoException;
|
||||
error.code = "EROFS";
|
||||
throw error;
|
||||
});
|
||||
|
||||
expect(isWritableDirectory("/usr/lib/node_modules/openclaw")).toBe(false);
|
||||
expect(accessSpy).not.toHaveBeenCalled();
|
||||
expect(mkdirSpy).toHaveBeenCalledWith(
|
||||
path.join("/usr/lib/node_modules/openclaw", ".openclaw-write-probe-"),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the npm cmd shim on Windows", () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
vi.spyOn(fs, "existsSync").mockImplementation(
|
||||
|
||||
@@ -263,12 +263,23 @@ function writeRetainedRuntimeDepsManifest(installRoot: string, specs: readonly s
|
||||
);
|
||||
}
|
||||
|
||||
function isWritableDirectory(dir: string): boolean {
|
||||
export function isWritableDirectory(dir: string): boolean {
|
||||
let probeDir: string | null = null;
|
||||
try {
|
||||
fs.accessSync(dir, fs.constants.W_OK);
|
||||
probeDir = fs.mkdtempSync(path.join(dir, ".openclaw-write-probe-"));
|
||||
fs.writeFileSync(path.join(probeDir, "probe"), "", "utf8");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
if (probeDir) {
|
||||
try {
|
||||
fs.rmSync(probeDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Best-effort cleanup. A failed cleanup should not turn a writable
|
||||
// probe into a hard runtime-dependency failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -436,6 +436,27 @@ describe("bundled plugin postinstall", () => {
|
||||
).toThrow("unsafe dist entry: dist/escape");
|
||||
});
|
||||
|
||||
it("ignores staged bundled plugin node_modules when pruning packaged dist", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-install-runtime-deps-");
|
||||
const staleFile = path.join(packageRoot, "dist", "stale-runtime.js");
|
||||
const packageJson = path.join(packageRoot, "dist", "extensions", "slack", "package.json");
|
||||
const binDir = path.join(packageRoot, "dist", "extensions", "slack", "node_modules", ".bin");
|
||||
await fs.mkdir(path.dirname(staleFile), { recursive: true });
|
||||
await fs.mkdir(path.dirname(packageJson), { recursive: true });
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await fs.writeFile(staleFile, "export {};\n");
|
||||
await fs.writeFile(packageJson, "{}\n");
|
||||
await fs.symlink("../fxparser/bin.js", path.join(binDir, "fxparser"));
|
||||
|
||||
expect(
|
||||
pruneInstalledPackageDist({
|
||||
packageRoot,
|
||||
expectedFiles: new Set(["dist/extensions/slack/package.json"]),
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).toEqual(["dist/stale-runtime.js"]);
|
||||
});
|
||||
|
||||
it("unlinks stale files instead of recursive pruning them", () => {
|
||||
const unlinkSync = vi.fn();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user