mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
fix(plugins): harden install ledger path handling
This commit is contained in:
@@ -29,8 +29,16 @@ cat > \"\$HOME/.openclaw/extensions/lossless-claw/package.json\" <<'JSON'
|
||||
JSON
|
||||
cat > \"\$HOME/.openclaw/openclaw.json\" <<'JSON'
|
||||
{
|
||||
\"plugins\": {
|
||||
\"installs\": {
|
||||
\"plugins\": {}
|
||||
}
|
||||
JSON
|
||||
mkdir -p \"\$HOME/.openclaw/plugins\"
|
||||
cat > \"\$HOME/.openclaw/plugins/installs.json\" <<'JSON'
|
||||
{
|
||||
\"version\": 1,
|
||||
\"warning\": \"DO NOT EDIT. This file is generated by OpenClaw plugin install/update/uninstall commands. Use `openclaw plugins install/update/uninstall` instead.\",
|
||||
\"updatedAtMs\": 1777118400000,
|
||||
\"records\": {
|
||||
\"lossless-claw\": {
|
||||
\"source\": \"npm\",
|
||||
\"spec\": \"@example/lossless-claw@0.9.0\",
|
||||
@@ -41,7 +49,6 @@ cat > \"\$HOME/.openclaw/openclaw.json\" <<'JSON'
|
||||
\"integrity\": \"sha512-same\",
|
||||
\"shasum\": \"same\"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
@@ -74,16 +74,30 @@ const config = fs.existsSync(configPath)
|
||||
const plugins = (config.plugins ??= {});
|
||||
const entries = (plugins.entries ??= {});
|
||||
entries[pluginId] = { ...(entries[pluginId] ?? {}), enabled };
|
||||
const installs = (plugins.installs ??= {});
|
||||
installs[pluginId] = {
|
||||
...(installs[pluginId] ?? {}),
|
||||
delete plugins.installs;
|
||||
plugins.allow = Array.from(new Set([...(plugins.allow ?? []), pluginId])).sort();
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
|
||||
const ledgerPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
||||
const ledger = fs.existsSync(ledgerPath)
|
||||
? JSON.parse(fs.readFileSync(ledgerPath, "utf8"))
|
||||
: {
|
||||
version: 1,
|
||||
warning:
|
||||
"DO NOT EDIT. This file is generated by OpenClaw plugin install/update/uninstall commands. Use `openclaw plugins install/update/uninstall` instead.",
|
||||
records: {},
|
||||
};
|
||||
ledger.updatedAtMs = Date.now();
|
||||
ledger.records ??= {};
|
||||
ledger.records[pluginId] = {
|
||||
...(ledger.records[pluginId] ?? {}),
|
||||
source: "path",
|
||||
installPath: pluginRoot,
|
||||
sourcePath: pluginRoot,
|
||||
};
|
||||
plugins.allow = Array.from(new Set([...(plugins.allow ?? []), pluginId])).sort();
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
||||
fs.writeFileSync(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
|
||||
NODE
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,25 @@ describe("json file helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces symlink targets instead of writing through them on Windows rename fallback", async () => {
|
||||
await withTempDir({ prefix: "openclaw-json-files-" }, async (base) => {
|
||||
const filePath = path.join(base, "state.json");
|
||||
const outsidePath = path.join(base, "outside.json");
|
||||
await fs.writeFile(outsidePath, "outside", "utf8");
|
||||
await fs.symlink(outsidePath, filePath);
|
||||
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
const renameError = Object.assign(new Error("EPERM"), { code: "EPERM" });
|
||||
vi.spyOn(fs, "rename").mockRejectedValueOnce(renameError);
|
||||
|
||||
await writeTextAtomic(filePath, "new");
|
||||
|
||||
await expect(fs.lstat(filePath)).resolves.toSatisfy((stat) => !stat.isSymbolicLink());
|
||||
await expect(fs.readFile(filePath, "utf8")).resolves.toBe("new");
|
||||
await expect(fs.readFile(outsidePath, "utf8")).resolves.toBe("outside");
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "serializes async lock callers even across rejections",
|
||||
|
||||
@@ -18,6 +18,13 @@ async function replaceFileWithWindowsFallback(tempPath: string, filePath: string
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await fs.lstat(filePath).catch(() => null);
|
||||
if (existing?.isSymbolicLink()) {
|
||||
await fs.rm(filePath, { force: true });
|
||||
await fs.rename(tempPath, filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.copyFile(tempPath, filePath);
|
||||
try {
|
||||
await fs.chmod(filePath, mode);
|
||||
|
||||
@@ -273,6 +273,7 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -410,6 +411,47 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("expands home-relative install paths before checking installed npm versions", async () => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-update-home-"));
|
||||
tempDirs.push(home);
|
||||
const installPath = path.join(home, ".openclaw", "extensions", "lossless-claw");
|
||||
fs.mkdirSync(installPath, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(installPath, "package.json"),
|
||||
JSON.stringify({ name: "@martian-engineering/lossless-claw", version: "0.9.0" }),
|
||||
);
|
||||
vi.stubEnv("HOME", home);
|
||||
mockNpmViewMetadata({
|
||||
name: "@martian-engineering/lossless-claw",
|
||||
version: "0.9.0",
|
||||
integrity: "sha512-same",
|
||||
shasum: "same",
|
||||
});
|
||||
installPluginFromNpmSpecMock.mockRejectedValue(new Error("installer should not run"));
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config: createNpmInstallConfig({
|
||||
pluginId: "lossless-claw",
|
||||
spec: "@martian-engineering/lossless-claw",
|
||||
installPath: "~/.openclaw/extensions/lossless-claw",
|
||||
resolvedName: "@martian-engineering/lossless-claw",
|
||||
resolvedSpec: "@martian-engineering/lossless-claw@0.9.0",
|
||||
integrity: "sha512-same",
|
||||
}),
|
||||
pluginIds: ["lossless-claw"],
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.outcomes).toEqual([
|
||||
expect.objectContaining({
|
||||
pluginId: "lossless-claw",
|
||||
status: "unchanged",
|
||||
currentVersion: "0.9.0",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls through to npm reinstall when the recorded integrity differs", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@martian-engineering/lossless-claw",
|
||||
|
||||
@@ -523,7 +523,9 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
|
||||
let installPath: string;
|
||||
try {
|
||||
installPath = record.installPath ?? resolvePluginInstallDir(pluginId);
|
||||
installPath = resolveUserPath(
|
||||
record.installPath?.trim() || resolvePluginInstallDir(pluginId),
|
||||
);
|
||||
} catch (err) {
|
||||
outcomes.push({
|
||||
pluginId,
|
||||
|
||||
Reference in New Issue
Block a user