From 92c7400a54a35543381e09b5dc70542ba4ad83e8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 2 Aug 2026 03:59:24 -0700 Subject: [PATCH] perf(cli): finish cold read paths (#117957) * perf(cli): finish cold read paths * test(cli): satisfy cold path lint gates * test(cli): fix cold path fixture typing --- .../memory-core/src/cli-runtime-common.ts | 2 +- extensions/memory-core/src/cli.test.ts | 4 + ...old-command-plugin-imports.process.test.ts | 173 ++++++++++++++++++ src/cli/command-catalog.ts | 31 +++- src/cli/command-path-policy.test.ts | 33 ++++ src/cli/command-startup-policy.test.ts | 13 ++ src/cli/hooks-cli.process.test.ts | 8 +- src/cli/hooks-cli.ts | 12 +- src/cli/program/preaction.test-helpers.ts | 37 ++++ src/cli/program/preaction.test.ts | 6 + src/cli/run-main.exit.test.ts | 10 +- src/cli/run-main.ts | 17 +- src/plugins/cli.test.ts | 14 ++ src/plugins/cli.ts | 24 ++- 14 files changed, 360 insertions(+), 24 deletions(-) create mode 100644 src/cli/cold-command-plugin-imports.process.test.ts create mode 100644 src/cli/program/preaction.test-helpers.ts diff --git a/extensions/memory-core/src/cli-runtime-common.ts b/extensions/memory-core/src/cli-runtime-common.ts index 430149b8b1b..41f488f9258 100644 --- a/extensions/memory-core/src/cli-runtime-common.ts +++ b/extensions/memory-core/src/cli-runtime-common.ts @@ -55,7 +55,7 @@ async function loadMemoryCommandConfig( commandName: string, mode?: "enforce_resolved" | "read_only_status", ) { - const config = getRuntimeConfig(); + const config = getRuntimeConfig({ skipPluginValidation: true }); try { const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({ config, diff --git a/extensions/memory-core/src/cli.test.ts b/extensions/memory-core/src/cli.test.ts index 4388d486d3a..40e6c6214ad 100644 --- a/extensions/memory-core/src/cli.test.ts +++ b/extensions/memory-core/src/cli.test.ts @@ -502,6 +502,8 @@ describe("memory cli", () => { const log = spyRuntimeLogs(defaultRuntime); await runMemoryCli(["status"]); + expect(getRuntimeConfig).toHaveBeenCalledWith({ skipPluginValidation: true }); + expect(probeVectorAvailability).not.toHaveBeenCalled(); expectLogged(log, "Vector store: ready"); expectLogged(log, "Semantic vectors: ready"); @@ -1744,6 +1746,8 @@ describe("memory cli", () => { const writeJson = spyRuntimeJson(defaultRuntime); await runMemoryCli(["search", "hidden codeword", "--agent", "main", "--json"]); + expect(getRuntimeConfig).toHaveBeenCalledWith({ skipPluginValidation: true }); + expect(firstWrittenJsonArg(writeJson)).toEqual({ results: [], stale: true, diff --git a/src/cli/cold-command-plugin-imports.process.test.ts b/src/cli/cold-command-plugin-imports.process.test.ts new file mode 100644 index 00000000000..3b26b4ba1fb --- /dev/null +++ b/src/cli/cold-command-plugin-imports.process.test.ts @@ -0,0 +1,173 @@ +// Real CLI processes must keep unrelated plugin runtimes cold on read-only command paths. +import { execFile } from "node:child_process"; +import fs from "node:fs"; +import { createServer, type Server } from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { afterAll, beforeAll, expect, it } from "vitest"; +import { + createColdPluginFixture, + isColdPluginRuntimeLoaded, +} from "../plugins/test-helpers/cold-plugin-fixtures.js"; + +const execFileAsync = promisify(execFile); +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cold-commands-")); +let clawHubServer: Server; +let clawHubUrl: string; + +beforeAll(async () => { + clawHubServer = createServer((_request, response) => { + response.writeHead(200, { "content-type": "application/json" }); + response.end('{"results":[]}'); + }); + await new Promise((resolve) => { + clawHubServer.listen(0, "127.0.0.1", resolve); + }); + const address = clawHubServer.address(); + if (!address || typeof address === "string") { + throw new Error("failed to bind the ClawHub fixture server"); + } + clawHubUrl = `http://127.0.0.1:${address.port}`; +}); + +afterAll(async () => { + await new Promise((resolve, reject) => { + clawHubServer.close((error) => (error ? reject(error) : resolve())); + }); + fs.rmSync(tempRoot, { recursive: true, force: true }); +}); + +const cases = [ + { label: "hooks", args: ["hooks", "--json"], needsMemoryOwner: false }, + { + label: "skills-search", + args: ["skills", "search", "fixture", "--json"], + needsMemoryOwner: false, + }, + { + label: "skills-info", + args: ["skills", "info", "fixture-skill", "--json"], + needsMemoryOwner: false, + }, + { + label: "memory-status", + args: ["memory", "status", "--agent", "main", "--json"], + needsMemoryOwner: true, + }, + { + label: "memory-search", + args: ["memory", "search", "no-hit", "--agent", "main", "--json"], + needsMemoryOwner: true, + }, +] as const; + +it.each(cases)( + "keeps an unrelated plugin runtime cold for $label", + async ({ args, label, needsMemoryOwner }) => { + const root = path.join(tempRoot, label); + const stateDir = path.join(root, "state"); + const workspaceDir = path.join(root, "workspace"); + const pluginDir = path.join(root, "cold-plugin"); + const memoryOwnerDir = path.join(root, "memory-owner"); + fs.mkdirSync(path.join(workspaceDir, "skills", "fixture-skill"), { recursive: true }); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync( + path.join(workspaceDir, "skills", "fixture-skill", "SKILL.md"), + "---\nname: fixture-skill\ndescription: Cold path fixture\n---\n", + ); + const fixture = createColdPluginFixture({ + rootDir: pluginDir, + pluginId: `cold-${label}`, + runtimeMessage: `${label} imported an unrelated plugin runtime`, + manifest: { providers: [], channels: [], providerAuthChoices: [], channelConfigs: {} }, + }); + if (needsMemoryOwner) { + // Keep this fixture about the ownership boundary: the selected CLI owner must + // execute while the unrelated sentinel stays cold. memory-core behavior has + // its own focused CLI suite, including the stale-search result contract. + fs.mkdirSync(memoryOwnerDir, { recursive: true }); + fs.writeFileSync( + path.join(memoryOwnerDir, "package.json"), + JSON.stringify({ + name: "@example/openclaw-memory-owner", + version: "1.0.0", + type: "commonjs", + openclaw: { extensions: ["./index.cjs"] }, + }), + ); + fs.writeFileSync( + path.join(memoryOwnerDir, "openclaw.plugin.json"), + JSON.stringify({ + id: "memory-owner", + name: "Memory Owner", + configSchema: { type: "object", additionalProperties: false, properties: {} }, + commandAliases: [{ name: "fixture-memory", kind: "runtime-slash", cliCommand: "memory" }], + }), + ); + fs.writeFileSync( + path.join(memoryOwnerDir, "index.cjs"), + [ + 'const fs = require("node:fs");', + "module.exports = {", + ' id: "memory-owner",', + " register(api) {", + ' fs.writeFileSync(process.env.MEMORY_OWNER_MARKER, "loaded", "utf8");', + " api.registerCli(({ program }) => {", + ' const memory = program.command("memory");', + ' memory.command("status").option("--agent ").option("--json").action(() => console.log("[]"));', + ' memory.command("search").argument("").option("--agent ").option("--json").action(() => console.log("{\\\"results\\\":[]}"));', + ' }, { descriptors: [{ name: "memory", description: "Memory fixture", hasSubcommands: true }] });', + " },", + "};", + "", + ].join("\n"), + ); + } + fs.mkdirSync(stateDir, { recursive: true }); + const configPath = path.join(stateDir, "openclaw.json"); + fs.writeFileSync( + configPath, + JSON.stringify({ + agents: { defaults: { workspace: workspaceDir } }, + plugins: { + load: { paths: [pluginDir, ...(needsMemoryOwner ? [memoryOwnerDir] : [])] }, + entries: { + [fixture.pluginId]: { enabled: true }, + ...(needsMemoryOwner ? { "memory-owner": { enabled: true } } : {}), + }, + }, + }), + ); + + const result = await execFileAsync( + process.execPath, + ["--import", "tsx", "src/entry.ts", ...args], + { + cwd: path.resolve("."), + env: { + ...process.env, + NODE_ENV: undefined, + VITEST: undefined, + OPENCLAW_CLAWHUB_URL: clawHubUrl, + OPENCLAW_CONFIG_PATH: configPath, + MEMORY_OWNER_MARKER: path.join(root, "memory-owner-loaded"), + OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", + OPENCLAW_DEV_SOURCE_ROOT: path.resolve("."), + OPENCLAW_HOME: path.join(root, "home"), + OPENCLAW_STATE_DIR: stateDir, + }, + maxBuffer: 4 * 1024 * 1024, + timeout: 60_000, + }, + ); + expect(`${result.stdout}\n${result.stderr}`).not.toContain( + `${label} imported an unrelated plugin runtime`, + ); + expect(isColdPluginRuntimeLoaded(fixture)).toBe(false); + if (needsMemoryOwner) { + expect(fs.existsSync(path.join(root, "memory-owner-loaded"))).toBe(true); + } + }, + 150_000, +); diff --git a/src/cli/command-catalog.ts b/src/cli/command-catalog.ts index 4ff22928c68..10cd7c82b5c 100644 --- a/src/cli/command-catalog.ts +++ b/src/cli/command-catalog.ts @@ -383,6 +383,11 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [ { commandPath: ["exec-approvals"], policy: { networkProxy: "bypass" } }, { commandPath: ["exec-policy"], policy: { networkProxy: "bypass" } }, { commandPath: ["hooks"], policy: { networkProxy: "bypass" } }, + { + commandPath: ["hooks"], + exact: true, + policy: { configGuard: "skip", loadPlugins: "never", networkProxy: "bypass" }, + }, { commandPath: ["logs"], policy: { networkProxy: "bypass" } }, { commandPath: ["mcp"], policy: { networkProxy: "bypass" } }, { @@ -538,14 +543,36 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [ exact: true, policy: { configGuard: "skip", loadPlugins: "never", networkProxy: "bypass" }, }, - { commandPath: ["skills", "info"], exact: true, policy: { networkProxy: "bypass" } }, + { + commandPath: ["skills", "info"], + exact: true, + policy: { configGuard: "skip", loadPlugins: "never", networkProxy: "bypass" }, + }, { commandPath: ["skills", "install"], exact: true }, { commandPath: ["skills", "list"], exact: true, policy: { configGuard: "skip", loadPlugins: "never", networkProxy: "bypass" }, }, - { commandPath: ["skills", "search"], exact: true }, + { + commandPath: ["skills", "search"], + exact: true, + policy: { configGuard: "skip", loadPlugins: "never" }, + }, + { + commandPath: ["memory", "search"], + exact: true, + policy: { configGuard: "skip", loadPlugins: "never" }, + }, + { + commandPath: ["memory", "status"], + exact: true, + policy: { + configGuard: ({ argv }) => + hasFlag(argv, "--index") || hasFlag(argv, "--fix") ? "run" : "skip", + loadPlugins: "never", + }, + }, { commandPath: ["skills", "update"], exact: true }, { commandPath: ["skills", "verify"], exact: true }, ]; diff --git a/src/cli/command-path-policy.test.ts b/src/cli/command-path-policy.test.ts index feb5782be5d..1a4c1928351 100644 --- a/src/cli/command-path-policy.test.ts +++ b/src/cli/command-path-policy.test.ts @@ -350,6 +350,39 @@ describe("command-path-policy", () => { loadPlugins: "never", networkProxy: "bypass", }); + for (const commandPath of [["hooks"], ["skills", "info"]]) { + expectResolvedPolicy(commandPath, { + configGuard: "skip", + loadPlugins: "never", + networkProxy: "bypass", + }); + } + for (const commandPath of [ + ["skills", "search"], + ["memory", "search"], + ]) { + expectResolvedPolicy(commandPath, { + configGuard: "skip", + loadPlugins: "never", + }); + } + const memoryStatusPolicy = resolveCliCommandPathPolicy(["memory", "status"]); + expectConfigGuardResolver(memoryStatusPolicy); + expect(memoryStatusPolicy.loadPlugins).toBe("never"); + expect( + memoryStatusPolicy.configGuard({ + argv: ["node", "openclaw", "memory", "status"], + commandPath: ["memory", "status"], + }), + ).toBe("skip"); + for (const flag of ["--index", "--fix"]) { + expect( + memoryStatusPolicy.configGuard({ + argv: ["node", "openclaw", "memory", "status", flag], + commandPath: ["memory", "status"], + }), + ).toBe("run"); + } }); it("keeps routed and Commander config reads ahead of observing startup guards", () => { diff --git a/src/cli/command-startup-policy.test.ts b/src/cli/command-startup-policy.test.ts index bf8c3cb441f..dbdd2374a4c 100644 --- a/src/cli/command-startup-policy.test.ts +++ b/src/cli/command-startup-policy.test.ts @@ -39,6 +39,11 @@ describe("command-startup-policy", () => { ["skills"], ["skills", "list"], ["skills", "check"], + ["skills", "info"], + ["skills", "search"], + ["hooks"], + ["memory", "search"], + ["memory", "status"], ["gateway", "stability"], ["gateway", "usage-cost"], ]) { @@ -57,6 +62,14 @@ describe("command-startup-policy", () => { }).skipConfigGuard, ).toBe(false); expect(resolvePolicy({ commandPath: ["config", "set"] }).skipConfigGuard).toBe(false); + for (const flag of ["--index", "--fix"]) { + expect( + resolvePolicy({ + argv: ["node", "openclaw", "memory", "status", flag], + commandPath: ["memory", "status"], + }).skipConfigGuard, + ).toBe(false); + } }); it("keeps every route-first command on the same config guard declaration as Commander", () => { diff --git a/src/cli/hooks-cli.process.test.ts b/src/cli/hooks-cli.process.test.ts index 77352972dc7..d286a46e235 100644 --- a/src/cli/hooks-cli.process.test.ts +++ b/src/cli/hooks-cli.process.test.ts @@ -59,6 +59,7 @@ async function createLingeringPluginFixture(): Promise<{ JSON.stringify({ id: "linger", name: "Linger", + activation: { onCapabilities: ["hook"] }, configSchema: { type: "object", additionalProperties: false, properties: {} }, }), ); @@ -69,8 +70,9 @@ async function createLingeringPluginFixture(): Promise<{ "export default {", ' id: "linger",', ' name: "Linger",', - " register() {", + " register(api) {", ' fs.writeFileSync(process.env.LINGER_MARKER, "registered\\n");', + ' api.registerHook("command:new", () => {}, { name: "fixture-hook", description: "Fixture hook" });', " setInterval(() => {}, 60_000);", " },", "};", @@ -408,7 +410,9 @@ describe("hooks CLI process lifecycle", () => { expect(listResult, listResult.stderr).toMatchObject({ code: 0, signal: null }); expect(listResult.stderr).not.toContain("Error:"); - expect(JSON.parse(listResult.stdout)).toMatchObject({ hooks: expect.any(Array) }); + expect(JSON.parse(listResult.stdout)).toMatchObject({ + hooks: expect.arrayContaining([expect.objectContaining({ name: "fixture-hook" })]), + }); await expect(fs.readFile(fixture.markerPath, "utf8")).resolves.toBe("registered\n"); expect(relayResult, relayResult.stderr).toMatchObject({ code: 0, signal: null }); expect(JSON.parse(relayResult.stdout)).toMatchObject({ diff --git a/src/cli/hooks-cli.ts b/src/cli/hooks-cli.ts index 48115a60aa9..9d85894c328 100644 --- a/src/cli/hooks-cli.ts +++ b/src/cli/hooks-cli.ts @@ -18,6 +18,7 @@ import { resolveHookEntries } from "../hooks/policy.js"; import type { HookEntry } from "../hooks/types.js"; import { loadWorkspaceHookEntries } from "../hooks/workspace.js"; import { formatErrorMessage } from "../infra/errors.js"; +import { resolveGatewayStartupPluginIds } from "../plugins/channel-plugin-ids.js"; import { buildPluginDiagnosticsReport } from "../plugins/status.js"; import { defaultRuntime } from "../runtime.js"; import { shortenHomePath } from "../utils.js"; @@ -54,7 +55,14 @@ function buildHooksReport(config: OpenClawConfig): HookStatusReport { // Plugin-managed and workspace hooks share one resolved policy view for status/actions. const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config }); - const pluginReport = buildPluginDiagnosticsReport({ config, workspaceDir }); + // Native plugin hooks only exist after registration. Match the Gateway's startup + // plan so active hooks remain visible without executing unrelated installed plugins. + const onlyPluginIds = resolveGatewayStartupPluginIds({ + config, + workspaceDir, + env: process.env, + }); + const pluginReport = buildPluginDiagnosticsReport({ config, workspaceDir, onlyPluginIds }); const pluginEntries = pluginReport.hooks.map((hook) => hook.entry); const entries = mergeHookEntries(pluginEntries, workspaceEntries); return buildWorkspaceHookStatus(workspaceDir, { config, entries }); @@ -622,7 +630,7 @@ export function registerHooksCli(program: Command): void { hooks.action(async (opts: HooksListOptions) => runOneShotHooksCliAction(async () => { - const config = getRuntimeConfig(); + const config = getRuntimeConfig({ skipPluginValidation: true }); const report = buildHooksReport(config); const json = hasJsonOutput(opts); writeHooksOutput(formatHooksList(report, { ...opts, json }), json); diff --git a/src/cli/program/preaction.test-helpers.ts b/src/cli/program/preaction.test-helpers.ts new file mode 100644 index 00000000000..27747dce624 --- /dev/null +++ b/src/cli/program/preaction.test-helpers.ts @@ -0,0 +1,37 @@ +import type { Command } from "commander"; + +export const COLD_READ_COMMAND_PATHS: string[][] = [ + ["skills", "info"], + ["skills", "search"], + ["hooks"], + ["memory", "status"], + ["memory", "search"], +]; + +export function registerColdReadCommandFixtures(program: Command, skills: Command): void { + for (const skillCommand of ["info", "search"]) { + skills + .command(skillCommand) + .argument("[value]") + .option("--json") + .action(() => {}); + } + program + .command("hooks") + .option("--json") + .action(() => {}); + const memory = program.command("memory"); + memory + .command("status") + .option("--agent ") + .option("--index") + .option("--fix") + .option("--json") + .action(() => {}); + memory + .command("search") + .argument("[query]") + .option("--agent ") + .option("--json") + .action(() => {}); +} diff --git a/src/cli/program/preaction.test.ts b/src/cli/program/preaction.test.ts index ac3292bba05..06674348398 100644 --- a/src/cli/program/preaction.test.ts +++ b/src/cli/program/preaction.test.ts @@ -7,6 +7,10 @@ import { shouldMigrateStateFromPath } from "../argv.js"; import { isConfigSetJsonParseOnly } from "../config-output-mode.js"; import { setCommandJsonMode } from "./json-mode.js"; import { applyParentDefaultHelpAction } from "./parent-default-help.js"; +import { + COLD_READ_COMMAND_PATHS, + registerColdReadCommandFixtures, +} from "./preaction.test-helpers.js"; const DISCORD_REPO_INSTALL_SPEC = repoInstallSpec("discord"); @@ -228,6 +232,7 @@ describe("registerPreActionHooks", () => { .option("--json") .action(() => {}); } + registerColdReadCommandFixtures(programLocal, skills); for (const skillCommand of ["install", "verify"]) { skills .command(skillCommand) @@ -362,6 +367,7 @@ describe("registerPreActionHooks", () => { ["skills"], ["skills", "list"], ["skills", "check"], + ...COLD_READ_COMMAND_PATHS, ["agents", "bindings"], ["gateway", "stability"], ["gateway", "usage-cost"], diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index 9644ae37c09..1cdf21649a9 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -2419,7 +2419,11 @@ describe("runCli exit behavior", () => { await runCli(argv); expect(loadDotEnvMock).toHaveBeenCalledWith({ loadGlobalEnv: false, quiet: true }); - expect(loadConfigMock).toHaveBeenCalledWith({ isolateEnv: true, observe: false }); + expect(loadConfigMock).toHaveBeenCalledWith({ + isolateEnv: true, + observe: false, + skipPluginValidation: true, + }); expect(startProxyMock).toHaveBeenCalledWith(undefined); }); @@ -2723,7 +2727,7 @@ describe("runCli exit behavior", () => { expect.anything(), undefined, undefined, - { mode: "lazy", primary: "memory" }, + { mode: "lazy", primary: "memory", skipPluginValidation: true }, ); expect(stderrDuringPluginRegistration).toBe(true); expect(stderrDuringParse).toBe(true); @@ -2828,7 +2832,7 @@ describe("runCli exit behavior", () => { expect.anything(), undefined, undefined, - { mode: "lazy", primary: "memory" }, + { mode: "lazy", primary: "memory", skipPluginValidation: false }, ); expect(stderrDuringPluginRegistration).toBe(false); expect(loggingState.forceConsoleToStderr).toBe(false); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 8dc36c7238c..3be3c814732 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -1184,9 +1184,10 @@ async function runCliWithPreparedOutputMode( const readBestEffortCliConfig = async (): Promise => { if (!bestEffortConfigPromise) { bestEffortConfigPromise = import("../config/io.js").then(({ readBestEffortConfig }) => - readBestEffortConfig( - isolateProxyConfigEnv ? { isolateEnv: true, observe: false } : undefined, - ), + readBestEffortConfig({ + ...(isolateProxyConfigEnv ? { isolateEnv: true, observe: false } : {}), + skipPluginValidation: true, + }), ); } return await bestEffortConfigPromise; @@ -1542,11 +1543,17 @@ async function runCliWithPreparedOutputMode( }); if (!shouldSkipPluginRegistration) { const config = await startupTrace.measure("register-plugin-commands", async () => { - const { registerPluginCliCommandsFromValidatedConfig } = - await import("../plugins/cli.js"); + const [{ registerPluginCliCommandsFromValidatedConfig }, { resolveCliStartupPolicy }] = + await Promise.all([import("../plugins/cli.js"), import("./command-startup-policy.js")]); + const startupPolicy = resolveCliStartupPolicy({ + argv: parseArgv, + commandPath: invocation.commandPath, + jsonOutputMode: suppressStartupProgress, + }); return await registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, { mode: "lazy", primary, + skipPluginValidation: startupPolicy.skipConfigGuard, }); }); if (config) { diff --git a/src/plugins/cli.test.ts b/src/plugins/cli.test.ts index 117fb1a1a85..d11bcfe7cda 100644 --- a/src/plugins/cli.test.ts +++ b/src/plugins/cli.test.ts @@ -561,6 +561,20 @@ describe("registerPluginCliCommands", () => { expect(mocks.loadConfig).not.toHaveBeenCalled(); }); + it("skips unrelated plugin validation for cold plugin-owned CLI commands", async () => { + const snapshotConfig = { plugins: { enabled: true } } as OpenClawConfig; + mocks.readConfigFileSnapshot.mockResolvedValueOnce({ + valid: true, + config: {}, + runtimeConfig: snapshotConfig, + }); + + await expect( + loadValidatedConfigForPluginRegistration({ skipPluginValidation: true }), + ).resolves.toBe(snapshotConfig); + expect(mocks.readConfigFileSnapshot).toHaveBeenCalledWith({ skipPluginValidation: true }); + }); + it("preserves an already-active runtime config snapshot", async () => { const snapshotConfig = { plugins: { enabled: true } } as OpenClawConfig; const activeConfig = { plugins: { enabled: false } } as OpenClawConfig; diff --git a/src/plugins/cli.ts b/src/plugins/cli.ts index 052be7c60c4..cf4074cdc39 100644 --- a/src/plugins/cli.ts +++ b/src/plugins/cli.ts @@ -16,6 +16,7 @@ type PluginCliRegistrationMode = "eager" | "lazy"; type RegisterPluginCliOptions = { mode?: PluginCliRegistrationMode; primary?: string | null; + skipPluginValidation?: boolean; }; type PluginCliRegistrationEntries = Awaited< @@ -75,14 +76,17 @@ function loaderOptionsKey(loaderOptions: PluginCliLoaderOptions | undefined): st return String(id); } -export const loadValidatedConfigForPluginRegistration = - async (): Promise => { - const snapshot = await readConfigFileSnapshot(); - if (!snapshot.valid) { - return null; - } - return getRuntimeConfigSnapshot() ?? snapshot.runtimeConfig; - }; +export const loadValidatedConfigForPluginRegistration = async (options?: { + skipPluginValidation?: boolean; +}): Promise => { + const snapshot = await readConfigFileSnapshot({ + skipPluginValidation: options?.skipPluginValidation, + }); + if (!snapshot.valid) { + return null; + } + return getRuntimeConfigSnapshot() ?? snapshot.runtimeConfig; +}; export async function getPluginCliCommandDescriptors( cfg?: OpenClawConfig, @@ -136,7 +140,9 @@ export async function registerPluginCliCommandsFromValidatedConfig( loaderOptions?: PluginCliLoaderOptions, options?: RegisterPluginCliOptions, ): Promise { - const config = await loadValidatedConfigForPluginRegistration(); + const config = await loadValidatedConfigForPluginRegistration({ + skipPluginValidation: options?.skipPluginValidation, + }); if (!config) { return null; }