mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 17:41:37 +00:00
561 lines
19 KiB
TypeScript
561 lines
19 KiB
TypeScript
/**
|
|
* Reads Codex plugin marketplace state and app inventory to decide which
|
|
* plugin-owned apps can be exposed to a native Codex thread.
|
|
*/
|
|
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
import type {
|
|
CodexAppInventoryCache,
|
|
CodexAppInventoryCacheRead,
|
|
CodexAppInventoryRequest,
|
|
} from "./app-inventory-cache.js";
|
|
import {
|
|
CODEX_PLUGINS_MARKETPLACE_NAME,
|
|
CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME,
|
|
resolveCodexPluginsPolicy,
|
|
type CodexPluginMarketplaceName,
|
|
type ResolvedCodexPluginPolicy,
|
|
type ResolvedCodexPluginsPolicy,
|
|
} from "./config.js";
|
|
import type {
|
|
CodexPluginMetadataCache,
|
|
CodexPluginMetadataQueryKind,
|
|
} from "./plugin-metadata-cache.js";
|
|
import type { CodexAppServerRequestResult, v2 } from "./protocol.js";
|
|
|
|
const CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME = `${CODEX_PLUGINS_MARKETPLACE_NAME}-remote`;
|
|
// Codex serves the curated catalog under this wire name for API-key/Bedrock
|
|
// accounts (codex-rs/core-plugins is_openai_curated_marketplace_name). It is
|
|
// the same logical catalog, so configured `openai-curated` plugins resolve
|
|
// from it and marketplace refs normalize back to CODEX_PLUGINS_MARKETPLACE_NAME.
|
|
const CODEX_PLUGINS_API_MARKETPLACE_NAME = "openai-api-curated";
|
|
|
|
/** Request callback used to call Codex app-server plugin/app methods. */
|
|
export type CodexPluginRuntimeRequest = (method: string, params?: unknown) => Promise<unknown>;
|
|
|
|
type CodexPluginMarketplaceResponse = v2.PluginInstalledResponse | v2.PluginListResponse;
|
|
|
|
/** Stable reference to a supported Codex plugin marketplace. */
|
|
export type CodexPluginMarketplaceRef = {
|
|
name: CodexPluginMarketplaceName;
|
|
path?: string;
|
|
remoteMarketplaceName?: string;
|
|
};
|
|
|
|
/** Machine-readable inventory diagnostic code used by thread config builders. */
|
|
type CodexPluginInventoryDiagnosticCode =
|
|
| "disabled"
|
|
| "marketplace_missing"
|
|
| "plugin_missing"
|
|
| "plugin_disabled"
|
|
| "plugin_detail_unavailable"
|
|
| "app_inventory_missing"
|
|
| "app_inventory_stale"
|
|
| "app_ownership_ambiguous";
|
|
|
|
/** Diagnostic explaining why a configured plugin or app cannot be exposed. */
|
|
export type CodexPluginInventoryDiagnostic = {
|
|
code: CodexPluginInventoryDiagnosticCode;
|
|
plugin?: ResolvedCodexPluginPolicy;
|
|
message: string;
|
|
};
|
|
|
|
/** App owned by a Codex plugin with current accessibility/auth state. */
|
|
export type CodexPluginOwnedApp = {
|
|
id: string;
|
|
name: string;
|
|
accessible: boolean;
|
|
enabled: boolean;
|
|
needsAuth: boolean;
|
|
};
|
|
|
|
/** Inventory record for one configured Codex plugin policy. */
|
|
export type CodexPluginInventoryRecord = {
|
|
policy: ResolvedCodexPluginPolicy;
|
|
summary: v2.PluginSummary;
|
|
detail?: v2.PluginDetail;
|
|
activationRequired: boolean;
|
|
authRequired: boolean;
|
|
appOwnership: "proven" | "ambiguous" | "none";
|
|
ownedAppIds: string[];
|
|
apps: CodexPluginOwnedApp[];
|
|
};
|
|
|
|
/** Complete inventory result for configured Codex plugins and owned apps. */
|
|
export type CodexPluginInventory = {
|
|
policy: ResolvedCodexPluginsPolicy;
|
|
records: CodexPluginInventoryRecord[];
|
|
diagnostics: CodexPluginInventoryDiagnostic[];
|
|
appInventory?: CodexAppInventoryCacheRead;
|
|
};
|
|
|
|
/** Inputs for reading plugin marketplace/detail state and cached app inventory. */
|
|
type ReadCodexPluginInventoryParams = {
|
|
pluginConfig?: unknown;
|
|
policy?: ResolvedCodexPluginsPolicy;
|
|
request: CodexPluginRuntimeRequest;
|
|
appCache?: CodexAppInventoryCache;
|
|
appCacheKey?: string;
|
|
configCwd?: string;
|
|
metadataCache?: CodexPluginMetadataCache;
|
|
nowMs?: number;
|
|
readPluginDetails?: boolean;
|
|
suppressAppInventoryRefresh?: boolean;
|
|
};
|
|
|
|
/** Reads configured Codex plugin state and maps owned apps to readiness diagnostics. */
|
|
export async function readCodexPluginInventory(
|
|
params: ReadCodexPluginInventoryParams,
|
|
): Promise<CodexPluginInventory> {
|
|
const policy = params.policy ?? resolveCodexPluginsPolicy(params.pluginConfig);
|
|
if (!policy.enabled) {
|
|
return {
|
|
policy,
|
|
records: [],
|
|
diagnostics: [
|
|
{
|
|
code: "disabled",
|
|
message: "Native Codex plugin support is disabled.",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const appInventory = readCachedAppInventory(params);
|
|
const installedPlugins = await readInstalledCodexPluginMetadata({ ...params, policy });
|
|
let curatedCatalog: Promise<v2.PluginListResponse> | undefined;
|
|
|
|
const diagnostics: CodexPluginInventoryDiagnostic[] = [];
|
|
const records: CodexPluginInventoryRecord[] = [];
|
|
if (appInventory?.state === "missing") {
|
|
diagnostics.push({
|
|
code: "app_inventory_missing",
|
|
message: "Cached Codex app inventory is missing; plugin apps are excluded for this setup.",
|
|
});
|
|
} else if (appInventory?.state === "stale") {
|
|
diagnostics.push({
|
|
code: "app_inventory_stale",
|
|
message: "Cached Codex app inventory is stale; using stale app readiness and refreshing.",
|
|
});
|
|
}
|
|
|
|
for (const pluginPolicy of policy.pluginPolicies) {
|
|
if (!pluginPolicy.enabled && !policy.allowAllPlugins) {
|
|
continue;
|
|
}
|
|
let listed: CodexPluginMarketplaceResponse = installedPlugins;
|
|
let resolvedPlugin =
|
|
pluginPolicy.marketplaceName === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME
|
|
? findWorkspaceMarketplacePlugin(listed, pluginPolicy.pluginName)
|
|
: findOpenAiCuratedMarketplacePlugin(listed, pluginPolicy.pluginName);
|
|
if (
|
|
!resolvedPlugin &&
|
|
pluginPolicy.enabled &&
|
|
pluginPolicy.marketplaceName === CODEX_PLUGINS_MARKETPLACE_NAME
|
|
) {
|
|
// The installed snapshot deliberately excludes remote catalog entries.
|
|
// Fetch the catalog only to install an explicitly requested missing plugin.
|
|
curatedCatalog ??= listCodexPluginMetadata(params);
|
|
listed = await curatedCatalog;
|
|
resolvedPlugin = findOpenAiCuratedMarketplacePlugin(listed, pluginPolicy.pluginName);
|
|
}
|
|
const hasMarketplace =
|
|
pluginPolicy.marketplaceName === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME
|
|
? listed.marketplaces.some(
|
|
(entry) => entry.name === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME,
|
|
)
|
|
: listed.marketplaces.some(isOpenAiCuratedMarketplace);
|
|
if (!hasMarketplace) {
|
|
diagnostics.push({
|
|
code: "marketplace_missing",
|
|
plugin: pluginPolicy,
|
|
message: `Codex marketplace ${pluginPolicy.marketplaceName} was not found.`,
|
|
});
|
|
continue;
|
|
}
|
|
if (!resolvedPlugin) {
|
|
diagnostics.push({
|
|
code: "plugin_missing",
|
|
plugin: pluginPolicy,
|
|
message: `${pluginPolicy.pluginName} was not found in ${pluginPolicy.marketplaceName}.`,
|
|
});
|
|
continue;
|
|
}
|
|
const { summary } = resolvedPlugin;
|
|
const pluginMarketplace = marketplaceRef(
|
|
resolvedPlugin.marketplace,
|
|
pluginPolicy.marketplaceName,
|
|
);
|
|
const detail = await readPluginDetail(
|
|
params,
|
|
pluginMarketplace,
|
|
pluginPolicy,
|
|
summary,
|
|
diagnostics,
|
|
);
|
|
const ownedAppIds =
|
|
detail?.apps
|
|
.map((app) => app.id)
|
|
.filter(Boolean)
|
|
.toSorted() ?? [];
|
|
const appOwnership = resolveAppOwnership({
|
|
detail,
|
|
appInventory,
|
|
summary,
|
|
});
|
|
if (appOwnership === "ambiguous") {
|
|
diagnostics.push({
|
|
code: "app_ownership_ambiguous",
|
|
plugin: pluginPolicy,
|
|
message: `${pluginPolicy.pluginName} has only display-name app matches; apps are not exposed until ownership is stable.`,
|
|
});
|
|
}
|
|
if (summary.installed && !summary.enabled) {
|
|
diagnostics.push({
|
|
code: "plugin_disabled",
|
|
plugin: pluginPolicy,
|
|
message: `${pluginPolicy.pluginName} is installed in Codex but disabled.`,
|
|
});
|
|
}
|
|
|
|
const apps = resolveOwnedApps({
|
|
pluginPolicy,
|
|
detail,
|
|
appInventory,
|
|
});
|
|
records.push({
|
|
policy: pluginPolicy,
|
|
summary,
|
|
...(detail ? { detail } : {}),
|
|
activationRequired: pluginPolicy.enabled && (!summary.installed || !summary.enabled),
|
|
authRequired: apps.some((app) => app.needsAuth || !app.accessible),
|
|
appOwnership,
|
|
ownedAppIds,
|
|
apps,
|
|
});
|
|
}
|
|
|
|
const inventory = {
|
|
policy,
|
|
records,
|
|
diagnostics,
|
|
...(appInventory ? { appInventory } : {}),
|
|
};
|
|
return inventory;
|
|
}
|
|
|
|
/** Finds one plugin summary in the OpenAI curated marketplace response. */
|
|
export function findOpenAiCuratedPluginSummary(
|
|
listed: CodexPluginMarketplaceResponse,
|
|
pluginName: string,
|
|
): { marketplace: CodexPluginMarketplaceRef; summary: v2.PluginSummary } | undefined {
|
|
const resolved = findOpenAiCuratedMarketplacePlugin(listed, pluginName);
|
|
return resolved
|
|
? {
|
|
marketplace: marketplaceRef(resolved.marketplace, CODEX_PLUGINS_MARKETPLACE_NAME),
|
|
summary: resolved.summary,
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
/** Builds plugin/read or plugin/install params from a marketplace reference. */
|
|
export function pluginReadParams(
|
|
marketplace: CodexPluginMarketplaceRef,
|
|
pluginName: string,
|
|
): v2.PluginReadParams {
|
|
return {
|
|
...(marketplace.path ? { marketplacePath: marketplace.path } : {}),
|
|
...(marketplace.remoteMarketplaceName
|
|
? { remoteMarketplaceName: marketplace.remoteMarketplaceName }
|
|
: {}),
|
|
pluginName,
|
|
};
|
|
}
|
|
|
|
/** Returns configured plugin keys whose current metadata may still recover. */
|
|
export function resolveRecoverableCodexPluginConfigKeys(params: {
|
|
policy: ResolvedCodexPluginsPolicy;
|
|
metadataCache: CodexPluginMetadataCache;
|
|
appCacheKey: string;
|
|
configCwd?: string;
|
|
}): string[] {
|
|
return params.policy.pluginPolicies
|
|
.filter(
|
|
(pluginPolicy) =>
|
|
pluginPolicy.enabled &&
|
|
!isSettledMissingPluginPolicy({
|
|
pluginPolicy,
|
|
metadataCache: params.metadataCache,
|
|
appCacheKey: params.appCacheKey,
|
|
configCwd: params.configCwd,
|
|
}),
|
|
)
|
|
.map((pluginPolicy) => pluginPolicy.configKey)
|
|
.toSorted();
|
|
}
|
|
|
|
async function listCodexPluginMetadata(
|
|
params: ReadCodexPluginInventoryParams,
|
|
): Promise<v2.PluginListResponse> {
|
|
const requestParams = {} satisfies v2.PluginListParams;
|
|
if (!params.metadataCache || !params.appCacheKey) {
|
|
return (await params.request("plugin/list", requestParams)) as v2.PluginListResponse;
|
|
}
|
|
const snapshot = await params.metadataCache.load({
|
|
appCacheKey: params.appCacheKey,
|
|
queryKind: "curated-global",
|
|
requestParams,
|
|
request: async (method, listedParams) =>
|
|
(await params.request(method, listedParams)) as v2.PluginListResponse,
|
|
// Upstream fail-open: with omitted marketplaceKinds a remote catalog fetch
|
|
// failure only warns and returns local marketplaces (no load error), which
|
|
// is indistinguishable from a genuinely absent plugin. Settle curated
|
|
// negatives only when the curated marketplace itself is present.
|
|
cacheable: (response: v2.PluginListResponse) =>
|
|
response.marketplaces.some((marketplace) => isOpenAiCuratedMarketplace(marketplace)),
|
|
});
|
|
return snapshot.response;
|
|
}
|
|
|
|
async function readInstalledCodexPluginMetadata(
|
|
params: ReadCodexPluginInventoryParams & { policy: ResolvedCodexPluginsPolicy },
|
|
): Promise<v2.PluginInstalledResponse> {
|
|
const requestParams = (
|
|
params.configCwd ? { cwds: [params.configCwd] } : {}
|
|
) satisfies v2.PluginInstalledParams;
|
|
if (!params.metadataCache || !params.appCacheKey) {
|
|
return (await params.request("plugin/installed", requestParams)) as v2.PluginInstalledResponse;
|
|
}
|
|
const snapshot = await params.metadataCache.load({
|
|
appCacheKey: params.appCacheKey,
|
|
queryKind: "installed",
|
|
requestParams,
|
|
request: async (method, installedParams) =>
|
|
(await params.request(method, installedParams)) as v2.PluginInstalledResponse,
|
|
// Codex can fail open to local-only marketplaces when its remote installed
|
|
// fetch fails. Never settle a snapshot that cannot prove a configured owner.
|
|
cacheable: (response) =>
|
|
params.policy.pluginPolicies.every((pluginPolicy) => {
|
|
if (!pluginPolicy.enabled && !params.policy.allowAllPlugins) {
|
|
return true;
|
|
}
|
|
return pluginPolicy.marketplaceName === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME
|
|
? findWorkspaceMarketplacePlugin(response, pluginPolicy.pluginName) !== undefined
|
|
: findOpenAiCuratedMarketplacePlugin(response, pluginPolicy.pluginName) !== undefined;
|
|
}),
|
|
});
|
|
return snapshot.response;
|
|
}
|
|
|
|
function isSettledMissingPluginPolicy(params: {
|
|
pluginPolicy: ResolvedCodexPluginPolicy;
|
|
metadataCache: CodexPluginMetadataCache;
|
|
appCacheKey: string;
|
|
configCwd?: string;
|
|
}): boolean {
|
|
const queryKind: CodexPluginMetadataQueryKind =
|
|
params.pluginPolicy.marketplaceName === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME
|
|
? "installed"
|
|
: "curated-global";
|
|
const requestParams =
|
|
queryKind === "installed" && params.configCwd ? { cwds: [params.configCwd] } : {};
|
|
const listed = params.metadataCache.read(params.appCacheKey, queryKind, requestParams)?.response;
|
|
if (!listed) {
|
|
return false;
|
|
}
|
|
if (queryKind === "installed") {
|
|
return !findWorkspaceMarketplacePlugin(listed, params.pluginPolicy.pluginName);
|
|
}
|
|
return !findOpenAiCuratedMarketplacePlugin(listed, params.pluginPolicy.pluginName);
|
|
}
|
|
|
|
function readCachedAppInventory(
|
|
params: ReadCodexPluginInventoryParams,
|
|
): CodexAppInventoryCacheRead | undefined {
|
|
if (!params.appCache || !params.appCacheKey) {
|
|
return undefined;
|
|
}
|
|
const request: CodexAppInventoryRequest = async (method, requestParams) =>
|
|
(await params.request(method, requestParams)) as CodexAppServerRequestResult<typeof method>;
|
|
return params.appCache.read({
|
|
key: params.appCacheKey,
|
|
request,
|
|
nowMs: params.nowMs,
|
|
suppressRefresh: params.suppressAppInventoryRefresh,
|
|
});
|
|
}
|
|
|
|
async function readPluginDetail(
|
|
params: ReadCodexPluginInventoryParams,
|
|
marketplace: CodexPluginMarketplaceRef,
|
|
pluginPolicy: ResolvedCodexPluginPolicy,
|
|
summary: v2.PluginSummary,
|
|
diagnostics: CodexPluginInventoryDiagnostic[],
|
|
): Promise<v2.PluginDetail | undefined> {
|
|
if (params.readPluginDetails === false) {
|
|
return undefined;
|
|
}
|
|
if (marketplace.remoteMarketplaceName && !summary.remotePluginId) {
|
|
diagnostics.push({
|
|
code: "plugin_detail_unavailable",
|
|
plugin: pluginPolicy,
|
|
message: `${pluginPolicy.pluginName} detail unavailable: Codex did not return a remote plugin id.`,
|
|
});
|
|
return undefined;
|
|
}
|
|
try {
|
|
const response = (await params.request(
|
|
"plugin/read",
|
|
pluginReadParams(
|
|
marketplace,
|
|
marketplace.remoteMarketplaceName && summary.remotePluginId
|
|
? summary.remotePluginId
|
|
: pluginPolicy.pluginName,
|
|
),
|
|
)) as v2.PluginReadResponse;
|
|
return response.plugin;
|
|
} catch (error) {
|
|
diagnostics.push({
|
|
code: "plugin_detail_unavailable",
|
|
plugin: pluginPolicy,
|
|
message: `${pluginPolicy.pluginName} detail unavailable: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
});
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function resolveAppOwnership(params: {
|
|
detail?: v2.PluginDetail;
|
|
appInventory?: CodexAppInventoryCacheRead;
|
|
summary: v2.PluginSummary;
|
|
}): "proven" | "ambiguous" | "none" {
|
|
if (params.detail && params.detail.apps.length > 0) {
|
|
return "proven";
|
|
}
|
|
const apps = params.appInventory?.snapshot?.apps ?? [];
|
|
const displayMatches = apps.filter((app) =>
|
|
app.pluginDisplayNames.some((displayName) => displayName === params.summary.name),
|
|
);
|
|
return displayMatches.length > 0 ? "ambiguous" : "none";
|
|
}
|
|
|
|
function resolveOwnedApps(params: {
|
|
pluginPolicy: ResolvedCodexPluginPolicy;
|
|
detail?: v2.PluginDetail;
|
|
appInventory?: CodexAppInventoryCacheRead;
|
|
}): CodexPluginOwnedApp[] {
|
|
const detailApps = params.detail?.apps ?? [];
|
|
if (detailApps.length === 0) {
|
|
return [];
|
|
}
|
|
if (params.appInventory?.state === "missing") {
|
|
embeddedAgentLog.warn("codex plugin inventory missing app inventory for detail apps", {
|
|
configKey: params.pluginPolicy.configKey,
|
|
pluginName: params.pluginPolicy.pluginName,
|
|
appIds: detailApps.map((app) => app.id).toSorted(),
|
|
});
|
|
return [];
|
|
}
|
|
const appInfoById = new Map(
|
|
(params.appInventory?.snapshot?.apps ?? []).map((app) => [app.id, app] as const),
|
|
);
|
|
return detailApps
|
|
.map((app) => {
|
|
const info = appInfoById.get(app.id);
|
|
if (!info) {
|
|
return {
|
|
id: app.id,
|
|
name: app.name,
|
|
accessible: false,
|
|
enabled: false,
|
|
needsAuth: true,
|
|
};
|
|
}
|
|
return {
|
|
id: app.id,
|
|
name: app.name,
|
|
accessible: info.isAccessible,
|
|
enabled: info.isEnabled,
|
|
// Modern plugin summaries carry no auth bit; account-authorized
|
|
// app/read metadata is the canonical connector access proof.
|
|
needsAuth: !info.isAccessible,
|
|
};
|
|
})
|
|
.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
}
|
|
|
|
function findPluginSummary(
|
|
marketplace: v2.PluginMarketplaceEntry,
|
|
pluginName: string,
|
|
): v2.PluginSummary | undefined {
|
|
return marketplace.plugins.find(
|
|
(plugin) =>
|
|
plugin.name === pluginName ||
|
|
plugin.id === pluginName ||
|
|
plugin.id === `${pluginName}@${marketplace.name}` ||
|
|
pluginNameFromPluginId(plugin.id, marketplace.name) === pluginName,
|
|
);
|
|
}
|
|
|
|
function findOpenAiCuratedMarketplacePlugin(
|
|
listed: CodexPluginMarketplaceResponse,
|
|
pluginName: string,
|
|
): { marketplace: v2.PluginMarketplaceEntry; summary: v2.PluginSummary } | undefined {
|
|
for (const marketplace of listed.marketplaces) {
|
|
if (!isOpenAiCuratedMarketplace(marketplace)) {
|
|
continue;
|
|
}
|
|
const summary = findPluginSummary(marketplace, pluginName);
|
|
if (summary) {
|
|
return { marketplace, summary };
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function findWorkspaceMarketplacePlugin(
|
|
listed: CodexPluginMarketplaceResponse,
|
|
pluginName: string,
|
|
): { marketplace: v2.PluginMarketplaceEntry; summary: v2.PluginSummary } | undefined {
|
|
// Workspace display names are not unique; the configured pluginName is the
|
|
// exact catalog id returned by plugin/list.
|
|
const marketplace = listed.marketplaces.find(
|
|
(entry) => entry.name === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME,
|
|
);
|
|
const summary = marketplace?.plugins.find((plugin) => plugin.id === pluginName);
|
|
return marketplace && summary ? { marketplace, summary } : undefined;
|
|
}
|
|
|
|
function pluginNameFromPluginId(pluginId: string, marketplaceName: string): string | undefined {
|
|
const trimmed = pluginId.trim();
|
|
if (!trimmed) {
|
|
return undefined;
|
|
}
|
|
const marketplaceSuffix = `@${marketplaceName}`;
|
|
const withoutMarketplaceSuffix = trimmed.endsWith(marketplaceSuffix)
|
|
? trimmed.slice(0, -marketplaceSuffix.length)
|
|
: trimmed;
|
|
return withoutMarketplaceSuffix.split("/").at(-1)?.trim() || undefined;
|
|
}
|
|
|
|
function marketplaceRef(
|
|
marketplace: v2.PluginMarketplaceEntry,
|
|
name: CodexPluginMarketplaceName,
|
|
): CodexPluginMarketplaceRef {
|
|
return {
|
|
name,
|
|
...(marketplace.path ? { path: marketplace.path } : {}),
|
|
...(!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}),
|
|
};
|
|
}
|
|
|
|
/** True for any supported OpenAI curated marketplace wire name, matching Codex's own curated predicate. */
|
|
export function isOpenAiCuratedMarketplace(marketplace: v2.PluginMarketplaceEntry): boolean {
|
|
return (
|
|
marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME ||
|
|
marketplace.name === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME ||
|
|
marketplace.name === CODEX_PLUGINS_API_MARKETPLACE_NAME
|
|
);
|
|
}
|