fix(plugins): trust official diagnostics installs (#77516)

This commit is contained in:
Peter Steinberger
2026-05-04 21:39:23 +01:00
committed by GitHub
parent 021373a454
commit 06056926a0
9 changed files with 203 additions and 6 deletions

View File

@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.
- Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked. Thanks @scoootscooob.
- CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc.
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.

View File

@@ -19,6 +19,7 @@ export function createPluginRecord(params: {
rootDir?: string;
origin: PluginRecord["origin"];
workspaceDir?: string;
trustedOfficialInstall?: boolean;
enabled: boolean;
compat?: readonly PluginCompatCode[];
activationState?: PluginActivationState;
@@ -41,6 +42,7 @@ export function createPluginRecord(params: {
rootDir: params.rootDir,
origin: params.origin,
workspaceDir: params.workspaceDir,
trustedOfficialInstall: params.trustedOfficialInstall,
enabled: params.enabled,
compat: params.compat,
explicitlyEnabled: params.activationState?.explicitlyEnabled,

View File

@@ -1739,6 +1739,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
rootDir: candidate.rootDir,
origin: candidate.origin,
workspaceDir: candidate.workspaceDir,
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
enabled: false,
compat: collectPluginManifestCompatCodes(manifestRecord),
activationState,
@@ -1777,6 +1778,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
rootDir: candidate.rootDir,
origin: candidate.origin,
workspaceDir: candidate.workspaceDir,
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
enabled: enableState.enabled,
compat: collectPluginManifestCompatCodes(manifestRecord),
activationState,
@@ -2563,6 +2565,7 @@ export async function loadOpenClawPluginCliRegistry(
rootDir: candidate.rootDir,
origin: candidate.origin,
workspaceDir: candidate.workspaceDir,
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
enabled: false,
compat: collectPluginManifestCompatCodes(manifestRecord),
activationState,
@@ -2601,6 +2604,7 @@ export async function loadOpenClawPluginCliRegistry(
rootDir: candidate.rootDir,
origin: candidate.origin,
workspaceDir: candidate.workspaceDir,
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
enabled: enableState.enabled,
compat: collectPluginManifestCompatCodes(manifestRecord),
activationState,

View File

@@ -534,6 +534,89 @@ describe("loadPluginManifestRegistry", () => {
expect(registry.plugins[0]?.origin).toBe("global");
});
it("marks official installed npm globals as trusted official installs", () => {
const dir = makeTempDir();
writeManifest(dir, { id: "diagnostics-prometheus", configSchema: { type: "object" } });
const registry = loadPluginManifestRegistry({
installRecords: {
"diagnostics-prometheus": {
source: "npm",
installPath: dir,
resolvedName: "@openclaw/diagnostics-prometheus",
resolvedVersion: "2026.5.3",
},
},
candidates: [
createPluginCandidate({
idHint: "diagnostics-prometheus",
rootDir: dir,
packageName: "@openclaw/diagnostics-prometheus",
origin: "global",
}),
],
});
expect(registry.plugins[0]?.trustedOfficialInstall).toBe(true);
});
it("preserves trusted official installs when a config path selects the installed package", () => {
const dir = makeTempDir();
writeManifest(dir, { id: "diagnostics-prometheus", configSchema: { type: "object" } });
const registry = loadPluginManifestRegistry({
installRecords: {
"diagnostics-prometheus": {
source: "npm",
installPath: dir,
resolvedName: "@openclaw/diagnostics-prometheus",
resolvedVersion: "2026.5.3",
},
},
candidates: [
createPluginCandidate({
idHint: "diagnostics-prometheus",
rootDir: dir,
packageName: "@openclaw/diagnostics-prometheus",
origin: "global",
}),
createPluginCandidate({
idHint: "diagnostics-prometheus",
rootDir: dir,
packageName: "@openclaw/diagnostics-prometheus",
origin: "config",
}),
],
});
expect(registry.plugins).toHaveLength(1);
expect(registry.plugins[0]).toEqual(
expect.objectContaining({
origin: "config",
trustedOfficialInstall: true,
}),
);
});
it("does not trust unrecorded globals that spoof official ids", () => {
const dir = makeTempDir();
writeManifest(dir, { id: "diagnostics-prometheus", configSchema: { type: "object" } });
const registry = loadPluginManifestRegistry({
installRecords: {},
candidates: [
createPluginCandidate({
idHint: "diagnostics-prometheus",
rootDir: dir,
packageName: "@openclaw/diagnostics-prometheus",
origin: "global",
}),
],
});
expect(registry.plugins[0]?.trustedOfficialInstall).toBeUndefined();
});
it("preserves provider auth env metadata from plugin manifests", () => {
const dir = makeTempDir();
writeManifest(dir, {

View File

@@ -46,6 +46,8 @@ import { checkMinHostVersion } from "./min-host-version.js";
import {
getOfficialExternalPluginCatalogEntryForPackage,
getOfficialExternalPluginCatalogManifest,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
} from "./official-external-plugin-catalog.js";
import { isPathInside, safeRealpathSync } from "./path-safety.js";
import type { PluginKind } from "./plugin-kind.types.js";
@@ -140,6 +142,7 @@ export type PluginManifestRecord = {
packageOptionalDependencies?: PluginDependencySpecMap;
packageChannel?: PluginPackageChannel;
packageInstall?: PluginPackageInstall;
trustedOfficialInstall?: boolean;
qaRunners?: PluginManifestQaRunner[];
skills: string[];
settingsFiles?: string[];
@@ -365,6 +368,7 @@ function buildRecord(params: {
schemaCacheKey?: string;
configSchema?: Record<string, unknown>;
bundledChannelConfigCollector?: BundledChannelConfigCollector;
trustedOfficialInstall?: boolean;
}): PluginManifestRecord {
const manifestChannelConfigs =
params.candidate.origin === "bundled" && params.bundledChannelConfigCollector
@@ -434,6 +438,7 @@ function buildRecord(params: {
packageOptionalDependencies: params.candidate.packageOptionalDependencies,
packageChannel: params.candidate.packageManifest?.channel,
packageInstall: params.candidate.packageManifest?.install,
trustedOfficialInstall: params.trustedOfficialInstall === true ? true : undefined,
qaRunners: params.manifest.qaRunners,
skills: params.manifest.skills ?? [],
settingsFiles: [],
@@ -634,7 +639,7 @@ function matchesInstalledPluginRecord(params: {
env: NodeJS.ProcessEnv;
installRecords: Record<string, PluginInstallRecord>;
}): boolean {
if (params.candidate.origin !== "global") {
if (params.candidate.origin !== "global" && params.candidate.origin !== "config") {
return false;
}
const record = params.installRecords[params.pluginId];
@@ -653,6 +658,72 @@ function matchesInstalledPluginRecord(params: {
});
}
function npmSpecMatchesPackage(value: string | undefined, packageName: string): boolean {
const normalized = value?.trim();
if (!normalized) {
return false;
}
if (normalized === packageName) {
return true;
}
return normalized.startsWith(`${packageName}@`);
}
function isTrustedOfficialPluginInstall(params: {
pluginId: string;
candidate: PluginCandidate;
env: NodeJS.ProcessEnv;
installRecords: Record<string, PluginInstallRecord>;
}): boolean {
if (
(params.candidate.origin !== "global" && params.candidate.origin !== "config") ||
!matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.candidate,
env: params.env,
installRecords: params.installRecords,
})
) {
return false;
}
const packageName = params.candidate.packageName?.trim();
if (!packageName) {
return false;
}
const catalogEntry = getOfficialExternalPluginCatalogEntryForPackage(packageName);
if (!catalogEntry || resolveOfficialExternalPluginId(catalogEntry) !== params.pluginId) {
return false;
}
const officialInstall = resolveOfficialExternalPluginInstall(catalogEntry);
const installRecord = params.installRecords[params.pluginId];
if (!installRecord) {
return false;
}
if (
installRecord.source === "npm" &&
officialInstall?.npmSpec === packageName &&
[
installRecord.resolvedName,
installRecord.spec,
installRecord.resolvedSpec,
params.candidate.packageName,
].some((value) => npmSpecMatchesPackage(value, packageName))
) {
return true;
}
if (
installRecord.source === "clawhub" &&
officialInstall?.clawhubSpec &&
installRecord.clawhubChannel === "official" &&
(installRecord.clawhubPackage === packageName ||
installRecord.spec === officialInstall.clawhubSpec ||
installRecord.resolvedSpec === officialInstall.clawhubSpec)
) {
return true;
}
return false;
}
function resolveDuplicatePrecedenceRank(params: {
pluginId: string;
candidate: PluginCandidate;
@@ -858,6 +929,12 @@ export function loadPluginManifestRegistry(
manifestPath: manifestRes.manifestPath,
schemaCacheKey,
configSchema,
trustedOfficialInstall: isTrustedOfficialPluginInstall({
pluginId: manifest.id,
candidate,
env,
installRecords: getInstallRecords(),
}),
...(params.bundledChannelConfigCollector
? { bundledChannelConfigCollector: params.bundledChannelConfigCollector }
: {}),

View File

@@ -205,6 +205,7 @@ export type PluginServiceRegistration = {
service: OpenClawPluginService;
source: string;
origin: PluginOrigin;
trustedOfficialInstall?: boolean;
rootDir?: string;
};
@@ -337,6 +338,7 @@ export type PluginRecord = {
rootDir?: string;
origin: PluginOrigin;
workspaceDir?: string;
trustedOfficialInstall?: boolean;
enabled: boolean;
explicitlyEnabled?: boolean;
activated?: boolean;

View File

@@ -1452,6 +1452,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
service,
source: record.source,
origin: record.origin,
trustedOfficialInstall: record.trustedOfficialInstall,
rootDir: record.rootDir,
});
};

View File

@@ -22,6 +22,7 @@ function createRegistry(
services: OpenClawPluginService[],
pluginId = "plugin:test",
origin: PluginOrigin = "workspace",
trustedOfficialInstall = false,
) {
const registry = createEmptyPluginRegistry();
registry.services = services.map((service) => ({
@@ -29,6 +30,7 @@ function createRegistry(
service,
source: "test",
origin,
...(trustedOfficialInstall ? { trustedOfficialInstall } : {}),
rootDir: "/plugins/test-plugin",
})) as typeof registry.services;
return registry;
@@ -181,7 +183,7 @@ describe("startPluginServices", () => {
expect(stopThrows).toHaveBeenCalledOnce();
});
it("grants internal diagnostics only to bundled diagnostics exporter services", async () => {
it("grants internal diagnostics only to trusted diagnostics exporter services", async () => {
const contexts: OpenClawPluginServiceContext[] = [];
const diagnosticsService = createTrackingService("diagnostics-otel", { contexts });
await startPluginServices({
@@ -204,6 +206,18 @@ describe("startPluginServices", () => {
expect(prometheusContexts[0]?.internalDiagnostics?.onEvent).toBeTypeOf("function");
expect(prometheusContexts[0]?.internalDiagnostics?.emit).toBeTypeOf("function");
const officialInstallContexts: OpenClawPluginServiceContext[] = [];
const officialInstallService = createTrackingService("diagnostics-prometheus", {
contexts: officialInstallContexts,
});
await startPluginServices({
registry: createRegistry([officialInstallService], "diagnostics-prometheus", "global", true),
config: createServiceConfig(),
});
expect(officialInstallContexts[0]?.internalDiagnostics?.onEvent).toBeTypeOf("function");
expect(officialInstallContexts[0]?.internalDiagnostics?.emit).toBeTypeOf("function");
const untrustedContexts: OpenClawPluginServiceContext[] = [];
const untrustedService = createTrackingService("diagnostics-otel", {
contexts: untrustedContexts,
@@ -214,5 +228,16 @@ describe("startPluginServices", () => {
});
expect(untrustedContexts[0]?.internalDiagnostics).toBeUndefined();
const spoofedContexts: OpenClawPluginServiceContext[] = [];
const spoofedService = createTrackingService("diagnostics-prometheus", {
contexts: spoofedContexts,
});
await startPluginServices({
registry: createRegistry([spoofedService], "not-diagnostics-prometheus", "global", true),
config: createServiceConfig(),
});
expect(spoofedContexts[0]?.internalDiagnostics).toBeUndefined();
});
});

View File

@@ -24,11 +24,13 @@ function createServiceContext(params: {
workspaceDir?: string;
service?: PluginServiceRegistration;
}): OpenClawPluginServiceContext {
const isDiagnosticsExporter =
params.service?.pluginId === params.service?.service.id &&
(params.service?.service.id === "diagnostics-otel" ||
params.service?.service.id === "diagnostics-prometheus");
const grantsInternalDiagnostics =
params.service?.origin === "bundled" &&
params.service.pluginId === params.service.service.id &&
(params.service.service.id === "diagnostics-otel" ||
params.service.service.id === "diagnostics-prometheus");
isDiagnosticsExporter &&
(params.service?.origin === "bundled" || params.service?.trustedOfficialInstall === true);
return {
config: params.config,