fix(plugins): surface configured runtime plugin doctor warnings (#81674)

Fixes #81326.

Summary:
- Warn from `openclaw plugins doctor` when configured runtime owner plugins are missing, disabled, or blocked.
- Share configured-runtime plugin install mapping with `openclaw doctor --fix`, including ACP/acpx.
- Keep implicit OpenAI Codex preferences quiet to avoid false-positive plugin doctor warnings.

Verification:
- `pnpm test src/agents/harness-runtimes.test.ts src/cli/plugins-cli.list.test.ts src/commands/doctor/shared/missing-configured-plugin-install.test.ts -- --reporter=verbose`
- `pnpm exec oxfmt --check CHANGELOG.md src/agents/harness-runtimes.ts src/agents/harness-runtimes.test.ts src/cli/plugins-cli.runtime.ts src/cli/plugins-cli.list.test.ts src/commands/doctor/shared/configured-runtime-plugin-installs.ts src/commands/doctor/shared/missing-configured-plugin-install.ts`
- `pnpm build:plugin-sdk:dts`
- `codex-review --mode branch`
- Testbox-through-Crabbox `pnpm check:changed`: provider `blacksmith-testbox`, id `tbx_01krt8vte22m7ht6wfss4jkeaa`, Actions run https://github.com/openclaw/openclaw/actions/runs/25983150787, exit 0

Co-authored-by: Zavian Wang <36817799+Zavianx@users.noreply.github.com>
This commit is contained in:
Zavian Wang
2026-05-17 14:13:55 +08:00
committed by GitHub
parent 826c2f4517
commit 9a11e76458
7 changed files with 483 additions and 36 deletions

View File

@@ -1,3 +1,7 @@
import {
collectConfiguredRuntimePluginIds,
resolveConfiguredRuntimePluginInstallCandidate,
} from "../commands/doctor/shared/configured-runtime-plugin-installs.js";
import {
assertConfigWriteAllowedInCurrentMode,
getRuntimeConfig,
@@ -67,6 +71,99 @@ function isErroredConfigSelectedShadowDiagnostic(params: {
);
}
function formatConfiguredRuntimePluginInstallSpec(params: {
clawhubSpec?: string;
defaultChoice?: string;
npmSpec?: string;
pluginId: string;
}): string {
const clawhubSpec = params.clawhubSpec?.trim();
const npmSpec = params.npmSpec?.trim();
if (clawhubSpec && params.defaultChoice !== "npm") {
return clawhubSpec;
}
return npmSpec ?? clawhubSpec ?? params.pluginId;
}
function pluginIdListIncludes(list: readonly string[] | undefined, pluginId: string): boolean {
return Array.isArray(list) && list.some((entry) => entry.trim() === pluginId);
}
function formatBlockedRuntimePluginGuidance(params: {
cfg: OpenClawConfig;
pluginId: string;
}): string | undefined {
const pluginId = params.pluginId;
const alternative =
pluginId === "acpx" ? "disable ACP/acpx in acp config" : 'change the runtime policy to "pi"';
if (params.cfg.plugins?.enabled === false) {
return `Enable plugin loading and the "${pluginId}" plugin, or ${alternative}.`;
}
if (pluginIdListIncludes(params.cfg.plugins?.deny, pluginId)) {
return `Remove "${pluginId}" from plugins.deny and enable the "${pluginId}" plugin, or ${alternative}.`;
}
if (params.cfg.plugins?.entries?.[pluginId]?.enabled === false) {
return `Set plugins.entries.${pluginId}.enabled=true or remove that disabled entry, or ${alternative}.`;
}
return undefined;
}
function formatDisabledRuntimePluginGuidance(params: {
cfg: OpenClawConfig;
pluginId: string;
}): string {
const allow = params.cfg.plugins?.allow;
const alternative =
params.pluginId === "acpx"
? "disable ACP/acpx in acp config"
: 'change the runtime policy to "pi"';
if (Array.isArray(allow) && allow.length > 0 && !allow.includes(params.pluginId)) {
return `Add "${params.pluginId}" to plugins.allow and enable the plugin, or ${alternative}.`;
}
return `Enable the "${params.pluginId}" plugin, or ${alternative}.`;
}
function collectConfiguredRuntimePluginWarnings(params: {
cfg: OpenClawConfig;
env: NodeJS.ProcessEnv;
plugins: readonly { enabled?: boolean; id: string; status?: string }[];
}): string[] {
const enabledPluginIds = new Set(
params.plugins
.filter((plugin) => plugin.enabled !== false && plugin.status !== "disabled")
.map((plugin) => plugin.id),
);
return collectConfiguredRuntimePluginIds(params.cfg, params.env, {
includeEnvRuntime: false,
includeImplicitRuntimePreferences: false,
includeLegacyAgentRuntimes: false,
}).flatMap((runtimeId) => {
const candidate = resolveConfiguredRuntimePluginInstallCandidate(runtimeId);
if (!candidate || enabledPluginIds.has(runtimeId)) {
return [];
}
const disabledPluginRecord = params.plugins.find((plugin) => plugin.id === runtimeId);
const blockedGuidance = formatBlockedRuntimePluginGuidance({
cfg: params.cfg,
pluginId: runtimeId,
});
if (blockedGuidance) {
return [
`- Configured runtime "${runtimeId}" requires the ${candidate.label} plugin, but "${runtimeId}" is blocked by plugin configuration. ${blockedGuidance}`,
];
}
if (disabledPluginRecord) {
return [
`- Configured runtime "${runtimeId}" requires the ${candidate.label} plugin, but "${runtimeId}" is disabled. ${formatDisabledRuntimePluginGuidance({ cfg: params.cfg, pluginId: runtimeId })}`,
];
}
const installSpec = formatConfiguredRuntimePluginInstallSpec(candidate);
return [
`- Configured runtime "${runtimeId}" requires the ${candidate.label} plugin, but no enabled "${runtimeId}" plugin was found. Run "openclaw doctor --fix" to install ${installSpec}, or install it manually with "openclaw plugins install ${installSpec}".`,
];
});
}
export async function runPluginsEnableCommand(id: string): Promise<void> {
assertConfigWriteAllowedInCurrentMode();
@@ -240,10 +337,16 @@ export async function runPluginsDoctorCommand(): Promise<void> {
doctorFixCommand: "openclaw doctor --fix",
autoRepairBlocked: isStalePluginAutoRepairBlocked(sourceCfg ?? cfg, process.env),
});
const configuredRuntimePluginWarnings = collectConfiguredRuntimePluginWarnings({
cfg: sourceCfg ?? cfg,
env: process.env,
plugins: report.plugins,
});
const hasInstallTreeIssues =
errors.length > 0 || diags.length > 0 || shadowed.length > 0 || compatibility.length > 0;
const pluginConfigWarnings = [...stalePluginConfigWarnings, ...configuredRuntimePluginWarnings];
if (!hasInstallTreeIssues && stalePluginConfigWarnings.length === 0) {
if (!hasInstallTreeIssues && pluginConfigWarnings.length === 0) {
defaultRuntime.log("No plugin issues detected.");
return;
}
@@ -301,14 +404,14 @@ export async function runPluginsDoctorCommand(): Promise<void> {
lines.push(`- ${formatPluginCompatibilityNotice(notice)} [${marker}]`);
}
}
if (stalePluginConfigWarnings.length > 0) {
if (pluginConfigWarnings.length > 0) {
if (lines.length > 0) {
lines.push("");
}
lines.push(theme.warn("Plugin configuration:"));
lines.push(...stalePluginConfigWarnings);
lines.push(...pluginConfigWarnings);
}
if (!hasInstallTreeIssues && stalePluginConfigWarnings.length > 0) {
if (!hasInstallTreeIssues && pluginConfigWarnings.length > 0) {
if (lines.length > 0) {
lines.push("");
}