fix: remove managed plugin files on uninstall

This commit is contained in:
Shakker
2026-04-26 04:15:56 +01:00
parent 48ba3a4198
commit 862b39976d
4 changed files with 51 additions and 6 deletions

View File

@@ -121,6 +121,7 @@ Docs: https://docs.openclaw.ai
- Image understanding: preserve configured provider-prefixed vision model metadata when callers request the model without the provider prefix, so custom image models keep their `input: ["text", "image"]` capability. Fixes #33185. Thanks @Kobe9312 and @vincentkoc.
- Plugins/install: restore the previous plugin index records if a concurrent config write conflict interrupts install, update, or uninstall metadata commits. Thanks @shakkernerd.
- Plugins/install: reject native plugin archives that do not include a valid `openclaw.plugin.json`, preventing manifestless archives from writing install records that later show missing-manifest diagnostics. Thanks @shakkernerd.
- Plugins/uninstall: remove tracked managed plugin install directories even when the persisted install path differs from the default id-derived target, while still refusing deletes outside the managed extensions root. Thanks @shakkernerd.
- Plugins/update: restore previous plugin index records if core update or channel setup hits a concurrent config write conflict after plugin metadata changes. Thanks @shakkernerd.
- Plugins/onboarding: defer channel/provider plugin install records until the owning config write commits, keeping setup failures from advancing the plugin index ahead of `openclaw.json`. Thanks @shakkernerd.
- Plugins/config: route configure and agent setup writes with pending plugin install records through the plugin index commit helper so provider onboarding metadata is not stripped by plain config writes. Thanks @shakkernerd.

View File

@@ -261,13 +261,10 @@ openclaw plugins uninstall <id> --keep-files
`uninstall` removes plugin records from `plugins.entries`, the persisted plugin
index, the plugin allowlist, and linked `plugins.load.paths` entries when
applicable.
applicable. Unless `--keep-files` is set, uninstall also removes the tracked
managed install directory when it is inside OpenClaw's plugin extensions root.
For active memory plugins, the memory slot resets to `memory-core`.
By default, uninstall also removes the plugin install directory under the active
state-dir plugin root. Use
`--keep-files` to keep files on disk.
`--keep-config` is supported as a deprecated alias for `--keep-files`.
### Update

View File

@@ -782,6 +782,25 @@ describe("uninstallPlugin", () => {
});
await expect(fs.access(outsideDir)).resolves.toBeUndefined();
});
it("deletes tracked managed install paths even when they are not the default target", async () => {
const extensionsDir = path.join(tempDir, "extensions");
const managedDir = path.join(extensionsDir, "archive-installs", "my-plugin");
await fs.mkdir(managedDir, { recursive: true });
await fs.writeFile(path.join(managedDir, "index.js"), "// plugin");
const result = await uninstallPlugin({
config: createSingleNpmInstallConfig(managedDir),
pluginId: "my-plugin",
deleteFiles: true,
extensionsDir,
});
expectSuccessfulUninstallActions(result, {
directory: true,
});
await expect(fs.access(managedDir)).rejects.toThrow();
});
});
describe("resolveUninstallDirectoryTarget", () => {
@@ -814,4 +833,22 @@ describe("resolveUninstallDirectoryTarget", () => {
expect(target).toBe(resolvePluginInstallDir("my-plugin", extensionsDir));
});
it("uses configured installPath when it stays inside the managed extensions dir", () => {
const extensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-safe");
const installPath = path.join(extensionsDir, "archive-installs", "my-plugin");
expect(
resolveUninstallDirectoryTarget({
pluginId: "my-plugin",
hasInstall: true,
installRecord: {
source: "archive",
sourcePath: "/tmp/my-plugin.zip",
installPath,
},
extensionsDir,
}),
).toBe(installPath);
});
});

View File

@@ -57,7 +57,12 @@ export function resolveUninstallDirectoryTarget(params: {
return configuredPath;
}
// Never trust configured installPath blindly for recursive deletes.
if (params.extensionsDir && isPathInsideOrEqual(params.extensionsDir, configuredPath)) {
return configuredPath;
}
// Never trust configured installPath blindly for recursive deletes outside
// the managed extensions directory.
return defaultPath;
}
@@ -101,6 +106,11 @@ function resolveComparablePath(value: string): string {
}
}
function isPathInsideOrEqual(parent: string, child: string): boolean {
const relative = path.relative(resolveComparablePath(parent), resolveComparablePath(child));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
/**
* Remove plugin references from config (pure config mutation).
* Returns a new config with the plugin removed from entries, installs, allow, load.paths, slots,