feat: carry Featured state into Control (#109418)

* feat: consume live featured catalog state

* fix(plugins): honor hosted curation for published bundled plugins

* fix(plugins): accept package-less trusted ClawHub provenance
This commit is contained in:
Patrick Erichsen
2026-07-16 20:27:07 -07:00
committed by GitHub
parent 6a89730c7b
commit bd2bf34b41
6 changed files with 722 additions and 18 deletions
@@ -0,0 +1,526 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
metadata: vi.fn(),
officialCatalog: vi.fn(),
}));
vi.mock("./plugin-metadata-snapshot.js", () => ({
loadPluginMetadataSnapshot: (...args: unknown[]) => mocks.metadata(...args),
}));
vi.mock("./official-external-plugin-catalog.js", async (importOriginal) => ({
...(await importOriginal<typeof import("./official-external-plugin-catalog.js")>()),
loadConfiguredHostedOfficialExternalPluginCatalogEntries: (...args: unknown[]) =>
mocks.officialCatalog(...args),
}));
const { clearManagedPluginOfficialCatalogCache, listManagedPlugins } =
await import("./management-service.js");
function metadataSnapshot(params: {
id?: string;
name?: string;
origin?: "bundled" | "global";
packageName?: string | null;
installRecord?: Record<string, unknown>;
featured?: boolean;
}) {
const id = params.id ?? "workboard";
const packageName =
params.packageName === null ? undefined : (params.packageName ?? `@openclaw/${id}`);
const manifest = {
id,
name: params.name ?? "Workboard",
description: "Coordinate agent work in a shared board.",
catalog: { featured: params.featured ?? true, order: 10 },
channels: [],
providers: [],
cliBackends: [],
skills: [],
hooks: [],
origin: params.origin ?? "bundled",
rootDir: `/tmp/${id}`,
source: `/tmp/${id}/index.ts`,
manifestPath: `/tmp/${id}/openclaw.plugin.json`,
};
return {
index: {
plugins: [
{
pluginId: id,
...(packageName ? { packageName } : {}),
origin: params.origin ?? "bundled",
enabled: true,
},
],
installRecords: params.installRecord ? { [id]: params.installRecord } : {},
},
byPluginId: new Map([[id, manifest]]),
plugins: [manifest],
diagnostics: [],
normalizePluginId: (pluginId: string) => pluginId,
};
}
function emptyMetadataSnapshot() {
return {
index: { plugins: [], installRecords: {} },
byPluginId: new Map(),
plugins: [],
diagnostics: [],
normalizePluginId: (pluginId: string) => pluginId,
};
}
function hostedCatalog(entries: unknown[]) {
return {
source: "hosted",
entries,
feed: { schemaVersion: 1, id: "test", generatedAt: "now", sequence: 1, entries: [] },
metadata: { url: "https://clawhub.ai/feed", status: 200, checksum: "hash" },
};
}
function hostedFeedEntry(params: {
packageName: string;
title: string;
featured?: boolean;
pluginId?: string;
catalogFeatured?: boolean;
order?: number;
}) {
return {
id: params.packageName,
title: params.title,
state: "available",
...(params.featured === undefined ? {} : { featured: params.featured }),
publisher: { id: "openclaw", trust: "official" },
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: params.packageName,
version: "1.0.0",
integrity: `sha256:${"b".repeat(64)}`,
},
],
},
...(params.pluginId
? {
openclaw: {
plugin: { id: params.pluginId, label: params.title },
catalog: {
...(params.catalogFeatured === undefined ? {} : { featured: params.catalogFeatured }),
...(params.order === undefined ? {} : { order: params.order }),
},
},
}
: {}),
};
}
const hostedFeedDiffsEntry = hostedFeedEntry({
packageName: "@openclaw/diffs",
title: "Diffs",
featured: true,
});
const hostedImpostorEntry = hostedFeedEntry({
packageName: "@community/impostor",
title: "Impostor",
featured: false,
pluginId: "workboard",
catalogFeatured: false,
});
describe("plugin management Featured authority", () => {
beforeEach(() => {
clearManagedPluginOfficialCatalogCache();
mocks.metadata.mockReset();
mocks.officialCatalog.mockReset();
mocks.officialCatalog.mockResolvedValue(hostedCatalog([]));
});
it("lets a live unfeature override bundled metadata without removing installability", async () => {
mocks.metadata.mockReturnValue(emptyMetadataSnapshot());
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([{ ...hostedFeedDiffsEntry, featured: false }]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "diffs",
featured: false,
order: 40,
install: { source: "official", pluginId: "diffs" },
}),
]);
});
it("treats a legacy hosted row without featured as unfeatured", async () => {
mocks.metadata.mockReturnValue(emptyMetadataSnapshot());
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([
hostedFeedEntry({
packageName: "@openclaw/diffs",
title: "Diffs",
}),
]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins[0]).toMatchObject({
id: "diffs",
featured: false,
install: { source: "official", pluginId: "diffs" },
});
});
it("surfaces a newly featured live official package without static fallback metadata", async () => {
mocks.metadata.mockReturnValue(emptyMetadataSnapshot());
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([
hostedFeedEntry({
packageName: "@openclaw/new-tool",
title: "New Tool",
featured: true,
}),
]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "@openclaw/new-tool",
name: "New Tool",
featured: true,
install: { source: "official", pluginId: "@openclaw/new-tool" },
}),
]);
});
it("clears stale embedded curation on an unmatched live official package", async () => {
mocks.metadata.mockReturnValue(emptyMetadataSnapshot());
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([
hostedFeedEntry({
packageName: "@openclaw/new-tool",
title: "New Tool",
featured: false,
pluginId: "new-tool",
catalogFeatured: true,
order: 80,
}),
]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "new-tool",
featured: false,
order: 80,
}),
]);
});
it("clears stale embedded curation on a matched package without bundled curation", async () => {
mocks.metadata.mockReturnValue(emptyMetadataSnapshot());
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([
hostedFeedEntry({
packageName: "@openclaw/copilot",
title: "Copilot",
featured: false,
pluginId: "copilot",
catalogFeatured: true,
order: 80,
}),
]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "copilot",
featured: false,
order: 80,
}),
]);
});
it("lets a live unfeature override an installed published plugin manifest", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "diffs",
name: "Diffs",
origin: "global",
installRecord: { source: "npm", spec: "@openclaw/diffs" },
}),
);
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([{ ...hostedFeedDiffsEntry, featured: false }]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "diffs",
installed: true,
featured: false,
order: 40,
}),
]);
});
it("applies live ClawHub curation to a bundled-known npm installation", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "diffs",
name: "Diffs",
origin: "global",
installRecord: { source: "npm", spec: "@openclaw/diffs" },
featured: false,
}),
);
mocks.officialCatalog.mockResolvedValue(hostedCatalog([hostedFeedDiffsEntry]));
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "diffs",
featured: true,
order: 40,
}),
]);
});
it.each([
{ id: "workboard", name: "Workboard", packageName: "@openclaw/workboard" },
{ id: "open-prose", name: "OpenProse", packageName: "@openclaw/open-prose" },
{ id: "memory-wiki", name: "Memory Wiki", packageName: "@openclaw/memory-wiki" },
])("keeps local curation for private bundled-only $name", async (plugin) => {
mocks.metadata.mockReturnValue(metadataSnapshot(plugin));
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([
hostedFeedEntry({
packageName: `@community/${plugin.id}`,
title: "Impostor",
featured: false,
pluginId: plugin.id,
catalogFeatured: false,
}),
]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: plugin.id,
name: plugin.name,
packageName: plugin.packageName,
featured: true,
order: 10,
}),
]);
});
it("applies hosted curation to the exact published package for bundled FireCrawl", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "firecrawl",
name: "FireCrawl",
packageName: "@openclaw/firecrawl-plugin",
featured: false,
}),
);
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([
hostedFeedEntry({
packageName: "@openclaw/firecrawl-plugin",
title: "FireCrawl",
featured: true,
pluginId: "firecrawl",
}),
]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "firecrawl",
name: "FireCrawl",
packageName: "@openclaw/firecrawl-plugin",
featured: true,
order: 10,
}),
]);
});
it("keeps local curation for an unproven global package identity", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "diffs",
name: "Private Diffs",
origin: "global",
packageName: "@openclaw/diffs",
}),
);
mocks.officialCatalog.mockResolvedValue(
hostedCatalog([{ ...hostedFeedDiffsEntry, featured: false }]),
);
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "diffs",
featured: true,
order: 10,
}),
]);
});
it("does not identify a package-less private bundled plugin by hosted runtime id", async () => {
mocks.metadata.mockReturnValue(metadataSnapshot({ packageName: null }));
mocks.officialCatalog.mockResolvedValue(hostedCatalog([hostedImpostorEntry]));
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "workboard",
featured: true,
order: 10,
}),
]);
});
it("does not identify a package-less global plugin by hosted runtime id alone", async () => {
mocks.metadata.mockReturnValue(metadataSnapshot({ origin: "global", packageName: null }));
mocks.officialCatalog.mockResolvedValue(hostedCatalog([hostedImpostorEntry]));
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "workboard",
featured: true,
order: 10,
}),
]);
});
it("clears local curation when a known published plugin is omitted from a live feed", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "diffs",
name: "Diffs",
origin: "global",
installRecord: { source: "npm", spec: "@openclaw/diffs" },
}),
);
mocks.officialCatalog.mockResolvedValue(hostedCatalog([]));
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "diffs",
packageName: "@openclaw/diffs",
featured: false,
order: 10,
}),
]);
});
it("preserves npm-only bundled curation outside the hosted producer identity", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "acpx",
name: "ACP Runtime",
origin: "global",
packageName: "@openclaw/acpx",
installRecord: { source: "npm", spec: "@openclaw/acpx" },
}),
);
mocks.officialCatalog.mockResolvedValue(hostedCatalog([]));
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "acpx",
featured: true,
order: 10,
}),
]);
});
it("clears hosted-only curation using trusted official install provenance", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "new-tool",
name: "New Tool",
origin: "global",
packageName: "@openclaw/new-tool",
installRecord: {
source: "clawhub",
clawhubUrl: "https://clawhub.ai",
clawhubChannel: "official",
clawhubPackage: "@openclaw/new-tool",
},
}),
);
mocks.officialCatalog.mockResolvedValue(hostedCatalog([]));
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "new-tool",
packageName: "@openclaw/new-tool",
featured: false,
order: 10,
}),
]);
});
it("accepts trusted official install provenance without discovered package metadata", async () => {
mocks.metadata.mockReturnValue(
metadataSnapshot({
id: "new-tool",
name: "New Tool",
origin: "global",
packageName: null,
installRecord: {
source: "clawhub",
clawhubUrl: "https://clawhub.ai",
clawhubChannel: "official",
clawhubPackage: "@openclaw/new-tool",
},
}),
);
mocks.officialCatalog.mockResolvedValue(hostedCatalog([]));
const catalog = await listManagedPlugins({ config: {}, env: {} });
expect(catalog.plugins).toEqual([
expect.objectContaining({
id: "new-tool",
featured: false,
order: 10,
}),
]);
});
});
+4 -4
View File
@@ -175,6 +175,7 @@ const hostedFeedDiffsEntry = {
id: "@openclaw/diffs",
title: "Diffs",
state: "available",
featured: true,
publisher: { id: "openclaw", trust: "official" },
install: {
candidates: [
@@ -212,13 +213,12 @@ describe("plugin management service", () => {
});
});
it("overlays bundled curation through the hosted catalog load path", async () => {
it("keeps bundled curation when the hosted catalog falls back offline", async () => {
mocks.metadata.mockReturnValue(emptyMetadataSnapshot());
mocks.officialCatalog.mockResolvedValue({
source: "hosted",
source: "bundled-fallback",
entries: [hostedDiffsEntry],
feed: { schemaVersion: 1, id: "test", generatedAt: "now", sequence: 1, entries: [] },
metadata: { url: "https://clawhub.ai/feed", status: 200, checksum: "hash" },
error: "offline",
});
const catalog = await listManagedPlugins({ config: {}, env: {} });
+170 -14
View File
@@ -39,6 +39,11 @@ import {
import { buildNpmResolutionInstallFields } from "./installs.js";
import type { PluginManifestRecord } from "./manifest-registry.js";
import type { PluginDiagnostic } from "./manifest-types.js";
import {
resolveTrustedOfficialClawHubPackageName,
resolveTrustedSourceLinkedOfficialClawHubSpec,
resolveTrustedSourceLinkedOfficialNpmSpec,
} from "./official-external-install-records.js";
import {
getOfficialExternalPluginCatalogManifest,
listOfficialExternalPluginCatalogEntries,
@@ -120,6 +125,7 @@ export class ManagedPluginLifecycleError extends Error {
type OfficialCatalogResult = Pick<HostedOfficialExternalPluginCatalogLoadResult, "entries"> & {
error?: string;
hostedFeaturedAuthoritative?: boolean;
};
let officialCatalogCache:
@@ -138,6 +144,7 @@ export function clearManagedPluginOfficialCatalogCache(): void {
function mergeCatalogMetadata(
hosted: OfficialExternalPluginCatalogEntry,
bundled: OfficialExternalPluginCatalogEntry,
options: { hostedFeaturedAuthoritative: boolean },
): OfficialExternalPluginCatalogEntry {
const hostedManifest = getOfficialExternalPluginCatalogManifest(hosted);
const bundledManifest = getOfficialExternalPluginCatalogManifest(bundled);
@@ -147,7 +154,18 @@ function mergeCatalogMetadata(
const bundledDescription = normalizeOptionalString(bundled.description);
const bundledKind = normalizeOptionalString(bundled.kind);
const bundledSource = normalizeOptionalString(bundled.source);
if (!bundledCatalog && !bundledPlugin) {
const hostedFeatured = typeof hosted.featured === "boolean" ? hosted.featured : false;
const mergedCatalog =
bundledCatalog ||
hostedManifest?.catalog ||
(options.hostedFeaturedAuthoritative && hostedFeatured)
? {
...hostedManifest?.catalog,
...bundledCatalog,
...(options.hostedFeaturedAuthoritative ? { featured: hostedFeatured } : {}),
}
: undefined;
if (!mergedCatalog && !bundledPlugin) {
return hosted;
}
return {
@@ -161,23 +179,28 @@ function mergeCatalogMetadata(
[MANIFEST_KEY]: {
...hostedManifest,
...(bundledPlugin ? { plugin: { ...hostedManifest?.plugin, ...bundledPlugin } } : {}),
...(bundledCatalog ? { catalog: { ...hostedManifest?.catalog, ...bundledCatalog } } : {}),
...(mergedCatalog ? { catalog: mergedCatalog } : {}),
},
};
}
type CatalogPackageSourceIdentity = {
source: "clawhub" | "npm";
packageName: string;
};
function resolveCatalogPackageSourceIdentities(
entry: OfficialExternalPluginCatalogEntry,
): Set<string> {
): CatalogPackageSourceIdentity[] {
const install = resolveOfficialExternalPluginInstall(entry);
const clawhubPackage = install?.clawhubSpec
? parseClawHubPluginSpec(install.clawhubSpec)?.name
: undefined;
const npmPackage = install?.npmSpec ? parseRegistryNpmSpec(install.npmSpec)?.name : undefined;
return new Set([
...(clawhubPackage ? [`clawhub:${clawhubPackage}`] : []),
...(npmPackage ? [`npm:${npmPackage}`] : []),
]);
return [
...(clawhubPackage ? [{ source: "clawhub" as const, packageName: clawhubPackage }] : []),
...(npmPackage ? [{ source: "npm" as const, packageName: npmPackage }] : []),
];
}
function matchesBundledCatalogIdentity(params: {
@@ -186,20 +209,49 @@ function matchesBundledCatalogIdentity(params: {
}): boolean {
const hostedSources = resolveCatalogPackageSourceIdentities(params.hosted);
const bundledSources = resolveCatalogPackageSourceIdentities(params.bundled);
return [...hostedSources].some((identity) => bundledSources.has(identity));
return hostedSources.some((hosted) =>
bundledSources.some(
(bundled) => bundled.source === hosted.source && bundled.packageName === hosted.packageName,
),
);
}
/** Overlay local runtime identity and editorial hints after an exact package/source match. */
/**
* Overlay local runtime identity and ordering after an exact package/source match.
* Hosted curation wins; bundled Featured state survives only in fallback mode.
*/
function overlayBundledOfficialPluginCatalogMetadata(
entries: readonly OfficialExternalPluginCatalogEntry[],
bundledEntries: readonly OfficialExternalPluginCatalogEntry[] = listOfficialExternalPluginCatalogEntries(),
options: { hostedFeaturedAuthoritative: boolean } = {
hostedFeaturedAuthoritative: false,
},
): OfficialExternalPluginCatalogEntry[] {
return entries.map((entry) => {
const matches = bundledEntries.filter((bundled) =>
matchesBundledCatalogIdentity({ hosted: entry, bundled }),
);
const bundled = matches.length === 1 ? matches[0] : undefined;
return bundled ? mergeCatalogMetadata(entry, bundled) : entry;
if (bundled) {
return mergeCatalogMetadata(entry, bundled, options);
}
if (!options.hostedFeaturedAuthoritative) {
return entry;
}
const hostedManifest = getOfficialExternalPluginCatalogManifest(entry);
if (entry.featured !== true && !hostedManifest?.catalog) {
return entry;
}
return {
...entry,
[MANIFEST_KEY]: {
...hostedManifest,
catalog: {
...hostedManifest?.catalog,
featured: entry.featured === true,
},
},
};
});
}
@@ -212,8 +264,13 @@ async function loadOfficialCatalog(config: OpenClawConfig): Promise<OfficialCata
};
}
const result = await officialCatalogCache.result;
const hostedFeaturedAuthoritative =
result.source === "hosted" || result.source === "hosted-snapshot";
return {
entries: overlayBundledOfficialPluginCatalogMetadata(result.entries),
entries: overlayBundledOfficialPluginCatalogMetadata(result.entries, undefined, {
hostedFeaturedAuthoritative,
}),
hostedFeaturedAuthoritative,
...("error" in result ? { error: result.error } : {}),
};
}
@@ -317,6 +374,23 @@ function compareCatalogEntries(
return order !== 0 ? order : left.name.localeCompare(right.name);
}
function resolveInstalledOfficialCatalogEntry(params: {
entries: readonly OfficialExternalPluginCatalogEntry[];
packageName?: string;
source: CatalogPackageSourceIdentity["source"];
}): OfficialExternalPluginCatalogEntry | undefined {
if (!params.packageName) {
return undefined;
}
const matches = params.entries.filter((entry) =>
resolveCatalogPackageSourceIdentities(entry).some(
(identity) =>
identity.source === params.source && identity.packageName === params.packageName,
),
);
return matches.length === 1 ? matches[0] : undefined;
}
/** Build cold installed state merged with the hosted official catalog and bundled curation. */
export async function listManagedPlugins(params: {
config: OpenClawConfig;
@@ -326,9 +400,91 @@ export async function listManagedPlugins(params: {
const env = params.env ?? process.env;
const metadata = loadPluginMetadataSnapshot({ config: params.config, env });
const officialCatalog = params.officialCatalog ?? (await loadOfficialCatalog(params.config));
const bundledOfficialEntries = listOfficialExternalPluginCatalogEntries();
const plugins = metadata.index.plugins.map((record): ManagedPluginCatalogEntry => {
const manifest = metadata.byPluginId.get(record.pluginId);
const catalog = normalizeCatalogMetadata(manifest?.catalog);
const localCatalog = normalizeCatalogMetadata(manifest?.catalog);
const installRecord = metadata.index.installRecords[record.pluginId];
const trustedOfficialClawHubSpec = installRecord
? resolveTrustedSourceLinkedOfficialClawHubSpec({
pluginId: record.pluginId,
record: installRecord,
})
: undefined;
const trustedOfficialNpmSpec = installRecord
? resolveTrustedSourceLinkedOfficialNpmSpec({
pluginId: record.pluginId,
record: installRecord,
})
: undefined;
const sourceLinkedOfficialClawHubPackage = trustedOfficialClawHubSpec
? parseClawHubPluginSpec(trustedOfficialClawHubSpec)?.name
: undefined;
const currentOfficialClawHubPackage = installRecord
? resolveTrustedOfficialClawHubPackageName(installRecord)
: undefined;
const trustedOfficialNpmPackage = trustedOfficialNpmSpec
? parseRegistryNpmSpec(trustedOfficialNpmSpec)?.name
: undefined;
const bundledPublishedEntry =
record.origin === "bundled"
? resolveInstalledOfficialCatalogEntry({
entries: bundledOfficialEntries,
packageName: record.packageName,
source: "npm",
})
: undefined;
const installedOfficialIdentity = sourceLinkedOfficialClawHubPackage
? { source: "clawhub" as const, packageName: sourceLinkedOfficialClawHubPackage }
: trustedOfficialNpmPackage
? { source: "npm" as const, packageName: trustedOfficialNpmPackage }
: currentOfficialClawHubPackage &&
(!record.packageName || record.packageName === currentOfficialClawHubPackage)
? { source: "clawhub" as const, packageName: currentOfficialClawHubPackage }
: bundledPublishedEntry && record.packageName
? { source: "npm" as const, packageName: record.packageName }
: undefined;
const hasInstalledOfficialProvenance = Boolean(
installedOfficialIdentity &&
(!record.packageName || record.packageName === installedOfficialIdentity.packageName),
);
const bundledOfficialEntry =
bundledPublishedEntry ??
resolveInstalledOfficialCatalogEntry({
entries: bundledOfficialEntries,
packageName: hasInstalledOfficialProvenance
? installedOfficialIdentity?.packageName
: undefined,
source: installedOfficialIdentity?.source ?? "clawhub",
});
const hostedPackageName =
installedOfficialIdentity?.source === "npm"
? (bundledOfficialEntry
? resolveCatalogPackageSourceIdentities(bundledOfficialEntry)
: []
).find((identity) => identity.source === "clawhub")?.packageName
: installedOfficialIdentity?.packageName;
const officialEntry = resolveInstalledOfficialCatalogEntry({
entries: officialCatalog.entries,
packageName: hasInstalledOfficialProvenance ? hostedPackageName : undefined,
source: "clawhub",
});
const hasHostedOfficialIdentity = Boolean(hasInstalledOfficialProvenance && hostedPackageName);
const officialCatalogMetadata = officialEntry
? normalizeCatalogMetadata(getOfficialExternalPluginCatalogManifest(officialEntry)?.catalog)
: undefined;
// Published plugin curation follows the live feed even after install, including
// omission. Private bundled plugins without an exact package/source match stay local.
const catalog =
hasHostedOfficialIdentity && officialCatalog.hostedFeaturedAuthoritative
? {
...localCatalog,
...officialCatalogMetadata,
featured: officialEntry?.featured === true,
}
: officialCatalogMetadata
? { ...localCatalog, ...officialCatalogMetadata }
: localCatalog;
const error = firstPluginError(metadata.diagnostics, record.pluginId);
const kind = normalizeKinds(manifest?.kind);
const category = derivePluginCategory(manifest);
@@ -370,8 +526,8 @@ export async function listManagedPlugins(params: {
// Hosted rows without a declared runtime id fall back to their package name,
// so id matching alone would keep them visible after a successful install.
const entryPackageInstalled = (entry: OfficialExternalPluginCatalogEntry) =>
[...resolveCatalogPackageSourceIdentities(entry)].some((identity) =>
installedPackageNames.has(identity.slice(identity.indexOf(":") + 1)),
resolveCatalogPackageSourceIdentities(entry).some((identity) =>
installedPackageNames.has(identity.packageName),
);
for (const entry of officialCatalog.entries) {
const pluginId = resolveOfficialExternalPluginId(entry);
@@ -77,6 +77,24 @@ function isOfficialClawHubInstallRecord(record: PluginInstallRecord): boolean {
return (record.clawhubUrl ?? "").trim().replace(/\/+$/, "") === "https://clawhub.ai";
}
/** Resolves one package identity from a current trusted official ClawHub install record. */
export function resolveTrustedOfficialClawHubPackageName(
record: PluginInstallRecord,
): string | undefined {
if (
record.source !== "clawhub" ||
record.clawhubChannel !== "official" ||
(record.clawhubUrl ?? "").trim().replace(/\/+$/, "") !== "https://clawhub.ai"
) {
return undefined;
}
const packageNames = resolveRecordedClawHubPackageNames(record);
if (!packageNames || packageNames.length === 0 || new Set(packageNames).size !== 1) {
return undefined;
}
return packageNames[0];
}
function hasTrustedClawHubSourceAuthority(
record: PluginInstallRecord,
officialClawHubSpec: string | undefined,
@@ -269,6 +269,7 @@ describe("official external plugin catalog", () => {
title: "Trusted",
version: "1.2.3",
state: "available",
featured: true,
publisher: { id: "acme", trust: "official" },
install: {
candidates: [
@@ -336,6 +337,8 @@ describe("official external plugin catalog", () => {
defaultChoice: "clawhub",
expectedIntegrity: "sha256-s1XdoEQDvsqri7qwaf0eewV4Ji50WeWYzFsZYVtb2rk=",
});
expect(trusted.featured).toBe(true);
expect(disabled).not.toHaveProperty("featured");
expect(resolveOfficialExternalPluginInstall(disabled)).toBeNull();
expect(resolveOfficialExternalPluginInstall(community)).toBeNull();
});
@@ -109,6 +109,7 @@ export type OfficialExternalPluginCatalogEntry = {
description?: string;
source?: string;
kind?: string;
featured?: boolean;
install?: {
candidates?: readonly OfficialExternalPluginCatalogInstallCandidate[];
};