UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

448 lines (447 loc) • 22.8 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { a as redactSensitiveUrl } from "./redact-sensitive-url-C4n2JwGL.mjs"; import { n as validatePluginUiCapabilities } from "./plugin-ui-capabilities-C5nEC6Su.mjs"; import { t as validatePluginCategories } from "./src-COX2Khl5.mjs"; import { a as isClawHubTelemetryDisabled, d as readClawHubStringArrayField, f as readClawHubStringField, g as readRequiredClawHubStringField, h as readRequiredClawHubStringArrayField, i as fetchClawHubJson, m as readRequiredClawHubNumberField, o as isDefaultClawHubBaseUrl, p as readRequiredClawHubBooleanField, u as readClawHubNonEmptyStringFields, v as resolveClawHubBaseUrl, y as resolveClawHubImageUrl } from "./clawhub-client-3QLl6MrZ.mjs"; import { a as parseClawHubPackageSecurityResponse } from "./clawhub-packages-gLY3AxU7.mjs"; //#region src/infra/clawhub-plugin-manifest.ts function parseClawHubPluginCapabilities(summary) { const result = {}; const ui = validatePluginUiCapabilities(summary.uiCapabilities); if (ui.ok && ui.capabilities !== void 0) result.uiCapabilities = ui.capabilities; for (const field of ["providers", "channels"]) { const names = readClawHubStringArrayField(summary, field, "plugin manifest summary"); if (names) result[field] = names; } if (summary.contracts != null) { if (!isRecord(summary.contracts)) throw new Error("Malformed ClawHub plugin manifest summary: expected contracts to be an object."); const contracts = summary.contracts; result.contracts = Object.fromEntries(Object.keys(contracts).toSorted().map((family) => [family, readRequiredClawHubStringArrayField(contracts, family, "plugin contracts")])); } return result; } function parseClawHubPluginCompatibility(value, context) { if (!value) return; const compatibility = readClawHubNonEmptyStringFields(value, [ "pluginApiRange", "builtWithOpenClawVersion", "pluginSdkVersion", "minGatewayVersion" ], context); return Object.keys(compatibility).length > 0 ? compatibility : void 0; } function parseClawHubPluginMcpServer(entry, index) { const context = `plugin MCP server ${index}`; if (!isRecord(entry)) throw new Error(`Malformed ClawHub ${context}: expected an object.`); const name = readRequiredClawHubStringField(entry, "name", context); const rawUrl = readClawHubStringField(entry, "url", context); const scope = readClawHubStringField(entry, "scope", context)?.slice(0, 1e3); const setup = readClawHubStringField(entry, "setup", context)?.slice(0, 2e3); let url; let endpointRedacted = entry.endpointRedacted === true; if (rawUrl && !endpointRedacted) { try { const parsed = new URL(rawUrl); const host = parsed.hostname.replace(/\.$/u, ""); if (rawUrl.length <= 2048 && parsed.protocol === "https:" && !parsed.username && !parsed.password && !parsed.hash && !parsed.port && host.includes(".") && !/^[\d.]+$/u.test(host) && !host.includes(":") && !/(?:^|\.)(?:localhost|local|internal|test|invalid)$/u.test(host) && redactSensitiveUrl(rawUrl) === rawUrl) url = rawUrl; } catch {} endpointRedacted = !url; } return { name, ...url ? { url } : {}, ...entry.transport === "streamable-http" || entry.transport === "sse" || entry.transport === "stdio" ? { transport: entry.transport } : {}, ...entry.auth === "oauth" || entry.auth === "api-key" || entry.auth === "none" ? { auth: entry.auth } : {}, ...scope ? { scope } : {}, ...setup ? { setup } : {}, ...endpointRedacted ? { endpointRedacted } : {} }; } //#endregion //#region src/infra/clawhub-plugin-release.ts function projectSecurity(value) { const trust = value.trust; const moderationStatus = trust.moderationState && trust.moderationState !== "approved" ? trust.moderationState : void 0; return { status: trust.blockedFromDownload ? "blocked" : trust.pending ? "pending" : trust.stale ? "stale" : moderationStatus ?? trust.scanStatus ?? "unknown", ...value.verdict ? { verdict: value.verdict } : {}, auditUrl: value.securityAuditUrl, summary: value.overview }; } /** Project exact release facts; listing and artifact URLs cannot prove stored download bytes. */ async function readClawHubPluginReleaseFacts(params) { const { value, versionRecord } = params; const selectedRelease = versionRecord ? { version: readRequiredClawHubStringField(versionRecord, "version", "selected plugin release"), ...versionRecord.createdAt != null ? { createdAt: readRequiredClawHubNumberField(versionRecord, "createdAt", "selected plugin release") } : {}, ...versionRecord.changelog != null ? { changelog: readClawHubStringField(versionRecord, "changelog", "selected plugin release") ?? void 0 } : {}, tags: readClawHubStringArrayField(versionRecord, "distTags", "selected plugin release") ?? [] } : null; if (params.version && selectedRelease?.version !== params.version) throw new Error("ClawHub did not return the requested plugin release."); const readme = readClawHubStringField(value, "readme", "plugin detail response"); if (readme && Buffer.byteLength(readme, "utf8") > 524288) throw new Error("ClawHub plugin README exceeded 524288 bytes."); let security; let trust; let downloadability = selectedRelease ? { status: "unknown", reason: "ClawHub does not expose artifact storage availability for this release." } : { status: "unavailable", reason: "The listing has no selected release." }; if (value.security != null) try { const parsedSecurity = parseClawHubPackageSecurityResponse(value.security); if (!selectedRelease || parsedSecurity.package?.name && parsedSecurity.package.name !== params.packageName || parsedSecurity.release?.version && parsedSecurity.release.version !== selectedRelease.version) throw new Error("ClawHub security metadata does not describe the selected release."); security = projectSecurity(parsedSecurity); const { assessClawHubTrust } = await import("./clawhub-install-trust-DZbCzmhq.mjs"); trust = { disposition: assessClawHubTrust(parsedSecurity.trust), reasons: parsedSecurity.trust.reasons, pending: parsedSecurity.trust.pending, stale: parsedSecurity.trust.stale }; if (parsedSecurity.trust.blockedFromDownload) downloadability = { status: "unavailable", reason: parsedSecurity.trust.reasons.join("; ") || "ClawHub blocks downloads of this release." }; } catch {} return { selectedRelease, readme, security, trust, downloadability }; } //#endregion //#region src/infra/clawhub-plugin-catalog.ts const BARE_ICON_KEY = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; const PLUGIN_CATEGORY_ICON_KEYS = /* @__PURE__ */ new Set([ "activity", "book-open", "brain", "bot", "database", "git-branch", "globe", "message-circle", "message-square", "mic", "monitor", "package", "palette", "shield", "wrench", "plug", "code-xml", "server", "files", "inbox", "list-todo", "calendar-days", "wallet-cards", "megaphone", "chart-no-axes-combined", "workflow", "search" ]); function readOptionalNonNegativeNumber(value, field, context, kind = "number") { const candidate = value[field]; if (candidate === void 0 || candidate === null) return; if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate < 0) throw new Error(`Malformed ClawHub ${context}: expected ${field} to be non-negative.`); if (kind === "integer" && !Number.isInteger(candidate)) throw new Error(`Malformed ClawHub ${context}: expected ${field} to be an integer.`); return candidate; } function readOptionalBoolean(value, field, context) { return value[field] == null ? void 0 : readRequiredClawHubBooleanField(value, field, context); } function parseCatalogPackage(value, context, baseUrl) { if (!isRecord(value)) throw new Error(`Malformed ClawHub ${context}: expected package to be an object.`); const family = readRequiredClawHubStringField(value, "family", context); if (family !== "code-plugin" && family !== "bundle-plugin") throw new Error(`Malformed ClawHub ${context}: unsupported package family ${family}.`); const stats = readOptionalRecord(value, "stats", context); const display = readClawHubNonEmptyStringFields(value, [ "summary", "ownerHandle", "latestVersion", "runtimeId" ], context); const icon = readClawHubStringField(value, "icon", context) ?? readClawHubStringField(value, "ownerImage", context); const iconUrl = resolveClawHubImageUrl(icon, baseUrl) ?? icon; const verificationTier = readClawHubStringField(value, "verificationTier", context); const featured = readOptionalBoolean(value, "featured", context); const trending = readOptionalBoolean(value, "trending", context); const featuredRank = readOptionalNonNegativeNumber(value, "featuredRank", context, "integer"); const trendingRank = readOptionalNonNegativeNumber(value, "trendingRank", context, "integer"); const downloads = stats ? readOptionalNonNegativeNumber(stats, "downloads", `${context} stats`) : void 0; const installs = stats ? readOptionalNonNegativeNumber(stats, "installs", `${context} stats`) : void 0; return { packageName: readRequiredClawHubStringField(value, "name", context), displayName: readRequiredClawHubStringField(value, "displayName", context), family, isOfficial: readOptionalBoolean(value, "isOfficial", context) === true, categories: readClawHubStringArrayField(value, "categories", context) ?? [], ...display, ...iconUrl ? { iconUrl } : {}, ...verificationTier ? { verificationTier } : {}, ...featured !== void 0 ? { featured } : {}, ...trending !== void 0 ? { trending } : {}, ...featuredRank !== void 0 ? { featuredRank } : {}, ...trendingRank !== void 0 ? { trendingRank } : {}, ...downloads !== void 0 ? { downloads } : {}, ...installs !== void 0 ? { installs } : {} }; } function parsePluginCategories(value) { if (!isRecord(value) || !Array.isArray(value.categories)) throw new Error("Malformed ClawHub plugin categories response: expected categories to be an array."); const seenSlugs = /* @__PURE__ */ new Set(); const seenOrders = /* @__PURE__ */ new Set(); return value.categories.map((entry, index) => { if (!isRecord(entry)) throw new Error(`Malformed ClawHub plugin category ${index}: expected an object.`); const slug = readRequiredClawHubStringField(entry, "slug", `plugin category ${index}`); const icon = readRequiredClawHubStringField(entry, "icon", `plugin category ${index}`); const order = readRequiredClawHubNumberField(entry, "order", `plugin category ${index}`); if (!BARE_ICON_KEY.test(icon)) throw new Error(`Malformed ClawHub plugin category ${slug}: invalid icon key.`); if (!Number.isInteger(order) || order < 0 || seenSlugs.has(slug) || seenOrders.has(order)) throw new Error(`Malformed ClawHub plugin category ${slug}: duplicate or invalid ordering.`); const pinnedPackages = readClawHubStringArrayField(entry, "pinnedPackages", `plugin category ${slug}`); if (pinnedPackages && (new Set(pinnedPackages).size !== pinnedPackages.length || pinnedPackages.some((name) => !name.trim() || name !== name.trim()))) throw new Error(`Malformed ClawHub plugin category ${slug}: duplicate or invalid pinned package.`); seenSlugs.add(slug); seenOrders.add(order); return { slug, label: readRequiredClawHubStringField(entry, "label", `plugin category ${slug}`), description: readRequiredClawHubStringField(entry, "description", `plugin category ${slug}`), icon: PLUGIN_CATEGORY_ICON_KEYS.has(icon) ? icon : "package", order, ...pinnedPackages ? { pinnedPackages } : {} }; }).toSorted((left, right) => left.order - right.order); } function parseCatalogList(value, baseUrl) { if (!isRecord(value) || !Array.isArray(value.items)) throw new Error("Malformed ClawHub plugin catalog response: expected items to be an array."); const nextCursor = readClawHubStringField(value, "nextCursor", "plugin catalog response"); return { items: value.items.map((item, index) => parseCatalogPackage(item, `plugin catalog item ${index}`, baseUrl)), ...nextCursor ? { nextCursor } : {}, ...value.categories !== void 0 ? { categories: parsePluginCategories(value) } : {} }; } function parseCatalogSearch(value, baseUrl) { if (!isRecord(value) || !Array.isArray(value.results)) throw new Error("Malformed ClawHub plugin search response: expected results to be an array."); return { items: value.results.map((result, index) => { if (!isRecord(result)) throw new Error(`Malformed ClawHub plugin search result ${index}: expected an object.`); return parseCatalogPackage(result.package, `plugin search result ${index}`, baseUrl); }) }; } function readOptionalRecord(source, field, context) { const value = source[field]; if (value === void 0 || value === null) return; if (!isRecord(value)) throw new Error(`Malformed ClawHub ${context}: expected ${field} to be an object.`); return value; } function parseManifest(value) { if (!value) return { configFields: [], mcpServers: [], skills: [] }; const { configFields, mcpServers, bundledSkills } = value; if (!Array.isArray(configFields) || !Array.isArray(mcpServers) || !Array.isArray(bundledSkills)) throw new Error("Malformed ClawHub plugin manifest summary: expected capability arrays."); const compatibility = parseClawHubPluginCompatibility(readOptionalRecord(value, "compatibility", "plugin manifest summary"), "plugin manifest compatibility"); const mcpServerDetails = mcpServers.map(parseClawHubPluginMcpServer); return { ...compatibility ? { compatibility } : {}, ...parseClawHubPluginCapabilities(value), configFields: configFields.map((entry, index) => { if (!isRecord(entry)) throw new Error(`Malformed ClawHub plugin config field ${index}: expected an object.`); const description = readClawHubStringField(entry, "description", `plugin config field ${index}`); return Object.assign({ name: readRequiredClawHubStringField(entry, "name", `plugin config field ${index}`), required: readRequiredClawHubBooleanField(entry, "required", `plugin config field ${index}`), sensitive: readRequiredClawHubBooleanField(entry, "sensitive", `plugin config field ${index}`) }, description ? { description } : {}); }), mcpServers: mcpServerDetails.map(({ name }) => name), ...mcpServerDetails.length ? { mcpServerDetails } : {}, skills: bundledSkills.map((entry, index) => { if (!isRecord(entry)) throw new Error(`Malformed ClawHub bundled skill ${index}: expected an object.`); const description = readClawHubStringField(entry, "description", `bundled skill ${index}`); return Object.assign({ name: readRequiredClawHubStringField(entry, "name", `bundled skill ${index}`) }, description ? { description } : {}); }) }; } function parseVerification(value) { if (!value) return; const display = readClawHubNonEmptyStringFields(value, [ "summary", "sourceRepo", "sourceCommit", "sourcePath", "scanStatus" ], "plugin verification"); return { tier: readRequiredClawHubStringField(value, "tier", "plugin verification"), ...display }; } function parseVersions(value) { if (!isRecord(value) || !Array.isArray(value.items)) throw new Error("Malformed ClawHub plugin versions response: expected items to be an array."); return value.items.map((entry, index) => { if (!isRecord(entry)) throw new Error(`Malformed ClawHub plugin version ${index}: expected an object.`); const changelog = readClawHubStringField(entry, "changelog", `plugin version ${index}`); return { version: readRequiredClawHubStringField(entry, "version", `plugin version ${index}`), createdAt: readRequiredClawHubNumberField(entry, "createdAt", `plugin version ${index}`), changelog: changelog ?? "", tags: readClawHubStringArrayField(entry, "distTags", `plugin version ${index}`) ?? [] }; }); } async function fetchClawHubPluginCatalog(params) { const query = params.query?.trim(); const shared = { baseUrl: params.baseUrl, token: params.token, timeoutMs: params.timeoutMs, fetchImpl: params.fetchImpl }; if (query) { const searchSource = isClawHubTelemetryDisabled() ? void 0 : params.searchSource; return parseCatalogSearch(await fetchClawHubJson({ ...shared, path: "/api/v1/plugins/search", retryTransientReads: searchSource === void 0, search: { q: query, searchSource, category: params.category, isOfficial: params.officialOnly || params.intent === "official" ? "true" : void 0, limit: params.limit ? String(params.limit) : void 0 } }), params.baseUrl); } return parseCatalogList(await fetchClawHubJson({ ...shared, path: "/api/v1/plugins", search: { category: params.category, cursor: params.cursor, featured: params.intent === "featured" ? "true" : void 0, isOfficial: params.officialOnly || params.intent === "official" ? "true" : void 0, curated: (params.intent ?? "all") === "all" && params.category ? "true" : void 0, sort: params.intent === "featured" ? void 0 : params.intent === "trending" ? "trending" : "downloads", limit: params.limit ? String(params.limit) : void 0 } }), params.baseUrl); } async function fetchClawHubPluginOverview(options = {}) { const value = await fetchClawHubJson({ ...options, skipAuth: options.skipAuth ?? (!options.token && isDefaultClawHubBaseUrl(options.baseUrl)), path: "/api/v1/plugins/overview" }); if (!isRecord(value) || !Array.isArray(value.items)) throw new Error("Malformed ClawHub plugin overview response: expected items to be an array."); return { items: value.items.map((item, index) => parseCatalogPackage(item, `plugin overview item ${index}`, options.baseUrl)), categories: parsePluginCategories(value) }; } async function fetchClawHubPluginCategories(options = {}) { return parsePluginCategories(await fetchClawHubJson({ ...options, skipAuth: options.skipAuth ?? (!options.token && isDefaultClawHubBaseUrl(options.baseUrl)), path: "/api/v1/plugins/categories" })); } /** Read effective categories for exact installed ClawHub package versions in one request. */ async function fetchClawHubPluginVersionCategories(params) { if (params.packages.length === 0) return []; if (params.packages.length > 200) throw new Error("ClawHub plugin category batch cannot exceed 200 packages."); const value = await fetchClawHubJson({ ...params, method: "POST", path: "/api/v1/packages/categories:batch", json: { packages: params.packages } }); if (!isRecord(value) || !Array.isArray(value.packages)) throw new Error("Malformed ClawHub plugin category batch response: expected packages to be an array."); return value.packages.map((entry, index) => { if (!isRecord(entry)) throw new Error(`Malformed ClawHub plugin category batch item ${index}: expected an object.`); const rawCategories = entry.categories; let categories; if (rawCategories === null) categories = null; else { const validation = validatePluginCategories(rawCategories); if (!validation.ok || !validation.categories) throw new Error(`Malformed ClawHub plugin category batch item ${index}: expected categories to be a string array or null.`); categories = validation.categories; } return { name: readRequiredClawHubStringField(entry, "name", `plugin category batch item ${index}`), version: readRequiredClawHubStringField(entry, "version", `plugin category batch item ${index}`), categories }; }); } async function fetchClawHubPluginDetail(params) { const value = await fetchClawHubJson({ ...params, path: `/api/v1/packages/${encodeURIComponent(params.packageName)}/detail`, search: { version: params.version } }); if (!isRecord(value)) throw new Error("Malformed ClawHub plugin detail response: expected an object."); if (!isRecord(value.package)) throw new Error("Malformed ClawHub plugin detail response: expected package to be an object."); const catalog = parseCatalogPackage(value.package, "plugin detail", params.baseUrl); if (catalog.packageName !== params.packageName.trim().toLowerCase()) throw new Error("ClawHub returned a different plugin package identity."); const tagsRecord = readOptionalRecord(value.package, "tags", "plugin detail") ?? {}; const tags = Object.fromEntries(Object.keys(tagsRecord).map((tag) => [tag, readRequiredClawHubStringField(tagsRecord, tag, "plugin tags")])); const topics = readClawHubStringArrayField(value.package, "topics", "plugin detail") ?? []; const createdAt = readOptionalNonNegativeNumber(value.package, "createdAt", "plugin detail"); const updatedAt = readOptionalNonNegativeNumber(value.package, "updatedAt", "plugin detail"); const packageCompatibility = parseClawHubPluginCompatibility(readOptionalRecord(value.package, "compatibility", "plugin detail"), "plugin compatibility"); const ownerRecord = readOptionalRecord(value, "owner", "plugin detail response"); const ownerFields = readClawHubNonEmptyStringFields(ownerRecord ?? {}, ["handle", "displayName"], "plugin owner"); const ownerHandle = ownerFields.handle; const ownerImageUrl = ownerRecord ? readClawHubStringField(ownerRecord, "image", "plugin owner") : void 0; const versionRecord = readOptionalRecord(value, "version", "plugin detail response"); const { selectedRelease, readme, security, trust, downloadability } = await readClawHubPluginReleaseFacts({ value, versionRecord, packageName: catalog.packageName, version: params.version }); const manifestRecord = versionRecord ? readOptionalRecord(versionRecord, "pluginManifestSummary", "plugin version") : void 0; const manifest = parseManifest(manifestRecord); const verification = parseVerification(versionRecord ? readOptionalRecord(versionRecord, "verification", "plugin version") : void 0); const owner = { ...ownerFields, ...ownerImageUrl ? { imageUrl: ownerImageUrl } : {}, ...typeof ownerRecord?.official === "boolean" ? { official: ownerRecord.official } : {} }; return { ...catalog, registry: resolveClawHubBaseUrl(params.baseUrl), tags, selectedRelease, downloadability, metadata: { manifest: manifestRecord ? "available" : "missing", readme: readme != null ? "available" : "missing", security: security ? "available" : "missing" }, ...trust ? { trust } : {}, ...ownerHandle && !catalog.ownerHandle ? { ownerHandle } : {}, ...Object.keys(owner).length > 0 ? { owner } : {}, topics, ...createdAt !== void 0 ? { createdAt } : {}, ...updatedAt !== void 0 ? { updatedAt } : {}, ...readme ? { readme } : {}, ...verification?.sourceRepo ? { repositoryUrl: verification.sourceRepo } : {}, ...packageCompatibility ? { compatibility: packageCompatibility } : {}, ...manifest, versions: parseVersions(value.versions), ...verification ? { verification } : {}, ...security ? { security } : {} }; } //#endregion export { fetchClawHubPluginVersionCategories as a, fetchClawHubPluginOverview as i, fetchClawHubPluginCategories as n, fetchClawHubPluginDetail as r, fetchClawHubPluginCatalog as t };