refactor: remove plugin install config fallback

This commit is contained in:
Shakker
2026-04-25 23:06:57 +01:00
parent c19f8a5223
commit d0dafd9dca
13 changed files with 35 additions and 492 deletions

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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;
}

View File

@@ -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",
}),
}),
],
},

View File

@@ -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",

View File

@@ -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"],

View File

@@ -360,7 +360,6 @@ const TARGET_KEYS = [
"plugins.entries.*.apiKey",
"plugins.entries.*.env",
"plugins.entries.*.config",
"plugins.installs",
"auth",
"auth.cooldowns",
"models",

View File

@@ -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).",

View File

@@ -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",
};

View File

@@ -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";

View File

@@ -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(),

View File

@@ -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 : {};
}

View File

@@ -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"];