mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 17:30:43 +00:00
fix: harden plugin install and uninstall transactions
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import { collectChannelDoctorStaleConfigMutations } from "../commands/doctor/shared/channel-doctor.js";
|
||||
import { loadConfig, readConfigFileSnapshot } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { readConfigFileSnapshot } from "../config/config.js";
|
||||
import { installHooksFromNpmSpec, installHooksFromPath } from "../hooks/install.js";
|
||||
import { resolveArchiveKind } from "../infra/archive.js";
|
||||
import { parseClawHubPluginSpec } from "../infra/clawhub.js";
|
||||
import { extractErrorCode, formatErrorMessage } from "../infra/errors.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { type BundledPluginSource, findBundledPluginSource } from "../plugins/bundled-sources.js";
|
||||
import { formatClawHubSpecifier, installPluginFromClawHub } from "../plugins/clawhub.js";
|
||||
import type { InstallSafetyOverrides } from "../plugins/install-security-scan.js";
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
formatPluginInstallWithHookFallbackError,
|
||||
} from "./plugins-command-helpers.js";
|
||||
import { persistHookPackInstall, persistPluginInstall } from "./plugins-install-persist.js";
|
||||
import type { ConfigSnapshotForInstallPersist } from "./plugins-install-persist.js";
|
||||
|
||||
function resolveInstallMode(force?: boolean): "install" | "update" {
|
||||
return force ? "update" : "install";
|
||||
@@ -53,23 +53,26 @@ function resolveInstallSafetyOverrides(overrides: InstallSafetyOverrides): Insta
|
||||
}
|
||||
|
||||
async function installBundledPluginSource(params: {
|
||||
config: OpenClawConfig;
|
||||
snapshot: ConfigSnapshotForInstallPersist;
|
||||
rawSpec: string;
|
||||
bundledSource: BundledPluginSource;
|
||||
warning: string;
|
||||
}) {
|
||||
const existing = params.config.plugins?.load?.paths ?? [];
|
||||
const existing = params.snapshot.config.plugins?.load?.paths ?? [];
|
||||
const mergedPaths = Array.from(new Set([...existing, params.bundledSource.localPath]));
|
||||
await persistPluginInstall({
|
||||
config: {
|
||||
...params.config,
|
||||
plugins: {
|
||||
...params.config.plugins,
|
||||
load: {
|
||||
...params.config.plugins?.load,
|
||||
paths: mergedPaths,
|
||||
snapshot: {
|
||||
config: {
|
||||
...params.snapshot.config,
|
||||
plugins: {
|
||||
...params.snapshot.config.plugins,
|
||||
load: {
|
||||
...params.snapshot.config.plugins?.load,
|
||||
paths: mergedPaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
baseHash: params.snapshot.baseHash,
|
||||
},
|
||||
pluginId: params.bundledSource.pluginId,
|
||||
install: {
|
||||
@@ -83,7 +86,7 @@ async function installBundledPluginSource(params: {
|
||||
}
|
||||
|
||||
async function tryInstallHookPackFromLocalPath(params: {
|
||||
config: OpenClawConfig;
|
||||
snapshot: ConfigSnapshotForInstallPersist;
|
||||
resolvedPath: string;
|
||||
installMode: "install" | "update";
|
||||
safetyOverrides?: InstallSafetyOverrides;
|
||||
@@ -107,22 +110,25 @@ async function tryInstallHookPackFromLocalPath(params: {
|
||||
return probe;
|
||||
}
|
||||
|
||||
const existing = params.config.hooks?.internal?.load?.extraDirs ?? [];
|
||||
const existing = params.snapshot.config.hooks?.internal?.load?.extraDirs ?? [];
|
||||
const merged = Array.from(new Set([...existing, params.resolvedPath]));
|
||||
await persistHookPackInstall({
|
||||
config: {
|
||||
...params.config,
|
||||
hooks: {
|
||||
...params.config.hooks,
|
||||
internal: {
|
||||
...params.config.hooks?.internal,
|
||||
enabled: true,
|
||||
load: {
|
||||
...params.config.hooks?.internal?.load,
|
||||
extraDirs: merged,
|
||||
snapshot: {
|
||||
config: {
|
||||
...params.snapshot.config,
|
||||
hooks: {
|
||||
...params.snapshot.config.hooks,
|
||||
internal: {
|
||||
...params.snapshot.config.hooks?.internal,
|
||||
enabled: true,
|
||||
load: {
|
||||
...params.snapshot.config.hooks?.internal?.load,
|
||||
extraDirs: merged,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseHash: params.snapshot.baseHash,
|
||||
},
|
||||
hookPackId: probe.hookPackId,
|
||||
hooks: probe.hooks,
|
||||
@@ -149,7 +155,7 @@ async function tryInstallHookPackFromLocalPath(params: {
|
||||
|
||||
const source: "archive" | "path" = resolveArchiveKind(params.resolvedPath) ? "archive" : "path";
|
||||
await persistHookPackInstall({
|
||||
config: params.config,
|
||||
snapshot: params.snapshot,
|
||||
hookPackId: result.hookPackId,
|
||||
hooks: result.hooks,
|
||||
install: {
|
||||
@@ -163,7 +169,7 @@ async function tryInstallHookPackFromLocalPath(params: {
|
||||
}
|
||||
|
||||
async function tryInstallHookPackFromNpmSpec(params: {
|
||||
config: OpenClawConfig;
|
||||
snapshot: ConfigSnapshotForInstallPersist;
|
||||
installMode: "install" | "update";
|
||||
spec: string;
|
||||
pin?: boolean;
|
||||
@@ -187,7 +193,7 @@ async function tryInstallHookPackFromNpmSpec(params: {
|
||||
theme.warn,
|
||||
);
|
||||
await persistHookPackInstall({
|
||||
config: params.config,
|
||||
snapshot: params.snapshot,
|
||||
hookPackId: result.hookPackId,
|
||||
hooks: result.hooks,
|
||||
install: installRecord,
|
||||
@@ -231,13 +237,13 @@ function buildInvalidPluginInstallConfigError(message: string): Error {
|
||||
|
||||
async function loadConfigFromSnapshotForInstall(
|
||||
request: PluginInstallRequestContext,
|
||||
): Promise<OpenClawConfig> {
|
||||
snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>,
|
||||
): Promise<ConfigSnapshotForInstallPersist> {
|
||||
if (resolvePluginInstallInvalidConfigPolicy(request) !== "allow-bundled-recovery") {
|
||||
throw buildInvalidPluginInstallConfigError(
|
||||
"Config invalid; run `openclaw doctor --fix` before installing plugins.",
|
||||
);
|
||||
}
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const parsed = (snapshot.parsed ?? {}) as Record<string, unknown>;
|
||||
if (!snapshot.exists || Object.keys(parsed).length === 0) {
|
||||
throw buildInvalidPluginInstallConfigError(
|
||||
@@ -260,20 +266,23 @@ async function loadConfigFromSnapshotForInstall(
|
||||
})) {
|
||||
nextConfig = mutation.config;
|
||||
}
|
||||
return nextConfig;
|
||||
return {
|
||||
config: nextConfig,
|
||||
baseHash: snapshot.hash,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadConfigForInstall(
|
||||
request: PluginInstallRequestContext,
|
||||
): Promise<OpenClawConfig> {
|
||||
try {
|
||||
return loadConfig();
|
||||
} catch (err) {
|
||||
if (extractErrorCode(err) !== "INVALID_CONFIG") {
|
||||
throw err;
|
||||
}
|
||||
): Promise<ConfigSnapshotForInstallPersist> {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.valid) {
|
||||
return {
|
||||
config: snapshot.sourceConfig,
|
||||
baseHash: snapshot.hash,
|
||||
};
|
||||
}
|
||||
return loadConfigFromSnapshotForInstall(request);
|
||||
return loadConfigFromSnapshotForInstall(request, snapshot);
|
||||
}
|
||||
|
||||
export async function runPluginInstallCommand(params: {
|
||||
@@ -322,13 +331,14 @@ export async function runPluginInstallCommand(params: {
|
||||
return defaultRuntime.exit(1);
|
||||
}
|
||||
const request = requestResolution.request;
|
||||
const cfg = await loadConfigForInstall(request).catch((error: unknown) => {
|
||||
const snapshot = await loadConfigForInstall(request).catch((error: unknown) => {
|
||||
defaultRuntime.error(formatErrorMessage(error));
|
||||
return null;
|
||||
});
|
||||
if (!cfg) {
|
||||
if (!snapshot) {
|
||||
return defaultRuntime.exit(1);
|
||||
}
|
||||
const cfg = snapshot.config;
|
||||
const installMode = resolveInstallMode(opts.force);
|
||||
const safetyOverrides = resolveInstallSafetyOverrides(opts);
|
||||
|
||||
@@ -347,7 +357,7 @@ export async function runPluginInstallCommand(params: {
|
||||
|
||||
clearPluginManifestRegistryCache();
|
||||
await persistPluginInstall({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
pluginId: result.pluginId,
|
||||
install: {
|
||||
source: "marketplace",
|
||||
@@ -381,7 +391,7 @@ export async function runPluginInstallCommand(params: {
|
||||
return defaultRuntime.exit(1);
|
||||
}
|
||||
const hookFallback = await tryInstallHookPackFromLocalPath({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
installMode,
|
||||
resolvedPath: resolved,
|
||||
safetyOverrides,
|
||||
@@ -397,15 +407,18 @@ export async function runPluginInstallCommand(params: {
|
||||
}
|
||||
|
||||
await persistPluginInstall({
|
||||
config: {
|
||||
...cfg,
|
||||
plugins: {
|
||||
...cfg.plugins,
|
||||
load: {
|
||||
...cfg.plugins?.load,
|
||||
paths: merged,
|
||||
snapshot: {
|
||||
config: {
|
||||
...cfg,
|
||||
plugins: {
|
||||
...cfg.plugins,
|
||||
load: {
|
||||
...cfg.plugins?.load,
|
||||
paths: merged,
|
||||
},
|
||||
},
|
||||
},
|
||||
baseHash: snapshot.baseHash,
|
||||
},
|
||||
pluginId: probe.pluginId,
|
||||
install: {
|
||||
@@ -431,7 +444,7 @@ export async function runPluginInstallCommand(params: {
|
||||
return defaultRuntime.exit(1);
|
||||
}
|
||||
const hookFallback = await tryInstallHookPackFromLocalPath({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
installMode,
|
||||
resolvedPath: resolved,
|
||||
safetyOverrides,
|
||||
@@ -448,7 +461,7 @@ export async function runPluginInstallCommand(params: {
|
||||
clearPluginManifestRegistryCache();
|
||||
const source: "archive" | "path" = resolveArchiveKind(resolved) ? "archive" : "path";
|
||||
await persistPluginInstall({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
pluginId: result.pluginId,
|
||||
install: {
|
||||
source,
|
||||
@@ -487,7 +500,7 @@ export async function runPluginInstallCommand(params: {
|
||||
});
|
||||
if (bundledPreNpmPlan) {
|
||||
await installBundledPluginSource({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
rawSpec: raw,
|
||||
bundledSource: bundledPreNpmPlan.bundledSource,
|
||||
warning: bundledPreNpmPlan.warning,
|
||||
@@ -510,7 +523,7 @@ export async function runPluginInstallCommand(params: {
|
||||
|
||||
clearPluginManifestRegistryCache();
|
||||
await persistPluginInstall({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
pluginId: result.pluginId,
|
||||
install: {
|
||||
source: "clawhub",
|
||||
@@ -542,7 +555,7 @@ export async function runPluginInstallCommand(params: {
|
||||
if (clawhubResult.ok) {
|
||||
clearPluginManifestRegistryCache();
|
||||
await persistPluginInstall({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
pluginId: clawhubResult.pluginId,
|
||||
install: {
|
||||
source: "clawhub",
|
||||
@@ -586,7 +599,7 @@ export async function runPluginInstallCommand(params: {
|
||||
});
|
||||
if (!bundledFallbackPlan) {
|
||||
const hookFallback = await tryInstallHookPackFromNpmSpec({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
installMode,
|
||||
spec: raw,
|
||||
pin: opts.pin,
|
||||
@@ -601,7 +614,7 @@ export async function runPluginInstallCommand(params: {
|
||||
}
|
||||
|
||||
await installBundledPluginSource({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
rawSpec: raw,
|
||||
bundledSource: bundledFallbackPlan.bundledSource,
|
||||
warning: bundledFallbackPlan.warning,
|
||||
@@ -620,7 +633,7 @@ export async function runPluginInstallCommand(params: {
|
||||
theme.warn,
|
||||
);
|
||||
await persistPluginInstall({
|
||||
config: cfg,
|
||||
snapshot,
|
||||
pluginId: result.pluginId,
|
||||
install: installRecord,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user