Add hosted catalog source profile validation

This commit is contained in:
Gio Della-Libera
2026-06-22 21:49:24 -07:00
parent f261e7680f
commit c3348ecc54
2 changed files with 860 additions and 23 deletions

View File

@@ -6,10 +6,12 @@ import officialExternalPluginCatalog from "../../scripts/lib/official-external-p
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { createSqliteHostedOfficialExternalPluginCatalogSnapshotStore } from "./official-external-plugin-catalog-snapshot-store.js";
import {
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL,
type OfficialExternalPluginCatalogEntry,
createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore,
getOfficialExternalPluginCatalogEntry,
isOfficialExternalPluginCatalogFeed,
filterOfficialExternalPluginCatalogEntriesBySourceRefs,
listOfficialExternalPluginCatalogEntries,
loadHostedOfficialExternalPluginCatalogEntries,
parseOfficialExternalPluginCatalogEntries,
@@ -19,6 +21,7 @@ import {
resolveOfficialExternalWebProviderContractPluginIdsForEnv,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
validateOfficialExternalPluginCatalogEntrySourceRefs,
} from "./official-external-plugin-catalog.js";
function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry {
@@ -139,6 +142,354 @@ 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(String(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: 1,
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:abc",
},
],
},
},
],
});
const result = await loadHostedOfficialExternalPluginCatalogEntries({
fetchImpl: vi.fn(async (url: RequestInfo | URL) => {
expect(String(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",
npmSpec: "@openclaw/live-feed-proof@1.0.0",
defaultChoice: "clawhub",
expectedIntegrity: "sha256:abc",
});
});
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(String(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(String(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([]);
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/top-level-candidate-only",
"@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,
@@ -282,7 +633,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: [{}] });
@@ -325,6 +676,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,
@@ -418,7 +822,7 @@ describe("official external plugin catalog", () => {
entries: [],
}),
metadata: {
url: "https://register.openclaw.ai/official-external-plugin-catalog.json",
url: "https://clawhub.ai/v1/feeds/plugins",
status: 200,
checksum: "sha256:not-current",
},
@@ -511,6 +915,73 @@ describe("official external plugin catalog", () => {
}
});
it("prefers feed install candidates before legacy install metadata", () => {
expect(
resolveOfficialExternalPluginInstall({
name: "@legacy/plain-package",
kind: "plugin",
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@openclaw/candidate-package",
version: "1.2.3",
integrity: "sha256:candidate",
},
],
},
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",
npmSpec: "@openclaw/candidate-package@1.2.3",
defaultChoice: "clawhub",
expectedIntegrity: "sha256:candidate",
minHostVersion: ">=2026.6.1",
allowInvalidConfigRecovery: true,
});
expect(
resolveOfficialExternalPluginInstall(
{
name: "@acme/private-package",
kind: "plugin",
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: "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"],

View File

@@ -71,20 +71,64 @@ export type OfficialExternalPluginCatalogManifest = {
};
providers?: readonly OfficialExternalProviderCatalogProvider[];
webSearchProviders?: readonly OfficialExternalWebSearchProvider[];
install?: PluginPackageInstall;
install?: PluginPackageInstall & { sourceRef?: string };
contracts?: PluginManifestContracts;
channelConfigs?: Record<string, PluginManifestChannelConfig>;
};
/** Raw official external catalog entry loaded from generated catalog JSON. */
export type OfficialExternalPluginCatalogEntry = {
id?: string;
title?: string;
type?: string;
state?: string;
publisher?: {
id?: string;
trust?: string;
};
name?: string;
version?: string;
description?: string;
source?: string;
kind?: string;
install?: {
candidates?: readonly OfficialExternalPluginCatalogInstallCandidate[];
};
} & Partial<Record<ManifestKey, OfficialExternalPluginCatalogManifest>>;
export type OfficialExternalPluginCatalogInstallCandidate = {
sourceRef?: string;
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. */
export type OfficialExternalPluginCatalogFeed = {
schemaVersion: 1;
@@ -155,11 +199,35 @@ const OFFICIAL_CATALOG_SOURCES = [
const OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSION = 1;
export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL =
"https://register.openclaw.ai/official-marketplace.json";
"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;
const OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_HOSTNAME_ALLOWLIST = ["register.openclaw.ai"];
const OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_HOSTNAME_ALLOWLIST = [
"clawhub.ai",
"register.openclaw.ai",
];
export function isOfficialExternalPluginCatalogFeed(
raw: unknown,
@@ -215,23 +283,166 @@ 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 getFeedEntryInstallCandidates(
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 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 = getFeedEntryInstallCandidates(entry);
if (candidates.length === 0 && params?.requireManifestInstallSourceRef) {
const manifestCandidate = getManifestInstallSourceRefCandidate(entry);
candidates = manifestCandidate ? [manifestCandidate] : [{}];
}
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) {
@@ -335,7 +546,9 @@ async function readHostedCatalogResponseText(params: {
function bundledOfficialExternalPluginCatalogEntries(): OfficialExternalPluginCatalogEntry[] {
return OFFICIAL_CATALOG_SOURCES.flatMap((source) =>
parseOfficialExternalPluginCatalogEntries(source),
filterOfficialExternalPluginCatalogEntriesBySourceRefs(
parseOfficialExternalPluginCatalogEntries(source),
),
);
}
@@ -373,6 +586,8 @@ function loadHostedCatalogSnapshotResult(params: {
snapshot: HostedOfficialExternalPluginCatalogSnapshot;
error: unknown;
expectedSha256?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
requireManifestInstallSourceRef?: boolean;
}): HostedOfficialExternalPluginCatalogLoadResult {
const checksum = sha256Hex(params.snapshot.body);
if (checksum !== params.snapshot.metadata.checksum) {
@@ -388,7 +603,13 @@ function loadHostedCatalogSnapshotResult(params: {
return {
source: "hosted-snapshot",
entries: dedupeOfficialExternalPluginCatalogEntries(
parseOfficialExternalPluginCatalogEntries(raw),
filterOfficialExternalPluginCatalogEntriesBySourceRefs(
parseOfficialExternalPluginCatalogEntries(raw),
{
catalogConfig: params.catalogConfig,
requireManifestInstallSourceRef: params.requireManifestInstallSourceRef,
},
),
),
feed: raw,
metadata: params.snapshot.metadata,
@@ -403,6 +624,8 @@ async function snapshotOrBundledFallbackResult(params: {
url: string;
metadata?: HostedOfficialExternalPluginCatalogLoadResult["metadata"];
expectedSha256?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
requireManifestInstallSourceRef?: boolean;
}): Promise<HostedOfficialExternalPluginCatalogLoadResult> {
if (params.snapshotStore) {
try {
@@ -412,6 +635,8 @@ async function snapshotOrBundledFallbackResult(params: {
snapshot,
error: params.error,
expectedSha256: params.expectedSha256,
catalogConfig: params.catalogConfig,
requireManifestInstallSourceRef: params.requireManifestInstallSourceRef,
});
}
} catch (snapshotErr) {
@@ -461,6 +686,8 @@ async function resolveHostedCatalogSnapshotStore(params: {
export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
feedUrl?: string;
feedProfile?: string;
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
fetchImpl?: FetchLike;
timeoutMs?: number;
maxBytes?: number;
@@ -474,12 +701,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,
@@ -517,7 +749,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;
@@ -530,6 +762,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
url: url.href,
metadata: base,
expectedSha256,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
}
if (!response.ok) {
@@ -539,6 +776,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
url: url.href,
metadata: base,
expectedSha256,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
}
const body = await readHostedCatalogResponseText({
@@ -556,6 +798,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
url: url.href,
metadata,
expectedSha256,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
}
const raw = JSON.parse(body) as unknown;
@@ -566,8 +813,23 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
url: url.href,
metadata,
expectedSha256,
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,
@@ -577,9 +839,7 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
.catch(() => undefined);
return {
source: "hosted",
entries: dedupeOfficialExternalPluginCatalogEntries(
parseOfficialExternalPluginCatalogEntries(raw),
),
entries: dedupeOfficialExternalPluginCatalogEntries(entries),
feed: raw,
metadata,
};
@@ -589,6 +849,11 @@ export async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
snapshotStore,
url: url.href,
expectedSha256,
catalogConfig: params?.catalogConfig,
requireManifestInstallSourceRef: shouldRequireManifestInstallSourceRef({
feedProfile: params?.feedProfile,
catalogConfig: params?.catalogConfig,
}),
});
} finally {
if (response?.bodyUsed !== true) {
@@ -602,6 +867,87 @@ 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);
const expectedIntegrity = normalizeOptionalString(candidate.integrity);
if (sourceType === "clawhub") {
return {
clawhubSpec: `clawhub:${spec}`,
npmSpec: spec,
defaultChoice: "clawhub",
...(expectedIntegrity ? { expectedIntegrity } : {}),
};
}
if (sourceType === "npm") {
return {
npmSpec: spec,
defaultChoice: "npm",
...(expectedIntegrity ? { expectedIntegrity } : {}),
};
}
return null;
}
/** Returns manifest metadata from an official external catalog entry when present. */
export function getOfficialExternalPluginCatalogManifest(
entry: OfficialExternalPluginCatalogEntry,
@@ -617,7 +963,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)
);
}
@@ -646,6 +993,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"
@@ -654,18 +1002,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 = getFeedEntryInstallCandidates(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 } : {}),