fix(plugins): trust chat catalog installs

This commit is contained in:
Vincent Koc
2026-05-04 13:46:11 -07:00
parent 06056926a0
commit 7b86481c94
3 changed files with 103 additions and 0 deletions

View File

@@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai
- Plugins/discovery: ignore managed npm plugin packages that only expose TypeScript source entries without compiled runtime output, so stale/broken installs cannot hide a working bundled or reinstallable channel plugin during setup. Thanks @vincentkoc.
- CLI/update: treat OpenClaw stable correction versions like `2026.5.3-1` as newer than their base stable release, so package updates no longer ask for downgrade confirmation. Thanks @vincentkoc.
- 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. Thanks @vincentkoc.
- Plugins/commands: suppress dangerous-pattern scanner warnings for trusted catalog npm installs from owner-gated `/plugins install` commands, so chat-driven installs match the CLI install trust path. Thanks @vincentkoc.
- 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

@@ -7,11 +7,13 @@ import { handlePluginsCommand } from "./commands-plugins.js";
import { buildPluginsCommandParams } from "./commands.test-harness.js";
const {
installPluginFromNpmSpecMock,
installPluginFromPathMock,
installPluginFromClawHubMock,
installPluginFromGitSpecMock,
persistPluginInstallMock,
} = vi.hoisted(() => ({
installPluginFromNpmSpecMock: vi.fn(),
installPluginFromPathMock: vi.fn(),
installPluginFromClawHubMock: vi.fn(),
installPluginFromGitSpecMock: vi.fn(),
@@ -24,6 +26,7 @@ vi.mock("../../plugins/install.js", async () => {
);
return {
...actual,
installPluginFromNpmSpec: installPluginFromNpmSpecMock,
installPluginFromPath: installPluginFromPathMock,
};
});
@@ -64,6 +67,7 @@ function buildPluginsParams(commandBodyNormalized: string, workspaceDir: string)
describe("handleCommands /plugins install", () => {
afterEach(async () => {
installPluginFromNpmSpecMock.mockReset();
installPluginFromPathMock.mockReset();
installPluginFromClawHubMock.mockReset();
installPluginFromGitSpecMock.mockReset();
@@ -253,4 +257,60 @@ describe("handleCommands /plugins install", () => {
);
});
});
it("trusts catalog npm package installs with alternate selectors", async () => {
installPluginFromNpmSpecMock.mockResolvedValue({
ok: true,
pluginId: "wecom-openclaw-plugin",
targetDir: "/tmp/wecom-openclaw-plugin",
version: "2026.4.23",
extensions: ["index.js"],
npmResolution: {
name: "@wecom/wecom-openclaw-plugin",
version: "2026.4.23",
resolvedSpec: "@wecom/wecom-openclaw-plugin@2026.4.23",
integrity: "sha512-wecom",
resolvedAt: "2026-05-04T20:00:00.000Z",
},
});
persistPluginInstallMock.mockResolvedValue({});
await withTempHome("openclaw-command-plugins-home-", async () => {
const workspaceDir = await workspaceHarness.createWorkspace();
const params = buildPluginsParams(
"/plugins install @wecom/wecom-openclaw-plugin@latest",
workspaceDir,
);
const result = await handlePluginsCommand(params, true);
if (result === null) {
throw new Error("expected plugin install result");
}
expect(result.reply?.text).toContain('Installed plugin "wecom-openclaw-plugin"');
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@wecom/wecom-openclaw-plugin@latest",
expectedPluginId: "wecom-openclaw-plugin",
trustedSourceLinkedOfficialInstall: true,
}),
);
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
expect.not.objectContaining({
expectedIntegrity: expect.any(String),
}),
);
expect(persistPluginInstallMock).toHaveBeenCalledWith(
expect.objectContaining({
pluginId: "wecom-openclaw-plugin",
install: expect.objectContaining({
source: "npm",
spec: "@wecom/wecom-openclaw-plugin@latest",
installPath: "/tmp/wecom-openclaw-plugin",
version: "2026.4.23",
resolvedName: "@wecom/wecom-openclaw-plugin",
resolvedVersion: "2026.4.23",
}),
}),
);
});
});
});

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import { buildNpmInstallRecordFields } from "../../cli/npm-resolution.js";
import { resolveOfficialExternalNpmPackageTrust } from "../../cli/plugin-install-plan.js";
import {
createPluginInstallLogger,
resolveFileNpmSpecToLocalPath,
@@ -20,6 +21,11 @@ import { installPluginFromClawHub } from "../../plugins/clawhub.js";
import { installPluginFromGitSpec, parseGitPluginSpec } from "../../plugins/git-install.js";
import { installPluginFromNpmSpec, installPluginFromPath } from "../../plugins/install.js";
import { loadInstalledPluginIndexInstallRecords } from "../../plugins/installed-plugin-index-records.js";
import {
getOfficialExternalPluginCatalogEntryForPackage,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
} from "../../plugins/official-external-plugin-catalog.js";
import type { PluginRecord } from "../../plugins/registry.js";
import {
buildAllPluginInspectReports,
@@ -159,6 +165,29 @@ function looksLikeLocalPluginInstallSpec(raw: string): boolean {
);
}
function findTrustedCatalogPackageInstall(packageName: string):
| {
pluginId: string;
npmSpec?: string;
expectedIntegrity?: string;
}
| undefined {
const entry = getOfficialExternalPluginCatalogEntryForPackage(packageName);
if (!entry) {
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 } : {}),
};
}
async function installPluginFromPluginsCommand(params: {
raw: string;
snapshot: ConfigSnapshotForInstallPersist;
@@ -254,8 +283,21 @@ async function installPluginFromPluginsCommand(params: {
return { ok: true, pluginId: result.pluginId };
}
const officialNpmTrust = resolveOfficialExternalNpmPackageTrust({
npmSpec: params.raw,
findOfficialExternalPackage: findTrustedCatalogPackageInstall,
});
const result = await installPluginFromNpmSpec({
spec: params.raw,
...(officialNpmTrust
? {
expectedPluginId: officialNpmTrust.pluginId,
...(officialNpmTrust.expectedIntegrity
? { expectedIntegrity: officialNpmTrust.expectedIntegrity }
: {}),
trustedSourceLinkedOfficialInstall: true,
}
: {}),
logger: createPluginInstallLogger(),
});
if (!result.ok) {