Skip to content

Commit 82d05bd

Browse files
committed
refactor(plugins): make external catalog feed-shaped
1 parent 6e8f30c commit 82d05bd

3 files changed

Lines changed: 102 additions & 2 deletions

File tree

scripts/lib/official-external-plugin-catalog.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
{
2+
"schemaVersion": 1,
3+
"id": "openclaw-official-external-plugins",
4+
"generatedAt": "2026-06-22T00:00:00.000Z",
5+
"sequence": 1,
6+
"description": "Bundled fallback feed for official external OpenClaw plugins.",
27
"entries": [
38
{
49
"name": "@openclaw/acpx",

src/plugins/official-external-plugin-catalog.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { describe, expect, it } from "vitest";
2+
import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type: "json" };
23
import {
34
type OfficialExternalPluginCatalogEntry,
45
getOfficialExternalPluginCatalogEntry,
6+
isOfficialExternalPluginCatalogFeed,
57
listOfficialExternalPluginCatalogEntries,
8+
parseOfficialExternalPluginCatalogEntries,
69
resolveOfficialExternalProviderContractPluginIds,
710
resolveOfficialExternalProviderPluginIds,
811
resolveOfficialExternalProviderPluginIdsForEnv,
@@ -20,6 +23,54 @@ function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry {
2023
}
2124

2225
describe("official external plugin catalog", () => {
26+
it("ships the official plugin catalog as a feed-shaped bundled fallback", () => {
27+
expect(isOfficialExternalPluginCatalogFeed(officialExternalPluginCatalog)).toBe(true);
28+
expect(officialExternalPluginCatalog).toMatchObject({
29+
schemaVersion: 1,
30+
id: "openclaw-official-external-plugins",
31+
sequence: 1,
32+
});
33+
expect(officialExternalPluginCatalog.entries.length).toBeGreaterThan(0);
34+
});
35+
36+
it("does not allow malformed feed wrappers to count as feed documents", () => {
37+
expect(
38+
isOfficialExternalPluginCatalogFeed({
39+
schemaVersion: 1,
40+
id: " ",
41+
generatedAt: "2026-06-22T00:00:00.000Z",
42+
sequence: 1,
43+
entries: [],
44+
}),
45+
).toBe(false);
46+
expect(
47+
isOfficialExternalPluginCatalogFeed({
48+
schemaVersion: 2,
49+
id: "openclaw-official-external-plugins",
50+
generatedAt: "2026-06-22T00:00:00.000Z",
51+
sequence: 1,
52+
entries: [],
53+
}),
54+
).toBe(false);
55+
});
56+
57+
it("keeps unsupported versioned feed wrappers out of legacy catalog parsing", () => {
58+
expect(
59+
parseOfficialExternalPluginCatalogEntries({
60+
schemaVersion: 2,
61+
id: "future-feed",
62+
generatedAt: "2026-06-22T00:00:00.000Z",
63+
sequence: 1,
64+
entries: [{ name: "should-not-load" }],
65+
}),
66+
).toEqual([]);
67+
expect(
68+
parseOfficialExternalPluginCatalogEntries({
69+
entries: [{ name: "legacy-catalog-entry" }],
70+
}),
71+
).toEqual([{ name: "legacy-catalog-entry" }]);
72+
});
73+
2374
it("lists the externalized provider and capability plugins with install metadata", () => {
2475
const providers = [
2576
["arcee", "@openclaw/arcee-provider"],

src/plugins/official-external-plugin-catalog.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ export type OfficialExternalPluginCatalogEntry = {
8484
kind?: string;
8585
} & Partial<Record<ManifestKey, OfficialExternalPluginCatalogManifest>>;
8686

87+
/** Feed-shaped wrapper used by the bundled external plugin catalog fallback. */
88+
export type OfficialExternalPluginCatalogFeed = {
89+
schemaVersion: number;
90+
id: string;
91+
generatedAt: string;
92+
sequence: number;
93+
description?: string;
94+
entries: readonly OfficialExternalPluginCatalogEntry[];
95+
};
96+
8797
type OfficialExternalProviderContract =
8898
| "embeddingProviders"
8999
| "mediaUnderstandingProviders"
@@ -97,13 +107,45 @@ const OFFICIAL_CATALOG_SOURCES = [
97107
officialExternalPluginCatalog,
98108
] as const;
99109

100-
function parseCatalogEntries(raw: unknown): OfficialExternalPluginCatalogEntry[] {
110+
const OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSION = 1;
111+
112+
export function isOfficialExternalPluginCatalogFeed(
113+
raw: unknown,
114+
): raw is OfficialExternalPluginCatalogFeed {
115+
if (!isRecord(raw)) {
116+
return false;
117+
}
118+
const sequence = raw.sequence;
119+
return (
120+
raw.schemaVersion === OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSION &&
121+
typeof raw.id === "string" &&
122+
raw.id.trim().length > 0 &&
123+
typeof raw.generatedAt === "string" &&
124+
raw.generatedAt.trim().length > 0 &&
125+
typeof sequence === "number" &&
126+
Number.isInteger(sequence) &&
127+
sequence >= 0 &&
128+
Array.isArray(raw.entries)
129+
);
130+
}
131+
132+
export function parseOfficialExternalPluginCatalogEntries(
133+
raw: unknown,
134+
): OfficialExternalPluginCatalogEntry[] {
101135
if (Array.isArray(raw)) {
102136
return raw.filter((entry): entry is OfficialExternalPluginCatalogEntry => isRecord(entry));
103137
}
138+
if (isOfficialExternalPluginCatalogFeed(raw)) {
139+
return raw.entries.filter((entry): entry is OfficialExternalPluginCatalogEntry =>
140+
isRecord(entry),
141+
);
142+
}
104143
if (!isRecord(raw)) {
105144
return [];
106145
}
146+
if ("schemaVersion" in raw) {
147+
return [];
148+
}
107149
const list = raw.entries ?? raw.packages ?? raw.plugins;
108150
if (!Array.isArray(list)) {
109151
return [];
@@ -191,7 +233,9 @@ export function resolveOfficialExternalPluginInstall(
191233
}
192234

193235
export function listOfficialExternalPluginCatalogEntries(): OfficialExternalPluginCatalogEntry[] {
194-
const entries = OFFICIAL_CATALOG_SOURCES.flatMap((source) => parseCatalogEntries(source));
236+
const entries = OFFICIAL_CATALOG_SOURCES.flatMap((source) =>
237+
parseOfficialExternalPluginCatalogEntries(source),
238+
);
195239
const resolved = new Map<string, OfficialExternalPluginCatalogEntry>();
196240
for (const entry of entries) {
197241
const pluginId = resolveOfficialExternalPluginId(entry);

0 commit comments

Comments
 (0)