mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(plugins): trust official diagnostics installs (#77516)
This commit is contained in:
committed by
GitHub
parent
021373a454
commit
06056926a0
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1452,6 +1452,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
service,
|
||||
source: record.source,
|
||||
origin: record.origin,
|
||||
trustedOfficialInstall: record.trustedOfficialInstall,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user