fix(plugins): quiet official npm install scan warnings

This commit is contained in:
Vincent Koc
2026-05-04 02:40:08 -07:00
parent 33e19fb5ae
commit 54300e5270
7 changed files with 179 additions and 26 deletions

View File

@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
- fix: harden backend message action gateway routing [AI]. (#76374) Thanks @pgondhi987.
- Gate QQBot streaming command auth [AI]. (#76375) Thanks @pgondhi987.
- Plugins/install: suppress dangerous-pattern scanner warnings for trusted official OpenClaw npm installs, so installing `@openclaw/discord` no longer prints credential-harvesting warnings for the official package.
- Plugins/release: make the published npm runtime verifier reject blank `openclaw.runtimeExtensions` entries instead of treating them as absent and passing via inferred outputs. Thanks @vincentkoc.
- Plugins/security: ignore inline and block comments when matching source-rule context in plugin install scans, so comment-only `fetch`/`post` references near environment defaults do not block clean plugins. Thanks @vincentkoc.
- Doctor/plugins: remove stale managed install records for bundled plugins even when the bundled plugin is not explicitly configured, so doctor cleanup cannot leave orphaned install metadata behind. Thanks @vincentkoc.

View File

@@ -6,6 +6,7 @@ import {
resolveBundledInstallPlanBeforeNpm,
resolveBundledInstallPlanForNpmFailure,
resolveOfficialExternalInstallPlanBeforeNpm,
resolveOfficialExternalNpmPackageTrust,
} from "./plugin-install-plan.js";
describe("plugin install plan helpers", () => {
@@ -86,6 +87,36 @@ describe("plugin install plan helpers", () => {
expect(result).toBeNull();
});
it("trusts exact official external npm packages without remapping the spec", () => {
const findOfficialExternalPackage = vi.fn().mockReturnValue({
pluginId: "discord",
npmSpec: "@openclaw/discord",
});
const result = resolveOfficialExternalNpmPackageTrust({
npmSpec: "@openclaw/discord",
findOfficialExternalPackage,
});
expect(findOfficialExternalPackage).toHaveBeenCalledWith("@openclaw/discord");
expect(result).toEqual({
pluginId: "discord",
trustedSourceLinkedOfficialInstall: true,
});
});
it("does not trust npm package names outside the official external catalog", () => {
const findOfficialExternalPackage = vi.fn();
const result = resolveOfficialExternalNpmPackageTrust({
npmSpec: "brave",
findOfficialExternalPackage,
});
expect(findOfficialExternalPackage).toHaveBeenCalledWith("brave");
expect(result).toBeNull();
});
it("prefers bundled catalog plugin by id before npm spec", () => {
const findBundledSource = vi
.fn()

View File

@@ -1,3 +1,4 @@
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import type { BundledPluginSource } from "../plugins/bundled-sources.js";
import { PLUGIN_INSTALL_ERROR_CODE } from "../plugins/install.js";
import { shortenHomePath } from "../utils.js";
@@ -15,6 +16,14 @@ type OfficialExternalPluginLookup = (pluginId: string) =>
}
| undefined;
type OfficialExternalPackageLookup = (packageName: string) =>
| {
pluginId: string;
npmSpec?: string;
expectedIntegrity?: string;
}
| undefined;
function isBareNpmPackageName(spec: string): boolean {
const trimmed = spec.trim();
return /^[a-z0-9][a-z0-9-._~]*$/.test(trimmed);
@@ -92,6 +101,36 @@ export function resolveOfficialExternalInstallPlanBeforeNpm(params: {
};
}
export function resolveOfficialExternalNpmPackageTrust(params: {
npmSpec: string;
findOfficialExternalPackage: OfficialExternalPackageLookup;
}): {
pluginId: string;
expectedIntegrity?: string;
trustedSourceLinkedOfficialInstall: true;
} | null {
const parsed = parseRegistryNpmSpec(params.npmSpec);
if (!parsed) {
return null;
}
const entry = params.findOfficialExternalPackage(parsed.name);
if (!entry?.pluginId) {
return null;
}
const catalogSpec = entry.npmSpec?.trim();
const catalogPackageName = catalogSpec ? parseRegistryNpmSpec(catalogSpec)?.name : undefined;
if (catalogPackageName && catalogPackageName !== parsed.name) {
return null;
}
return {
pluginId: entry.pluginId,
...(entry.expectedIntegrity && catalogSpec === params.npmSpec.trim()
? { expectedIntegrity: entry.expectedIntegrity }
: {}),
trustedSourceLinkedOfficialInstall: true,
};
}
export function resolveBundledInstallPlanForNpmFailure(params: {
rawSpec: string;
code?: string;

View File

@@ -942,6 +942,56 @@ describe("plugins cli install", () => {
expect(installPluginFromClawHub).not.toHaveBeenCalled();
});
it("marks explicit official npm package installs as trusted", async () => {
const cfg = createEmptyPluginConfig();
const enabledCfg = createEnabledPluginConfig("discord");
loadConfig.mockReturnValue(cfg);
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("discord"));
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
recordPluginInstall.mockReturnValue(enabledCfg);
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "npm:@openclaw/discord"]);
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/discord",
expectedPluginId: "discord",
trustedSourceLinkedOfficialInstall: true,
}),
);
expect(installPluginFromClawHub).not.toHaveBeenCalled();
});
it("marks scoped official npm package installs as trusted", async () => {
const cfg = createEmptyPluginConfig();
const enabledCfg = createEnabledPluginConfig("discord");
loadConfig.mockReturnValue(cfg);
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("discord"));
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
recordPluginInstall.mockReturnValue(enabledCfg);
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "@openclaw/discord"]);
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/discord",
expectedPluginId: "discord",
trustedSourceLinkedOfficialInstall: true,
}),
);
expect(installPluginFromClawHub).not.toHaveBeenCalled();
});
it("passes the active profile extensions dir to npm installs", async () => {
const extensionsDir = useProfileExtensionsDir();
const cfg = createEmptyPluginConfig();

View File

@@ -22,6 +22,7 @@ import {
resolveMarketplaceInstallShortcut,
} from "../plugins/marketplace.js";
import {
getOfficialExternalPluginCatalogEntryForPackage,
getOfficialExternalPluginCatalogEntry,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
@@ -42,6 +43,7 @@ import {
resolveBundledInstallPlanBeforeNpm,
resolveBundledInstallPlanForNpmFailure,
resolveOfficialExternalInstallPlanBeforeNpm,
resolveOfficialExternalNpmPackageTrust,
} from "./plugin-install-plan.js";
import {
createHookPackInstallLogger,
@@ -62,6 +64,29 @@ function resolveInstallSafetyOverrides(overrides: InstallSafetyOverrides): Insta
};
}
function findTrustedOfficialExternalPackageInstall(packageName: string):
| {
pluginId: string;
npmSpec?: string;
expectedIntegrity?: string;
}
| undefined {
const entry = getOfficialExternalPluginCatalogEntryForPackage(packageName);
if (entry?.source !== "official") {
return undefined;
}
const pluginId = resolveOfficialExternalPluginId(entry);
if (!pluginId) {
return undefined;
}
const install = resolveOfficialExternalPluginInstall(entry);
return {
pluginId,
...(install?.npmSpec ? { npmSpec: install.npmSpec } : {}),
...(install?.expectedIntegrity ? { expectedIntegrity: install.expectedIntegrity } : {}),
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -693,6 +718,10 @@ export async function runPluginInstallCommand(params: {
runtime.error("unsupported npm: spec: missing package");
return runtime.exit(1);
}
const officialNpmTrust = resolveOfficialExternalNpmPackageTrust({
npmSpec: npmPrefixSpec,
findOfficialExternalPackage: findTrustedOfficialExternalPackageInstall,
});
const npmPrefixResult = await tryInstallPluginOrHookPackFromNpmSpec({
snapshot,
installMode,
@@ -701,6 +730,15 @@ export async function runPluginInstallCommand(params: {
safetyOverrides,
allowBundledFallback: false,
extensionsDir,
...(officialNpmTrust
? {
expectedPluginId: officialNpmTrust.pluginId,
...(officialNpmTrust.expectedIntegrity
? { expectedIntegrity: officialNpmTrust.expectedIntegrity }
: {}),
trustedSourceLinkedOfficialInstall: true,
}
: {}),
runtime,
});
if (!npmPrefixResult.ok) {
@@ -827,6 +865,10 @@ export async function runPluginInstallCommand(params: {
return;
}
const officialNpmTrust = resolveOfficialExternalNpmPackageTrust({
npmSpec: raw,
findOfficialExternalPackage: findTrustedOfficialExternalPackageInstall,
});
const npmResult = await tryInstallPluginOrHookPackFromNpmSpec({
snapshot,
installMode,
@@ -835,6 +877,15 @@ export async function runPluginInstallCommand(params: {
safetyOverrides,
allowBundledFallback: true,
extensionsDir,
...(officialNpmTrust
? {
expectedPluginId: officialNpmTrust.pluginId,
...(officialNpmTrust.expectedIntegrity
? { expectedIntegrity: officialNpmTrust.expectedIntegrity }
: {}),
trustedSourceLinkedOfficialInstall: true,
}
: {}),
runtime,
});
if (!npmResult.ok) {

View File

@@ -561,6 +561,7 @@ async function scanDirectoryTarget(params: {
includeFiles?: string[];
logger: InstallScanLogger;
path: string;
suppressBuiltinWarnings?: boolean;
suspiciousMessage: string;
targetName: string;
warningMessage: string;
@@ -571,6 +572,9 @@ async function scanDirectoryTarget(params: {
includeFiles: params.includeFiles,
});
const builtinScan = buildBuiltinScanFromSummary(scanSummary);
if (params.suppressBuiltinWarnings) {
return builtinScan;
}
if (scanSummary.critical > 0) {
params.logger.warn?.(
`${params.warningMessage}: ${buildCriticalDetails({ findings: scanSummary.findings })}`,
@@ -632,16 +636,6 @@ function logDangerousForceUnsafeInstall(params: {
);
}
function logTrustedSourceLinkedOfficialInstall(params: {
findings: Array<{ file: string; line: number; message: string; severity: string }>;
logger: InstallScanLogger;
targetLabel: string;
}) {
params.logger.warn?.(
`WARNING: ${params.targetLabel} allowed because it is an official OpenClaw package: ${buildCriticalDetails({ findings: params.findings })}`,
);
}
function resolveBuiltinScanDecision(
params: InstallSafetyOverrides & {
builtinScan: BuiltinInstallScan;
@@ -661,12 +655,6 @@ function resolveBuiltinScanDecision(
logger: params.logger,
targetLabel: params.targetLabel,
});
} else if (params.trustedSourceLinkedOfficialInstall && params.builtinScan.critical > 0) {
logTrustedSourceLinkedOfficialInstall({
findings: params.builtinScan.findings,
logger: params.logger,
targetLabel: params.targetLabel,
});
}
return builtinBlocked;
}
@@ -857,6 +845,7 @@ export async function scanPackageInstallSourceRuntime(
includeFiles: forcedScanEntries,
logger: params.logger,
path: params.packageDir,
suppressBuiltinWarnings: params.trustedSourceLinkedOfficialInstall === true,
suspiciousMessage: `Plugin "{target}" has {count} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
targetName: params.pluginId,
warningMessage: `WARNING: Plugin "${params.pluginId}" contains dangerous code patterns`,

View File

@@ -771,11 +771,7 @@ describe("installPluginFromArchive", () => {
});
expect(result.ok).toBe(true);
expect(
warnings.some((warning) =>
warning.includes("allowed because it is an official OpenClaw package"),
),
).toBe(true);
expect(warnings).toEqual([]);
});
it("installs flat-root plugin archives from ClawHub-style downloads", async () => {
@@ -1990,11 +1986,7 @@ describe("installPluginFromArchive", () => {
});
expect(result.ok).toBe(true);
expect(
warnings.some((warning) =>
warning.includes("allowed because it is an official OpenClaw package"),
),
).toBe(true);
expect(warnings).toEqual([]);
});
it("does not flag the real qa-matrix plugin as dangerous install code", async () => {