mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(plugins): quiet official npm install scan warnings
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user