mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00:00
refactor: remove plugin install config fallback
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
3b9a8841973205560a5396e7a18d301852941a95a561900984ad618e69a99d05 config-baseline.json
|
||||
089ab9493c8482687f19da89d37e069fc402543696c92e6e3be86072c1e48c68 config-baseline.core.json
|
||||
f5236ba3f34837485d1e319262d4d73ecd46ea8890d3f4c26a069834f376b796 config-baseline.json
|
||||
484b36513ecb4a13cc945c3916fbe5ac712b5e0ab2c4ffa2dc811758da4ec7a6 config-baseline.core.json
|
||||
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
||||
17eb3f8887193579ff32e35f9bd520ba2bd6049e52ab18855c5d41fcbf195d83 config-baseline.plugin.json
|
||||
|
||||
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginCandidate } from "../plugins/discovery.js";
|
||||
import { readPersistedPluginInstallLedger } from "../plugins/install-ledger-store.js";
|
||||
import {
|
||||
readPersistedInstalledPluginIndex,
|
||||
writePersistedInstalledPluginIndex,
|
||||
@@ -68,7 +67,7 @@ function createCurrentIndex(): InstalledPluginIndex {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 2,
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [],
|
||||
@@ -77,30 +76,7 @@ function createCurrentIndex(): InstalledPluginIndex {
|
||||
}
|
||||
|
||||
describe("maybeRepairPluginRegistryState", () => {
|
||||
it("reports legacy config install records without mutating state outside repair mode", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const nextConfig = await maybeRepairPluginRegistryState({
|
||||
stateDir,
|
||||
env: hermeticEnv(),
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter: { shouldRepair: false },
|
||||
});
|
||||
|
||||
expect(nextConfig.plugins?.installs?.demo?.resolvedName).toBe("@vendor/demo");
|
||||
await expect(readPersistedPluginInstallLedger({ stateDir })).resolves.toBeNull();
|
||||
expect(vi.mocked(note).mock.calls.join("\n")).toContain("plugins.installs");
|
||||
});
|
||||
|
||||
it("moves legacy config install records into the ledger and refreshes an existing registry", async () => {
|
||||
it("refreshes an existing registry during repair", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
@@ -110,45 +86,22 @@ describe("maybeRepairPluginRegistryState", () => {
|
||||
stateDir,
|
||||
candidates: [createCandidate(pluginDir)],
|
||||
env: hermeticEnv(),
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {},
|
||||
prompter: { shouldRepair: true },
|
||||
});
|
||||
|
||||
expect(nextConfig.plugins?.installs).toBeUndefined();
|
||||
await expect(readPersistedPluginInstallLedger({ stateDir })).resolves.toMatchObject({
|
||||
records: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(nextConfig).toEqual({});
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||
refreshReason: "migration",
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
installRecord: expect.objectContaining({
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not mutate legacy install records when registry migration is disabled", async () => {
|
||||
it("does not repair when registry migration is disabled", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
|
||||
const nextConfig = await maybeRepairPluginRegistryState({
|
||||
@@ -156,21 +109,11 @@ describe("maybeRepairPluginRegistryState", () => {
|
||||
env: hermeticEnv({
|
||||
[DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV]: "1",
|
||||
}),
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {},
|
||||
prompter: { shouldRepair: true },
|
||||
});
|
||||
|
||||
expect(nextConfig.plugins?.installs?.demo?.resolvedName).toBe("@vendor/demo");
|
||||
await expect(readPersistedPluginInstallLedger({ stateDir })).resolves.toBeNull();
|
||||
expect(nextConfig).toEqual({});
|
||||
expect(vi.mocked(note).mock.calls.join("\n")).toContain(DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import {
|
||||
readPersistedPluginInstallLedger,
|
||||
resolvePluginInstallLedgerStorePath,
|
||||
withoutPluginInstallRecords,
|
||||
writePersistedPluginInstallLedger,
|
||||
type PluginInstallLedgerStoreOptions,
|
||||
} from "../plugins/install-ledger-store.js";
|
||||
import type { InstalledPluginIndexRecordStoreOptions } from "../plugins/installed-plugin-index-records.js";
|
||||
import { refreshPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
@@ -20,74 +13,11 @@ import {
|
||||
} from "./doctor/shared/plugin-registry-migration.js";
|
||||
|
||||
type PluginRegistryDoctorRepairParams = Omit<PluginRegistryInstallMigrationParams, "config"> &
|
||||
PluginInstallLedgerStoreOptions & {
|
||||
InstalledPluginIndexRecordStoreOptions & {
|
||||
config: OpenClawConfig;
|
||||
prompter: Pick<DoctorPrompter, "shouldRepair">;
|
||||
};
|
||||
|
||||
type LegacyInstallLedgerMigrationResult = {
|
||||
config: OpenClawConfig;
|
||||
migrated: boolean;
|
||||
};
|
||||
|
||||
function countRecords(records: Record<string, unknown> | undefined): number {
|
||||
return Object.keys(records ?? {}).length;
|
||||
}
|
||||
|
||||
function mergeInstallRecords(
|
||||
legacyRecords: Record<string, PluginInstallRecord>,
|
||||
ledgerRecords: Record<string, PluginInstallRecord> | undefined,
|
||||
): Record<string, PluginInstallRecord> {
|
||||
return {
|
||||
...legacyRecords,
|
||||
...ledgerRecords,
|
||||
};
|
||||
}
|
||||
|
||||
async function maybeMigrateLegacyInstallLedger(
|
||||
params: PluginRegistryDoctorRepairParams,
|
||||
): Promise<LegacyInstallLedgerMigrationResult> {
|
||||
const legacyRecords = params.config.plugins?.installs;
|
||||
const legacyCount = countRecords(legacyRecords);
|
||||
if (!legacyRecords || legacyCount === 0) {
|
||||
return {
|
||||
config: params.config,
|
||||
migrated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const ledgerPath = resolvePluginInstallLedgerStorePath(params);
|
||||
if (!params.prompter.shouldRepair) {
|
||||
note(
|
||||
[
|
||||
`Legacy plugin install records still live in config at \`plugins.installs\`.`,
|
||||
`Repair with ${formatCliCommand("openclaw doctor --fix")} to move them to ${shortenHomePath(ledgerPath)} and remove the config copy.`,
|
||||
].join("\n"),
|
||||
"Plugin registry",
|
||||
);
|
||||
return {
|
||||
config: params.config,
|
||||
migrated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const existingLedger = await readPersistedPluginInstallLedger(params);
|
||||
const nextRecords = mergeInstallRecords(legacyRecords, existingLedger?.records);
|
||||
await writePersistedPluginInstallLedger(nextRecords, params);
|
||||
const nextConfig = withoutPluginInstallRecords(params.config);
|
||||
note(
|
||||
[
|
||||
`Moved ${legacyCount} legacy plugin install record${legacyCount === 1 ? "" : "s"} from config to ${shortenHomePath(ledgerPath)}.`,
|
||||
"Removed the legacy `plugins.installs` config copy.",
|
||||
].join("\n"),
|
||||
"Plugin registry",
|
||||
);
|
||||
return {
|
||||
config: nextConfig,
|
||||
migrated: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function maybeRepairPluginRegistryState(
|
||||
params: PluginRegistryDoctorRepairParams,
|
||||
): Promise<OpenClawConfig> {
|
||||
@@ -103,13 +33,9 @@ export async function maybeRepairPluginRegistryState(
|
||||
return params.config;
|
||||
}
|
||||
|
||||
let nextConfig = params.config;
|
||||
const ledgerMigration = await maybeMigrateLegacyInstallLedger(params);
|
||||
nextConfig = ledgerMigration.config;
|
||||
|
||||
const migrationParams = {
|
||||
...params,
|
||||
config: nextConfig,
|
||||
config: params.config,
|
||||
};
|
||||
if (!params.prompter.shouldRepair) {
|
||||
if (preflight.action === "migrate") {
|
||||
@@ -121,7 +47,7 @@ export async function maybeRepairPluginRegistryState(
|
||||
"Plugin registry",
|
||||
);
|
||||
}
|
||||
return nextConfig;
|
||||
return params.config;
|
||||
}
|
||||
|
||||
if (preflight.action === "migrate") {
|
||||
@@ -134,10 +60,10 @@ export async function maybeRepairPluginRegistryState(
|
||||
"Plugin registry",
|
||||
);
|
||||
}
|
||||
return nextConfig;
|
||||
return params.config;
|
||||
}
|
||||
|
||||
if (ledgerMigration.migrated) {
|
||||
if (preflight.action === "skip-existing") {
|
||||
const index = await refreshPluginRegistry({
|
||||
...migrationParams,
|
||||
reason: "migration",
|
||||
@@ -150,5 +76,5 @@ export async function maybeRepairPluginRegistryState(
|
||||
);
|
||||
}
|
||||
|
||||
return nextConfig;
|
||||
return params.config;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function createCurrentIndex(): InstalledPluginIndex {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 2,
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [],
|
||||
@@ -83,7 +83,7 @@ function createCurrentIndex(): InstalledPluginIndex {
|
||||
describe("plugin registry install migration", () => {
|
||||
it("short-circuits when a current registry file already exists", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const filePath = path.join(stateDir, "plugins", "installed-index.json");
|
||||
const filePath = path.join(stateDir, "plugins", "installs.json");
|
||||
await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir });
|
||||
const readConfig = vi.fn(async () => ({}));
|
||||
|
||||
@@ -106,11 +106,11 @@ describe("plugin registry install migration", () => {
|
||||
|
||||
it("migrates when an existing registry file is not current", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const filePath = path.join(stateDir, "plugins", "installed-index.json");
|
||||
const filePath = path.join(stateDir, "plugins", "installs.json");
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify({ version: 1, migrationVersion: 1 }), "utf8");
|
||||
fs.writeFileSync(filePath, JSON.stringify({ version: 1, migrationVersion: 0 }), "utf8");
|
||||
|
||||
await expect(
|
||||
migratePluginRegistryForInstall({
|
||||
@@ -127,7 +127,7 @@ describe("plugin registry install migration", () => {
|
||||
});
|
||||
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||
migrationVersion: 2,
|
||||
migrationVersion: 1,
|
||||
plugins: [expect.objectContaining({ pluginId: "demo" })],
|
||||
});
|
||||
});
|
||||
@@ -202,10 +202,10 @@ describe("plugin registry install migration", () => {
|
||||
},
|
||||
});
|
||||
expect(readConfig).not.toHaveBeenCalled();
|
||||
expect(fs.existsSync(path.join(stateDir, "plugins", "installed-index.json"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(stateDir, "plugins", "installs.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("migrates missing registry state from legacy discovery and config inputs", async () => {
|
||||
it("builds missing registry state from discovered plugin manifests", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
@@ -215,17 +215,7 @@ describe("plugin registry install migration", () => {
|
||||
migratePluginRegistryForInstall({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
readConfig: async () => ({
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
readConfig: async () => ({}),
|
||||
env: hermeticEnv(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@@ -233,15 +223,10 @@ describe("plugin registry install migration", () => {
|
||||
migrated: true,
|
||||
current: {
|
||||
refreshReason: "migration",
|
||||
migrationVersion: 2,
|
||||
migrationVersion: 1,
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
installRecord: expect.objectContaining({
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import { normalizeProviderId } from "../../../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import {
|
||||
loadPluginInstallRecords,
|
||||
writePersistedPluginInstallLedger,
|
||||
} from "../../../plugins/install-ledger-store.js";
|
||||
import { loadInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js";
|
||||
import {
|
||||
inspectPersistedInstalledPluginIndex,
|
||||
readPersistedInstalledPluginIndexSync,
|
||||
@@ -256,10 +253,11 @@ export async function migratePluginRegistryForInstall(
|
||||
}
|
||||
|
||||
const config = await readMigrationConfig(params);
|
||||
const installRecords = await loadPluginInstallRecords({ ...params, config });
|
||||
const installRecords = await loadInstalledPluginIndexInstallRecords(params);
|
||||
const migrationParams = {
|
||||
...params,
|
||||
config,
|
||||
installRecords,
|
||||
};
|
||||
const inspection = await inspectPersistedInstalledPluginIndex(migrationParams);
|
||||
const candidateIndex = loadInstalledPluginIndex({
|
||||
@@ -275,9 +273,6 @@ export async function migratePluginRegistryForInstall(
|
||||
installRecords,
|
||||
}),
|
||||
};
|
||||
if (Object.keys(installRecords).length > 0) {
|
||||
await writePersistedPluginInstallLedger(installRecords, params);
|
||||
}
|
||||
await writePersistedInstalledPluginIndex(current, params);
|
||||
return {
|
||||
status: "migrated",
|
||||
|
||||
@@ -23023,164 +23023,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
description:
|
||||
"Per-plugin settings keyed by plugin ID including enablement and plugin-specific runtime configuration payloads. Use this for scoped plugin tuning without changing global loader policy.",
|
||||
},
|
||||
installs: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
anyOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
const: "npm",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "archive",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "path",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "clawhub",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "marketplace",
|
||||
},
|
||||
],
|
||||
title: "Plugin Install Source",
|
||||
description: 'Install source ("npm", "archive", or "path").',
|
||||
},
|
||||
spec: {
|
||||
type: "string",
|
||||
title: "Plugin Install Spec",
|
||||
description: "Original npm spec used for install (if source is npm).",
|
||||
},
|
||||
sourcePath: {
|
||||
type: "string",
|
||||
title: "Plugin Install Source Path",
|
||||
description: "Original archive/path used for install (if any).",
|
||||
},
|
||||
installPath: {
|
||||
type: "string",
|
||||
title: "Plugin Install Path",
|
||||
description: "Resolved install directory for the installed plugin bundle.",
|
||||
},
|
||||
version: {
|
||||
type: "string",
|
||||
title: "Plugin Install Version",
|
||||
description: "Version recorded at install time (if available).",
|
||||
},
|
||||
resolvedName: {
|
||||
type: "string",
|
||||
title: "Plugin Resolved Package Name",
|
||||
description: "Resolved npm package name from the fetched artifact.",
|
||||
},
|
||||
resolvedVersion: {
|
||||
type: "string",
|
||||
title: "Plugin Resolved Package Version",
|
||||
description:
|
||||
"Resolved npm package version from the fetched artifact (useful for non-pinned specs).",
|
||||
},
|
||||
resolvedSpec: {
|
||||
type: "string",
|
||||
title: "Plugin Resolved Package Spec",
|
||||
description:
|
||||
"Resolved exact npm spec (<name>@<version>) from the fetched artifact.",
|
||||
},
|
||||
integrity: {
|
||||
type: "string",
|
||||
title: "Plugin Resolved Integrity",
|
||||
description:
|
||||
"Resolved npm dist integrity hash for the fetched artifact (if reported by npm).",
|
||||
},
|
||||
shasum: {
|
||||
type: "string",
|
||||
title: "Plugin Resolved Shasum",
|
||||
description:
|
||||
"Resolved npm dist shasum for the fetched artifact (if reported by npm).",
|
||||
},
|
||||
resolvedAt: {
|
||||
type: "string",
|
||||
title: "Plugin Resolution Time",
|
||||
description:
|
||||
"ISO timestamp when npm package metadata was last resolved for this install record.",
|
||||
},
|
||||
installedAt: {
|
||||
type: "string",
|
||||
title: "Plugin Install Time",
|
||||
description: "ISO timestamp of last install/update.",
|
||||
},
|
||||
clawhubUrl: {
|
||||
type: "string",
|
||||
},
|
||||
clawhubPackage: {
|
||||
type: "string",
|
||||
},
|
||||
clawhubFamily: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
const: "code-plugin",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "bundle-plugin",
|
||||
},
|
||||
],
|
||||
},
|
||||
clawhubChannel: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
const: "official",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "community",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "private",
|
||||
},
|
||||
],
|
||||
},
|
||||
marketplaceName: {
|
||||
type: "string",
|
||||
title: "Plugin Marketplace Name",
|
||||
description:
|
||||
"Marketplace display name recorded for marketplace-backed plugin installs (if available).",
|
||||
},
|
||||
marketplaceSource: {
|
||||
type: "string",
|
||||
title: "Plugin Marketplace Source",
|
||||
description:
|
||||
"Original marketplace source used to resolve the install (for example a repo path or Git URL).",
|
||||
},
|
||||
marketplacePlugin: {
|
||||
type: "string",
|
||||
title: "Plugin Marketplace Plugin",
|
||||
description:
|
||||
"Plugin entry name inside the source marketplace, used for later updates.",
|
||||
},
|
||||
},
|
||||
required: ["source"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
title: "Plugin Install Records",
|
||||
description:
|
||||
"Deprecated compatibility fallback for legacy CLI-managed install metadata. New plugin installs use the state-managed `plugins/installs.json` ledger.",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
title: "Plugins",
|
||||
@@ -27687,86 +27529,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: "Plugin-defined configuration payload interpreted by that plugin's own schema and validation rules. Use only documented fields from the plugin to prevent ignored or invalid settings.",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs": {
|
||||
label: "Plugin Install Records",
|
||||
help: "Deprecated compatibility fallback for legacy CLI-managed install metadata. New plugin installs use the state-managed `plugins/installs.json` ledger.",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.source": {
|
||||
label: "Plugin Install Source",
|
||||
help: 'Install source ("npm", "archive", or "path").',
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.spec": {
|
||||
label: "Plugin Install Spec",
|
||||
help: "Original npm spec used for install (if source is npm).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.sourcePath": {
|
||||
label: "Plugin Install Source Path",
|
||||
help: "Original archive/path used for install (if any).",
|
||||
tags: ["storage"],
|
||||
},
|
||||
"plugins.installs.*.installPath": {
|
||||
label: "Plugin Install Path",
|
||||
help: "Resolved install directory for the installed plugin bundle.",
|
||||
tags: ["storage"],
|
||||
},
|
||||
"plugins.installs.*.version": {
|
||||
label: "Plugin Install Version",
|
||||
help: "Version recorded at install time (if available).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.resolvedName": {
|
||||
label: "Plugin Resolved Package Name",
|
||||
help: "Resolved npm package name from the fetched artifact.",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.resolvedVersion": {
|
||||
label: "Plugin Resolved Package Version",
|
||||
help: "Resolved npm package version from the fetched artifact (useful for non-pinned specs).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.resolvedSpec": {
|
||||
label: "Plugin Resolved Package Spec",
|
||||
help: "Resolved exact npm spec (<name>@<version>) from the fetched artifact.",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.integrity": {
|
||||
label: "Plugin Resolved Integrity",
|
||||
help: "Resolved npm dist integrity hash for the fetched artifact (if reported by npm).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.shasum": {
|
||||
label: "Plugin Resolved Shasum",
|
||||
help: "Resolved npm dist shasum for the fetched artifact (if reported by npm).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.resolvedAt": {
|
||||
label: "Plugin Resolution Time",
|
||||
help: "ISO timestamp when npm package metadata was last resolved for this install record.",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.installedAt": {
|
||||
label: "Plugin Install Time",
|
||||
help: "ISO timestamp of last install/update.",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.marketplaceName": {
|
||||
label: "Plugin Marketplace Name",
|
||||
help: "Marketplace display name recorded for marketplace-backed plugin installs (if available).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.marketplaceSource": {
|
||||
label: "Plugin Marketplace Source",
|
||||
help: "Original marketplace source used to resolve the install (for example a repo path or Git URL).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"plugins.installs.*.marketplacePlugin": {
|
||||
label: "Plugin Marketplace Plugin",
|
||||
help: "Plugin entry name inside the source marketplace, used for later updates.",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"models.providers.*.headers.*": {
|
||||
sensitive: true,
|
||||
tags: ["security", "models"],
|
||||
|
||||
@@ -360,7 +360,6 @@ const TARGET_KEYS = [
|
||||
"plugins.entries.*.apiKey",
|
||||
"plugins.entries.*.env",
|
||||
"plugins.entries.*.config",
|
||||
"plugins.installs",
|
||||
"auth",
|
||||
"auth.cooldowns",
|
||||
"models",
|
||||
|
||||
@@ -1170,31 +1170,6 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Per-plugin environment variable map injected for that plugin runtime context only. Use this to scope provider credentials to one plugin instead of sharing global process environment.",
|
||||
"plugins.entries.*.config":
|
||||
"Plugin-defined configuration payload interpreted by that plugin's own schema and validation rules. Use only documented fields from the plugin to prevent ignored or invalid settings.",
|
||||
"plugins.installs":
|
||||
"Deprecated compatibility fallback for legacy CLI-managed install metadata. New plugin installs use the state-managed `plugins/installs.json` ledger.",
|
||||
"plugins.installs.*.source": 'Install source ("npm", "archive", or "path").',
|
||||
"plugins.installs.*.spec": "Original npm spec used for install (if source is npm).",
|
||||
"plugins.installs.*.sourcePath": "Original archive/path used for install (if any).",
|
||||
"plugins.installs.*.installPath": "Resolved install directory for the installed plugin bundle.",
|
||||
"plugins.installs.*.version": "Version recorded at install time (if available).",
|
||||
"plugins.installs.*.resolvedName": "Resolved npm package name from the fetched artifact.",
|
||||
"plugins.installs.*.resolvedVersion":
|
||||
"Resolved npm package version from the fetched artifact (useful for non-pinned specs).",
|
||||
"plugins.installs.*.resolvedSpec":
|
||||
"Resolved exact npm spec (<name>@<version>) from the fetched artifact.",
|
||||
"plugins.installs.*.integrity":
|
||||
"Resolved npm dist integrity hash for the fetched artifact (if reported by npm).",
|
||||
"plugins.installs.*.shasum":
|
||||
"Resolved npm dist shasum for the fetched artifact (if reported by npm).",
|
||||
"plugins.installs.*.resolvedAt":
|
||||
"ISO timestamp when npm package metadata was last resolved for this install record.",
|
||||
"plugins.installs.*.installedAt": "ISO timestamp of last install/update.",
|
||||
"plugins.installs.*.marketplaceName":
|
||||
"Marketplace display name recorded for marketplace-backed plugin installs (if available).",
|
||||
"plugins.installs.*.marketplaceSource":
|
||||
"Original marketplace source used to resolve the install (for example a repo path or Git URL).",
|
||||
"plugins.installs.*.marketplacePlugin":
|
||||
"Plugin entry name inside the source marketplace, used for later updates.",
|
||||
"agents.list.*.identity.avatar":
|
||||
"Agent avatar (workspace-relative path, http(s) URL, or data URI).",
|
||||
"agents.defaults.model.primary": "Primary model (provider/model).",
|
||||
|
||||
@@ -871,20 +871,4 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"plugins.entries.*.apiKey": "Plugin API Key", // pragma: allowlist secret
|
||||
"plugins.entries.*.env": "Plugin Environment Variables",
|
||||
"plugins.entries.*.config": "Plugin Config",
|
||||
"plugins.installs": "Plugin Install Records",
|
||||
"plugins.installs.*.source": "Plugin Install Source",
|
||||
"plugins.installs.*.spec": "Plugin Install Spec",
|
||||
"plugins.installs.*.sourcePath": "Plugin Install Source Path",
|
||||
"plugins.installs.*.installPath": "Plugin Install Path",
|
||||
"plugins.installs.*.version": "Plugin Install Version",
|
||||
"plugins.installs.*.resolvedName": "Plugin Resolved Package Name",
|
||||
"plugins.installs.*.resolvedVersion": "Plugin Resolved Package Version",
|
||||
"plugins.installs.*.resolvedSpec": "Plugin Resolved Package Spec",
|
||||
"plugins.installs.*.integrity": "Plugin Resolved Integrity",
|
||||
"plugins.installs.*.shasum": "Plugin Resolved Shasum",
|
||||
"plugins.installs.*.resolvedAt": "Plugin Resolution Time",
|
||||
"plugins.installs.*.installedAt": "Plugin Install Time",
|
||||
"plugins.installs.*.marketplaceName": "Plugin Marketplace Name",
|
||||
"plugins.installs.*.marketplaceSource": "Plugin Marketplace Source",
|
||||
"plugins.installs.*.marketplacePlugin": "Plugin Marketplace Plugin",
|
||||
};
|
||||
|
||||
@@ -50,6 +50,11 @@ export type PluginsConfig = {
|
||||
load?: PluginsLoadConfig;
|
||||
slots?: PluginSlotsConfig;
|
||||
entries?: Record<string, PluginEntryConfig>;
|
||||
/**
|
||||
* Internal transient carrier for plugin install records during command flows.
|
||||
* This is intentionally omitted from the config schema and must not be
|
||||
* persisted to openclaw.json.
|
||||
*/
|
||||
installs?: Record<string, PluginInstallRecord>;
|
||||
};
|
||||
import type { InstallRecordBase } from "./types.installs.js";
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
SecretsConfigSchema,
|
||||
} from "./zod-schema.core.js";
|
||||
import { HookMappingSchema, HooksGmailSchema, InternalHooksSchema } from "./zod-schema.hooks.js";
|
||||
import { PluginInstallRecordShape } from "./zod-schema.installs.js";
|
||||
import { ChannelsSchema } from "./zod-schema.providers.js";
|
||||
import { sensitive } from "./zod-schema.sensitive.js";
|
||||
import {
|
||||
@@ -1001,16 +1000,6 @@ export const OpenClawSchema = z
|
||||
.strict()
|
||||
.optional(),
|
||||
entries: z.record(z.string(), PluginEntrySchema).optional(),
|
||||
installs: z
|
||||
.record(
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
...PluginInstallRecordShape,
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
|
||||
@@ -203,7 +203,7 @@ function matchRule(path: string): ReloadRule | null {
|
||||
|
||||
function isPluginInstallTimestampPath(path: string): boolean {
|
||||
// Legacy compatibility only: new plugin install metadata lives in the
|
||||
// managed install ledger, but old config writes may still touch this path.
|
||||
// managed plugin index, but old config writes may still touch this path.
|
||||
return /^plugins\.installs\..+\.(installedAt|resolvedAt)$/.test(path);
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ function getPluginInstallRecords(config: unknown): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
// Keep legacy config install records out of gateway restart decisions while
|
||||
// migration/doctor moves them into the managed plugin install ledger.
|
||||
// migration/doctor moves them into the managed plugin index install records.
|
||||
const installs = plugins.installs;
|
||||
return isPlainObject(installs) ? installs : {};
|
||||
}
|
||||
|
||||
@@ -223,26 +223,6 @@ export const PLUGIN_COMPAT_RECORDS = [
|
||||
diagnostics: ["persisted-registry-disabled"],
|
||||
tests: ["src/plugins/plugin-registry.test.ts"],
|
||||
},
|
||||
{
|
||||
code: "legacy-config-plugin-installs",
|
||||
status: "deprecated",
|
||||
owner: "config",
|
||||
introduced: "2026-04-25",
|
||||
deprecated: "2026-04-25",
|
||||
warningStarts: "2026-04-25",
|
||||
replacement: "state-managed `plugins/installs.json` plugin install ledger",
|
||||
docsPath: "/cli/plugins#install-ledger",
|
||||
surfaces: ["plugins.installs", "plugin install/update/uninstall", "plugin registry migration"],
|
||||
diagnostics: ["plugin install ledger compatibility"],
|
||||
tests: [
|
||||
"src/plugins/install-ledger-store.test.ts",
|
||||
"src/cli/plugins-install-persist.test.ts",
|
||||
"src/cli/plugins-cli.update.test.ts",
|
||||
"src/cli/plugins-cli.uninstall.test.ts",
|
||||
],
|
||||
releaseNote:
|
||||
"`plugins.installs` remains readable as a legacy compatibility fallback while new plugin install metadata moves to the state-managed install ledger.",
|
||||
},
|
||||
] as const satisfies readonly PluginCompatRecord[];
|
||||
|
||||
export type PluginCompatCode = (typeof PLUGIN_COMPAT_RECORDS)[number]["code"];
|
||||
|
||||
Reference in New Issue
Block a user