Skip to content

Commit 0605692

Browse files
authored
fix(plugins): trust official diagnostics installs (#77516)
1 parent 021373a commit 0605692

9 files changed

Lines changed: 203 additions & 6 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
4747

4848
### Fixes
4949

50+
- 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.
5051
- 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.
5152
- 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.
5253
- 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.

src/plugins/loader-records.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export function createPluginRecord(params: {
1919
rootDir?: string;
2020
origin: PluginRecord["origin"];
2121
workspaceDir?: string;
22+
trustedOfficialInstall?: boolean;
2223
enabled: boolean;
2324
compat?: readonly PluginCompatCode[];
2425
activationState?: PluginActivationState;
@@ -41,6 +42,7 @@ export function createPluginRecord(params: {
4142
rootDir: params.rootDir,
4243
origin: params.origin,
4344
workspaceDir: params.workspaceDir,
45+
trustedOfficialInstall: params.trustedOfficialInstall,
4446
enabled: params.enabled,
4547
compat: params.compat,
4648
explicitlyEnabled: params.activationState?.explicitlyEnabled,

src/plugins/loader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
17391739
rootDir: candidate.rootDir,
17401740
origin: candidate.origin,
17411741
workspaceDir: candidate.workspaceDir,
1742+
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
17421743
enabled: false,
17431744
compat: collectPluginManifestCompatCodes(manifestRecord),
17441745
activationState,
@@ -1777,6 +1778,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
17771778
rootDir: candidate.rootDir,
17781779
origin: candidate.origin,
17791780
workspaceDir: candidate.workspaceDir,
1781+
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
17801782
enabled: enableState.enabled,
17811783
compat: collectPluginManifestCompatCodes(manifestRecord),
17821784
activationState,
@@ -2563,6 +2565,7 @@ export async function loadOpenClawPluginCliRegistry(
25632565
rootDir: candidate.rootDir,
25642566
origin: candidate.origin,
25652567
workspaceDir: candidate.workspaceDir,
2568+
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
25662569
enabled: false,
25672570
compat: collectPluginManifestCompatCodes(manifestRecord),
25682571
activationState,
@@ -2601,6 +2604,7 @@ export async function loadOpenClawPluginCliRegistry(
26012604
rootDir: candidate.rootDir,
26022605
origin: candidate.origin,
26032606
workspaceDir: candidate.workspaceDir,
2607+
trustedOfficialInstall: manifestRecord.trustedOfficialInstall,
26042608
enabled: enableState.enabled,
26052609
compat: collectPluginManifestCompatCodes(manifestRecord),
26062610
activationState,

src/plugins/manifest-registry.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,89 @@ describe("loadPluginManifestRegistry", () => {
534534
expect(registry.plugins[0]?.origin).toBe("global");
535535
});
536536

537+
it("marks official installed npm globals as trusted official installs", () => {
538+
const dir = makeTempDir();
539+
writeManifest(dir, { id: "diagnostics-prometheus", configSchema: { type: "object" } });
540+
541+
const registry = loadPluginManifestRegistry({
542+
installRecords: {
543+
"diagnostics-prometheus": {
544+
source: "npm",
545+
installPath: dir,
546+
resolvedName: "@openclaw/diagnostics-prometheus",
547+
resolvedVersion: "2026.5.3",
548+
},
549+
},
550+
candidates: [
551+
createPluginCandidate({
552+
idHint: "diagnostics-prometheus",
553+
rootDir: dir,
554+
packageName: "@openclaw/diagnostics-prometheus",
555+
origin: "global",
556+
}),
557+
],
558+
});
559+
560+
expect(registry.plugins[0]?.trustedOfficialInstall).toBe(true);
561+
});
562+
563+
it("preserves trusted official installs when a config path selects the installed package", () => {
564+
const dir = makeTempDir();
565+
writeManifest(dir, { id: "diagnostics-prometheus", configSchema: { type: "object" } });
566+
567+
const registry = loadPluginManifestRegistry({
568+
installRecords: {
569+
"diagnostics-prometheus": {
570+
source: "npm",
571+
installPath: dir,
572+
resolvedName: "@openclaw/diagnostics-prometheus",
573+
resolvedVersion: "2026.5.3",
574+
},
575+
},
576+
candidates: [
577+
createPluginCandidate({
578+
idHint: "diagnostics-prometheus",
579+
rootDir: dir,
580+
packageName: "@openclaw/diagnostics-prometheus",
581+
origin: "global",
582+
}),
583+
createPluginCandidate({
584+
idHint: "diagnostics-prometheus",
585+
rootDir: dir,
586+
packageName: "@openclaw/diagnostics-prometheus",
587+
origin: "config",
588+
}),
589+
],
590+
});
591+
592+
expect(registry.plugins).toHaveLength(1);
593+
expect(registry.plugins[0]).toEqual(
594+
expect.objectContaining({
595+
origin: "config",
596+
trustedOfficialInstall: true,
597+
}),
598+
);
599+
});
600+
601+
it("does not trust unrecorded globals that spoof official ids", () => {
602+
const dir = makeTempDir();
603+
writeManifest(dir, { id: "diagnostics-prometheus", configSchema: { type: "object" } });
604+
605+
const registry = loadPluginManifestRegistry({
606+
installRecords: {},
607+
candidates: [
608+
createPluginCandidate({
609+
idHint: "diagnostics-prometheus",
610+
rootDir: dir,
611+
packageName: "@openclaw/diagnostics-prometheus",
612+
origin: "global",
613+
}),
614+
],
615+
});
616+
617+
expect(registry.plugins[0]?.trustedOfficialInstall).toBeUndefined();
618+
});
619+
537620
it("preserves provider auth env metadata from plugin manifests", () => {
538621
const dir = makeTempDir();
539622
writeManifest(dir, {

src/plugins/manifest-registry.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import { checkMinHostVersion } from "./min-host-version.js";
4646
import {
4747
getOfficialExternalPluginCatalogEntryForPackage,
4848
getOfficialExternalPluginCatalogManifest,
49+
resolveOfficialExternalPluginId,
50+
resolveOfficialExternalPluginInstall,
4951
} from "./official-external-plugin-catalog.js";
5052
import { isPathInside, safeRealpathSync } from "./path-safety.js";
5153
import type { PluginKind } from "./plugin-kind.types.js";
@@ -140,6 +142,7 @@ export type PluginManifestRecord = {
140142
packageOptionalDependencies?: PluginDependencySpecMap;
141143
packageChannel?: PluginPackageChannel;
142144
packageInstall?: PluginPackageInstall;
145+
trustedOfficialInstall?: boolean;
143146
qaRunners?: PluginManifestQaRunner[];
144147
skills: string[];
145148
settingsFiles?: string[];
@@ -365,6 +368,7 @@ function buildRecord(params: {
365368
schemaCacheKey?: string;
366369
configSchema?: Record<string, unknown>;
367370
bundledChannelConfigCollector?: BundledChannelConfigCollector;
371+
trustedOfficialInstall?: boolean;
368372
}): PluginManifestRecord {
369373
const manifestChannelConfigs =
370374
params.candidate.origin === "bundled" && params.bundledChannelConfigCollector
@@ -434,6 +438,7 @@ function buildRecord(params: {
434438
packageOptionalDependencies: params.candidate.packageOptionalDependencies,
435439
packageChannel: params.candidate.packageManifest?.channel,
436440
packageInstall: params.candidate.packageManifest?.install,
441+
trustedOfficialInstall: params.trustedOfficialInstall === true ? true : undefined,
437442
qaRunners: params.manifest.qaRunners,
438443
skills: params.manifest.skills ?? [],
439444
settingsFiles: [],
@@ -634,7 +639,7 @@ function matchesInstalledPluginRecord(params: {
634639
env: NodeJS.ProcessEnv;
635640
installRecords: Record<string, PluginInstallRecord>;
636641
}): boolean {
637-
if (params.candidate.origin !== "global") {
642+
if (params.candidate.origin !== "global" && params.candidate.origin !== "config") {
638643
return false;
639644
}
640645
const record = params.installRecords[params.pluginId];
@@ -653,6 +658,72 @@ function matchesInstalledPluginRecord(params: {
653658
});
654659
}
655660

661+
function npmSpecMatchesPackage(value: string | undefined, packageName: string): boolean {
662+
const normalized = value?.trim();
663+
if (!normalized) {
664+
return false;
665+
}
666+
if (normalized === packageName) {
667+
return true;
668+
}
669+
return normalized.startsWith(`${packageName}@`);
670+
}
671+
672+
function isTrustedOfficialPluginInstall(params: {
673+
pluginId: string;
674+
candidate: PluginCandidate;
675+
env: NodeJS.ProcessEnv;
676+
installRecords: Record<string, PluginInstallRecord>;
677+
}): boolean {
678+
if (
679+
(params.candidate.origin !== "global" && params.candidate.origin !== "config") ||
680+
!matchesInstalledPluginRecord({
681+
pluginId: params.pluginId,
682+
candidate: params.candidate,
683+
env: params.env,
684+
installRecords: params.installRecords,
685+
})
686+
) {
687+
return false;
688+
}
689+
const packageName = params.candidate.packageName?.trim();
690+
if (!packageName) {
691+
return false;
692+
}
693+
const catalogEntry = getOfficialExternalPluginCatalogEntryForPackage(packageName);
694+
if (!catalogEntry || resolveOfficialExternalPluginId(catalogEntry) !== params.pluginId) {
695+
return false;
696+
}
697+
const officialInstall = resolveOfficialExternalPluginInstall(catalogEntry);
698+
const installRecord = params.installRecords[params.pluginId];
699+
if (!installRecord) {
700+
return false;
701+
}
702+
if (
703+
installRecord.source === "npm" &&
704+
officialInstall?.npmSpec === packageName &&
705+
[
706+
installRecord.resolvedName,
707+
installRecord.spec,
708+
installRecord.resolvedSpec,
709+
params.candidate.packageName,
710+
].some((value) => npmSpecMatchesPackage(value, packageName))
711+
) {
712+
return true;
713+
}
714+
if (
715+
installRecord.source === "clawhub" &&
716+
officialInstall?.clawhubSpec &&
717+
installRecord.clawhubChannel === "official" &&
718+
(installRecord.clawhubPackage === packageName ||
719+
installRecord.spec === officialInstall.clawhubSpec ||
720+
installRecord.resolvedSpec === officialInstall.clawhubSpec)
721+
) {
722+
return true;
723+
}
724+
return false;
725+
}
726+
656727
function resolveDuplicatePrecedenceRank(params: {
657728
pluginId: string;
658729
candidate: PluginCandidate;
@@ -858,6 +929,12 @@ export function loadPluginManifestRegistry(
858929
manifestPath: manifestRes.manifestPath,
859930
schemaCacheKey,
860931
configSchema,
932+
trustedOfficialInstall: isTrustedOfficialPluginInstall({
933+
pluginId: manifest.id,
934+
candidate,
935+
env,
936+
installRecords: getInstallRecords(),
937+
}),
861938
...(params.bundledChannelConfigCollector
862939
? { bundledChannelConfigCollector: params.bundledChannelConfigCollector }
863940
: {}),

src/plugins/registry-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export type PluginServiceRegistration = {
205205
service: OpenClawPluginService;
206206
source: string;
207207
origin: PluginOrigin;
208+
trustedOfficialInstall?: boolean;
208209
rootDir?: string;
209210
};
210211

@@ -337,6 +338,7 @@ export type PluginRecord = {
337338
rootDir?: string;
338339
origin: PluginOrigin;
339340
workspaceDir?: string;
341+
trustedOfficialInstall?: boolean;
340342
enabled: boolean;
341343
explicitlyEnabled?: boolean;
342344
activated?: boolean;

src/plugins/registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
14521452
service,
14531453
source: record.source,
14541454
origin: record.origin,
1455+
trustedOfficialInstall: record.trustedOfficialInstall,
14551456
rootDir: record.rootDir,
14561457
});
14571458
};

src/plugins/services.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ function createRegistry(
2222
services: OpenClawPluginService[],
2323
pluginId = "plugin:test",
2424
origin: PluginOrigin = "workspace",
25+
trustedOfficialInstall = false,
2526
) {
2627
const registry = createEmptyPluginRegistry();
2728
registry.services = services.map((service) => ({
2829
pluginId,
2930
service,
3031
source: "test",
3132
origin,
33+
...(trustedOfficialInstall ? { trustedOfficialInstall } : {}),
3234
rootDir: "/plugins/test-plugin",
3335
})) as typeof registry.services;
3436
return registry;
@@ -181,7 +183,7 @@ describe("startPluginServices", () => {
181183
expect(stopThrows).toHaveBeenCalledOnce();
182184
});
183185

184-
it("grants internal diagnostics only to bundled diagnostics exporter services", async () => {
186+
it("grants internal diagnostics only to trusted diagnostics exporter services", async () => {
185187
const contexts: OpenClawPluginServiceContext[] = [];
186188
const diagnosticsService = createTrackingService("diagnostics-otel", { contexts });
187189
await startPluginServices({
@@ -204,6 +206,18 @@ describe("startPluginServices", () => {
204206
expect(prometheusContexts[0]?.internalDiagnostics?.onEvent).toBeTypeOf("function");
205207
expect(prometheusContexts[0]?.internalDiagnostics?.emit).toBeTypeOf("function");
206208

209+
const officialInstallContexts: OpenClawPluginServiceContext[] = [];
210+
const officialInstallService = createTrackingService("diagnostics-prometheus", {
211+
contexts: officialInstallContexts,
212+
});
213+
await startPluginServices({
214+
registry: createRegistry([officialInstallService], "diagnostics-prometheus", "global", true),
215+
config: createServiceConfig(),
216+
});
217+
218+
expect(officialInstallContexts[0]?.internalDiagnostics?.onEvent).toBeTypeOf("function");
219+
expect(officialInstallContexts[0]?.internalDiagnostics?.emit).toBeTypeOf("function");
220+
207221
const untrustedContexts: OpenClawPluginServiceContext[] = [];
208222
const untrustedService = createTrackingService("diagnostics-otel", {
209223
contexts: untrustedContexts,
@@ -214,5 +228,16 @@ describe("startPluginServices", () => {
214228
});
215229

216230
expect(untrustedContexts[0]?.internalDiagnostics).toBeUndefined();
231+
232+
const spoofedContexts: OpenClawPluginServiceContext[] = [];
233+
const spoofedService = createTrackingService("diagnostics-prometheus", {
234+
contexts: spoofedContexts,
235+
});
236+
await startPluginServices({
237+
registry: createRegistry([spoofedService], "not-diagnostics-prometheus", "global", true),
238+
config: createServiceConfig(),
239+
});
240+
241+
expect(spoofedContexts[0]?.internalDiagnostics).toBeUndefined();
217242
});
218243
});

src/plugins/services.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ function createServiceContext(params: {
2424
workspaceDir?: string;
2525
service?: PluginServiceRegistration;
2626
}): OpenClawPluginServiceContext {
27+
const isDiagnosticsExporter =
28+
params.service?.pluginId === params.service?.service.id &&
29+
(params.service?.service.id === "diagnostics-otel" ||
30+
params.service?.service.id === "diagnostics-prometheus");
2731
const grantsInternalDiagnostics =
28-
params.service?.origin === "bundled" &&
29-
params.service.pluginId === params.service.service.id &&
30-
(params.service.service.id === "diagnostics-otel" ||
31-
params.service.service.id === "diagnostics-prometheus");
32+
isDiagnosticsExporter &&
33+
(params.service?.origin === "bundled" || params.service?.trustedOfficialInstall === true);
3234

3335
return {
3436
config: params.config,

0 commit comments

Comments
 (0)