fix: trusted installs

This commit is contained in:
Vincent Koc
2026-05-02 16:14:52 -07:00
parent dd43caa27a
commit 5ed7f1fd26
3 changed files with 79 additions and 2 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
- Doctor/plugins: update configured plugin installs whose stale manifests still declare channels without `channelConfigs`, so beta upgrades repair old Discord-style package payloads during `doctor --fix`.
- Active Memory: keep non-empty `memory_search` results from being fast-failed as empty when debug telemetry reports zero hits.
- Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc.
- Plugins/install: allow official catalog-matched npm channel plugins such as Feishu to pass the trusted install scanner path while keeping spoofed package names blocked. Thanks @vincentkoc.
- Upgrade/config: validate configured web-search providers and statically suppressed model/provider pairs against the active plugin set at config load, so stale plugin state fails loud before runtime fallback.
- Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses.
- Doctor/plugins: preserve unmanaged third-party plugin `node_modules` during `doctor --fix`, while still pruning OpenClaw-managed runtime dependency caches.

View File

@@ -298,6 +298,61 @@ describe("installPluginFromNpmSpec", () => {
});
});
it("allows official catalog-matched npm plugins through the trusted scanner path", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const warnings: string[] = [];
mockNpmViewAndInstall({
spec: "@openclaw/feishu@2026.5.2",
packageName: "@openclaw/feishu",
version: "2026.5.2",
pluginId: "feishu",
npmRoot,
indexJs: `const token = process.env.FEISHU_BOT_TOKEN;\nfetch("https://open.feishu.cn/open-apis/bot/v2/hook", { headers: { authorization: token } });`,
});
const result = await installPluginFromNpmSpec({
spec: "@openclaw/feishu@2026.5.2",
expectedPluginId: "feishu",
npmDir: npmRoot,
logger: {
info: () => {},
warn: (msg: string) => warnings.push(msg),
},
});
expect(result.ok).toBe(true);
expect(
warnings.some((warning) =>
warning.includes("allowed because it is an official OpenClaw package"),
),
).toBe(true);
});
it("keeps blocking dangerous npm installs that do not match the official catalog", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
mockNpmViewAndInstall({
spec: "@openclaw/feishu-spoof@2026.5.2",
packageName: "@openclaw/feishu-spoof",
version: "2026.5.2",
pluginId: "feishu",
npmRoot,
indexJs: `const token = process.env.FEISHU_BOT_TOKEN;\nfetch("https://open.feishu.cn/open-apis/bot/v2/hook", { headers: { authorization: token } });`,
});
const result = await installPluginFromNpmSpec({
spec: "@openclaw/feishu-spoof@2026.5.2",
expectedPluginId: "feishu",
npmDir: npmRoot,
logger: { info: () => {}, warn: () => {} },
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
expect(result.error).toContain("dangerous code patterns detected");
}
});
it("rejects non-registry npm specs", async () => {
const result = await installPluginFromNpmSpec({ spec: "github:evil/evil" });
expect(result.ok).toBe(false);

View File

@@ -34,6 +34,11 @@ import {
resolvePackageExtensionEntries,
type PackageManifest as PluginPackageManifest,
} from "./manifest.js";
import {
listOfficialExternalPluginCatalogEntries,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
} from "./official-external-plugin-catalog.js";
import { validatePackageExtensionEntriesForInstall } from "./package-entry-resolution.js";
import { linkOpenClawPeerDependencies } from "./plugin-peer-link.js";
@@ -110,7 +115,23 @@ type PluginInstallPolicyRequest = {
};
const defaultLogger: PluginInstallLogger = {};
const TRUSTED_OFFICIAL_NPM_PLUGIN_PACKAGES = new Map([["@openclaw/codex", "codex"]]);
function listTrustedOfficialNpmPluginPackages(): Map<string, string> {
const packages = new Map<string, string>();
for (const entry of listOfficialExternalPluginCatalogEntries()) {
if (entry.source !== "official") {
continue;
}
const pluginId = resolveOfficialExternalPluginId(entry);
const install = resolveOfficialExternalPluginInstall(entry);
const npmSpec = install?.npmSpec ? parseRegistryNpmSpec(install.npmSpec) : null;
if (!pluginId || !npmSpec) {
continue;
}
packages.set(npmSpec.name, pluginId);
}
return packages;
}
function ensureOpenClawExtensions(params: { manifest: PackageManifest }):
| {
@@ -198,7 +219,7 @@ function isTrustedOfficialNpmPluginInstall(params: {
if (!requested) {
return false;
}
const expectedPluginId = TRUSTED_OFFICIAL_NPM_PLUGIN_PACKAGES.get(requested.name);
const expectedPluginId = listTrustedOfficialNpmPluginPackages().get(requested.name);
return (
expectedPluginId !== undefined &&
params.packageName === requested.name &&