From a57d681db9b237348c5d446c8d371b2c1057f016 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 26 Apr 2026 06:28:41 +0100 Subject: [PATCH] fix: keep plugin command status on cold index --- CHANGELOG.md | 2 ++ src/auto-reply/reply/commands-plugins.test.ts | 14 +++++++------- src/auto-reply/reply/commands-plugins.ts | 6 +++--- src/commands/doctor-workspace-status.test.ts | 11 ++++++----- src/commands/doctor-workspace-status.ts | 7 +++++-- .../shared/legacy-web-search-migrate.test.ts | 4 ++-- .../doctor/shared/legacy-web-search-migrate.ts | 10 +++++++--- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7fcc424fc..96027a254c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,8 @@ Docs: https://docs.openclaw.ai - Telegram: preserve exact selected quote text when sending native quote replies, and retry with legacy replies if Telegram rejects quote parameters. (#71952) Thanks @rubencu. - Plugins/CLI: preserve manifest name, description, format, and source metadata in cold `openclaw plugins list` output without importing plugin runtime. Thanks @shakkernerd. - Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd. +- Plugins/chat: keep `/plugins list`, `/plugins enable`, and `/plugins disable` on the persisted plugin index path so chat plugin management does not load diagnostic/runtime plugin registries before execution. Thanks @shakkernerd. +- Plugins/doctor: read workspace plugin status and legacy web-search ownership through installed-index manifest metadata instead of broad manifest registry scans. Thanks @shakkernerd. - Logging: redact configured secret patterns at console and file-log sink exits so credentials that reach the logger are masked before terminal display or JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan. diff --git a/src/auto-reply/reply/commands-plugins.test.ts b/src/auto-reply/reply/commands-plugins.test.ts index d32586bae61..829d1e19b94 100644 --- a/src/auto-reply/reply/commands-plugins.test.ts +++ b/src/auto-reply/reply/commands-plugins.test.ts @@ -6,7 +6,7 @@ import { buildPluginsCommandParams } from "./commands.test-harness.js"; const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn()); const validateConfigObjectWithPluginsMock = vi.hoisted(() => vi.fn()); const writeConfigFileMock = vi.hoisted(() => vi.fn(async () => undefined)); -const buildPluginSnapshotReportMock = vi.hoisted(() => vi.fn()); +const buildPluginRegistrySnapshotReportMock = vi.hoisted(() => vi.fn()); const buildPluginDiagnosticsReportMock = vi.hoisted(() => vi.fn()); const buildPluginInspectReportMock = vi.hoisted(() => vi.fn()); const buildAllPluginInspectReportsMock = vi.hoisted(() => vi.fn()); @@ -69,7 +69,7 @@ vi.mock("../../plugins/status.js", () => ({ buildAllPluginInspectReports: buildAllPluginInspectReportsMock, buildPluginDiagnosticsReport: buildPluginDiagnosticsReportMock, buildPluginInspectReport: buildPluginInspectReportMock, - buildPluginSnapshotReport: buildPluginSnapshotReportMock, + buildPluginRegistrySnapshotReport: buildPluginRegistrySnapshotReportMock, formatPluginCompatibilityNotice: formatPluginCompatibilityNoticeMock, })); @@ -121,7 +121,7 @@ describe("handlePluginsCommand", () => { config: buildCfg(), issues: [], }); - buildPluginSnapshotReportMock.mockReturnValue({ + buildPluginRegistrySnapshotReportMock.mockReturnValue({ workspaceDir: "/tmp/plugins-workspace", plugins: [ { @@ -260,8 +260,8 @@ describe("handlePluginsCommand", () => { ); }); - it("resolves write targets by runtime-derived plugin name", async () => { - buildPluginDiagnosticsReportMock.mockReturnValue({ + it("resolves write targets by indexed plugin name without loading diagnostics", async () => { + buildPluginRegistrySnapshotReportMock.mockReturnValue({ workspaceDir: "/tmp/plugins-workspace", plugins: [ { @@ -280,8 +280,8 @@ describe("handlePluginsCommand", () => { const result = await handlePluginsCommand(params, true); expect(result?.reply?.text).toContain('Plugin "superpowers" enabled'); - expect(buildPluginDiagnosticsReportMock).toHaveBeenCalled(); - expect(buildPluginSnapshotReportMock).not.toHaveBeenCalled(); + expect(buildPluginRegistrySnapshotReportMock).toHaveBeenCalled(); + expect(buildPluginDiagnosticsReportMock).not.toHaveBeenCalled(); }); it("returns an explicit unauthorized reply for native /plugins list", async () => { diff --git a/src/auto-reply/reply/commands-plugins.ts b/src/auto-reply/reply/commands-plugins.ts index 71eb8278247..e404d140e23 100644 --- a/src/auto-reply/reply/commands-plugins.ts +++ b/src/auto-reply/reply/commands-plugins.ts @@ -26,7 +26,7 @@ import { buildAllPluginInspectReports, buildPluginDiagnosticsReport, buildPluginInspectReport, - buildPluginSnapshotReport, + buildPluginRegistrySnapshotReport, formatPluginCompatibilityNotice, type PluginStatusReport, } from "../../plugins/status.js"; @@ -308,7 +308,7 @@ async function loadPluginCommandState( report: options?.loadModules === true ? buildPluginDiagnosticsReport({ config, workspaceDir }) - : buildPluginSnapshotReport({ config, workspaceDir }), + : buildPluginRegistrySnapshotReport({ config, workspaceDir }), }; } @@ -399,7 +399,7 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm } const loaded = await loadPluginCommandState(params.workspaceDir, { - loadModules: pluginsCommand.action !== "list", + loadModules: pluginsCommand.action === "inspect", }); if (!loaded.ok) { return { diff --git a/src/commands/doctor-workspace-status.test.ts b/src/commands/doctor-workspace-status.test.ts index 77ad5daefef..02b02b94263 100644 --- a/src/commands/doctor-workspace-status.test.ts +++ b/src/commands/doctor-workspace-status.test.ts @@ -11,7 +11,7 @@ const mocks = vi.hoisted(() => ({ resolveAgentWorkspaceDir: vi.fn(), resolveDefaultAgentId: vi.fn(), buildWorkspaceSkillStatus: vi.fn(), - buildPluginSnapshotReport: vi.fn(), + buildPluginRegistrySnapshotReport: vi.fn(), buildPluginCompatibilityWarnings: vi.fn(), listTaskFlowRecords: vi.fn<() => unknown[]>(() => []), listTasksForFlowId: vi.fn<(flowId: string) => unknown[]>((_flowId: string) => []), @@ -27,7 +27,8 @@ vi.mock("../agents/skills-status.js", () => ({ })); vi.mock("../plugins/status.js", () => ({ - buildPluginSnapshotReport: (...args: unknown[]) => mocks.buildPluginSnapshotReport(...args), + buildPluginRegistrySnapshotReport: (...args: unknown[]) => + mocks.buildPluginRegistrySnapshotReport(...args), buildPluginCompatibilityWarnings: (...args: unknown[]) => mocks.buildPluginCompatibilityWarnings(...args), })); @@ -53,7 +54,7 @@ async function runNoteWorkspaceStatusForTest( mocks.buildWorkspaceSkillStatus.mockReturnValue({ skills: [], }); - mocks.buildPluginSnapshotReport.mockReturnValue({ + mocks.buildPluginRegistrySnapshotReport.mockReturnValue({ workspaceDir: "/workspace", ...loadResult, }); @@ -85,7 +86,7 @@ describe("noteWorkspaceStatus", () => { }), ); try { - expect(mocks.buildPluginSnapshotReport).toHaveBeenCalledWith({ + expect(mocks.buildPluginRegistrySnapshotReport).toHaveBeenCalledWith({ config: {}, workspaceDir: "/workspace", }); @@ -183,7 +184,7 @@ describe("noteWorkspaceStatus", () => { "legacy-plugin still uses legacy before_agent_start", ]); try { - expect(mocks.buildPluginSnapshotReport).toHaveBeenCalledWith({ + expect(mocks.buildPluginRegistrySnapshotReport).toHaveBeenCalledWith({ config: {}, workspaceDir: "/workspace", }); diff --git a/src/commands/doctor-workspace-status.ts b/src/commands/doctor-workspace-status.ts index d3bd7595863..c8f98a7846c 100644 --- a/src/commands/doctor-workspace-status.ts +++ b/src/commands/doctor-workspace-status.ts @@ -2,7 +2,10 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent import { buildWorkspaceSkillStatus } from "../agents/skills-status.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { buildPluginCompatibilityWarnings, buildPluginSnapshotReport } from "../plugins/status.js"; +import { + buildPluginCompatibilityWarnings, + buildPluginRegistrySnapshotReport, +} from "../plugins/status.js"; import { listTasksForFlowId } from "../tasks/runtime-internal.js"; import { listTaskFlowRecords } from "../tasks/task-flow-runtime-internal.js"; import { note } from "../terminal/note.js"; @@ -69,7 +72,7 @@ export function noteWorkspaceStatus(cfg: OpenClawConfig) { "Skills status", ); - const pluginRegistry = buildPluginSnapshotReport({ + const pluginRegistry = buildPluginRegistrySnapshotReport({ config: cfg, workspaceDir, }); diff --git a/src/commands/doctor/shared/legacy-web-search-migrate.test.ts b/src/commands/doctor/shared/legacy-web-search-migrate.test.ts index 955a9c08853..52ca27a6b0b 100644 --- a/src/commands/doctor/shared/legacy-web-search-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-web-search-migrate.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; -vi.mock("../../../plugins/manifest-registry.js", () => ({ - loadPluginManifestRegistry: () => ({ +vi.mock("../../../plugins/plugin-registry.js", () => ({ + loadPluginManifestRegistryForPluginRegistry: () => ({ plugins: [ { id: "brave", diff --git a/src/commands/doctor/shared/legacy-web-search-migrate.ts b/src/commands/doctor/shared/legacy-web-search-migrate.ts index cbca3338e48..854464d65e4 100644 --- a/src/commands/doctor/shared/legacy-web-search-migrate.ts +++ b/src/commands/doctor/shared/legacy-web-search-migrate.ts @@ -1,7 +1,9 @@ import { mergeMissing } from "../../../config/legacy.shared.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; -import { loadPluginManifestRegistry } from "../../../plugins/manifest-registry.js"; -import { resolveManifestContractOwnerPluginId } from "../../../plugins/plugin-registry.js"; +import { + loadPluginManifestRegistryForPluginRegistry, + resolveManifestContractOwnerPluginId, +} from "../../../plugins/plugin-registry.js"; import { cloneRecord, ensureRecord, @@ -20,7 +22,9 @@ let legacyWebSearchProviderIdsCache: string[] | undefined; let legacyWebSearchProviderIdSetCache: Set | undefined; function getLegacyWebSearchProviderIds(): string[] { - legacyWebSearchProviderIdsCache ??= loadPluginManifestRegistry({ cache: true }) + legacyWebSearchProviderIdsCache ??= loadPluginManifestRegistryForPluginRegistry({ + includeDisabled: true, + }) .plugins.filter((plugin) => plugin.origin === "bundled") .flatMap((plugin) => plugin.contracts?.webSearchProviders ?? []) .filter((providerId) => !NON_MIGRATED_LEGACY_WEB_SEARCH_PROVIDER_IDS.has(providerId))