Files
openclaw/src/plugins/installed-plugin-index-records.ts
Vincent Koc 767e8280ac fix(cli): harden official plugin recovery (#93325)
* fix(cli): harden official plugin recovery

* fix(config): preserve include write context

* fix(config): reject external include mutations

* fix(config): bind snapshots to config paths

* fix(config): preserve write ownership

* fix(cli): preflight plugin config mutations

* chore(plugin-sdk): refresh api baseline

* test(config): prove install env policy mutations

* fix(cli): preflight plugin updates

* fix(cli): preflight non-npm id migrations

* chore(plugin-sdk): refresh api baseline

* fix(cli): satisfy plugin recovery checks
2026-06-15 23:07:29 +08:00

127 lines
4.4 KiB
TypeScript

/** Builds and compares installed plugin index records for refresh decisions. */
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import {
clearLoadInstalledPluginIndexInstallRecordsCache,
loadInstalledPluginIndexInstallRecords,
loadInstalledPluginIndexInstallRecordsSync,
readPersistedInstalledPluginIndexInstallRecords,
readPersistedInstalledPluginIndexInstallRecordsSync,
} from "./installed-plugin-index-record-reader.js";
import { resolveInstalledPluginIndexStorePath } from "./installed-plugin-index-store-path.js";
import {
refreshPersistedInstalledPluginIndex,
refreshPersistedInstalledPluginIndexSync,
} from "./installed-plugin-index-store.js";
import type { RefreshInstalledPluginIndexParams } from "./installed-plugin-index.js";
import { recordPluginInstall, type PluginInstallUpdate } from "./installs.js";
export {
clearLoadInstalledPluginIndexInstallRecordsCache,
loadInstalledPluginIndexInstallRecords,
loadInstalledPluginIndexInstallRecordsSync,
readPersistedInstalledPluginIndexInstallRecords,
readPersistedInstalledPluginIndexInstallRecordsSync,
};
/** Config path for legacy plugin install records kept for migration/doctor flows. */
export const PLUGIN_INSTALLS_CONFIG_PATH = ["plugins", "installs"] as const;
/** Options shared by installed plugin index record storage helpers. */
export type InstalledPluginIndexRecordStoreOptions = {
env?: NodeJS.ProcessEnv;
stateDir?: string;
filePath?: string;
};
type InstalledPluginIndexRecordRefreshOptions = InstalledPluginIndexRecordStoreOptions &
Partial<Omit<RefreshInstalledPluginIndexParams, "reason" | "installRecords">> & {
now?: () => Date;
};
/** Resolves the installed plugin index record store path. */
export function resolveInstalledPluginIndexRecordsStorePath(
options: InstalledPluginIndexRecordStoreOptions = {},
): string {
return resolveInstalledPluginIndexStorePath(options);
}
/** Refreshes persisted installed plugin index records asynchronously. */
export async function writePersistedInstalledPluginIndexInstallRecords(
records: Record<string, PluginInstallRecord>,
options: InstalledPluginIndexRecordRefreshOptions = {},
): Promise<string> {
await refreshPersistedInstalledPluginIndex({
...options,
reason: "source-changed",
installRecords: records,
});
return resolveInstalledPluginIndexRecordsStorePath(options);
}
/** Refreshes persisted installed plugin index records synchronously. */
export function writePersistedInstalledPluginIndexInstallRecordsSync(
records: Record<string, PluginInstallRecord>,
options: InstalledPluginIndexRecordRefreshOptions = {},
): string {
refreshPersistedInstalledPluginIndexSync({
...options,
reason: "source-changed",
installRecords: records,
});
return resolveInstalledPluginIndexRecordsStorePath(options);
}
/** Returns config with plugin install records attached at the canonical config path. */
export function withPluginInstallRecords(
config: OpenClawConfig,
records: Record<string, PluginInstallRecord>,
): OpenClawConfig {
return {
...config,
plugins: {
...config.plugins,
installs: records,
},
};
}
/** Returns config with legacy plugin install records removed. */
export function withoutPluginInstallRecords(
config: OpenClawConfig,
options: { preserveEmptyPlugins?: boolean } = {},
): OpenClawConfig {
if (!config.plugins?.installs) {
return config;
}
const { installs: _installs, ...plugins } = config.plugins;
if (Object.keys(plugins).length === 0) {
if (options.preserveEmptyPlugins) {
return { ...config, plugins: {} };
}
const { plugins: _plugins, ...rest } = config;
return rest;
}
return {
...config,
plugins,
};
}
/** Applies one install update to an in-memory install record map. */
export function recordPluginInstallInRecords(
records: Record<string, PluginInstallRecord>,
update: PluginInstallUpdate,
): Record<string, PluginInstallRecord> {
return recordPluginInstall({ plugins: { installs: records } }, update).plugins?.installs ?? {};
}
/** Removes one plugin install record from an in-memory record map. */
export function removePluginInstallRecordFromRecords(
records: Record<string, PluginInstallRecord>,
pluginId: string,
): Record<string, PluginInstallRecord> {
const { [pluginId]: _removed, ...rest } = records;
return rest;
}