mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00:00
fix(plugins): trust chat catalog installs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user