mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(telemetry): report only publicly known plugin identities (#128603)
This commit is contained in:
committed by
GitHub
parent
af384662f7
commit
4ac8cd3dad
@@ -94,7 +94,8 @@ When you explicitly enable feature statistics, the same daily request becomes a
|
||||
"features": {
|
||||
"channels": ["discord", "telegram"],
|
||||
"providerFamilies": ["anthropic", "openai"],
|
||||
"pluginsEnabled": 7,
|
||||
"plugins": ["codex", "diagnostics-otel"],
|
||||
"pluginsEnabled": 9,
|
||||
"sessionsLast24h": 14
|
||||
}
|
||||
}
|
||||
@@ -107,11 +108,18 @@ When you explicitly enable feature statistics, the same daily request becomes a
|
||||
| `platform` | Operating system and CPU architecture. |
|
||||
| `node` | Running Node.js version. |
|
||||
| `surface` | Request origin: `gateway` or `cli`. |
|
||||
| `features.channels` | Enabled channel plugin names, sorted alphabetically. |
|
||||
| `features.providerFamilies` | Configured provider names, sorted alphabetically; never model names. |
|
||||
| `features.pluginsEnabled` | Number of enabled plugins, without plugin names or configuration details. |
|
||||
| `features.channels` | Publicly known enabled channel plugin names, sorted alphabetically. |
|
||||
| `features.providerFamilies` | Publicly known configured provider names; never model names. |
|
||||
| `features.plugins` | Publicly known enabled plugin names, sorted alphabetically. |
|
||||
| `features.pluginsEnabled` | Total enabled plugins, including privately developed plugins never named. |
|
||||
| `features.sessionsLast24h` | Number of sessions observed during the preceding 24 hours. |
|
||||
|
||||
OpenClaw names only plugins and channels that are bundled with OpenClaw or
|
||||
already appear in its official plugin catalog. Privately developed plugins are
|
||||
counted but never named because a private plugin name could identify its
|
||||
organization. Subtract `features.plugins.length` from `features.pluginsEnabled`
|
||||
to find the number of unnamed private plugins.
|
||||
|
||||
The sender and `openclaw telemetry show` use the same payload builder, so the
|
||||
JSON displayed by the CLI is the same payload the sender would use at that
|
||||
moment.
|
||||
|
||||
@@ -9,6 +9,93 @@ type MergedModelProviderEntry = {
|
||||
providerConfig: ModelProviderConfig;
|
||||
};
|
||||
|
||||
const BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS = new Set([
|
||||
"amazon-bedrock",
|
||||
"amazon-bedrock-mantle",
|
||||
"anthropic",
|
||||
"anthropic-vertex",
|
||||
"arcee",
|
||||
"azure-openai-responses",
|
||||
"byteplus",
|
||||
"byteplus-plan",
|
||||
"cerebras",
|
||||
"chutes",
|
||||
"claude-cli",
|
||||
"clawrouter",
|
||||
"cloudflare-ai-gateway",
|
||||
"codex",
|
||||
"comfy",
|
||||
"copilot-proxy",
|
||||
"dashscope",
|
||||
"deepinfra",
|
||||
"deepseek",
|
||||
"fal",
|
||||
"fireworks",
|
||||
"github-copilot",
|
||||
"gmi",
|
||||
"gmi-cloud",
|
||||
"gmicloud",
|
||||
"google",
|
||||
"google-antigravity",
|
||||
"google-gemini-cli",
|
||||
"google-vertex",
|
||||
"groq",
|
||||
"huggingface",
|
||||
"kilocode",
|
||||
"kimi",
|
||||
"kimi-coding",
|
||||
"litellm",
|
||||
"lmstudio",
|
||||
"meta",
|
||||
"microsoft-foundry",
|
||||
"minimax",
|
||||
"minimax-portal",
|
||||
"mistral",
|
||||
"modelstudio",
|
||||
"moonshot",
|
||||
"moonshot-ai",
|
||||
"moonshotai",
|
||||
"nvidia",
|
||||
"novita",
|
||||
"novita-ai",
|
||||
"novitaai",
|
||||
"ollama",
|
||||
"ollama-cloud",
|
||||
"openai",
|
||||
"opencode",
|
||||
"opencode-go",
|
||||
"openrouter",
|
||||
"qianfan",
|
||||
"qwen",
|
||||
"qwen-token-plan",
|
||||
"qwencloud",
|
||||
"sglang",
|
||||
"stepfun",
|
||||
"stepfun-plan",
|
||||
"synthetic",
|
||||
"tencent-tokenhub",
|
||||
"tencent-tokenplan",
|
||||
"together",
|
||||
"venice",
|
||||
"vercel-ai-gateway",
|
||||
"vllm",
|
||||
"volcengine",
|
||||
"volcengine-plan",
|
||||
"vydra",
|
||||
"x-ai",
|
||||
"xai",
|
||||
"xiaomi",
|
||||
"xiaomi-token-plan",
|
||||
"z.ai",
|
||||
"z-ai",
|
||||
"zai",
|
||||
]);
|
||||
|
||||
/** Identifies provider overlays already known to the bundled config contract. */
|
||||
export function isBuiltInModelProviderOverlayId(providerId: string): boolean {
|
||||
return BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS.has(normalizeProviderId(providerId));
|
||||
}
|
||||
|
||||
/** Indexes configured model rows after caller-owned model-id normalization. */
|
||||
export function resolveMergedModelProviderModels(params: {
|
||||
models: readonly ModelDefinitionConfig[] | undefined;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Defines core Zod schema fragments for canonical config parsing.
|
||||
import path from "node:path";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||
import { z } from "zod";
|
||||
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
||||
@@ -10,12 +9,15 @@ import {
|
||||
isValidFileSecretRefId,
|
||||
SECRET_PROVIDER_ALIAS_PATTERN,
|
||||
} from "../secrets/ref-contract.js";
|
||||
import { isBuiltInModelProviderOverlayId } from "./model-provider-config.js";
|
||||
import type { ModelCompatConfig } from "./types.models.js";
|
||||
import { MODEL_APIS, MODEL_THINKING_FORMATS } from "./types.models.js";
|
||||
import { ENV_SECRET_REF_ID_RE } from "./types.secrets.js";
|
||||
import { createAllowDenyChannelRulesSchema } from "./zod-schema.allowdeny.js";
|
||||
import { sensitive } from "./zod-schema.sensitive.js";
|
||||
|
||||
export { isBuiltInModelProviderOverlayId } from "./model-provider-config.js";
|
||||
|
||||
const WINDOWS_ABS_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
|
||||
const WINDOWS_UNC_PATH_PATTERN = /^\\\\[^\\]+\\[^\\]+/;
|
||||
|
||||
@@ -451,92 +453,6 @@ const ModelProviderLocalServiceSchema = z
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS = new Set([
|
||||
"amazon-bedrock",
|
||||
"amazon-bedrock-mantle",
|
||||
"anthropic",
|
||||
"anthropic-vertex",
|
||||
"arcee",
|
||||
"azure-openai-responses",
|
||||
"byteplus",
|
||||
"byteplus-plan",
|
||||
"cerebras",
|
||||
"chutes",
|
||||
"claude-cli",
|
||||
"clawrouter",
|
||||
"cloudflare-ai-gateway",
|
||||
"codex",
|
||||
"comfy",
|
||||
"copilot-proxy",
|
||||
"dashscope",
|
||||
"deepinfra",
|
||||
"deepseek",
|
||||
"fal",
|
||||
"fireworks",
|
||||
"github-copilot",
|
||||
"gmi",
|
||||
"gmi-cloud",
|
||||
"gmicloud",
|
||||
"google",
|
||||
"google-antigravity",
|
||||
"google-gemini-cli",
|
||||
"google-vertex",
|
||||
"groq",
|
||||
"huggingface",
|
||||
"kilocode",
|
||||
"kimi",
|
||||
"kimi-coding",
|
||||
"litellm",
|
||||
"lmstudio",
|
||||
"meta",
|
||||
"microsoft-foundry",
|
||||
"minimax",
|
||||
"minimax-portal",
|
||||
"mistral",
|
||||
"modelstudio",
|
||||
"moonshot",
|
||||
"moonshot-ai",
|
||||
"moonshotai",
|
||||
"nvidia",
|
||||
"novita",
|
||||
"novita-ai",
|
||||
"novitaai",
|
||||
"ollama",
|
||||
"ollama-cloud",
|
||||
"openai",
|
||||
"opencode",
|
||||
"opencode-go",
|
||||
"openrouter",
|
||||
"qianfan",
|
||||
"qwen",
|
||||
"qwen-token-plan",
|
||||
"qwencloud",
|
||||
"sglang",
|
||||
"stepfun",
|
||||
"stepfun-plan",
|
||||
"synthetic",
|
||||
"tencent-tokenhub",
|
||||
"tencent-tokenplan",
|
||||
"together",
|
||||
"venice",
|
||||
"vercel-ai-gateway",
|
||||
"vllm",
|
||||
"volcengine",
|
||||
"volcengine-plan",
|
||||
"vydra",
|
||||
"x-ai",
|
||||
"xai",
|
||||
"xiaomi",
|
||||
"xiaomi-token-plan",
|
||||
"z.ai",
|
||||
"z-ai",
|
||||
"zai",
|
||||
]);
|
||||
|
||||
export function isBuiltInModelProviderOverlayId(providerId: string): boolean {
|
||||
return BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS.has(normalizeProviderId(providerId));
|
||||
}
|
||||
|
||||
const ModelProviderSchema = z
|
||||
.object({
|
||||
// Bundled provider overlays are materialized with an empty-string sentinel.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import * as pluginRuntime from "../plugins/runtime.js";
|
||||
import { createPluginRecord } from "../plugins/status.test-helpers.js";
|
||||
import { readConfigMachineState } from "../state/config-machine-state.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { setTestEnvValue } from "../test-utils/env.js";
|
||||
@@ -17,6 +20,12 @@ const TELEMETRY_URL = "https://telemetry.openclaw.ai/api/latest-version";
|
||||
const TELEMETRY_STATE_KEY = "telemetry.updateCheck";
|
||||
const mockHttp = useMockHttp();
|
||||
|
||||
function installPluginRegistry(...plugins: Parameters<typeof createPluginRecord>[0][]): void {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(...plugins.map((plugin) => createPluginRecord(plugin)));
|
||||
pluginRuntime.setActivePluginRegistry(registry);
|
||||
}
|
||||
|
||||
function createFeatureConfig(enabled = true): OpenClawConfig {
|
||||
return {
|
||||
telemetry: { enabled },
|
||||
@@ -32,6 +41,7 @@ function createFeatureConfig(enabled = true): OpenClawConfig {
|
||||
channels: {
|
||||
telegram: { enabled: true, botToken: "private-telegram-token" },
|
||||
discord: { enabled: true, token: "private-discord-token" },
|
||||
"acme-internal-crm": { enabled: true },
|
||||
slack: { enabled: false, botToken: "private-slack-token" },
|
||||
defaults: { groupPolicy: "allowlist" },
|
||||
modelByChannel: { telegram: { "private-account-id": "openai/private-model" } },
|
||||
@@ -48,6 +58,10 @@ function createFeatureConfig(enabled = true): OpenClawConfig {
|
||||
apiKey: "private-anthropic-api-key",
|
||||
models: [],
|
||||
},
|
||||
"acme-llm": {
|
||||
baseUrl: "https://private-llm.example.invalid/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
@@ -55,6 +69,8 @@ function createFeatureConfig(enabled = true): OpenClawConfig {
|
||||
telegram: { enabled: true },
|
||||
discord: { enabled: true },
|
||||
memory: { enabled: true },
|
||||
"acme-internal-crm": { enabled: true },
|
||||
"acme-internal-workflows": { enabled: true },
|
||||
disabled: { enabled: false },
|
||||
},
|
||||
},
|
||||
@@ -76,6 +92,16 @@ describe("anonymous telemetry", () => {
|
||||
OPENCLAW_TELEMETRY_ENDPOINT: undefined,
|
||||
},
|
||||
});
|
||||
installPluginRegistry(
|
||||
{ id: "telegram", origin: "bundled", channelIds: ["telegram"] },
|
||||
{ id: "discord", origin: "bundled", channelIds: ["discord"] },
|
||||
{ id: "memory", origin: "bundled" },
|
||||
{ id: "acme-internal-crm", channelIds: ["acme-internal-crm"] },
|
||||
{ id: "acme-internal-workflows" },
|
||||
{ id: "disabled", origin: "bundled", enabled: false, status: "disabled" },
|
||||
{ id: "load-error", origin: "bundled", status: "error" },
|
||||
{ id: "deferred", origin: "bundled", imported: false },
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -96,7 +122,8 @@ describe("anonymous telemetry", () => {
|
||||
features: {
|
||||
channels: ["discord", "telegram"],
|
||||
providerFamilies: ["anthropic", "openai"],
|
||||
pluginsEnabled: 3,
|
||||
plugins: ["discord", "memory", "telegram"],
|
||||
pluginsEnabled: 5,
|
||||
sessionsLast24h: expect.any(Number),
|
||||
},
|
||||
});
|
||||
@@ -104,20 +131,27 @@ describe("anonymous telemetry", () => {
|
||||
/"(?:id|accountId|userId|machineId|installId|token|apiKey|secret|password|prompt|message|host|hostname|baseUrl|path|email|models)"\s*:/iu,
|
||||
);
|
||||
expect(serialized).not.toContain("private-");
|
||||
expect(serialized).not.toContain("acme-internal-crm");
|
||||
expect(serialized).not.toContain("acme-internal-workflows");
|
||||
expect(serialized).not.toContain("acme-llm");
|
||||
expect(serialized).not.toContain("example.invalid");
|
||||
expect(serialized).not.toContain("@");
|
||||
expect(serialized).not.toContain(testState.stateDir);
|
||||
expect(payload.features.sessionsLast24h).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("counts auto-enabled channel plugins and provider families configured through agent models", () => {
|
||||
it("counts loaded default plugins instead of unloaded config entries and accepts official provider families", () => {
|
||||
installPluginRegistry(
|
||||
{ id: "whatsapp", origin: "bundled", channelIds: ["whatsapp"] },
|
||||
{ id: "diagnostics-otel", origin: "bundled" },
|
||||
);
|
||||
const payload = buildTelemetryPayload(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/private-model",
|
||||
fallbacks: ["openai/private-fallback"],
|
||||
fallbacks: ["openai/private-fallback", "cohere/private-official-model"],
|
||||
},
|
||||
},
|
||||
entries: {
|
||||
@@ -125,19 +159,57 @@ describe("anonymous telemetry", () => {
|
||||
},
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
|
||||
plugins: { entries: { "never-loaded": { enabled: true } } },
|
||||
},
|
||||
{ surface: "gateway" },
|
||||
);
|
||||
|
||||
expect(payload.features).toMatchObject({
|
||||
channels: ["whatsapp"],
|
||||
providerFamilies: ["anthropic", "google", "openai"],
|
||||
pluginsEnabled: 1,
|
||||
providerFamilies: ["anthropic", "cohere", "google", "openai"],
|
||||
plugins: ["diagnostics-otel", "whatsapp"],
|
||||
pluginsEnabled: 2,
|
||||
});
|
||||
expect(JSON.stringify(payload)).not.toContain("private-");
|
||||
expect(JSON.stringify(payload)).not.toContain("+15555550123");
|
||||
});
|
||||
|
||||
it("classifies configured channels by their loaded plugin owner", () => {
|
||||
installPluginRegistry(
|
||||
{ id: "public-channel-owner", origin: "bundled", channelIds: ["public-alias"] },
|
||||
{ id: "acme-internal-crm", channelIds: ["telegram"] },
|
||||
);
|
||||
|
||||
const payload = buildTelemetryPayload(
|
||||
{ channels: { "public-alias": { enabled: true }, telegram: { enabled: true } } },
|
||||
{ surface: "gateway" },
|
||||
);
|
||||
|
||||
expect(payload.features.channels).toEqual(["public-alias"]);
|
||||
expect(payload.features.plugins).toEqual(["public-channel-owner"]);
|
||||
expect(payload.features.pluginsEnabled).toBe(2);
|
||||
expect(JSON.stringify(payload)).not.toContain("acme-internal-crm");
|
||||
});
|
||||
|
||||
it("uses manifest-owned plugin activation when a CLI has no active runtime registry", () => {
|
||||
const activeRegistry = vi.spyOn(pluginRuntime, "getActivePluginRegistry").mockReturnValue(null);
|
||||
try {
|
||||
const payload = buildTelemetryPayload(
|
||||
{
|
||||
channels: { telegram: { enabled: true } },
|
||||
plugins: { allow: ["telegram"] },
|
||||
},
|
||||
{ surface: "cli" },
|
||||
);
|
||||
|
||||
expect(payload.features.channels).toEqual(["telegram"]);
|
||||
expect(payload.features.plugins).toContain("telegram");
|
||||
expect(payload.features.pluginsEnabled).toBe(payload.features.plugins.length);
|
||||
} finally {
|
||||
activeRegistry.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("counts only session creation events from the previous 24 hours", async () => {
|
||||
const { recordSessionStateEvent } = await import("../sessions/session-state-events.js");
|
||||
const now = Date.now();
|
||||
|
||||
+21
-18
@@ -3,8 +3,12 @@ import { parseModelCatalogRef } from "@openclaw/model-catalog-core/model-catalog
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { z } from "zod";
|
||||
import { isChannelConfigMetadataKey } from "../channels/config-metadata.js";
|
||||
import { isBuiltInModelProviderOverlayId } from "../config/model-provider-config.js";
|
||||
import { resolveIsNixMode } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveOfficialExternalProviderPluginIds } from "../plugins/official-external-plugin-catalog.js";
|
||||
import { isPubliclyKnownPluginId } from "../plugins/plugin-public-identity.js";
|
||||
import { listEnabledPluginRecords } from "../plugins/plugin-runtime-inventory.js";
|
||||
import { readConfigMachineState, writeConfigMachineState } from "../state/config-machine-state.js";
|
||||
import { withExistingOpenClawStateDatabaseReadOnly } from "../state/openclaw-state-db-readonly.js";
|
||||
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
||||
@@ -42,6 +46,7 @@ type TelemetryPayload = {
|
||||
features: {
|
||||
channels: string[];
|
||||
providerFamilies: string[];
|
||||
plugins: string[];
|
||||
pluginsEnabled: number;
|
||||
sessionsLast24h: number;
|
||||
};
|
||||
@@ -81,17 +86,6 @@ function isDoNotTrackEnabled(): boolean {
|
||||
return value === "1" || value === "true";
|
||||
}
|
||||
|
||||
function isEnabledPlugin(config: OpenClawConfig, pluginId: string): boolean {
|
||||
const plugins = config.plugins;
|
||||
if (plugins?.enabled === false || plugins?.deny?.includes(pluginId)) {
|
||||
return false;
|
||||
}
|
||||
if (plugins?.allow && plugins.allow.length > 0 && !plugins.allow.includes(pluginId)) {
|
||||
return false;
|
||||
}
|
||||
return plugins?.entries?.[pluginId]?.enabled !== false;
|
||||
}
|
||||
|
||||
function countRecentSessions(nowMs: number): number {
|
||||
try {
|
||||
return (
|
||||
@@ -163,6 +157,9 @@ export function buildTelemetryPayload(
|
||||
config: OpenClawConfig,
|
||||
options: { surface: TelemetrySurface },
|
||||
): TelemetryPayload {
|
||||
const enabledPlugins = listEnabledPluginRecords(config);
|
||||
const publicPlugins = enabledPlugins.filter(isPubliclyKnownPluginId);
|
||||
const publicChannelIds = new Set(publicPlugins.flatMap((plugin) => plugin.channelIds));
|
||||
const channels = Object.entries(config.channels ?? {})
|
||||
.filter(
|
||||
([channelId, channelConfig]) =>
|
||||
@@ -170,7 +167,7 @@ export function buildTelemetryPayload(
|
||||
!isChannelConfigMetadataKey(channelId) &&
|
||||
isRecord(channelConfig) &&
|
||||
channelConfig.enabled !== false &&
|
||||
isEnabledPlugin(config, channelId),
|
||||
publicChannelIds.has(channelId),
|
||||
)
|
||||
.map(([channelId]) => channelId)
|
||||
.toSorted();
|
||||
@@ -185,12 +182,17 @@ export function buildTelemetryPayload(
|
||||
),
|
||||
];
|
||||
const providerFamilies = [...new Set(configuredProviders)]
|
||||
.filter((providerId) => SAFE_FEATURE_NAME.test(providerId))
|
||||
.filter(
|
||||
(providerId) =>
|
||||
SAFE_FEATURE_NAME.test(providerId) &&
|
||||
(isBuiltInModelProviderOverlayId(providerId) ||
|
||||
resolveOfficialExternalProviderPluginIds({ providerIds: new Set([providerId]) }).length >
|
||||
0),
|
||||
)
|
||||
.toSorted();
|
||||
const plugins = [...new Set(publicPlugins.map((plugin) => plugin.id))]
|
||||
.filter((pluginId) => SAFE_FEATURE_NAME.test(pluginId))
|
||||
.toSorted();
|
||||
const enabledPluginEntries = Object.keys(config.plugins?.entries ?? {}).filter(
|
||||
(pluginId) => SAFE_FEATURE_NAME.test(pluginId) && isEnabledPlugin(config, pluginId),
|
||||
);
|
||||
const pluginsEnabled = new Set([...channels, ...enabledPluginEntries]).size;
|
||||
|
||||
return {
|
||||
schema: 1,
|
||||
@@ -201,7 +203,8 @@ export function buildTelemetryPayload(
|
||||
features: {
|
||||
channels,
|
||||
providerFamilies,
|
||||
pluginsEnabled,
|
||||
plugins,
|
||||
pluginsEnabled: enabledPlugins.length,
|
||||
sessionsLast24h: countRecentSessions(Date.now()),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as officialCatalog from "./official-external-plugin-catalog.js";
|
||||
import { isPubliclyKnownPluginId } from "./plugin-public-identity.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("isPubliclyKnownPluginId", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "bundled plugins",
|
||||
plugin: { id: "bundled-plugin", origin: "bundled" as const },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "verified official installs",
|
||||
plugin: { id: "official-plugin", origin: "global" as const, trustedOfficialInstall: true },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "official source checkouts with their catalog package",
|
||||
plugin: {
|
||||
id: "opencode",
|
||||
origin: "workspace" as const,
|
||||
packageName: "@openclaw/opencode-provider",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "private packages squatting on an official plugin id",
|
||||
plugin: { id: "opencode", origin: "workspace" as const, packageName: "@acme/private" },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "unknown private plugin ids",
|
||||
plugin: { id: "acme-internal-crm", origin: "workspace" as const },
|
||||
expected: false,
|
||||
},
|
||||
])("classifies $name", ({ plugin, expected }) => {
|
||||
expect(isPubliclyKnownPluginId(plugin)).toBe(expected);
|
||||
});
|
||||
|
||||
it("never consults the hosted catalog when classifying private plugin identities", () => {
|
||||
const hostedCatalog = vi
|
||||
.spyOn(officialCatalog, "loadConfiguredHostedOfficialExternalPluginCatalogEntries")
|
||||
.mockResolvedValue({
|
||||
source: "bundled-fallback",
|
||||
entries: [{ id: "acme-internal-crm", name: "@acme/internal-crm" }],
|
||||
error: "hosted fixture",
|
||||
});
|
||||
|
||||
expect(
|
||||
isPubliclyKnownPluginId({
|
||||
id: "acme-internal-crm",
|
||||
origin: "global",
|
||||
packageName: "@acme/internal-crm",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(hostedCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
getOfficialExternalPluginCatalogEntryForPackage,
|
||||
isOfficialExternalPluginId,
|
||||
resolveOfficialExternalPluginId,
|
||||
} from "./official-external-plugin-catalog.js";
|
||||
import type { PluginRecord } from "./registry-types.js";
|
||||
|
||||
type PluginPublicIdentityInput = Pick<
|
||||
PluginRecord,
|
||||
"id" | "origin" | "packageName" | "trustedOfficialInstall"
|
||||
>;
|
||||
|
||||
/** True when a plugin identity is already public and safe to report. */
|
||||
export function isPubliclyKnownPluginId(plugin: PluginPublicIdentityInput): boolean {
|
||||
if (plugin.origin === "bundled" || plugin.trustedOfficialInstall === true) {
|
||||
return true;
|
||||
}
|
||||
if (!isOfficialExternalPluginId(plugin.id)) {
|
||||
return false;
|
||||
}
|
||||
const catalogEntry = getOfficialExternalPluginCatalogEntryForPackage(plugin.packageName);
|
||||
return catalogEntry !== undefined && resolveOfficialExternalPluginId(catalogEntry) === plugin.id;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js";
|
||||
import { isPluginEnabledByDefaultForPlatform } from "./default-enablement.js";
|
||||
import { loadPluginManifestRegistryCore } from "./manifest-registry.js";
|
||||
import type { PluginRecord } from "./registry-types.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
type EnabledPluginRecord = Pick<
|
||||
PluginRecord,
|
||||
"id" | "origin" | "packageName" | "trustedOfficialInstall" | "channelIds"
|
||||
>;
|
||||
|
||||
/** Lists loaded plugin identities, or configured manifest identities before runtime activation. */
|
||||
export function listEnabledPluginRecords(config: OpenClawConfig): EnabledPluginRecord[] {
|
||||
const registry = getActivePluginRegistry();
|
||||
if (registry) {
|
||||
return registry.plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.enabled &&
|
||||
plugin.status === "loaded" &&
|
||||
(plugin.format === "bundle" || plugin.imported !== false),
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedConfig = normalizePluginsConfig(config.plugins);
|
||||
return loadPluginManifestRegistryCore({ config })
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
resolveEffectivePluginActivationState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: normalizedConfig,
|
||||
rootConfig: config,
|
||||
enabledByDefault: isPluginEnabledByDefaultForPlatform(plugin),
|
||||
}).enabled,
|
||||
)
|
||||
.map((plugin) => ({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
packageName: plugin.packageName,
|
||||
trustedOfficialInstall: plugin.trustedOfficialInstall,
|
||||
channelIds: plugin.channels,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user