mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 10:21:40 +00:00
470 lines
17 KiB
TypeScript
470 lines
17 KiB
TypeScript
import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.js";
|
|
import { normalizeTrimmedStringList } from "../../packages/normalization-core/src/string-normalization.js";
|
|
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
|
|
import { isRecord } from "../utils.js";
|
|
import type {
|
|
PluginManifestCapabilityProviderAuthSignal,
|
|
PluginManifestCapabilityProviderConfigSignal,
|
|
PluginManifestCapabilityProviderMetadata,
|
|
PluginManifestCapabilityProviderModeConfigSignal,
|
|
PluginManifestCatalog,
|
|
PluginManifestConfigContracts,
|
|
PluginManifestConfigLiteral,
|
|
PluginManifestContracts,
|
|
PluginManifestDangerousConfigFlag,
|
|
PluginManifestMcpServer,
|
|
PluginManifestMediaUnderstandingCapability,
|
|
PluginManifestMediaUnderstandingProviderMetadata,
|
|
PluginManifestProviderBaseUrlGuard,
|
|
PluginManifestSecretInputContracts,
|
|
PluginManifestSecretInputPath,
|
|
PluginManifestToolMetadata,
|
|
} from "./manifest-types.js";
|
|
|
|
export function normalizeStringListRecord(value: unknown): Record<string, string[]> | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: Record<string, string[]> = Object.create(null);
|
|
for (const [key, rawValues] of Object.entries(value)) {
|
|
const providerId = normalizeOptionalString(key) ?? "";
|
|
if (!providerId || isBlockedObjectKey(providerId)) {
|
|
continue;
|
|
}
|
|
const values = normalizeTrimmedStringList(rawValues);
|
|
if (values.length === 0) {
|
|
continue;
|
|
}
|
|
normalized[providerId] = values;
|
|
}
|
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
export function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: Record<string, string> = Object.create(null);
|
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
const key = normalizeOptionalString(rawKey) ?? "";
|
|
const valueLocal = normalizeOptionalString(rawValue) ?? "";
|
|
if (!key || isBlockedObjectKey(key) || !valueLocal) {
|
|
continue;
|
|
}
|
|
normalized[key] = valueLocal;
|
|
}
|
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
export function normalizeManifestMcpServers(
|
|
value: unknown,
|
|
): Record<string, PluginManifestMcpServer> | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: Record<string, PluginManifestMcpServer> = Object.create(null);
|
|
for (const [rawName, rawServer] of Object.entries(value)) {
|
|
const name = normalizeOptionalString(rawName) ?? "";
|
|
if (!name || isBlockedObjectKey(name) || !isRecord(rawServer)) {
|
|
continue;
|
|
}
|
|
normalized[name] = { ...rawServer };
|
|
}
|
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
function normalizeNamedMetadataRecord<T>(
|
|
value: unknown,
|
|
normalizeEntry: (entry: Record<string, unknown>) => T | undefined,
|
|
): Record<string, T> | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: Record<string, T> = Object.create(null);
|
|
for (const [rawId, rawEntry] of Object.entries(value)) {
|
|
const id = normalizeOptionalString(rawId) ?? "";
|
|
const entry =
|
|
!id || isBlockedObjectKey(id) || !isRecord(rawEntry) ? undefined : normalizeEntry(rawEntry);
|
|
if (entry) {
|
|
normalized[id] = entry;
|
|
}
|
|
}
|
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
const MEDIA_UNDERSTANDING_CAPABILITIES = new Set(["image", "audio", "video"]);
|
|
|
|
function normalizeMediaUnderstandingCapabilityRecord(
|
|
value: unknown,
|
|
): Partial<Record<PluginManifestMediaUnderstandingCapability, string>> | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: Partial<Record<PluginManifestMediaUnderstandingCapability, string>> = {};
|
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
if (!MEDIA_UNDERSTANDING_CAPABILITIES.has(rawKey)) {
|
|
continue;
|
|
}
|
|
const model = normalizeOptionalString(rawValue);
|
|
if (model) {
|
|
normalized[rawKey as PluginManifestMediaUnderstandingCapability] = model;
|
|
}
|
|
}
|
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
function normalizeMediaUnderstandingPriorityRecord(
|
|
value: unknown,
|
|
): Partial<Record<PluginManifestMediaUnderstandingCapability, number>> | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: Partial<Record<PluginManifestMediaUnderstandingCapability, number>> = {};
|
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
if (
|
|
!MEDIA_UNDERSTANDING_CAPABILITIES.has(rawKey) ||
|
|
typeof rawValue !== "number" ||
|
|
!Number.isFinite(rawValue)
|
|
) {
|
|
continue;
|
|
}
|
|
normalized[rawKey as PluginManifestMediaUnderstandingCapability] = rawValue;
|
|
}
|
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
function normalizeMediaUnderstandingCapabilities(
|
|
value: unknown,
|
|
): PluginManifestMediaUnderstandingCapability[] | undefined {
|
|
const values = normalizeTrimmedStringList(value).filter((entry) =>
|
|
MEDIA_UNDERSTANDING_CAPABILITIES.has(entry),
|
|
) as PluginManifestMediaUnderstandingCapability[];
|
|
return values.length > 0 ? values : undefined;
|
|
}
|
|
|
|
function normalizeMediaUnderstandingNativeDocumentInputs(value: unknown): Array<"pdf"> | undefined {
|
|
const values = normalizeTrimmedStringList(value).filter((entry) => entry === "pdf");
|
|
return values.length > 0 ? values : undefined;
|
|
}
|
|
|
|
function normalizeMediaUnderstandingDocumentModels(
|
|
value: unknown,
|
|
): PluginManifestMediaUnderstandingProviderMetadata["documentModels"] | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const pdfRaw = value.pdf;
|
|
if (!isRecord(pdfRaw)) {
|
|
return undefined;
|
|
}
|
|
const textExtraction = normalizeOptionalString(pdfRaw.textExtraction);
|
|
const image: string | false | undefined =
|
|
pdfRaw.image === false ? false : normalizeOptionalString(pdfRaw.image);
|
|
const pdf = {
|
|
...(textExtraction ? { textExtraction } : {}),
|
|
...(image !== undefined ? { image } : {}),
|
|
};
|
|
return Object.keys(pdf).length > 0 ? { pdf } : undefined;
|
|
}
|
|
|
|
export function normalizeMediaUnderstandingProviderMetadata(
|
|
value: unknown,
|
|
): Record<string, PluginManifestMediaUnderstandingProviderMetadata> | undefined {
|
|
return normalizeNamedMetadataRecord(value, (rawMetadata) => {
|
|
const capabilities = normalizeMediaUnderstandingCapabilities(rawMetadata.capabilities);
|
|
const defaultModels = normalizeMediaUnderstandingCapabilityRecord(rawMetadata.defaultModels);
|
|
const autoPriority = normalizeMediaUnderstandingPriorityRecord(rawMetadata.autoPriority);
|
|
const nativeDocumentInputs = normalizeMediaUnderstandingNativeDocumentInputs(
|
|
rawMetadata.nativeDocumentInputs,
|
|
);
|
|
const documentModels = normalizeMediaUnderstandingDocumentModels(rawMetadata.documentModels);
|
|
const metadata = {
|
|
...(capabilities ? { capabilities } : {}),
|
|
...(defaultModels ? { defaultModels } : {}),
|
|
...(autoPriority ? { autoPriority } : {}),
|
|
...(nativeDocumentInputs ? { nativeDocumentInputs } : {}),
|
|
...(documentModels ? { documentModels } : {}),
|
|
} satisfies PluginManifestMediaUnderstandingProviderMetadata;
|
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
});
|
|
}
|
|
|
|
function normalizeProviderBaseUrlGuard(
|
|
value: unknown,
|
|
): PluginManifestProviderBaseUrlGuard | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const provider = normalizeOptionalString(value.provider);
|
|
const allowedBaseUrls = normalizeTrimmedStringList(value.allowedBaseUrls);
|
|
if (!provider || allowedBaseUrls.length === 0) {
|
|
return undefined;
|
|
}
|
|
const defaultBaseUrl = normalizeOptionalString(value.defaultBaseUrl);
|
|
return {
|
|
provider,
|
|
...(defaultBaseUrl ? { defaultBaseUrl } : {}),
|
|
allowedBaseUrls,
|
|
};
|
|
}
|
|
|
|
function normalizeCapabilityProviderAuthSignals(
|
|
value: unknown,
|
|
): PluginManifestCapabilityProviderAuthSignal[] | undefined {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const signals: PluginManifestCapabilityProviderAuthSignal[] = [];
|
|
for (const rawSignal of value) {
|
|
if (!isRecord(rawSignal)) {
|
|
continue;
|
|
}
|
|
const provider = normalizeOptionalString(rawSignal.provider);
|
|
if (!provider) {
|
|
continue;
|
|
}
|
|
const providerBaseUrl = normalizeProviderBaseUrlGuard(rawSignal.providerBaseUrl);
|
|
signals.push({
|
|
provider,
|
|
...(providerBaseUrl ? { providerBaseUrl } : {}),
|
|
});
|
|
}
|
|
return signals.length > 0 ? signals : undefined;
|
|
}
|
|
|
|
function normalizeCapabilityProviderModeConfigSignal(
|
|
value: unknown,
|
|
): PluginManifestCapabilityProviderModeConfigSignal | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const pathResult = normalizeOptionalString(value.path);
|
|
const defaultValue = normalizeOptionalString(value.default);
|
|
const allowed = normalizeTrimmedStringList(value.allowed);
|
|
const disallowed = normalizeTrimmedStringList(value.disallowed);
|
|
const signal = {
|
|
...(pathResult ? { path: pathResult } : {}),
|
|
...(defaultValue ? { default: defaultValue } : {}),
|
|
...(allowed.length > 0 ? { allowed } : {}),
|
|
...(disallowed.length > 0 ? { disallowed } : {}),
|
|
} satisfies PluginManifestCapabilityProviderModeConfigSignal;
|
|
return Object.keys(signal).length > 0 ? signal : undefined;
|
|
}
|
|
|
|
function normalizeCapabilityProviderConfigSignals(
|
|
value: unknown,
|
|
): PluginManifestCapabilityProviderConfigSignal[] | undefined {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const signals: PluginManifestCapabilityProviderConfigSignal[] = [];
|
|
for (const rawSignal of value) {
|
|
if (!isRecord(rawSignal)) {
|
|
continue;
|
|
}
|
|
const rootPath = normalizeOptionalString(rawSignal.rootPath);
|
|
if (!rootPath) {
|
|
continue;
|
|
}
|
|
const overlayPath = normalizeOptionalString(rawSignal.overlayPath);
|
|
const overlayMapPath = normalizeOptionalString(rawSignal.overlayMapPath);
|
|
const required = normalizeTrimmedStringList(rawSignal.required);
|
|
const requiredAny = normalizeTrimmedStringList(rawSignal.requiredAny);
|
|
const mode = normalizeCapabilityProviderModeConfigSignal(rawSignal.mode);
|
|
const signal = {
|
|
rootPath,
|
|
...(overlayPath ? { overlayPath } : {}),
|
|
...(overlayMapPath ? { overlayMapPath } : {}),
|
|
...(required.length > 0 ? { required } : {}),
|
|
...(requiredAny.length > 0 ? { requiredAny } : {}),
|
|
...(mode ? { mode } : {}),
|
|
} satisfies PluginManifestCapabilityProviderConfigSignal;
|
|
if (required.length > 0 || requiredAny.length > 0 || mode) {
|
|
signals.push(signal);
|
|
}
|
|
}
|
|
return signals.length > 0 ? signals : undefined;
|
|
}
|
|
|
|
function normalizeCapabilityProviderMetadataEntry(
|
|
rawMetadata: Record<string, unknown>,
|
|
): PluginManifestCapabilityProviderMetadata | undefined {
|
|
const aliases = normalizeTrimmedStringList(rawMetadata.aliases);
|
|
const authProviders = normalizeTrimmedStringList(rawMetadata.authProviders);
|
|
const authSignals = normalizeCapabilityProviderAuthSignals(rawMetadata.authSignals);
|
|
const configSignals = normalizeCapabilityProviderConfigSignals(rawMetadata.configSignals);
|
|
const referenceAudioInputs = rawMetadata.referenceAudioInputs === true ? true : undefined;
|
|
const metadata = {
|
|
...(aliases.length > 0 ? { aliases } : {}),
|
|
...(authProviders.length > 0 ? { authProviders } : {}),
|
|
...(authSignals ? { authSignals } : {}),
|
|
...(configSignals ? { configSignals } : {}),
|
|
...(referenceAudioInputs ? { referenceAudioInputs } : {}),
|
|
} satisfies PluginManifestCapabilityProviderMetadata;
|
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
}
|
|
|
|
export function normalizeCapabilityProviderMetadata(
|
|
value: unknown,
|
|
): Record<string, PluginManifestCapabilityProviderMetadata> | undefined {
|
|
return normalizeNamedMetadataRecord(value, normalizeCapabilityProviderMetadataEntry);
|
|
}
|
|
|
|
export function normalizePluginToolMetadata(
|
|
value: unknown,
|
|
): Record<string, PluginManifestToolMetadata> | undefined {
|
|
return normalizeNamedMetadataRecord(value, (rawMetadata) => {
|
|
const providerMetadata = normalizeCapabilityProviderMetadataEntry(rawMetadata);
|
|
const metadata = {
|
|
...providerMetadata,
|
|
...(rawMetadata.optional === true ? { optional: true } : {}),
|
|
...(rawMetadata.replaySafe === true ? { replaySafe: true } : {}),
|
|
} satisfies PluginManifestToolMetadata;
|
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
});
|
|
}
|
|
|
|
export function normalizeManifestCatalog(value: unknown): PluginManifestCatalog | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const featured = typeof value.featured === "boolean" ? value.featured : undefined;
|
|
const order =
|
|
typeof value.order === "number" && Number.isFinite(value.order) ? value.order : undefined;
|
|
if (featured === undefined && order === undefined) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(featured !== undefined ? { featured } : {}),
|
|
...(order !== undefined ? { order } : {}),
|
|
};
|
|
}
|
|
|
|
const MANIFEST_CONTRACT_KEYS = [
|
|
"embeddedExtensionFactories",
|
|
"agentToolResultMiddleware",
|
|
"trustedToolPolicies",
|
|
"externalAuthProviders",
|
|
"embeddingProviders",
|
|
"memoryEmbeddingProviders",
|
|
"speechProviders",
|
|
"realtimeTranscriptionProviders",
|
|
"realtimeVoiceProviders",
|
|
"mediaUnderstandingProviders",
|
|
"transcriptSourceProviders",
|
|
"documentExtractors",
|
|
"imageGenerationProviders",
|
|
"videoGenerationProviders",
|
|
"musicGenerationProviders",
|
|
"webContentExtractors",
|
|
"webFetchProviders",
|
|
"webSearchProviders",
|
|
"workerProviders",
|
|
"usageProviders",
|
|
"migrationProviders",
|
|
"gatewayMethodDispatch",
|
|
"tools",
|
|
] as const satisfies readonly (keyof PluginManifestContracts)[];
|
|
|
|
export function normalizeManifestContracts(value: unknown): PluginManifestContracts | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const contracts: PluginManifestContracts = {};
|
|
for (const key of MANIFEST_CONTRACT_KEYS) {
|
|
const entries = normalizeTrimmedStringList(value[key]);
|
|
if (entries.length > 0) {
|
|
contracts[key] = entries;
|
|
}
|
|
}
|
|
return Object.keys(contracts).length > 0 ? contracts : undefined;
|
|
}
|
|
|
|
function isManifestConfigLiteral(value: unknown): value is PluginManifestConfigLiteral {
|
|
return (
|
|
value === null ||
|
|
typeof value === "string" ||
|
|
typeof value === "number" ||
|
|
typeof value === "boolean"
|
|
);
|
|
}
|
|
|
|
function normalizeManifestDangerousConfigFlags(
|
|
value: unknown,
|
|
): PluginManifestDangerousConfigFlag[] | undefined {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: PluginManifestDangerousConfigFlag[] = [];
|
|
for (const entry of value) {
|
|
if (!isRecord(entry)) {
|
|
continue;
|
|
}
|
|
const pathValue = normalizeOptionalString(entry.path) ?? "";
|
|
if (!pathValue || !isManifestConfigLiteral(entry.equals)) {
|
|
continue;
|
|
}
|
|
normalized.push({ path: pathValue, equals: entry.equals });
|
|
}
|
|
return normalized.length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
function normalizeManifestSecretInputPaths(
|
|
value: unknown,
|
|
): PluginManifestSecretInputPath[] | undefined {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized: PluginManifestSecretInputPath[] = [];
|
|
for (const entry of value) {
|
|
if (!isRecord(entry)) {
|
|
continue;
|
|
}
|
|
const pathLocal = normalizeOptionalString(entry.path) ?? "";
|
|
if (!pathLocal) {
|
|
continue;
|
|
}
|
|
const expected = entry.expected === "string" ? entry.expected : undefined;
|
|
const ownerKind = entry.ownerKind === "route" ? entry.ownerKind : undefined;
|
|
normalized.push({
|
|
path: pathLocal,
|
|
...(expected ? { expected } : {}),
|
|
...(ownerKind ? { ownerKind } : {}),
|
|
});
|
|
}
|
|
return normalized.length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
export function normalizeManifestConfigContracts(
|
|
value: unknown,
|
|
): PluginManifestConfigContracts | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const compatibilityMigrationPaths = normalizeTrimmedStringList(value.compatibilityMigrationPaths);
|
|
const compatibilityRuntimePaths = normalizeTrimmedStringList(value.compatibilityRuntimePaths);
|
|
const rawSecretInputs = isRecord(value.secretInputs) ? value.secretInputs : undefined;
|
|
const dangerousFlags = normalizeManifestDangerousConfigFlags(value.dangerousFlags);
|
|
const secretInputPaths = rawSecretInputs
|
|
? normalizeManifestSecretInputPaths(rawSecretInputs.paths)
|
|
: undefined;
|
|
const secretInputs =
|
|
secretInputPaths && secretInputPaths.length > 0
|
|
? ({
|
|
...(rawSecretInputs?.bundledDefaultEnabled === true
|
|
? { bundledDefaultEnabled: true }
|
|
: rawSecretInputs?.bundledDefaultEnabled === false
|
|
? { bundledDefaultEnabled: false }
|
|
: {}),
|
|
paths: secretInputPaths,
|
|
} satisfies PluginManifestSecretInputContracts)
|
|
: undefined;
|
|
const configContracts = {
|
|
...(compatibilityMigrationPaths.length > 0 ? { compatibilityMigrationPaths } : {}),
|
|
...(compatibilityRuntimePaths.length > 0 ? { compatibilityRuntimePaths } : {}),
|
|
...(dangerousFlags ? { dangerousFlags } : {}),
|
|
...(secretInputs ? { secretInputs } : {}),
|
|
} satisfies PluginManifestConfigContracts;
|
|
return Object.keys(configContracts).length > 0 ? configContracts : undefined;
|
|
}
|