Files
openclaw/src/cli/plugins-install-persist.ts
Peter Steinberger eee3aeae00 [codex] add Crestodian plugin management (#75869)
Summary:
- The branch adds ClawHub plugin search and Crestodian plugin list/search/install/uninstall flows, with docs, changelog, tests, runtime injection, and regenerated config baseline hashes.
- Reproducibility: not applicable. as a bug reproduction request. The high-confidence verification path is cur ... surface search plus exact-head diff/source inspection against the PR's targeted tests and queued CI checks.

ClawSweeper fixups:
- Included follow-up commit: Repair Crestodian plugin management config schema drift

Validation:
- ClawSweeper review passed for head c29cda6005.
- Required merge gates passed before the squash merge.

Prepared head SHA: c29cda6005
Review: https://github.com/openclaw/openclaw/pull/75869#issuecomment-4362360704

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-02 03:12:38 +00:00

154 lines
4.9 KiB
TypeScript

import { replaceConfigFile } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { type HookInstallUpdate, recordHookInstall } from "../hooks/installs.js";
import { enablePluginInConfig } from "../plugins/enable.js";
import {
loadInstalledPluginIndexInstallRecords,
recordPluginInstallInRecords,
withoutPluginInstallRecords,
} from "../plugins/installed-plugin-index-records.js";
import type { PluginInstallUpdate } from "../plugins/installs.js";
import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trace.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { theme } from "../terminal/theme.js";
import {
applySlotSelectionForPlugin,
enableInternalHookEntries,
logHookPackRestartHint,
logSlotWarnings,
} from "./plugins-command-helpers.js";
import { commitPluginInstallRecordsWithConfig } from "./plugins-install-record-commit.js";
import { refreshPluginRegistryAfterConfigMutation } from "./plugins-registry-refresh.js";
function addInstalledPluginToAllowlist(cfg: OpenClawConfig, pluginId: string): OpenClawConfig {
const allow = cfg.plugins?.allow;
if (!Array.isArray(allow) || allow.length === 0 || allow.includes(pluginId)) {
return cfg;
}
return {
...cfg,
plugins: {
...cfg.plugins,
allow: [...allow, pluginId].toSorted(),
},
};
}
function removeInstalledPluginFromDenylist(cfg: OpenClawConfig, pluginId: string): OpenClawConfig {
const deny = cfg.plugins?.deny;
if (!Array.isArray(deny) || !deny.includes(pluginId)) {
return cfg;
}
const nextDeny = deny.filter((id) => id !== pluginId);
const plugins = {
...cfg.plugins,
...(nextDeny.length > 0 ? { deny: nextDeny } : {}),
};
if (nextDeny.length === 0) {
delete plugins.deny;
}
return {
...cfg,
plugins,
};
}
export type ConfigSnapshotForInstallPersist = {
config: OpenClawConfig;
baseHash: string | undefined;
};
export async function persistPluginInstall(params: {
snapshot: ConfigSnapshotForInstallPersist;
pluginId: string;
install: Omit<PluginInstallUpdate, "pluginId">;
enable?: boolean;
successMessage?: string;
warningMessage?: string;
runtime?: RuntimeEnv;
}): Promise<OpenClawConfig> {
const runtime = params.runtime ?? defaultRuntime;
const installConfig =
params.enable === false
? params.snapshot.config
: removeInstalledPluginFromDenylist(
addInstalledPluginToAllowlist(params.snapshot.config, params.pluginId),
params.pluginId,
);
let next =
params.enable === false
? installConfig
: enablePluginInConfig(installConfig, params.pluginId, {
updateChannelConfig: false,
}).config;
const installRecords = await tracePluginLifecyclePhaseAsync(
"install records load",
() => loadInstalledPluginIndexInstallRecords(),
{ command: "install" },
);
const nextInstallRecords = recordPluginInstallInRecords(installRecords, {
pluginId: params.pluginId,
...params.install,
});
const slotResult =
params.enable === false
? { config: next, warnings: [] }
: await tracePluginLifecyclePhaseAsync(
"slot selection",
async () => applySlotSelectionForPlugin(next, params.pluginId),
{ command: "install", pluginId: params.pluginId },
);
next = withoutPluginInstallRecords(slotResult.config);
await tracePluginLifecyclePhaseAsync(
"config mutation",
() =>
commitPluginInstallRecordsWithConfig({
previousInstallRecords: installRecords,
nextInstallRecords,
nextConfig: next,
baseHash: params.snapshot.baseHash,
}),
{ command: "install" },
);
await refreshPluginRegistryAfterConfigMutation({
config: next,
reason: "source-changed",
installRecords: nextInstallRecords,
traceCommand: "install",
logger: {
warn: (message) => runtime.log(theme.warn(message)),
},
});
logSlotWarnings(slotResult.warnings, runtime);
if (params.warningMessage) {
runtime.log(theme.warn(params.warningMessage));
}
runtime.log(params.successMessage ?? `Installed plugin: ${params.pluginId}`);
runtime.log("Restart the gateway to load plugins.");
return next;
}
export async function persistHookPackInstall(params: {
snapshot: ConfigSnapshotForInstallPersist;
hookPackId: string;
hooks: string[];
install: Omit<HookInstallUpdate, "hookId" | "hooks">;
successMessage?: string;
runtime?: RuntimeEnv;
}): Promise<OpenClawConfig> {
const runtime = params.runtime ?? defaultRuntime;
let next = enableInternalHookEntries(params.snapshot.config, params.hooks);
next = recordHookInstall(next, {
hookId: params.hookPackId,
hooks: params.hooks,
...params.install,
});
await replaceConfigFile({
nextConfig: next,
baseHash: params.snapshot.baseHash,
});
runtime.log(params.successMessage ?? `Installed hook pack: ${params.hookPackId}`);
logHookPackRestartHint(runtime);
return next;
}