openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
416 lines (415 loc) • 20.8 kB
JavaScript
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { r as getPluginRegistryVersion } from "./runtime-state-Cd-Ga9cg.mjs";
import { r as getPluginRegistryForContext } from "./gateway-request-scope-BSmCVVpP.mjs";
import { t as ManagedPluginLifecycleError } from "./management-lifecycle-error-ySeX1uI2.mjs";
import { c as readConfigFileSnapshot } from "./io.runtime-BsUXSsD4.mjs";
import "./config-DLycWFvB.mjs";
import { Jn as validatePluginsCatalogCategoriesParams, Yn as validatePluginsCatalogGetParams, ar as validatePluginsSearchParams, er as validatePluginsInspectParams, nr as validatePluginsListParams, qn as validatePluginsCatalogBrowseParams, ym as validatePluginsSkillsReadParams } from "./src-l2dWvEsN.mjs";
import { ii as validatePluginsCredentialsInspectParams } from "./sessions-DJqMAtE3.mjs";
import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs";
import { f as errorShape } from "./error-codes-Dh56LniQ.mjs";
import "./skill-library-Bgib5R_8.mjs";
import { b as withClawHubResponse, g as readRequiredClawHubStringField, i as fetchClawHubJson, l as readClawHubBytes, m as readRequiredClawHubNumberField, v as resolveClawHubBaseUrl } from "./clawhub-client-D-mEumEV.mjs";
import { t as SKILL_LIBRARY_MAX_TREE_ENTRIES } from "./bundle-Cu4zt9nj.mjs";
import { a as pluginSkillFileFromBytes, o as validatePluginSkillPath, t as readPluginSkill } from "./plugin-skills-D6rWOyPc.mjs";
import { y as withManagedPluginCache } from "./management-catalog-Hcvn1oTJ.mjs";
import { d as resolvePluginCredentialDescriptors, f as fetchClawHubPluginCatalog, h as fetchClawHubPluginOverview, m as fetchClawHubPluginDetail, n as listManagedPlugins, p as fetchClawHubPluginCategories, s as resolveManagedPluginMetadata, t as inspectManagedPlugin, u as inspectPluginCredentialValue } from "./management-service-CegC-Bvf.mjs";
import { a as joinClawHubPluginDetail, c as resolvePluginDiscoveryIdentity, i as joinClawHubPluginCatalog, n as encodePluginDiscoveryId, o as joinLocalPluginDetail, r as findLocalPluginByIdentity, t as encodeLocalPluginDiscoveryId } from "./catalog-discovery-DHZUEx3K.mjs";
import { n as registerClawHubCatalogIconUrls } from "./catalog-icon-registry-QbEIkWWZ.mjs";
import { t as searchInstallablePluginPackages } from "./catalog-search-m4ClEdqP.mjs";
import { n as defineValidatedGatewayHandler } from "./validation-Bu27N0nv.mjs";
import { n as listPluginServiceHealthFailures } from "./service-health-DdlAkI7p.mjs";
import { createHash } from "node:crypto";
//#region src/infra/clawhub-plugin-skills.ts
/** A release inventory supplies both the declared boundary and exact hashes; no archive or install. */
async function fetchClawHubPluginSkill(params) {
if (params.path !== void 0) validatePluginSkillPath(params.path);
const deadline = Date.now() + (params.timeoutMs ?? 3e4);
const remainingMs = () => Math.max(1, deadline - Date.now());
const basePath = `/api/v1/packages/${encodeURIComponent(params.packageName)}`;
const value = await fetchClawHubJson({
...params,
timeoutMs: remainingMs(),
retryTransientReads: false,
path: `${basePath}/versions/${encodeURIComponent(params.version)}`
});
if (!isRecord(value) || !isRecord(value.package) || value.package.name !== params.packageName || !isRecord(value.version) || value.version.version !== params.version || !Array.isArray(value.version.files) || !isRecord(value.version.pluginManifestSummary) || !Array.isArray(value.version.pluginManifestSummary.bundledSkills)) throw new Error("ClawHub did not return the selected plugin version and skill inventory.");
const matches = value.version.pluginManifestSummary.bundledSkills.filter((skill) => isRecord(skill) && skill.name === params.skillName);
const selected = matches[0];
if (matches.length !== 1 || !isRecord(selected)) throw new Error(matches.length ? "Plugin skill name is ambiguous." : "Plugin skill not found.");
const rootPath = readRequiredClawHubStringField(selected, "rootPath", "bundled skill");
const rawEntry = readRequiredClawHubStringField(selected, "skillMdPath", "bundled skill");
const entry = rawEntry.replace(/^(?:\.\/)+/u, "");
if (rootPath !== ".") validatePluginSkillPath(rootPath);
const prefix = rootPath === "." ? "" : `${rootPath}/`;
const entryPath = entry.slice(prefix.length);
if (!entry.startsWith(prefix) || entryPath.toLowerCase() !== "skill.md") throw new Error("ClawHub skill entry is outside its declared bundle.");
const files = value.version.files.flatMap((candidate) => {
if (!isRecord(candidate)) throw new Error("Invalid ClawHub file inventory.");
const fullPath = readRequiredClawHubStringField(candidate, "path", "skill file");
const bundlePath = fullPath.replace(/^(?:\.\/)+/u, "");
if (!bundlePath.startsWith(prefix)) return [];
const filePath = bundlePath.slice(prefix.length);
validatePluginSkillPath(filePath);
const size = readRequiredClawHubNumberField(candidate, "size", "skill file");
const hash = readRequiredClawHubStringField(candidate, "sha256", "skill file");
if (!Number.isSafeInteger(size) || size < 0 || !/^[a-f0-9]{64}$/u.test(hash)) throw new Error("Invalid ClawHub skill file integrity metadata.");
return [{
path: filePath,
fullPath,
size,
hash
}];
}).toSorted((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
if (files.length > 256) throw new Error("Skill bundle exceeds file count limit.");
if (new Set(files.map((file) => file.path.toLowerCase())).size !== files.length || !files.some((file) => file.fullPath === rawEntry && file.path === entryPath)) throw new Error("ClawHub skill inventory is incomplete or ambiguous.");
const selectedPath = params.path ?? entryPath;
if (!files.some((file) => file.path === selectedPath)) throw new Error("Plugin skill file not found.");
const directories = new Set(files.flatMap((file) => {
const parts = file.path.split("/");
return parts.slice(1).map((_, index) => parts.slice(0, index + 1).join("/"));
}));
if (files.length + directories.size > SKILL_LIBRARY_MAX_TREE_ENTRIES) throw new Error("Skill bundle exceeds inventory limits.");
const result = {
name: params.skillName,
rootPath,
entryPath,
version: params.version,
files: [],
directories: [],
inventoryComplete: true
};
let totalBytes = 0;
for (const file of files) {
if (file.size > 1048576 || totalBytes + file.size > 8388608) {
result.files.push({
path: file.path,
sizeBytes: file.size,
status: "too-large"
});
continue;
}
totalBytes += file.size;
if (file.path !== selectedPath) {
result.files.push({
path: file.path,
sizeBytes: file.size,
status: "deferred"
});
continue;
}
if (Date.now() >= deadline) {
result.files.push({
path: file.path,
sizeBytes: file.size,
status: "unavailable"
});
continue;
}
try {
const read = await withClawHubResponse({
...params,
timeoutMs: remainingMs(),
retryTransientReads: false,
path: `${basePath}/file`,
search: {
path: file.fullPath,
version: params.version
},
headers: { Accept: "application/octet-stream" }
}, async ({ response }) => {
if (!response.ok) return {
path: file.path,
sizeBytes: file.size,
status: response.status === 413 ? "too-large" : "unavailable"
};
const buffer = await readClawHubBytes({
response,
maxBytes: Math.max(1, file.size),
timeoutMs: remainingMs(),
resourceLabel: "plugin skill file"
});
if (buffer.length !== file.size || createHash("sha256").update(buffer).digest("hex") !== file.hash) return {
path: file.path,
sizeBytes: file.size,
status: "unavailable"
};
return pluginSkillFileFromBytes(file.path, buffer);
});
result.files.push(read);
} catch {
result.files.push({
path: file.path,
sizeBytes: file.size,
status: "unavailable"
});
}
}
result.directories = [...directories].toSorted();
return result;
}
//#endregion
//#region src/plugins/management-skill-read.ts
const readManagedPluginSkill = withManagedPluginCache(async (params) => {
const metadata = resolveManagedPluginMetadata(params.config, params.env ?? process.env);
const manifest = metadata.byPluginId.get(metadata.normalizePluginId(params.pluginId));
if (!manifest) throw new ManagedPluginLifecycleError("Installed plugin not found.");
return readPluginSkill(manifest, params.skillName, params);
});
//#endregion
//#region src/gateway/server-methods/plugins.credentials.ts
const pluginCredentialHandlers = { "plugins.credentials.inspect": defineValidatedGatewayHandler("plugins.credentials.inspect", validatePluginsCredentialsInspectParams, async ({ params, client, context, respond, signal, hasCurrentClientAuthority }) => {
const authorized = () => Boolean(client && !client.invalidated && !client.connectionSignal?.aborted && client.connect.scopes?.includes("operator.admin") && !signal?.aborted && (!hasCurrentClientAuthority || hasCurrentClientAuthority()));
const denied = () => respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Current administrator access is required to inspect plugin credentials."));
if (!authorized()) {
denied();
return;
}
try {
const snapshot = await readConfigFileSnapshot();
if (!authorized()) {
denied();
return;
}
const baseHash = snapshot.hash ? context.configRevisionProjector.projectRawHash(snapshot.hash) : void 0;
if (!baseHash || baseHash !== params.baseHash) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Configuration changed. Reload Settings before inspecting this reference."));
return;
}
const manifest = resolveManagedPluginMetadata(context.getRuntimeConfig(), process.env).byPluginId.get(params.pluginId);
const descriptor = manifest && resolvePluginCredentialDescriptors(manifest).find((field) => JSON.stringify(field.path) === JSON.stringify(params.path));
if (!descriptor) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "This installed plugin does not declare that credential field."));
return;
}
respond(true, {
baseHash,
credential: inspectPluginCredentialValue(snapshot.sourceConfig, descriptor, process.env, params.reveal === true)
}, void 0);
} catch {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "Credential metadata is unavailable. Reload Settings and try again."));
}
}) };
//#endregion
//#region src/gateway/server-methods/plugins.ts
function pluginReadError(error) {
return errorShape(error instanceof ManagedPluginLifecycleError && error.kind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error));
}
const pluginsHandlers = {
...pluginCredentialHandlers,
"plugins.skills.read": defineValidatedGatewayHandler("plugins.skills.read", validatePluginsSkillsReadParams, async ({ params, respond, context }) => {
if (params.path !== void 0) try {
validatePluginSkillPath(params.path);
} catch {
throw new ManagedPluginLifecycleError("Invalid plugin skill bundle path.");
}
if (params.source === "installed") {
respond(true, await readManagedPluginSkill({
config: context.getRuntimeConfig(),
pluginId: params.pluginId,
skillName: params.skillName,
path: params.path,
version: params.version
}), void 0);
return;
}
const identity = resolvePluginDiscoveryIdentity(params.catalogId);
if (!identity || identity.origin !== "clawhub") throw new ManagedPluginLifecycleError("Unknown ClawHub plugin identity.");
respond(true, await fetchClawHubPluginSkill({
packageName: identity.identity,
version: params.version,
skillName: params.skillName,
path: params.path
}), void 0);
}, pluginReadError),
"plugins.list": defineValidatedGatewayHandler("plugins.list", validatePluginsListParams, async ({ respond, context }) => {
const catalog = await listManagedPlugins({ config: context.getRuntimeConfig() });
const registry = getPluginRegistryForContext();
const records = new Map(registry?.plugins.toReversed().map((record) => [record.id, record]));
const failures = new Map(registry ? listPluginServiceHealthFailures(registry).map((failure) => [failure.pluginId, failure]) : []);
respond(true, {
...catalog,
generation: getPluginRegistryVersion(registry),
plugins: catalog.plugins.map((plugin) => {
const record = records.get(plugin.id);
const failure = failures.get(plugin.id);
const error = failure ? `${failure.serviceId}: ${failure.error}` : record?.error;
return Object.assign({}, plugin, {
catalogId: plugin.clawhubPackage ? encodePluginDiscoveryId(plugin.clawhubPackage) : encodeLocalPluginDiscoveryId(plugin.id),
runtime: {
state: record?.status === "loaded" ? failure ? "service-failed" : "active" : record?.status === "disabled" ? "disabled" : "unloaded",
...error ? { error: error.slice(0, 2e3) } : {}
}
});
})
}, void 0);
}, (error) => errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error))),
"plugins.inspect": defineValidatedGatewayHandler("plugins.inspect", validatePluginsInspectParams, async ({ params, respond, context }) => {
let target;
if ("pluginId" in params) target = { pluginId: params.pluginId };
else if ("source" in params) target = { clawhub: {
packageName: params.packageName,
version: params.version
} };
else {
const identity = resolvePluginDiscoveryIdentity(params.catalogId);
if (!identity) throw new ManagedPluginLifecycleError("Unknown plugin catalog identity.", { kind: "invalid-request" });
if (identity.origin === "local" && params.version) throw new ManagedPluginLifecycleError("Local plugin inspection does not select releases.", { kind: "invalid-request" });
target = identity.origin === "clawhub" ? { clawhub: {
packageName: identity.identity,
version: params.version
} } : { pluginId: identity.identity };
}
const remote = "clawhub" in target;
const inspected = await inspectManagedPlugin({
config: context.getRuntimeConfig(),
...target
});
const { inspectDecisionProviders } = await import("./runtime-C13YWgrp.mjs");
respond(true, {
...inspected,
decisions: remote ? [] : inspectDecisionProviders(context.getRuntimeConfig()).filter((entry) => entry.pluginId === inspected.plugin.id)
}, void 0);
}, pluginReadError),
"plugins.search": defineValidatedGatewayHandler("plugins.search", validatePluginsSearchParams, async ({ params, respond }) => {
respond(true, { results: (await searchInstallablePluginPackages({
query: params.query,
limit: params.limit
})).flatMap((entry) => {
if (entry.package.family !== "code-plugin" && entry.package.family !== "bundle-plugin") return [];
const downloads = entry.package.stats?.downloads;
return [{
score: entry.score,
package: {
name: entry.package.name,
displayName: entry.package.displayName,
family: entry.package.family,
channel: entry.package.channel,
isOfficial: entry.package.isOfficial,
...entry.package.summary ? { summary: entry.package.summary } : {},
...entry.package.latestVersion ? { latestVersion: entry.package.latestVersion } : {},
...entry.package.runtimeId ? { runtimeId: entry.package.runtimeId } : {},
...typeof downloads === "number" && Number.isFinite(downloads) && downloads >= 0 ? { downloads } : {},
...entry.package.verificationTier ? { verificationTier: entry.package.verificationTier } : {}
}
}];
}) }, void 0);
}, (error) => errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error))),
"plugins.catalog.browse": defineValidatedGatewayHandler("plugins.catalog.browse", validatePluginsCatalogBrowseParams, async ({ params, respond, context }) => {
if (params.query?.trim() && params.cursor) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Plugin search does not accept a browse cursor."));
return;
}
const query = params.query?.trim();
const intent = params.intent ?? "all";
const includeBundledOnly = intent === "bundled" || intent === "official" || intent === "all";
const overviewRequest = intent === "all" && !query && !params.category && !params.cursor;
const remoteRequest = overviewRequest ? fetchClawHubPluginOverview() : intent === "bundled" ? Promise.resolve({ items: [] }) : fetchClawHubPluginCatalog({
query,
...params.searchSource ? { searchSource: params.searchSource } : {},
intent,
category: params.category,
cursor: params.cursor,
limit: params.pageSize ?? 20
});
const [[remoteResult], local] = await Promise.all([Promise.allSettled([remoteRequest]), listManagedPlugins({ config: context.getRuntimeConfig() })]);
const catalogOptions = {
local,
includeBundledOnly,
intent,
category: params.category,
query: params.query,
cursor: params.cursor
};
try {
if (remoteResult.status === "rejected") throw remoteResult.reason;
const remote = remoteResult.value;
const items = joinClawHubPluginCatalog({
...catalogOptions,
remote: remote.items,
categories: remote.categories
});
registerClawHubCatalogIconUrls(items.map((item) => item.catalog.imageUrl));
respond(true, {
items,
...overviewRequest ? { categories: remote.categories } : {},
...remote.nextCursor ? { nextCursor: remote.nextCursor } : {}
}, void 0);
} catch (error) {
respond(true, {
items: joinClawHubPluginCatalog({
...catalogOptions,
remote: []
}),
...params.cursor ? { nextCursor: params.cursor } : {},
remoteError: `ClawHub is unavailable: ${formatErrorMessage(error)}.${intent === "all" ? " Installed plugins remain available." : includeBundledOnly ? " Bundled plugins remain available." : ""}`
}, void 0);
}
}, (error) => errorShape(ErrorCodes.UNAVAILABLE, `Plugin discovery is unavailable: ${formatErrorMessage(error)}. Retry to reconnect to ClawHub.`)),
"plugins.catalog.categories": defineValidatedGatewayHandler("plugins.catalog.categories", validatePluginsCatalogCategoriesParams, async ({ respond }) => {
respond(true, { categories: await fetchClawHubPluginCategories() }, void 0);
}, (error) => errorShape(ErrorCodes.UNAVAILABLE, `Plugin categories are unavailable: ${formatErrorMessage(error)}. Retry to reconnect to ClawHub.`)),
"plugins.catalog.get": defineValidatedGatewayHandler("plugins.catalog.get", validatePluginsCatalogGetParams, async ({ params, respond, context }) => {
const identity = resolvePluginDiscoveryIdentity(params.id);
if (!identity) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Unknown plugin discovery identity."));
return;
}
if (identity.origin === "local" && params.version) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Local plugin details do not select releases."));
return;
}
const local = await listManagedPlugins({ config: context.getRuntimeConfig() });
const localPlugin = findLocalPluginByIdentity(local, identity.identity, identity.origin);
let remoteError;
if (identity.origin !== "local") try {
const remote = await fetchClawHubPluginDetail({
packageName: identity.identity,
...params.version ? { version: params.version } : {}
});
registerClawHubCatalogIconUrls([remote.iconUrl, remote.owner?.imageUrl]);
respond(true, joinClawHubPluginDetail({
remote,
local
}), void 0);
return;
} catch (error) {
if (!localPlugin) throw error;
remoteError = `ClawHub details are unavailable: ${formatErrorMessage(error)}. Showing installed plugin metadata.`;
}
else if (!localPlugin) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Unknown local plugin discovery identity."));
return;
}
const inspectionPluginId = localPlugin.installed ? localPlugin.id : identity.origin === "local" && localPlugin.install?.source === "official" ? localPlugin.install.pluginId : void 0;
const inspection = inspectionPluginId ? await inspectManagedPlugin({
config: context.getRuntimeConfig(),
pluginId: inspectionPluginId
}) : void 0;
const result = joinLocalPluginDetail({
plugin: localPlugin,
local,
inspection
});
if (remoteError) {
result.plugin.id = params.id;
result.plugin.catalog.packageName = identity.identity;
result.detail = {
...result.detail,
packageName: identity.identity,
registry: resolveClawHubBaseUrl(),
remoteError,
...params.version ? { requestedVersion: params.version } : {},
selectedRelease: null,
downloadability: {
status: "unknown",
reason: "ClawHub release metadata is unavailable."
}
};
}
respond(true, result, void 0);
}, (error) => errorShape(ErrorCodes.UNAVAILABLE, `Plugin details are unavailable: ${formatErrorMessage(error)}. Retry to reconnect to ClawHub.`))
};
//#endregion
export { pluginsHandlers };