mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 04:31:41 +00:00
* fix(plugins): harden concurrent lifecycle mutations * fix(update): preserve post-core restart snapshot * fix(update): retain authoritative plugin records on resume * fix(update): reconcile plugin records after handoff
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import type { BundledPluginSource } from "./bundled-sources.js";
|
|
import {
|
|
persistPluginInstall,
|
|
type ConfigSnapshotForInstallPersist,
|
|
} from "./install-persistence.js";
|
|
import { validateJsonSchemaValue } from "./schema-validator.js";
|
|
|
|
type BundledPluginConfigEnablement =
|
|
| { mode: "ready" }
|
|
| { mode: "missing" }
|
|
| { mode: "invalid"; error: string };
|
|
|
|
function resolveBundledPluginConfigEnablement(params: {
|
|
bundledSource: BundledPluginSource;
|
|
existingEntry: unknown;
|
|
}): BundledPluginConfigEnablement {
|
|
if (!params.bundledSource.requiresConfig) {
|
|
return { mode: "ready" };
|
|
}
|
|
const entry = isRecord(params.existingEntry) ? params.existingEntry : undefined;
|
|
if (!entry || !Object.hasOwn(entry, "config")) {
|
|
return { mode: "missing" };
|
|
}
|
|
const config = entry.config;
|
|
if (!params.bundledSource.configSchema) {
|
|
return isRecord(config) && Object.keys(config).length > 0
|
|
? { mode: "ready" }
|
|
: { mode: "invalid", error: "config must be a non-empty object" };
|
|
}
|
|
const result = validateJsonSchemaValue({
|
|
schema: params.bundledSource.configSchema,
|
|
cacheKey: `bundled-install:${params.bundledSource.pluginId}`,
|
|
value: config,
|
|
applyDefaults: true,
|
|
});
|
|
return result.ok
|
|
? { mode: "ready" }
|
|
: { mode: "invalid", error: result.errors[0]?.text ?? "invalid plugin config" };
|
|
}
|
|
|
|
function prepareConfigForDisabledBundledInstall(
|
|
config: OpenClawConfig,
|
|
pluginId: string,
|
|
): OpenClawConfig {
|
|
const entry = config.plugins?.entries?.[pluginId];
|
|
const policy = isRecord(entry) ? { ...entry } : {};
|
|
delete policy.config;
|
|
return {
|
|
...config,
|
|
plugins: {
|
|
...config.plugins,
|
|
entries: {
|
|
...config.plugins?.entries,
|
|
[pluginId]: { ...policy, enabled: false },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function installBundledPluginSource(params: {
|
|
snapshot: ConfigSnapshotForInstallPersist;
|
|
rawSpec: string;
|
|
bundledSource: BundledPluginSource;
|
|
warning?: string;
|
|
invalidateRuntimeCache?: boolean;
|
|
runtime?: RuntimeEnv;
|
|
}): Promise<{ pluginId: string; warnings: string[] }> {
|
|
// Bundled plugins with required config are recorded but not enabled until config validates.
|
|
const existingEntry = params.snapshot.config.plugins?.entries?.[params.bundledSource.pluginId];
|
|
const configEnablement = resolveBundledPluginConfigEnablement({
|
|
bundledSource: params.bundledSource,
|
|
existingEntry,
|
|
});
|
|
if (configEnablement.mode === "invalid") {
|
|
throw new Error(
|
|
`Plugin "${params.bundledSource.pluginId}" has invalid configured settings: ${configEnablement.error}. Fix plugins.entries.${params.bundledSource.pluginId}.config, then rerun the install.`,
|
|
);
|
|
}
|
|
const shouldEnable = configEnablement.mode === "ready";
|
|
const configBase = shouldEnable
|
|
? params.snapshot.config
|
|
: prepareConfigForDisabledBundledInstall(params.snapshot.config, params.bundledSource.pluginId);
|
|
const configWarning = shouldEnable
|
|
? undefined
|
|
: `Installed bundled plugin "${params.bundledSource.pluginId}" without enabling it because it requires configuration first. Configure it, then run \`openclaw plugins enable ${params.bundledSource.pluginId}\`.`;
|
|
const warnings = [params.warning, configWarning].filter((warning): warning is string =>
|
|
Boolean(warning),
|
|
);
|
|
await persistPluginInstall({
|
|
snapshot: {
|
|
...params.snapshot,
|
|
config: configBase,
|
|
},
|
|
pluginId: params.bundledSource.pluginId,
|
|
install: {
|
|
source: "path",
|
|
spec: params.rawSpec,
|
|
sourcePath: params.bundledSource.localPath,
|
|
installPath: params.bundledSource.localPath,
|
|
},
|
|
enable: shouldEnable,
|
|
invalidateRuntimeCache: params.invalidateRuntimeCache,
|
|
...(warnings.length > 0 ? { warningMessage: warnings.join("\n") } : {}),
|
|
runtime: params.runtime,
|
|
});
|
|
return { pluginId: params.bundledSource.pluginId, warnings };
|
|
}
|