Add hosted catalog source profile validation (#95969)

Merged via squash.

Prepared head SHA: 6dc6ca154c
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
This commit is contained in:
Gio Della-Libera
2026-06-27 20:56:54 -07:00
committed by GitHub
parent 830467bc93
commit 1b0766080a
2 changed files with 991 additions and 28 deletions
@@ -11,6 +11,7 @@ import {
createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore,
getOfficialExternalPluginCatalogEntry,
isOfficialExternalPluginCatalogFeed,
filterOfficialExternalPluginCatalogEntriesBySourceRefs,
listOfficialExternalPluginCatalogEntries,
loadHostedOfficialExternalPluginCatalogEntries,
parseOfficialExternalPluginCatalogEntries,
@@ -20,6 +21,7 @@ import {
resolveOfficialExternalWebProviderContractPluginIdsForEnv,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
validateOfficialExternalPluginCatalogEntrySourceRefs,
} from "./official-external-plugin-catalog.js";
function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry {
@@ -30,6 +32,10 @@ function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry {
return entry;
}
function expectRequestUrl(input: RequestInfo | URL): string {
return typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
}
describe("official external plugin catalog", () => {
it("keeps hosted fetch guard loading lazy for bundled catalog import paths", () => {
const source = readFileSync(
@@ -61,6 +67,15 @@ describe("official external plugin catalog", () => {
entries: [],
}),
).toBe(false);
expect(
isOfficialExternalPluginCatalogFeed({
schemaVersion: 2,
id: "openclaw-official-external-plugins",
generatedAt: "2026-06-22T00:00:00.000Z",
sequence: 1,
entries: [],
}),
).toBe(true);
expect(
isOfficialExternalPluginCatalogFeed({
schemaVersion: 3,
@@ -84,7 +99,7 @@ describe("official external plugin catalog", () => {
).toBe(true);
});
it("keeps live ClawHub marketplace entries as metadata-only feed entries", () => {
it("accepts live ClawHub marketplace entries with trusted install candidates", () => {
const [entry] = parseOfficialExternalPluginCatalogEntries({
schemaVersion: 2,
id: "clawhub-official",
@@ -125,8 +140,12 @@ describe("official external plugin catalog", () => {
title: "Expedia Travel",
version: "1.0.4",
});
expect(resolveOfficialExternalPluginId(entry)).toBeUndefined();
expect(resolveOfficialExternalPluginInstall(entry)).toBeNull();
expect(resolveOfficialExternalPluginId(entry)).toBe("@expediagroup/expedia-openclaw");
expect(resolveOfficialExternalPluginInstall(entry)).toEqual({
clawhubSpec: "clawhub:@expediagroup/expedia-openclaw@1.0.4",
defaultChoice: "clawhub",
expectedIntegrity: "sha256-s1XdoEQDvsqri7qwaf0eewV4Ji50WeWYzFsZYVtb2rk=",
});
});
it("does not synthesize trusted installs for unavailable or untrusted hosted entries", () => {
@@ -170,6 +189,55 @@ describe("official external plugin catalog", () => {
],
},
},
{
type: "plugin",
id: "@example/missing-state",
title: "Missing State",
version: "1.0.0",
publisher: { id: "example", trust: "official" },
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@example/missing-state",
version: "1.0.0",
},
],
},
},
{
type: "plugin",
id: "@example/missing-trust",
title: "Missing Trust",
version: "1.0.0",
state: "available",
publisher: { id: "example" },
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@example/missing-trust",
version: "1.0.0",
},
],
},
},
{
type: "plugin",
id: "@example/missing-publisher",
title: "Missing Publisher",
version: "1.0.0",
state: "available",
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@example/missing-publisher",
version: "1.0.0",
},
],
},
},
{
type: "plugin",
id: "@example/private-source",
@@ -190,9 +258,9 @@ describe("official external plugin catalog", () => {
],
});
expect(entries).toHaveLength(3);
expect(entries).toHaveLength(6);
for (const entry of entries) {
expect(resolveOfficialExternalPluginId(entry)).toBeUndefined();
expect(resolveOfficialExternalPluginId(entry)).toBe(entry.id);
expect(resolveOfficialExternalPluginInstall(entry)).toBeNull();
}
});
@@ -318,6 +386,351 @@ describe("official external plugin catalog", () => {
});
});
it("uses the default local feed profile for hosted catalog loading", async () => {
const body = JSON.stringify({
schemaVersion: 1,
id: "openclaw-official-external-plugins",
generatedAt: "2026-06-22T00:00:00.000Z",
sequence: 8,
entries: [
{
name: "@openclaw/default-profile-proof",
kind: "plugin",
openclaw: { plugin: { id: "default-profile-proof" } },
},
],
});
const fetchImpl = vi.fn(async (url: RequestInfo | URL) => {
expect(expectRequestUrl(url)).toBe(DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL);
return new Response(body, { status: 200 });
});
const result = await loadHostedOfficialExternalPluginCatalogEntries({
feedProfile: "clawhub-public",
catalogConfig: {
sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } },
},
fetchImpl,
snapshotStore: null,
});
expect(result.source).toBe("hosted");
expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/default-profile-proof"]);
});
it("accepts the live ClawHub feed source ref by default", async () => {
const body = JSON.stringify({
schemaVersion: 2,
id: "clawhub-official",
generatedAt: "2026-06-23T09:38:53.000Z",
sequence: 4,
entries: [
{
type: "plugin",
id: "@openclaw/live-feed-proof",
title: "Live Feed Proof",
version: "1.0.0",
state: "available",
publisher: { id: "openclaw", trust: "official" },
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@openclaw/live-feed-proof",
version: "1.0.0",
integrity:
"sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9",
},
],
},
},
],
});
const result = await loadHostedOfficialExternalPluginCatalogEntries({
fetchImpl: vi.fn(async (url: RequestInfo | URL) => {
expect(expectRequestUrl(url)).toBe(DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL);
return new Response(body, { status: 200 });
}),
snapshotStore: null,
});
expect(result.source).toBe("hosted");
expect(result.entries.map((entry) => entry.id)).toEqual(["@openclaw/live-feed-proof"]);
expect(resolveOfficialExternalPluginInstall(result.entries[0])).toEqual({
clawhubSpec: "clawhub:@openclaw/live-feed-proof@1.0.0",
defaultChoice: "clawhub",
expectedIntegrity: "sha256-s1XdoEQDvsqri7qwaf0eewV4Ji50WeWYzFsZYVtb2rk=",
});
});
it("allows named local feed profiles to authorize their configured HTTPS host", async () => {
const body = JSON.stringify({
schemaVersion: 1,
id: "openclaw-official-external-plugins",
generatedAt: "2026-06-22T00:00:00.000Z",
sequence: 9,
entries: [
{
name: "@acme/private-proof",
kind: "plugin",
install: {
candidates: [
{
sourceRef: "acme-npm",
package: "@acme/private-proof",
version: "1.0.0",
},
],
},
openclaw: {
plugin: { id: "private-proof" },
install: { sourceRef: "acme-npm", npmSpec: "@acme/private-proof" },
},
},
],
});
const fetchImpl = vi.fn(async (url: RequestInfo | URL) => {
expect(expectRequestUrl(url)).toBe("https://packages.acme.example/openclaw/feed");
return new Response(body, { status: 200 });
});
const result = await loadHostedOfficialExternalPluginCatalogEntries({
feedProfile: "acme",
catalogConfig: {
feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } },
sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } },
},
fetchImpl,
snapshotStore: null,
});
expect(result.source).toBe("hosted");
expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/private-proof"]);
});
it("keeps direct hosted feed URL overrides constrained to the public allowlist", async () => {
const fetchImpl = vi.fn(async () => new Response("{}", { status: 200 }));
const result = await loadHostedOfficialExternalPluginCatalogEntries({
feedUrl: "https://packages.acme.example/openclaw/feed",
fetchImpl,
snapshotStore: null,
});
expect(result.source).toBe("bundled-fallback");
expect(fetchImpl).not.toHaveBeenCalled();
if (result.source === "bundled-fallback") {
expect(result.error).toContain("hostname is not allowed");
}
});
it("requires manifest install source refs when the default feed profile URL is overridden", async () => {
const body = JSON.stringify({
schemaVersion: 1,
id: "openclaw-official-external-plugins",
generatedAt: "2026-06-22T00:00:00.000Z",
sequence: 13,
entries: [
{
name: "@acme/default-override-missing-source-ref",
kind: "plugin",
openclaw: {
plugin: { id: "default-override-missing-source-ref" },
install: { npmSpec: "@acme/default-override-missing-source-ref" },
},
},
{
name: "@acme/default-override-known-source-ref",
kind: "plugin",
openclaw: {
plugin: { id: "default-override-known-source-ref" },
install: { sourceRef: "acme-npm", npmSpec: "@acme/default-override-known-source-ref" },
},
},
],
});
const result = await loadHostedOfficialExternalPluginCatalogEntries({
catalogConfig: {
feeds: { "clawhub-public": { url: "https://packages.acme.example/openclaw/feed" } },
sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } },
},
fetchImpl: vi.fn(async (url: RequestInfo | URL) => {
expect(expectRequestUrl(url)).toBe("https://packages.acme.example/openclaw/feed");
return new Response(body, { status: 200 });
}),
snapshotStore: null,
});
expect(result.source).toBe("hosted");
expect(result.entries.map((entry) => entry.name)).toEqual([
"@acme/default-override-known-source-ref",
]);
});
it("requires manifest install source refs for custom local feed profiles", async () => {
const missingManifestSourceRef = {
name: "@acme/missing-manifest-source-ref",
kind: "plugin",
openclaw: {
plugin: { id: "missing-manifest-source-ref" },
install: { npmSpec: "@acme/missing-manifest-source-ref" },
},
};
const implicitNameInstall = {
name: "@acme/implicit-name-install",
kind: "plugin",
openclaw: { plugin: { id: "implicit-name-install" } },
};
const topLevelCandidateOnly = {
name: "@acme/top-level-candidate-only",
kind: "plugin",
install: {
candidates: [{ sourceRef: "acme-npm", package: "@acme/top-level-candidate-only" }],
},
openclaw: {
plugin: { id: "top-level-candidate-only" },
install: { npmSpec: "@acme/top-level-candidate-only" },
},
};
const knownManifestSourceRef = {
name: "@acme/known-manifest-source-ref",
kind: "plugin",
openclaw: {
plugin: { id: "known-manifest-source-ref" },
install: { npmSpec: "@acme/known-manifest-source-ref", sourceRef: "acme-npm" },
},
};
const body = JSON.stringify({
schemaVersion: 1,
id: "openclaw-official-external-plugins",
generatedAt: "2026-06-22T00:00:00.000Z",
sequence: 11,
entries: [
missingManifestSourceRef,
implicitNameInstall,
topLevelCandidateOnly,
knownManifestSourceRef,
],
});
const catalogConfig = {
feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } },
sources: { "acme-npm": { type: "npm" as const } },
};
expect(
validateOfficialExternalPluginCatalogEntrySourceRefs(missingManifestSourceRef, {
catalogConfig,
requireManifestInstallSourceRef: true,
}),
).toEqual(["feed install candidate is missing sourceRef"]);
expect(
validateOfficialExternalPluginCatalogEntrySourceRefs(implicitNameInstall, {
catalogConfig,
requireManifestInstallSourceRef: true,
}),
).toEqual(["feed install candidate is missing sourceRef"]);
expect(
validateOfficialExternalPluginCatalogEntrySourceRefs(topLevelCandidateOnly, {
catalogConfig,
requireManifestInstallSourceRef: true,
}),
).toEqual(["feed install candidate is missing sourceRef"]);
expect(
validateOfficialExternalPluginCatalogEntrySourceRefs(knownManifestSourceRef, {
catalogConfig,
requireManifestInstallSourceRef: true,
}),
).toEqual([]);
const result = await loadHostedOfficialExternalPluginCatalogEntries({
feedProfile: "acme",
catalogConfig,
fetchImpl: vi.fn(async () => new Response(body, { status: 200 })),
snapshotStore: null,
});
expect(result.source).toBe("hosted");
expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/known-manifest-source-ref"]);
});
it("filters hosted feed entries that reference unknown local source profiles", async () => {
const knownEntry = {
name: "@openclaw/source-ref-known",
kind: "plugin",
install: {
candidates: [{ sourceRef: "public-clawhub", package: "@openclaw/source-ref-known" }],
},
openclaw: { plugin: { id: "source-ref-known" } },
};
const unknownEntry = {
name: "@openclaw/source-ref-unknown",
kind: "plugin",
install: {
candidates: [{ sourceRef: "attacker-npm", package: "@openclaw/source-ref-unknown" }],
},
openclaw: { plugin: { id: "source-ref-unknown" } },
};
const missingEntry = {
name: "@openclaw/source-ref-missing",
kind: "plugin",
install: { candidates: [{ package: "@openclaw/source-ref-missing" }] },
openclaw: { plugin: { id: "source-ref-missing" } },
};
const manifestInstallWithoutSourceRef = {
name: "@openclaw/source-ref-manifest-missing",
kind: "plugin",
install: {
candidates: [
{ sourceRef: "public-clawhub", package: "@openclaw/source-ref-manifest-missing" },
],
},
openclaw: {
plugin: { id: "source-ref-manifest-missing" },
install: { npmSpec: "@openclaw/source-ref-manifest-missing" },
},
};
const body = JSON.stringify({
schemaVersion: 1,
id: "openclaw-official-external-plugins",
generatedAt: "2026-06-22T00:00:00.000Z",
sequence: 10,
entries: [knownEntry, unknownEntry, missingEntry, manifestInstallWithoutSourceRef],
});
expect(validateOfficialExternalPluginCatalogEntrySourceRefs(knownEntry)).toEqual([]);
expect(validateOfficialExternalPluginCatalogEntrySourceRefs(unknownEntry)).toEqual([
'feed install candidate references unknown sourceRef "attacker-npm"',
]);
expect(validateOfficialExternalPluginCatalogEntrySourceRefs(missingEntry)).toEqual([
"feed install candidate is missing sourceRef",
]);
expect(
validateOfficialExternalPluginCatalogEntrySourceRefs(manifestInstallWithoutSourceRef),
).toEqual([]);
expect(
filterOfficialExternalPluginCatalogEntriesBySourceRefs([
knownEntry,
unknownEntry,
missingEntry,
manifestInstallWithoutSourceRef,
]).map((entry) => entry.name),
).toEqual(["@openclaw/source-ref-known", "@openclaw/source-ref-manifest-missing"]);
const result = await loadHostedOfficialExternalPluginCatalogEntries({
fetchImpl: vi.fn(async () => new Response(body, { status: 200 })),
snapshotStore: null,
});
expect(result.source).toBe("hosted");
expect(result.entries.map((entry) => entry.name)).toEqual([
"@openclaw/source-ref-known",
"@openclaw/source-ref-manifest-missing",
]);
});
it("falls back to the bundled catalog when hosted feed validation fails", async () => {
const result = await loadHostedOfficialExternalPluginCatalogEntries({
snapshotStore: null,
@@ -461,7 +874,7 @@ describe("official external plugin catalog", () => {
const stateDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-hosted-store-"));
try {
const store = createSqliteHostedOfficialExternalPluginCatalogSnapshotStore({ stateDir });
const url = "https://register.openclaw.ai/official-external-plugin-catalog.json";
const url = "https://clawhub.ai/v1/feeds/plugins";
const firstBody = JSON.stringify({ entries: [] });
const secondBody = JSON.stringify({ entries: [{}] });
@@ -504,6 +917,59 @@ describe("official external plugin catalog", () => {
}
});
it("applies custom source-ref validation to exception snapshot fallback", async () => {
const body = JSON.stringify({
schemaVersion: 1,
id: "openclaw-official-external-plugins",
generatedAt: "2026-06-22T00:00:00.000Z",
sequence: 12,
entries: [
{
name: "@acme/snapshot-missing-source-ref",
kind: "plugin",
openclaw: {
plugin: { id: "snapshot-missing-source-ref" },
install: { npmSpec: "@acme/snapshot-missing-source-ref" },
},
},
{
name: "@acme/snapshot-known-source-ref",
kind: "plugin",
openclaw: {
plugin: { id: "snapshot-known-source-ref" },
install: { sourceRef: "acme-npm", npmSpec: "@acme/snapshot-known-source-ref" },
},
},
],
});
const catalogConfig = {
feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } },
sources: { "acme-npm": { type: "npm" as const } },
};
const seeded = await loadHostedOfficialExternalPluginCatalogEntries({
feedProfile: "acme",
catalogConfig,
snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(),
fetchImpl: vi.fn(async () => new Response(body, { status: 200 })),
});
if (seeded.source !== "hosted") {
throw new Error("expected seeded hosted feed");
}
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([
{ body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" },
]);
const result = await loadHostedOfficialExternalPluginCatalogEntries({
feedProfile: "acme",
catalogConfig,
snapshotStore,
fetchImpl: vi.fn(async () => new Response("{ nope", { status: 200 })),
});
expect(result.source).toBe("hosted-snapshot");
expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/snapshot-known-source-ref"]);
});
it("uses the last known good snapshot when the hosted feed returns HTTP 304", async () => {
const body = JSON.stringify({
schemaVersion: 1,
@@ -797,6 +1263,125 @@ describe("official external plugin catalog", () => {
}
});
it("prefers feed install candidates before legacy install metadata", () => {
expect(
resolveOfficialExternalPluginInstall({
name: "@legacy/plain-package",
kind: "plugin",
state: "available",
publisher: { id: "openclaw", trust: "official" },
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@openclaw/candidate-package",
version: "1.2.3",
integrity: "sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9",
},
],
},
openclaw: {
plugin: { id: "candidate-package" },
install: {
npmSpec: "@legacy/plain-package",
minHostVersion: ">=2026.6.1",
expectedIntegrity: "sha256:manifest",
allowInvalidConfigRecovery: true,
},
},
}),
).toEqual({
clawhubSpec: "clawhub:@openclaw/candidate-package@1.2.3",
defaultChoice: "clawhub",
expectedIntegrity: "sha256-s1XdoEQDvsqri7qwaf0eewV4Ji50WeWYzFsZYVtb2rk=",
minHostVersion: ">=2026.6.1",
allowInvalidConfigRecovery: true,
});
expect(
resolveOfficialExternalPluginInstall(
{
name: "@acme/private-package",
kind: "plugin",
state: "available",
publisher: { id: "acme", trust: "official" },
install: {
candidates: [
{ sourceRef: "acme-npm", package: "@acme/private-package", version: "4.5.6" },
],
},
},
{ catalogConfig: { sources: { "acme-npm": { type: "npm" } } } },
),
).toEqual({ npmSpec: "@acme/private-package@4.5.6", defaultChoice: "npm" });
expect(
resolveOfficialExternalPluginInstall(
{
name: "@acme/private-sha-package",
kind: "plugin",
state: "available",
publisher: { id: "acme", trust: "official" },
install: {
candidates: [
{
sourceRef: "acme-npm",
package: "@acme/private-sha-package",
version: "4.5.6",
integrity:
"sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9",
},
],
},
},
{ catalogConfig: { sources: { "acme-npm": { type: "npm" } } } },
),
).toEqual({ npmSpec: "@acme/private-sha-package@4.5.6", defaultChoice: "npm" });
expect(
resolveOfficialExternalPluginInstall(
{
name: "@acme/private-sri-package",
kind: "plugin",
state: "available",
publisher: { id: "acme", trust: "official" },
install: {
candidates: [
{
sourceRef: "acme-npm",
package: "@acme/private-sri-package",
version: "4.5.6",
integrity: "sha512-abc=",
},
],
},
},
{ catalogConfig: { sources: { "acme-npm": { type: "npm" } } } },
),
).toEqual({
npmSpec: "@acme/private-sri-package@4.5.6",
defaultChoice: "npm",
expectedIntegrity: "sha512-abc=",
});
expect(
resolveOfficialExternalPluginInstall(
{
name: "git-only-package",
kind: "plugin",
install: {
candidates: [{ sourceRef: "acme-git", package: "git@example.com:acme/plugin.git" }],
},
},
{ catalogConfig: { sources: { "acme-git": { type: "git" } } } },
),
).toBeNull();
expect(
resolveOfficialExternalPluginInstall({ id: "metadata-only", title: "Metadata only" }),
).toBeNull();
});
it("lists the externalized provider and capability plugins with install metadata", () => {
const providers = [
["arcee", "@openclaw/arcee-provider"],
+400 -22
View File
@@ -6,6 +6,7 @@ import officialExternalChannelCatalog from "../../scripts/lib/official-external-
import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type: "json" };
import officialExternalProviderCatalog from "../../scripts/lib/official-external-provider-catalog.json" with { type: "json" };
import { MANIFEST_KEY } from "../compat/legacy-names.js";
import { normalizeClawHubSha256Integrity } from "../infra/clawhub.js";
import { isRecord } from "../utils.js";
import type {
PluginManifestChannelConfig,
@@ -71,7 +72,7 @@ export type OfficialExternalPluginCatalogManifest = {
};
providers?: readonly OfficialExternalProviderCatalogProvider[];
webSearchProviders?: readonly OfficialExternalWebSearchProvider[];
install?: PluginPackageInstall;
install?: PluginPackageInstall & { sourceRef?: string };
contracts?: PluginManifestContracts;
channelConfigs?: Record<string, PluginManifestChannelConfig>;
};
@@ -101,6 +102,32 @@ export type OfficialExternalPluginCatalogInstallCandidate = {
package?: string;
version?: string;
integrity?: string;
repo?: string;
path?: string;
commit?: string;
};
export type OfficialExternalPluginCatalogSourceProfile =
| {
type: "npm";
registry?: string;
}
| {
type: "clawhub";
baseUrl?: string;
}
| {
type: "git";
baseUrl?: string;
};
export type OfficialExternalPluginCatalogFeedProfile = {
url: string;
};
export type OfficialExternalPluginCatalogProfileConfig = {
feeds?: Record<string, OfficialExternalPluginCatalogFeedProfile>;
sources?: Record<string, OfficialExternalPluginCatalogSourceProfile>;
};
/** Feed-shaped wrapper used by the bundled external plugin catalog fallback. */
@@ -171,9 +198,30 @@ const OFFICIAL_CATALOG_SOURCES = [
officialExternalPluginCatalog,
] as const;
const OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSIONS = new Set<unknown>([1, 2]);
const SUPPORTED_OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSIONS = new Set([1, 2]);
export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL =
"https://clawhub.ai/v1/feeds/plugins";
export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE = "clawhub-public";
export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_SOURCE_REF = "public-clawhub";
export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_NPM_SOURCE_REF = "public-npm";
export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_PROFILE_CONFIG: OfficialExternalPluginCatalogProfileConfig =
{
feeds: {
[DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE]: {
url: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL,
},
},
sources: {
[DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_SOURCE_REF]: {
type: "clawhub",
baseUrl: "https://clawhub.ai",
},
[DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_NPM_SOURCE_REF]: {
type: "npm",
registry: "https://registry.npmjs.org/",
},
},
};
const DEFAULT_HOSTED_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_TIMEOUT_MS = 5000;
const DEFAULT_HOSTED_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_MAX_BYTES = 1024 * 1024;
const DEFAULT_HOSTED_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CHUNK_TIMEOUT_MS = 5000;
@@ -188,7 +236,8 @@ export function isOfficialExternalPluginCatalogFeed(
const sequence = raw.sequence;
const entries = raw.entries;
return (
OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSIONS.has(raw.schemaVersion) &&
typeof raw.schemaVersion === "number" &&
SUPPORTED_OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSIONS.has(raw.schemaVersion) &&
typeof raw.id === "string" &&
raw.id.trim().length > 0 &&
typeof raw.generatedAt === "string" &&
@@ -233,23 +282,184 @@ function sha256Hex(value: string): string {
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
}
function resolveHostedCatalogFeedUrl(feedUrl: string | undefined): URL {
const raw = feedUrl?.trim() || DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL;
function resolveHostedCatalogFeedUrl(raw: string): URL {
let parsed: URL;
try {
parsed = new URL(raw);
parsed = new URL(raw.trim());
} catch {
throw new Error("hosted catalog feed URL is invalid");
}
if (parsed.protocol !== "https:") {
throw new Error("hosted catalog feed URL must use HTTPS");
}
if (!OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_HOSTNAME_ALLOWLIST.includes(parsed.hostname)) {
throw new Error("hosted catalog feed URL hostname is not allowed");
}
return parsed;
}
function resolveOfficialExternalPluginCatalogProfileConfig(
config?: OfficialExternalPluginCatalogProfileConfig,
): Required<OfficialExternalPluginCatalogProfileConfig> {
return {
feeds: {
...DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_PROFILE_CONFIG.feeds,
...config?.feeds,
},
sources: {
...DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_PROFILE_CONFIG.sources,
...config?.sources,
},
};
}
function resolveHostedCatalogFeedSource(params: {
feedUrl?: string;
feedProfile?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
}): { url: URL; hostnameAllowlist: string[] } {
const profileConfig = resolveOfficialExternalPluginCatalogProfileConfig(params.catalogConfig);
const explicitFeedUrl = normalizeOptionalString(params.feedUrl);
if (explicitFeedUrl) {
const url = resolveHostedCatalogFeedUrl(explicitFeedUrl);
if (!OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_HOSTNAME_ALLOWLIST.includes(url.hostname)) {
throw new Error("hosted catalog feed URL hostname is not allowed");
}
return { url, hostnameAllowlist: OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_HOSTNAME_ALLOWLIST };
}
const profileName =
normalizeOptionalString(params.feedProfile) ??
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE;
const profile = profileConfig.feeds[profileName];
if (!profile) {
throw new Error(`hosted catalog feed profile "${profileName}" is not configured`);
}
const url = resolveHostedCatalogFeedUrl(profile.url);
return {
url,
hostnameAllowlist: uniqueStrings([
...OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_HOSTNAME_ALLOWLIST,
url.hostname,
]),
};
}
function getOfficialExternalPluginCatalogSourceRefs(
config?: OfficialExternalPluginCatalogProfileConfig,
): Set<string> {
return new Set(Object.keys(resolveOfficialExternalPluginCatalogProfileConfig(config).sources));
}
function getFeedEntryInstallCandidateRecords(
entry: OfficialExternalPluginCatalogEntry,
): OfficialExternalPluginCatalogInstallCandidate[] {
const install = isRecord(entry.install) ? entry.install : undefined;
const candidates = install?.candidates;
if (!Array.isArray(candidates)) {
return [];
}
return candidates.filter(
(candidate): candidate is OfficialExternalPluginCatalogInstallCandidate => isRecord(candidate),
);
}
function getFeedEntryInstallCandidates(
entry: OfficialExternalPluginCatalogEntry,
): OfficialExternalPluginCatalogInstallCandidate[] {
const state = normalizeOptionalString(entry.state);
if (state !== "available") {
return [];
}
const publisherTrust = normalizeOptionalString(entry.publisher?.trust);
if (publisherTrust !== "official") {
return [];
}
return getFeedEntryInstallCandidateRecords(entry);
}
function shouldRequireManifestInstallSourceRef(params: {
feedProfile?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
}): boolean {
const profileName =
normalizeOptionalString(params.feedProfile) ??
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE;
if (profileName !== DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE) {
return true;
}
const profileConfig = resolveOfficialExternalPluginCatalogProfileConfig(params.catalogConfig);
const profileUrl = normalizeOptionalString(profileConfig.feeds[profileName]?.url);
try {
return (
resolveHostedCatalogFeedUrl(profileUrl ?? DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL)
.href !==
resolveHostedCatalogFeedUrl(DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL).href
);
} catch {
return true;
}
}
function getManifestInstallSourceRefCandidate(
entry: OfficialExternalPluginCatalogEntry,
): OfficialExternalPluginCatalogInstallCandidate | undefined {
const install = getOfficialExternalPluginCatalogManifest(entry)?.install;
if (!install) {
return undefined;
}
const hasInstallSpec = Boolean(
normalizeOptionalString(install.clawhubSpec) ||
normalizeOptionalString(install.npmSpec) ||
normalizeOptionalString(install.localPath),
);
if (!hasInstallSpec) {
return undefined;
}
return {
sourceRef: normalizeOptionalString(install.sourceRef),
package:
normalizeOptionalString(install.npmSpec) ?? normalizeOptionalString(install.clawhubSpec),
};
}
export function validateOfficialExternalPluginCatalogEntrySourceRefs(
entry: OfficialExternalPluginCatalogEntry,
params?: {
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
requireManifestInstallSourceRef?: boolean;
},
): string[] {
const configuredSourceRefs = getOfficialExternalPluginCatalogSourceRefs(params?.catalogConfig);
const errors: string[] = [];
let candidates = getFeedEntryInstallCandidateRecords(entry);
if (params?.requireManifestInstallSourceRef) {
const manifestCandidate = getManifestInstallSourceRefCandidate(entry);
if (manifestCandidate) {
candidates = [...candidates, manifestCandidate];
} else if (candidates.length === 0) {
candidates = [{}];
}
}
for (const candidate of candidates) {
const sourceRef = normalizeOptionalString(candidate.sourceRef);
if (!sourceRef) {
errors.push("feed install candidate is missing sourceRef");
} else if (!configuredSourceRefs.has(sourceRef)) {
errors.push(`feed install candidate references unknown sourceRef "${sourceRef}"`);
}
}
return errors;
}
export function filterOfficialExternalPluginCatalogEntriesBySourceRefs(
entries: OfficialExternalPluginCatalogEntry[],
params?: {
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
requireManifestInstallSourceRef?: boolean;
},
): OfficialExternalPluginCatalogEntry[] {
return entries.filter(
(entry) => validateOfficialExternalPluginCatalogEntrySourceRefs(entry, params).length === 0,
);
}
function parseHostedCatalogContentLength(raw: string | null, maxBytes: number): void {
const normalized = normalizeOptionalString(raw);
if (!normalized) {
@@ -353,7 +563,9 @@ async function readHostedCatalogResponseText(params: {
function bundledOfficialExternalPluginCatalogEntries(): OfficialExternalPluginCatalogEntry[] {
return OFFICIAL_CATALOG_SOURCES.flatMap((source) =>
parseOfficialExternalPluginCatalogEntries(source),
filterOfficialExternalPluginCatalogEntriesBySourceRefs(
parseOfficialExternalPluginCatalogEntries(source),
),
);
}
@@ -410,6 +622,8 @@ function loadHostedCatalogSnapshotResult(params: {
expectedSha256?: string;
ifNoneMatch?: string;
ifModifiedSince?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
requireManifestInstallSourceRef?: boolean;
}): HostedOfficialExternalPluginCatalogLoadResult {
assertSnapshotMatchesRequestValidators({
snapshot: params.snapshot,
@@ -425,12 +639,18 @@ function loadHostedCatalogSnapshotResult(params: {
}
const raw = JSON.parse(params.snapshot.body) as unknown;
if (!isOfficialExternalPluginCatalogFeed(raw)) {
throw new Error("hosted catalog snapshot did not match schema version 1");
throw new Error("hosted catalog snapshot did not match a supported schema version");
}
return {
source: "hosted-snapshot",
entries: dedupeOfficialExternalPluginCatalogEntries(
parseOfficialExternalPluginCatalogEntries(raw),
filterOfficialExternalPluginCatalogEntriesBySourceRefs(
parseOfficialExternalPluginCatalogEntries(raw),
{
catalogConfig: params.catalogConfig,
requireManifestInstallSourceRef: params.requireManifestInstallSourceRef,
},
),
),
feed: raw,
metadata: params.snapshot.metadata,
@@ -464,6 +684,8 @@ async function snapshotOrBundledFallbackResult(params: {
expectedSha256?: string;
ifNoneMatch?: string;
ifModifiedSince?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
requireManifestInstallSourceRef?: boolean;
}): Promise<HostedOfficialExternalPluginCatalogLoadResult> {
if (params.snapshotStore) {
try {
@@ -475,6 +697,8 @@ async function snapshotOrBundledFallbackResult(params: {
expectedSha256: params.expectedSha256,
ifNoneMatch: params.ifNoneMatch,
ifModifiedSince: params.ifModifiedSince,
catalogConfig: params.catalogConfig,
requireManifestInstallSourceRef: params.requireManifestInstallSourceRef,
});
}
} catch (snapshotErr) {
@@ -524,6 +748,8 @@ async function resolveHostedCatalogSnapshotStore(params: {
export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
feedUrl?: string;
feedProfile?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
fetchImpl?: FetchLike;
timeoutMs?: number;
maxBytes?: number;
@@ -537,12 +763,17 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
stateDatabasePath?: string;
now?: () => Date;
}): Promise<HostedOfficialExternalPluginCatalogLoadResult> {
let url: URL;
let source: { url: URL; hostnameAllowlist: string[] };
try {
url = resolveHostedCatalogFeedUrl(params?.feedUrl);
source = resolveHostedCatalogFeedSource({
feedUrl: params?.feedUrl,
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
});
} catch (err) {
return bundledFallbackResult(err);
}
const { url } = source;
const snapshotStore = await resolveHostedCatalogSnapshotStore({
snapshotStore: params?.snapshotStore,
env: params?.env,
@@ -580,7 +811,7 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
requireHttps: true,
maxRedirects: 2,
timeoutMs: params?.timeoutMs ?? DEFAULT_HOSTED_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_TIMEOUT_MS,
policy: { hostnameAllowlist: OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_HOSTNAME_ALLOWLIST },
policy: { hostnameAllowlist: source.hostnameAllowlist },
auditContext: "official-external-plugin-catalog-feed",
});
response = guarded.response;
@@ -595,6 +826,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
expectedSha256,
ifNoneMatch,
ifModifiedSince,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
}
if (!response.ok) {
@@ -606,6 +842,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
expectedSha256,
ifNoneMatch,
ifModifiedSince,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
}
const body = await readHostedCatalogResponseText({
@@ -625,6 +866,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
expectedSha256,
ifNoneMatch,
ifModifiedSince,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
}
const raw = JSON.parse(body) as unknown;
@@ -637,8 +883,23 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
expectedSha256,
ifNoneMatch,
ifModifiedSince,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
}
const entries = filterOfficialExternalPluginCatalogEntriesBySourceRefs(
parseOfficialExternalPluginCatalogEntries(raw),
{
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
},
);
await snapshotStore
?.write({
body,
@@ -648,9 +909,7 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
.catch(() => undefined);
return {
source: "hosted",
entries: dedupeOfficialExternalPluginCatalogEntries(
parseOfficialExternalPluginCatalogEntries(raw),
),
entries: dedupeOfficialExternalPluginCatalogEntries(entries),
feed: raw,
metadata,
};
@@ -662,6 +921,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
expectedSha256,
ifNoneMatch,
ifModifiedSince,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
} finally {
if (response?.bodyUsed !== true) {
@@ -675,6 +939,100 @@ function normalizeDefaultChoice(value: unknown): PluginPackageInstall["defaultCh
return value === "clawhub" || value === "npm" || value === "local" ? value : undefined;
}
function formatFeedInstallCandidateSpec(
candidate: OfficialExternalPluginCatalogInstallCandidate,
): string | undefined {
const packageName = normalizeOptionalString(candidate.package);
if (!packageName) {
return undefined;
}
const version = normalizeOptionalString(candidate.version);
if (!version || packageName.endsWith(`@${version}`)) {
return packageName;
}
return `${packageName}@${version}`;
}
function getFeedEntryCandidateSourceType(
candidate: OfficialExternalPluginCatalogInstallCandidate,
config?: OfficialExternalPluginCatalogProfileConfig,
): OfficialExternalPluginCatalogSourceProfile["type"] | undefined {
const sourceRef = normalizeOptionalString(candidate.sourceRef);
if (!sourceRef) {
return undefined;
}
return resolveOfficialExternalPluginCatalogProfileConfig(config).sources[sourceRef]?.type;
}
function getPreferredFeedEntryInstallCandidate(params: {
entry: OfficialExternalPluginCatalogEntry;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
}): OfficialExternalPluginCatalogInstallCandidate | undefined {
const candidates = getFeedEntryInstallCandidates(params.entry).filter((candidate) =>
Boolean(normalizeOptionalString(candidate.package)),
);
return (
candidates.find(
(candidate) =>
normalizeOptionalString(candidate.sourceRef) ===
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_SOURCE_REF,
) ??
candidates.find(
(candidate) =>
normalizeOptionalString(candidate.sourceRef) ===
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_NPM_SOURCE_REF,
) ??
candidates.find((candidate) =>
Boolean(getFeedEntryCandidateSourceType(candidate, params.catalogConfig)),
)
);
}
function resolveFeedEntryInstallCandidate(params: {
entry: OfficialExternalPluginCatalogEntry;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
}): PluginPackageInstall | null {
const candidate = getPreferredFeedEntryInstallCandidate(params);
if (!candidate) {
return null;
}
const spec = formatFeedInstallCandidateSpec(candidate);
if (!spec) {
return null;
}
const sourceType = getFeedEntryCandidateSourceType(candidate, params.catalogConfig);
if (sourceType === "clawhub") {
const expectedIntegrity = normalizeClawHubSha256ExpectedIntegrity(candidate.integrity);
return {
clawhubSpec: `clawhub:${spec}`,
defaultChoice: "clawhub",
...(expectedIntegrity ? { expectedIntegrity } : {}),
};
}
if (sourceType === "npm") {
const expectedIntegrity = normalizeNpmExpectedIntegrity(candidate.integrity);
return {
npmSpec: spec,
defaultChoice: "npm",
...(expectedIntegrity ? { expectedIntegrity } : {}),
};
}
return null;
}
function normalizeClawHubSha256ExpectedIntegrity(value: unknown): string | undefined {
const integrity = normalizeOptionalString(value);
return integrity ? (normalizeClawHubSha256Integrity(integrity) ?? undefined) : undefined;
}
function normalizeNpmExpectedIntegrity(value: unknown): string | undefined {
const integrity = normalizeOptionalString(value);
if (!integrity || !/^[a-z0-9]+-[A-Za-z0-9+/=]+$/i.test(integrity)) {
return undefined;
}
return integrity;
}
/** Returns manifest metadata from an official external catalog entry when present. */
export function getOfficialExternalPluginCatalogManifest(
entry: OfficialExternalPluginCatalogEntry,
@@ -690,7 +1048,8 @@ export function resolveOfficialExternalPluginId(
return (
normalizeOptionalString(manifest?.plugin?.id) ??
normalizeOptionalString(manifest?.channel?.id) ??
normalizeOptionalString(manifest?.providers?.[0]?.id)
normalizeOptionalString(manifest?.providers?.[0]?.id) ??
normalizeOptionalString(entry.id)
);
}
@@ -719,6 +1078,7 @@ export function resolveOfficialExternalPluginLabel(
normalizeOptionalString(manifest?.plugin?.label) ??
normalizeOptionalString(manifest?.channel?.label) ??
normalizeOptionalString(manifest?.providers?.[0]?.name) ??
normalizeOptionalString(entry.title) ??
normalizeOptionalString(entry.name) ??
resolveOfficialExternalPluginId(entry) ??
"plugin"
@@ -727,18 +1087,36 @@ export function resolveOfficialExternalPluginLabel(
export function resolveOfficialExternalPluginInstall(
entry: OfficialExternalPluginCatalogEntry,
params?: { catalogConfig?: OfficialExternalPluginCatalogProfileConfig },
): PluginPackageInstall | null {
const manifest = getOfficialExternalPluginCatalogManifest(entry);
const install = manifest?.install;
const clawhubSpec = normalizeOptionalString(install?.clawhubSpec);
const npmSpec = normalizeOptionalString(install?.npmSpec) ?? normalizeOptionalString(entry.name);
const manifestNpmSpec = normalizeOptionalString(install?.npmSpec);
const localPath = normalizeOptionalString(install?.localPath);
if (!clawhubSpec && !npmSpec && !localPath) {
return null;
const candidateInstall = resolveFeedEntryInstallCandidate({
entry,
catalogConfig: params?.catalogConfig,
});
if (candidateInstall) {
return {
...candidateInstall,
...(install?.minHostVersion ? { minHostVersion: install.minHostVersion } : {}),
...(install?.expectedIntegrity && !candidateInstall.expectedIntegrity
? { expectedIntegrity: install.expectedIntegrity }
: {}),
...(install?.allowInvalidConfigRecovery === true ? { allowInvalidConfigRecovery: true } : {}),
};
}
const hasFeedInstallCandidates = getFeedEntryInstallCandidateRecords(entry).length > 0;
const npmSpec =
manifestNpmSpec ?? (hasFeedInstallCandidates ? undefined : normalizeOptionalString(entry.name));
const defaultChoice =
normalizeDefaultChoice(install?.defaultChoice) ??
(npmSpec ? "npm" : clawhubSpec ? "clawhub" : localPath ? "local" : undefined);
if (!clawhubSpec && !npmSpec && !localPath) {
return null;
}
return {
...(clawhubSpec ? { clawhubSpec } : {}),
...(npmSpec ? { npmSpec } : {}),