import fs from "node:fs";
import path from "node:path";
import { normalizeModelCatalog } from "@openclaw/model-catalog-core/model-catalog-normalize";
import { normalizeModelCatalogProviderId } from "@openclaw/model-catalog-core/model-catalog-refs";
import type {
  ModelCatalog,
  ModelCatalogAlias,
  ModelCatalogCost,
  ModelCatalogDiscovery,
  ModelCatalogInput,
  ModelCatalogModel,
  ModelCatalogProvider,
  ModelCatalogStatus,
  ModelCatalogSuppression,
  ModelCatalogTieredCost,
} from "@openclaw/model-catalog-core/model-catalog-types";
import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.config.js";
import { MANIFEST_KEY } from "../compat/legacy-names.js";
import { ENV_SECRET_REF_ID_RE } from "../config/types.secrets.js";
import { matchRootFileOpenFailure, openRootFileSync } from "../infra/boundary-file-read.js";
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
import type { JsonSchemaObject } from "../shared/json-schema.types.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeTrimmedStringList } from "../shared/string-normalization.js";
import { isRecord } from "../utils.js";
import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
import {
  normalizeManifestCommandAliases,
  type PluginManifestCommandAlias,
} from "./manifest-command-aliases.js";
import type { PluginConfigUiHint } from "./manifest-types.js";
import { createPluginCacheKey, PluginLruCache } from "./plugin-cache-primitives.js";
import type { PluginKind } from "./plugin-kind.types.js";

export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const;
export const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024;
const MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES = 512;
const MAX_SECRET_PROVIDER_EXEC_ARGS = 128;
const MAX_SECRET_PROVIDER_EXEC_ARG_BYTES = 1024;
const MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS = 120_000;
const MAX_SECRET_PROVIDER_EXEC_OUTPUT_BYTES = 20 * 1024 * 1024;
const MAX_SECRET_PROVIDER_EXEC_PASS_ENV = 128;
const SECRET_PROVIDER_NODE_COMMAND_PLACEHOLDER = "${node}";

type PluginManifestLoadCacheEntry = {
  result: PluginManifestLoadResult;
  size: number;
  mtimeMs: number;
  ctimeMs: number;
};

const pluginManifestLoadCache = new PluginLruCache<PluginManifestLoadCacheEntry>(
  MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES,
);

export function clearPluginManifestLoadCache(): void {
  pluginManifestLoadCache.clear();
}

export type PluginManifestChannelConfig = {
  schema: JsonSchemaObject;
  uiHints?: Record<string, PluginConfigUiHint>;
  runtime?: ChannelConfigRuntimeSchema;
  label?: string;
  description?: string;
  preferOver?: string[];
  commands?: PluginManifestChannelCommandDefaults;
};

export type PluginManifestChannelCommandDefaults = {
  nativeCommandsAutoEnabled?: boolean;
  nativeSkillsAutoEnabled?: boolean;
};

export type PluginManifestModelSupport = {
  /**
   * Cheap manifest-owned model-id prefixes for transparent provider activation
   * from shorthand model refs such as `gpt-5.4` or `claude-sonnet-4.6`.
   */
  modelPrefixes?: string[];
  /**
   * Regex sources matched against the raw model id after profile suffixes are
   * stripped. Use this when simple prefixes are not expressive enough.
   */
  modelPatterns?: string[];
};

export type PluginManifestModelCatalogInput = ModelCatalogInput;
export type PluginManifestModelCatalogDiscovery = ModelCatalogDiscovery;
export type PluginManifestModelCatalogStatus = ModelCatalogStatus;
export type PluginManifestModelCatalogTieredCost = ModelCatalogTieredCost;
export type PluginManifestModelCatalogCost = ModelCatalogCost;
export type PluginManifestModelCatalogModel = ModelCatalogModel;
export type PluginManifestModelCatalogProvider = ModelCatalogProvider;
export type PluginManifestModelCatalogAlias = ModelCatalogAlias;
export type PluginManifestModelCatalogSuppression = ModelCatalogSuppression;
export type PluginManifestModelCatalog = ModelCatalog;

export type PluginManifestModelPricingModelIdTransform = "version-dots";

export type PluginManifestModelPricingSource = {
  provider?: string;
  passthroughProviderModel?: boolean;
  modelIdTransforms?: PluginManifestModelPricingModelIdTransform[];
};

export type PluginManifestModelPricingProvider = {
  external?: boolean;
  openRouter?: PluginManifestModelPricingSource | false;
  liteLLM?: PluginManifestModelPricingSource | false;
};

export type PluginManifestModelPricing = {
  providers?: Record<string, PluginManifestModelPricingProvider>;
};

export type PluginManifestModelIdPrefixRule = {
  modelPrefix: string;
  prefix: string;
};

export type PluginManifestModelIdNormalizationProvider = {
  aliases?: Record<string, string>;
  stripPrefixes?: string[];
  prefixWhenBare?: string;
  prefixWhenBareAfterAliasStartsWith?: PluginManifestModelIdPrefixRule[];
};

export type PluginManifestModelIdNormalization = {
  providers?: Record<string, PluginManifestModelIdNormalizationProvider>;
};

export type PluginManifestProviderEndpoint = {
  /**
   * Core endpoint class this plugin-owned endpoint should map to. Core must
   * already know the class; manifests own host/baseUrl matching metadata.
   */
  endpointClass: string;
  /** Hostnames that should resolve to this endpoint class. */
  hosts?: string[];
  /** Host suffixes that should resolve to this endpoint class. */
  hostSuffixes?: string[];
  /** Exact normalized base URLs that should resolve to this endpoint class. */
  baseUrls?: string[];
  /** Static Google Vertex region metadata for exact global hosts. */
  googleVertexRegion?: string;
  /** Host suffix whose prefix should be exposed as the Google Vertex region. */
  googleVertexRegionHostSuffix?: string;
};

export type PluginManifestProviderRequestProvider = {
  family?: string;
  compatibilityFamily?: "moonshot";
  openAICompletions?: {
    supportsStreamingUsage?: boolean;
  };
};

export type PluginManifestProviderRequest = {
  providers?: Record<string, PluginManifestProviderRequestProvider>;
};

export type PluginManifestSecretProviderIntegration = {
  providerAlias?: string;
  displayName?: string;
  description?: string;
  source: "exec";
  command: "${node}";
  args?: string[];
  timeoutMs?: number;
  noOutputTimeoutMs?: number;
  maxOutputBytes?: number;
  jsonOnly?: boolean;
  env?: Record<string, string>;
  passEnv?: string[];
  allowInsecurePath?: boolean;
};

export type PluginManifestActivationCapability = "provider" | "channel" | "tool" | "hook";

export type PluginManifestActivation = {
  /**
   * Explicit Gateway startup activation. Set true when the plugin must be
   * imported during Gateway startup; set false when narrower activation
   * triggers should load it on demand.
   */
  onStartup?: boolean;
  /**
   * Provider ids that should include this plugin in activation/load plans.
   * This is planner metadata only; runtime behavior still comes from register().
   */
  onProviders?: string[];
  /** Agent harness runtime ids that should include this plugin in activation/load plans. */
  onAgentHarnesses?: string[];
  /** Command ids that should include this plugin in activation/load plans. */
  onCommands?: string[];
  /** Channel ids that should include this plugin in activation/load plans. */
  onChannels?: string[];
  /** Route kinds that should include this plugin in activation/load plans. */
  onRoutes?: string[];
  /** Root-relative config paths that should include this plugin in startup/load plans. */
  onConfigPaths?: string[];
  /** Broad capability hints for activation/load plans. Prefer narrower ownership metadata. */
  onCapabilities?: PluginManifestActivationCapability[];
};

export type PluginManifestDefaultPlatform = NodeJS.Platform;

export type PluginManifestSetupProvider = {
  /** Provider id surfaced during setup/onboarding. */
  id: string;
  /** Setup/auth methods that this provider supports. */
  authMethods?: string[];
  /** Environment variables that can satisfy setup without runtime loading. */
  envVars?: string[];
  /**
   * Cheap local evidence that a provider can authenticate without loading
   * runtime code. Evidence checks must not read secrets, shell out, or call
   * provider APIs.
   */
  authEvidence?: PluginManifestSetupProviderAuthEvidence[];
};

export type PluginManifestSetupProviderAuthEvidence = {
  /** Generic local file evidence gated by required environment metadata. */
  type: "local-file-with-env";
  /** Optional env var containing an explicit credential file path. */
  fileEnvVar?: string;
  /** Optional fallback credential file paths. Supports `${HOME}` and `${APPDATA}`. */
  fallbackPaths?: string[];
  /** At least one of these env vars must be non-empty when provided. */
  requiresAnyEnv?: string[];
  /** Every env var listed here must be non-empty when provided. */
  requiresAllEnv?: string[];
  /** Non-secret marker returned when this evidence is present. */
  credentialMarker: string;
  /** Human-readable auth source label. */
  source?: string;
};

export type PluginManifestSetup = {
  /** Cheap provider setup metadata exposed before runtime loads. */
  providers?: PluginManifestSetupProvider[];
  /** Setup-time backend ids available without full runtime activation. */
  cliBackends?: string[];
  /** Config migration ids owned by this plugin's setup surface. */
  configMigrations?: string[];
  /**
   * Whether setup still needs plugin runtime execution after descriptor lookup.
   * Defaults to false when omitted.
   */
  requiresRuntime?: boolean;
};

export type PluginManifestQaRunner = {
  /** Subcommand mounted beneath `openclaw qa`, for example `matrix`. */
  commandName: string;
  /** Optional user-facing help text for fallback host stubs. */
  description?: string;
};

export type PluginManifestConfigLiteral = string | number | boolean | null;

export type PluginManifestDangerousConfigFlag = {
  /**
   * Dot-separated config path relative to `plugins.entries.<id>.config`.
   * Supports `*` wildcards for map/array segments.
   */
  path: string;
  /** Exact literal that marks this config value as dangerous. */
  equals: PluginManifestConfigLiteral;
};

export type PluginManifestSecretInputPath = {
  /**
   * Dot-separated config path relative to `plugins.entries.<id>.config`.
   * Supports `*` wildcards for map/array segments.
   */
  path: string;
  /** Expected resolved type for SecretRef materialization. */
  expected?: "string";
};

export type PluginManifestSecretInputContracts = {
  /**
   * Override bundled-plugin default enablement when deciding whether this
   * SecretRef surface is active. Use this when the plugin is bundled but the
   * surface should stay inactive until explicitly enabled in config.
   */
  bundledDefaultEnabled?: boolean;
  paths: PluginManifestSecretInputPath[];
};

export type PluginManifestConfigContracts = {
  /**
   * Root-relative config paths that indicate this plugin's setup-time
   * compatibility migrations might apply. Use this to keep generic runtime
   * config reads from loading every plugin setup surface when the config does
   * not reference the plugin at all.
   */
  compatibilityMigrationPaths?: string[];
  /**
   * Root-relative compatibility paths that this plugin can service during
   * runtime before plugin code fully activates. Use this for legacy surfaces
   * that should cheaply narrow bundled candidate sets without importing every
   * compatible plugin runtime.
   */
  compatibilityRuntimePaths?: string[];
  dangerousFlags?: PluginManifestDangerousConfigFlag[];
  secretInputs?: PluginManifestSecretInputContracts;
};

export type PluginManifest = {
  id: string;
  configSchema: JsonSchemaObject;
  /** Plugin ids that must also be installed for this plugin to have effect. */
  requiresPlugins?: string[];
  enabledByDefault?: boolean;
  enabledByDefaultOnPlatforms?: PluginManifestDefaultPlatform[];
  /** Legacy plugin ids that should normalize to this plugin id. */
  legacyPluginIds?: string[];
  /** Provider ids that should auto-enable this plugin when referenced in auth/config/models. */
  autoEnableWhenConfiguredProviders?: string[];
  kind?: PluginKind | PluginKind[];
  channels?: string[];
  providers?: string[];
  /**
   * Optional lightweight module that exports provider plugin metadata for
   * auth/catalog discovery. It should not import the full plugin runtime.
   */
  providerCatalogEntry?: string;
  /**
   * Cheap model-family ownership metadata used before plugin runtime loads.
   * Use this for shorthand model refs that omit an explicit provider prefix.
   */
  modelSupport?: PluginManifestModelSupport;
  /**
   * Declarative model catalog metadata used by future read-only listing,
   * onboarding, and model picker surfaces before provider runtime loads.
   */
  modelCatalog?: PluginManifestModelCatalog;
  /** Manifest-owned external pricing lookup policy for provider refs. */
  modelPricing?: PluginManifestModelPricing;
  /** Manifest-owned model-id normalization used before provider runtime loads. */
  modelIdNormalization?: PluginManifestModelIdNormalization;
  /** Cheap provider endpoint metadata used before provider runtime loads. */
  providerEndpoints?: PluginManifestProviderEndpoint[];
  /** Cheap provider request metadata used before provider runtime loads. */
  providerRequest?: PluginManifestProviderRequest;
  /** Declarative SecretRef provider presets owned by this plugin. */
  secretProviderIntegrations?: Record<string, PluginManifestSecretProviderIntegration>;
  /** Cheap startup activation lookup for plugin-owned CLI inference backends. */
  cliBackends?: string[];
  /**
   * Provider or CLI backend refs whose plugin-owned synthetic auth hook should
   * be probed during cold model discovery before the runtime registry exists.
   */
  syntheticAuthRefs?: string[];
  /**
   * Bundled-plugin-owned placeholder API key values that represent non-secret
   * local, OAuth, or ambient credential state.
   */
  nonSecretAuthMarkers?: string[];
  /**
   * Plugin-owned command aliases that should resolve to this plugin during
   * config diagnostics before runtime loads.
   */
  commandAliases?: PluginManifestCommandAlias[];
  /**
   * Cheap provider-auth env lookup without booting plugin runtime.
   *
   * @deprecated Prefer setup.providers[].envVars for generic setup/status env
   * metadata. This field remains supported through the provider env-var
   * compatibility adapter during the deprecation window.
   */
  providerAuthEnvVars?: Record<string, string[]>;
  /** Provider ids that should reuse another provider id for auth lookup. */
  providerAuthAliases?: Record<string, string>;
  /** Cheap channel env lookup without booting plugin runtime. */
  channelEnvVars?: Record<string, string[]>;
  /**
   * Cheap onboarding/auth-choice metadata used by config validation, CLI help,
   * and non-runtime auth-choice routing before provider runtime loads.
   */
  providerAuthChoices?: PluginManifestProviderAuthChoice[];
  /** Cheap activation planner metadata exposed before plugin runtime loads. */
  activation?: PluginManifestActivation;
  /** Cheap setup/onboarding metadata exposed before plugin runtime loads. */
  setup?: PluginManifestSetup;
  /** Cheap QA runner metadata exposed before plugin runtime loads. */
  qaRunners?: PluginManifestQaRunner[];
  skills?: string[];
  name?: string;
  description?: string;
  version?: string;
  uiHints?: Record<string, PluginConfigUiHint>;
  /**
   * Static capability ownership snapshot used for manifest-driven discovery,
   * compat wiring, and contract coverage without importing plugin runtime.
   */
  contracts?: PluginManifestContracts;
  /** Cheap media-understanding provider defaults without importing plugin runtime. */
  mediaUnderstandingProviderMetadata?: Record<
    string,
    PluginManifestMediaUnderstandingProviderMetadata
  >;
  /** Cheap image-generation provider auth metadata without importing plugin runtime. */
  imageGenerationProviderMetadata?: Record<string, PluginManifestCapabilityProviderMetadata>;
  /** Cheap video-generation provider auth metadata without importing plugin runtime. */
  videoGenerationProviderMetadata?: Record<string, PluginManifestCapabilityProviderMetadata>;
  /** Cheap music-generation provider auth metadata without importing plugin runtime. */
  musicGenerationProviderMetadata?: Record<string, PluginManifestCapabilityProviderMetadata>;
  /** Cheap plugin-tool availability metadata without importing plugin runtime. */
  toolMetadata?: Record<string, PluginManifestToolMetadata>;
  /** Manifest-owned config behavior consumed by generic core helpers. */
  configContracts?: PluginManifestConfigContracts;
  channelConfigs?: Record<string, PluginManifestChannelConfig>;
};

export type PluginManifestContracts = {
  embeddedExtensionFactories?: string[];
  agentToolResultMiddleware?: string[];
  /**
   * Provider ids whose external auth profile hook can contribute runtime-only
   * credentials. Declaring this lets auth-store overlays load only the owning
   * plugin instead of every provider plugin.
   */
  externalAuthProviders?: string[];
  embeddingProviders?: string[];
  memoryEmbeddingProviders?: string[];
  speechProviders?: string[];
  realtimeTranscriptionProviders?: string[];
  realtimeVoiceProviders?: string[];
  mediaUnderstandingProviders?: string[];
  transcriptSourceProviders?: string[];
  documentExtractors?: string[];
  imageGenerationProviders?: string[];
  videoGenerationProviders?: string[];
  musicGenerationProviders?: string[];
  webContentExtractors?: string[];
  webFetchProviders?: string[];
  webSearchProviders?: string[];
  migrationProviders?: string[];
  gatewayMethodDispatch?: string[];
  tools?: string[];
};

export type PluginManifestMediaUnderstandingCapability = "image" | "audio" | "video";

export type PluginManifestMediaUnderstandingProviderMetadata = {
  capabilities?: PluginManifestMediaUnderstandingCapability[];
  defaultModels?: Partial<Record<PluginManifestMediaUnderstandingCapability, string>>;
  autoPriority?: Partial<Record<PluginManifestMediaUnderstandingCapability, number>>;
  nativeDocumentInputs?: Array<"pdf">;
  documentModels?: Partial<
    Record<
      "pdf",
      {
        textExtraction?: string;
        image?: string | false;
      }
    >
  >;
};

export type PluginManifestProviderBaseUrlGuard = {
  provider: string;
  defaultBaseUrl?: string;
  allowedBaseUrls: string[];
};

export type PluginManifestCapabilityProviderAuthSignal = {
  provider: string;
  providerBaseUrl?: PluginManifestProviderBaseUrlGuard;
};

export type PluginManifestCapabilityProviderModeConfigSignal = {
  path?: string;
  default?: string;
  allowed?: string[];
  disallowed?: string[];
};

export type PluginManifestCapabilityProviderConfigSignal = {
  rootPath: string;
  overlayPath?: string;
  required?: string[];
  requiredAny?: string[];
  mode?: PluginManifestCapabilityProviderModeConfigSignal;
};

export type PluginManifestCapabilityProviderMetadata = {
  aliases?: string[];
  authProviders?: string[];
  authSignals?: PluginManifestCapabilityProviderAuthSignal[];
  configSignals?: PluginManifestCapabilityProviderConfigSignal[];
  referenceAudioInputs?: boolean;
};

export type PluginManifestToolMetadata = PluginManifestCapabilityProviderMetadata & {
  optional?: boolean;
};

export type PluginManifestProviderAuthChoice = {
  /** Provider id owned by this manifest entry. */
  provider: string;
  /** Provider auth method id that this choice should dispatch to. */
  method: string;
  /** Stable auth-choice id used by onboarding and other CLI auth flows. */
  choiceId: string;
  /** Optional user-facing choice label/hint for grouped onboarding UI. */
  choiceLabel?: string;
  choiceHint?: string;
  /** Lower values sort earlier in interactive assistant pickers. */
  assistantPriority?: number;
  /** Keep the choice out of interactive assistant pickers while preserving manual CLI support. */
  assistantVisibility?: "visible" | "manual-only";
  /** Legacy choice ids that should point users at this replacement choice. */
  deprecatedChoiceIds?: string[];
  /** Optional grouping metadata for auth-choice pickers. */
  groupId?: string;
  groupLabel?: string;
  groupHint?: string;
  /**
   * Surface this group in the featured tier of the interactive onboarding
   * picker. Featured groups appear before the "More…" entry.
   */
  onboardingFeatured?: boolean;
  /** Optional CLI flag metadata for one-flag auth flows such as API keys. */
  optionKey?: string;
  cliFlag?: string;
  cliOption?: string;
  cliDescription?: string;
  /**
   * Interactive onboarding surfaces where this auth choice should appear.
   * Defaults to `["text-inference"]` when omitted.
   */
  onboardingScopes?: PluginManifestOnboardingScope[];
};

export type PluginManifestOnboardingScope =
  | "text-inference"
  | "image-generation"
  | "music-generation";

export type PluginManifestLoadResult =
  | { ok: true; manifest: PluginManifest; manifestPath: string }
  | { ok: false; error: string; manifestPath: string };

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;
}

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 value = normalizeOptionalString(rawValue) ?? "";
    if (!key || isBlockedObjectKey(key) || !value) {
      continue;
    }
    normalized[key] = value;
  }
  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;
}

function normalizeMediaUnderstandingProviderMetadata(
  value: unknown,
): Record<string, PluginManifestMediaUnderstandingProviderMetadata> | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const normalized: Record<string, PluginManifestMediaUnderstandingProviderMetadata> =
    Object.create(null);
  for (const [rawProviderId, rawMetadata] of Object.entries(value)) {
    const providerId = normalizeOptionalString(rawProviderId) ?? "";
    if (!providerId || isBlockedObjectKey(providerId) || !isRecord(rawMetadata)) {
      continue;
    }
    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;
    if (Object.keys(metadata).length > 0) {
      normalized[providerId] = metadata;
    }
  }
  return Object.keys(normalized).length > 0 ? normalized : 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 path = normalizeOptionalString(value.path);
  const defaultValue = normalizeOptionalString(value.default);
  const allowed = normalizeTrimmedStringList(value.allowed);
  const disallowed = normalizeTrimmedStringList(value.disallowed);
  const signal = {
    ...(path ? { path } : {}),
    ...(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 required = normalizeTrimmedStringList(rawSignal.required);
    const requiredAny = normalizeTrimmedStringList(rawSignal.requiredAny);
    const mode = normalizeCapabilityProviderModeConfigSignal(rawSignal.mode);
    const signal = {
      rootPath,
      ...(overlayPath ? { overlayPath } : {}),
      ...(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;
}

function normalizeCapabilityProviderMetadata(
  value: unknown,
): Record<string, PluginManifestCapabilityProviderMetadata> | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const normalized: Record<string, PluginManifestCapabilityProviderMetadata> = Object.create(null);
  for (const [rawProviderId, rawMetadata] of Object.entries(value)) {
    const providerId = normalizeOptionalString(rawProviderId) ?? "";
    if (!providerId || isBlockedObjectKey(providerId) || !isRecord(rawMetadata)) {
      continue;
    }
    const metadata = normalizeCapabilityProviderMetadataEntry(rawMetadata);
    if (metadata) {
      normalized[providerId] = metadata;
    }
  }
  return Object.keys(normalized).length > 0 ? normalized : undefined;
}

function normalizePluginToolMetadata(
  value: unknown,
): Record<string, PluginManifestToolMetadata> | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const normalized: Record<string, PluginManifestToolMetadata> = Object.create(null);
  for (const [rawToolName, rawMetadata] of Object.entries(value)) {
    const toolName = normalizeOptionalString(rawToolName) ?? "";
    if (!toolName || isBlockedObjectKey(toolName) || !isRecord(rawMetadata)) {
      continue;
    }
    const providerMetadata = normalizeCapabilityProviderMetadataEntry(rawMetadata);
    const metadata = {
      ...providerMetadata,
      ...(rawMetadata.optional === true ? { optional: true } : {}),
    } satisfies PluginManifestToolMetadata;
    if (Object.keys(metadata).length > 0) {
      normalized[toolName] = metadata;
    }
  }
  return Object.keys(normalized).length > 0 ? normalized : undefined;
}

function normalizeManifestContracts(value: unknown): PluginManifestContracts | undefined {
  if (!isRecord(value)) {
    return undefined;
  }

  const embeddedExtensionFactories = normalizeTrimmedStringList(value.embeddedExtensionFactories);
  const agentToolResultMiddleware = normalizeTrimmedStringList(value.agentToolResultMiddleware);
  const externalAuthProviders = normalizeTrimmedStringList(value.externalAuthProviders);
  const embeddingProviders = normalizeTrimmedStringList(value.embeddingProviders);
  const memoryEmbeddingProviders = normalizeTrimmedStringList(value.memoryEmbeddingProviders);
  const speechProviders = normalizeTrimmedStringList(value.speechProviders);
  const realtimeTranscriptionProviders = normalizeTrimmedStringList(
    value.realtimeTranscriptionProviders,
  );
  const realtimeVoiceProviders = normalizeTrimmedStringList(value.realtimeVoiceProviders);
  const mediaUnderstandingProviders = normalizeTrimmedStringList(value.mediaUnderstandingProviders);
  const transcriptSourceProviders = normalizeTrimmedStringList(value.transcriptSourceProviders);
  const documentExtractors = normalizeTrimmedStringList(value.documentExtractors);
  const imageGenerationProviders = normalizeTrimmedStringList(value.imageGenerationProviders);
  const videoGenerationProviders = normalizeTrimmedStringList(value.videoGenerationProviders);
  const musicGenerationProviders = normalizeTrimmedStringList(value.musicGenerationProviders);
  const webContentExtractors = normalizeTrimmedStringList(value.webContentExtractors);
  const webFetchProviders = normalizeTrimmedStringList(value.webFetchProviders);
  const webSearchProviders = normalizeTrimmedStringList(value.webSearchProviders);
  const migrationProviders = normalizeTrimmedStringList(value.migrationProviders);
  const gatewayMethodDispatch = normalizeTrimmedStringList(value.gatewayMethodDispatch);
  const tools = normalizeTrimmedStringList(value.tools);
  const contracts = {
    ...(embeddedExtensionFactories.length > 0 ? { embeddedExtensionFactories } : {}),
    ...(agentToolResultMiddleware.length > 0 ? { agentToolResultMiddleware } : {}),
    ...(externalAuthProviders.length > 0 ? { externalAuthProviders } : {}),
    ...(embeddingProviders.length > 0 ? { embeddingProviders } : {}),
    ...(memoryEmbeddingProviders.length > 0 ? { memoryEmbeddingProviders } : {}),
    ...(speechProviders.length > 0 ? { speechProviders } : {}),
    ...(realtimeTranscriptionProviders.length > 0 ? { realtimeTranscriptionProviders } : {}),
    ...(realtimeVoiceProviders.length > 0 ? { realtimeVoiceProviders } : {}),
    ...(mediaUnderstandingProviders.length > 0 ? { mediaUnderstandingProviders } : {}),
    ...(transcriptSourceProviders.length > 0 ? { transcriptSourceProviders } : {}),
    ...(documentExtractors.length > 0 ? { documentExtractors } : {}),
    ...(imageGenerationProviders.length > 0 ? { imageGenerationProviders } : {}),
    ...(videoGenerationProviders.length > 0 ? { videoGenerationProviders } : {}),
    ...(musicGenerationProviders.length > 0 ? { musicGenerationProviders } : {}),
    ...(webContentExtractors.length > 0 ? { webContentExtractors } : {}),
    ...(webFetchProviders.length > 0 ? { webFetchProviders } : {}),
    ...(webSearchProviders.length > 0 ? { webSearchProviders } : {}),
    ...(migrationProviders.length > 0 ? { migrationProviders } : {}),
    ...(gatewayMethodDispatch.length > 0 ? { gatewayMethodDispatch } : {}),
    ...(tools.length > 0 ? { tools } : {}),
  } satisfies PluginManifestContracts;

  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 path = normalizeOptionalString(entry.path) ?? "";
    if (!path || !isManifestConfigLiteral(entry.equals)) {
      continue;
    }
    normalized.push({ path, 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 path = normalizeOptionalString(entry.path) ?? "";
    if (!path) {
      continue;
    }
    const expected = entry.expected === "string" ? entry.expected : undefined;
    normalized.push({
      path,
      ...(expected ? { expected } : {}),
    });
  }
  return normalized.length > 0 ? normalized : undefined;
}

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;
}

function normalizeManifestModelSupport(value: unknown): PluginManifestModelSupport | undefined {
  if (!isRecord(value)) {
    return undefined;
  }

  const modelPrefixes = normalizeTrimmedStringList(value.modelPrefixes);
  const modelPatterns = normalizeTrimmedStringList(value.modelPatterns);
  const modelSupport = {
    ...(modelPrefixes.length > 0 ? { modelPrefixes } : {}),
    ...(modelPatterns.length > 0 ? { modelPatterns } : {}),
  } satisfies PluginManifestModelSupport;

  return Object.keys(modelSupport).length > 0 ? modelSupport : undefined;
}

function normalizeManifestModelPricingSource(
  value: unknown,
): PluginManifestModelPricingSource | false | undefined {
  if (value === false) {
    return false;
  }
  if (!isRecord(value)) {
    return undefined;
  }
  const provider = normalizeModelCatalogProviderId(normalizeOptionalString(value.provider) ?? "");
  const modelIdTransforms = normalizeTrimmedStringList(value.modelIdTransforms).filter(
    (entry): entry is PluginManifestModelPricingModelIdTransform => entry === "version-dots",
  );
  const source = {
    ...(provider ? { provider } : {}),
    ...(value.passthroughProviderModel === true ? { passthroughProviderModel: true } : {}),
    ...(modelIdTransforms.length > 0 ? { modelIdTransforms } : {}),
  } satisfies PluginManifestModelPricingSource;
  return Object.keys(source).length > 0 ? source : undefined;
}

function normalizeManifestModelPricingProvider(
  value: unknown,
): PluginManifestModelPricingProvider | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const openRouter = normalizeManifestModelPricingSource(value.openRouter);
  const liteLLM = normalizeManifestModelPricingSource(value.liteLLM);
  const policy = {
    ...(typeof value.external === "boolean" ? { external: value.external } : {}),
    ...(openRouter !== undefined ? { openRouter } : {}),
    ...(liteLLM !== undefined ? { liteLLM } : {}),
  } satisfies PluginManifestModelPricingProvider;
  return Object.keys(policy).length > 0 ? policy : undefined;
}

function normalizeManifestModelPricing(
  value: unknown,
  params: { ownedProviders: ReadonlySet<string> },
): PluginManifestModelPricing | undefined {
  if (!isRecord(value) || !isRecord(value.providers)) {
    return undefined;
  }
  const ownedProviders = new Set(
    [...params.ownedProviders]
      .map((provider) => normalizeModelCatalogProviderId(provider))
      .filter(Boolean),
  );
  const providers: Record<string, PluginManifestModelPricingProvider> = {};
  for (const [rawProviderId, rawPolicy] of Object.entries(value.providers)) {
    const providerId = normalizeModelCatalogProviderId(rawProviderId);
    if (!providerId || !ownedProviders.has(providerId)) {
      continue;
    }
    const policy = normalizeManifestModelPricingProvider(rawPolicy);
    if (policy) {
      providers[providerId] = policy;
    }
  }
  return Object.keys(providers).length > 0 ? { providers } : undefined;
}

function normalizeManifestModelIdPrefixRules(
  value: unknown,
): PluginManifestModelIdPrefixRule[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }
  const rules: PluginManifestModelIdPrefixRule[] = [];
  for (const rawRule of value) {
    if (!isRecord(rawRule)) {
      continue;
    }
    const modelPrefix = normalizeOptionalString(rawRule.modelPrefix);
    const prefix = normalizeOptionalString(rawRule.prefix);
    if (!modelPrefix || !prefix) {
      continue;
    }
    rules.push({ modelPrefix, prefix });
  }
  return rules.length > 0 ? rules : undefined;
}

function normalizeManifestModelIdNormalizationProvider(
  value: unknown,
): PluginManifestModelIdNormalizationProvider | undefined {
  if (!isRecord(value)) {
    return undefined;
  }

  const aliases: Record<string, string> = {};
  if (isRecord(value.aliases)) {
    for (const [rawAlias, rawCanonical] of Object.entries(value.aliases)) {
      const alias = normalizeModelCatalogProviderId(rawAlias);
      const canonical = normalizeOptionalString(rawCanonical);
      if (alias && canonical) {
        aliases[alias] = canonical;
      }
    }
  }
  const stripPrefixes = normalizeTrimmedStringList(value.stripPrefixes);
  const prefixWhenBare = normalizeOptionalString(value.prefixWhenBare);
  const prefixWhenBareAfterAliasStartsWith = normalizeManifestModelIdPrefixRules(
    value.prefixWhenBareAfterAliasStartsWith,
  );
  const normalization = {
    ...(Object.keys(aliases).length > 0 ? { aliases } : {}),
    ...(stripPrefixes.length > 0 ? { stripPrefixes } : {}),
    ...(prefixWhenBare ? { prefixWhenBare } : {}),
    ...(prefixWhenBareAfterAliasStartsWith ? { prefixWhenBareAfterAliasStartsWith } : {}),
  } satisfies PluginManifestModelIdNormalizationProvider;

  return Object.keys(normalization).length > 0 ? normalization : undefined;
}

function normalizeManifestModelIdNormalization(
  value: unknown,
  params: { ownedProviders: ReadonlySet<string> },
): PluginManifestModelIdNormalization | undefined {
  if (!isRecord(value) || !isRecord(value.providers)) {
    return undefined;
  }
  const ownedProviders = new Set(
    [...params.ownedProviders]
      .map((provider) => normalizeModelCatalogProviderId(provider))
      .filter(Boolean),
  );
  const providers: Record<string, PluginManifestModelIdNormalizationProvider> = {};
  for (const [rawProviderId, rawPolicy] of Object.entries(value.providers)) {
    const providerId = normalizeModelCatalogProviderId(rawProviderId);
    if (!providerId || !ownedProviders.has(providerId)) {
      continue;
    }
    const policy = normalizeManifestModelIdNormalizationProvider(rawPolicy);
    if (policy) {
      providers[providerId] = policy;
    }
  }
  return Object.keys(providers).length > 0 ? { providers } : undefined;
}

function normalizeManifestProviderEndpoints(
  value: unknown,
): PluginManifestProviderEndpoint[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }

  const endpoints: PluginManifestProviderEndpoint[] = [];
  for (const rawEndpoint of value) {
    if (!isRecord(rawEndpoint)) {
      continue;
    }
    const endpointClass = normalizeOptionalString(rawEndpoint.endpointClass);
    if (!endpointClass) {
      continue;
    }
    const hosts = normalizeTrimmedStringList(rawEndpoint.hosts).map((host) => host.toLowerCase());
    const hostSuffixes = normalizeTrimmedStringList(rawEndpoint.hostSuffixes).map((host) =>
      host.toLowerCase(),
    );
    const baseUrls = normalizeTrimmedStringList(rawEndpoint.baseUrls);
    const googleVertexRegion = normalizeOptionalString(rawEndpoint.googleVertexRegion);
    const googleVertexRegionHostSuffix = normalizeOptionalString(
      rawEndpoint.googleVertexRegionHostSuffix,
    )?.toLowerCase();
    if (hosts.length === 0 && hostSuffixes.length === 0 && baseUrls.length === 0) {
      continue;
    }
    endpoints.push({
      endpointClass,
      ...(hosts.length > 0 ? { hosts } : {}),
      ...(hostSuffixes.length > 0 ? { hostSuffixes } : {}),
      ...(baseUrls.length > 0 ? { baseUrls } : {}),
      ...(googleVertexRegion ? { googleVertexRegion } : {}),
      ...(googleVertexRegionHostSuffix ? { googleVertexRegionHostSuffix } : {}),
    });
  }

  return endpoints.length > 0 ? endpoints : undefined;
}

function normalizeManifestProviderRequestProvider(
  value: unknown,
): PluginManifestProviderRequestProvider | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const family = normalizeOptionalString(value.family);
  const compatibilityFamily =
    normalizeOptionalString(value.compatibilityFamily) === "moonshot" ? "moonshot" : undefined;
  const supportsStreamingUsage = isRecord(value.openAICompletions)
    ? value.openAICompletions.supportsStreamingUsage
    : undefined;
  const openAICompletions =
    typeof supportsStreamingUsage === "boolean" ? { supportsStreamingUsage } : undefined;
  const providerRequest = {
    ...(family ? { family } : {}),
    ...(compatibilityFamily ? { compatibilityFamily } : {}),
    ...(openAICompletions && Object.keys(openAICompletions).length > 0
      ? { openAICompletions }
      : {}),
  } satisfies PluginManifestProviderRequestProvider;
  return Object.keys(providerRequest).length > 0 ? providerRequest : undefined;
}

function normalizeManifestProviderRequest(
  value: unknown,
  params: { ownedProviders: ReadonlySet<string> },
): PluginManifestProviderRequest | undefined {
  if (!isRecord(value) || !isRecord(value.providers)) {
    return undefined;
  }
  const ownedProviders = new Set(
    [...params.ownedProviders]
      .map((provider) => normalizeModelCatalogProviderId(provider))
      .filter(Boolean),
  );
  const providers: Record<string, PluginManifestProviderRequestProvider> = {};
  for (const [rawProviderId, rawPolicy] of Object.entries(value.providers)) {
    const providerId = normalizeModelCatalogProviderId(rawProviderId);
    if (!providerId || !ownedProviders.has(providerId)) {
      continue;
    }
    const policy = normalizeManifestProviderRequestProvider(rawPolicy);
    if (policy) {
      providers[providerId] = policy;
    }
  }
  return Object.keys(providers).length > 0 ? { providers } : undefined;
}

function normalizeManifestStringArray(
  value: unknown,
  options?: { maxItems?: number; maxLength?: number; pattern?: RegExp },
): string[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }
  const normalized: string[] = [];
  for (const entry of value) {
    if (typeof entry !== "string") {
      continue;
    }
    if (options?.maxLength !== undefined && entry.length > options.maxLength) {
      continue;
    }
    if (options?.pattern && !options.pattern.test(entry)) {
      continue;
    }
    normalized.push(entry);
    if (options?.maxItems !== undefined && normalized.length >= options.maxItems) {
      break;
    }
  }
  return normalized.length > 0 ? normalized : undefined;
}

function normalizeManifestTrimmedStringArray(
  value: unknown,
  options?: { maxItems?: number; pattern?: RegExp },
): string[] | undefined {
  const normalized = normalizeTrimmedStringList(value).filter(
    (entry) => !options?.pattern || options.pattern.test(entry),
  );
  const limited =
    options?.maxItems !== undefined ? normalized.slice(0, options.maxItems) : normalized;
  return limited.length > 0 ? limited : undefined;
}

function normalizeManifestPositiveInteger(value: unknown, max: number): number | undefined {
  return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= max
    ? value
    : undefined;
}

function normalizeManifestSecretProviderIntegrations(
  value: unknown,
): Record<string, PluginManifestSecretProviderIntegration> | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const normalized: Record<string, PluginManifestSecretProviderIntegration> = Object.create(null);
  for (const [rawId, rawIntegration] of Object.entries(value)) {
    const id = normalizeOptionalString(rawId) ?? "";
    if (!id || isBlockedObjectKey(id) || !isRecord(rawIntegration)) {
      continue;
    }
    const command = normalizeOptionalString(rawIntegration.command);
    if (rawIntegration.source !== "exec" || command !== SECRET_PROVIDER_NODE_COMMAND_PLACEHOLDER) {
      continue;
    }
    const providerAlias = normalizeOptionalString(rawIntegration.providerAlias);
    const displayName = normalizeOptionalString(rawIntegration.displayName);
    const description = normalizeOptionalString(rawIntegration.description);
    const args = normalizeManifestStringArray(rawIntegration.args, {
      maxItems: MAX_SECRET_PROVIDER_EXEC_ARGS,
      maxLength: MAX_SECRET_PROVIDER_EXEC_ARG_BYTES,
    });
    const timeoutMs = normalizeManifestPositiveInteger(
      rawIntegration.timeoutMs,
      MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS,
    );
    const noOutputTimeoutMs = normalizeManifestPositiveInteger(
      rawIntegration.noOutputTimeoutMs,
      MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS,
    );
    const maxOutputBytes = normalizeManifestPositiveInteger(
      rawIntegration.maxOutputBytes,
      MAX_SECRET_PROVIDER_EXEC_OUTPUT_BYTES,
    );
    const env = normalizeStringRecord(rawIntegration.env);
    const passEnv = normalizeManifestTrimmedStringArray(rawIntegration.passEnv, {
      maxItems: MAX_SECRET_PROVIDER_EXEC_PASS_ENV,
      pattern: ENV_SECRET_REF_ID_RE,
    });
    normalized[id] = {
      ...(providerAlias ? { providerAlias } : {}),
      ...(displayName ? { displayName } : {}),
      ...(description ? { description } : {}),
      source: "exec",
      command,
      ...(args ? { args } : {}),
      ...(timeoutMs !== undefined ? { timeoutMs } : {}),
      ...(noOutputTimeoutMs !== undefined ? { noOutputTimeoutMs } : {}),
      ...(maxOutputBytes !== undefined ? { maxOutputBytes } : {}),
      ...(typeof rawIntegration.jsonOnly === "boolean"
        ? { jsonOnly: rawIntegration.jsonOnly }
        : {}),
      ...(env ? { env } : {}),
      ...(passEnv ? { passEnv } : {}),
      ...(rawIntegration.allowInsecurePath === true ? { allowInsecurePath: true } : {}),
    };
  }
  return Object.keys(normalized).length > 0 ? normalized : undefined;
}

export function normalizeManifestActivation(value: unknown): PluginManifestActivation | undefined {
  if (!isRecord(value)) {
    return undefined;
  }

  const onProviders = normalizeTrimmedStringList(value.onProviders);
  const onAgentHarnesses = normalizeTrimmedStringList(value.onAgentHarnesses);
  const onCommands = normalizeTrimmedStringList(value.onCommands);
  const onChannels = normalizeTrimmedStringList(value.onChannels);
  const onRoutes = normalizeTrimmedStringList(value.onRoutes);
  const onConfigPaths = normalizeTrimmedStringList(value.onConfigPaths);
  const onStartup = typeof value.onStartup === "boolean" ? value.onStartup : undefined;
  const onCapabilities = normalizeTrimmedStringList(value.onCapabilities).filter(
    (capability): capability is PluginManifestActivationCapability =>
      capability === "provider" ||
      capability === "channel" ||
      capability === "tool" ||
      capability === "hook",
  );

  const activation = {
    ...(onStartup !== undefined ? { onStartup } : {}),
    ...(onProviders.length > 0 ? { onProviders } : {}),
    ...(onAgentHarnesses.length > 0 ? { onAgentHarnesses } : {}),
    ...(onCommands.length > 0 ? { onCommands } : {}),
    ...(onChannels.length > 0 ? { onChannels } : {}),
    ...(onRoutes.length > 0 ? { onRoutes } : {}),
    ...(onConfigPaths.length > 0 ? { onConfigPaths } : {}),
    ...(onCapabilities.length > 0 ? { onCapabilities } : {}),
  } satisfies PluginManifestActivation;

  return Object.keys(activation).length > 0 ? activation : undefined;
}

const MANIFEST_DEFAULT_ENABLEMENT_PLATFORMS = new Set<PluginManifestDefaultPlatform>([
  "aix",
  "android",
  "darwin",
  "freebsd",
  "haiku",
  "linux",
  "openbsd",
  "sunos",
  "win32",
  "cygwin",
  "netbsd",
]);

function normalizeManifestDefaultPlatforms(value: unknown): PluginManifestDefaultPlatform[] {
  return normalizeTrimmedStringList(value).filter(
    (platform): platform is PluginManifestDefaultPlatform =>
      MANIFEST_DEFAULT_ENABLEMENT_PLATFORMS.has(platform as PluginManifestDefaultPlatform),
  );
}

function normalizeManifestSetupProviders(
  value: unknown,
): PluginManifestSetupProvider[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }
  const normalized: PluginManifestSetupProvider[] = [];
  for (const entry of value) {
    if (!isRecord(entry)) {
      continue;
    }
    const id = normalizeOptionalString(entry.id) ?? "";
    if (!id) {
      continue;
    }
    const authMethods = normalizeTrimmedStringList(entry.authMethods);
    const envVars = normalizeTrimmedStringList(entry.envVars);
    const authEvidence = normalizeManifestSetupProviderAuthEvidence(entry.authEvidence);
    normalized.push({
      id,
      ...(authMethods.length > 0 ? { authMethods } : {}),
      ...(envVars.length > 0 ? { envVars } : {}),
      ...(authEvidence ? { authEvidence } : {}),
    });
  }
  return normalized.length > 0 ? normalized : undefined;
}

function normalizeManifestSetupProviderAuthEvidence(
  value: unknown,
): PluginManifestSetupProviderAuthEvidence[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }
  const normalized: PluginManifestSetupProviderAuthEvidence[] = [];
  for (const entry of value) {
    if (!isRecord(entry) || entry.type !== "local-file-with-env") {
      continue;
    }
    const credentialMarker = normalizeOptionalString(entry.credentialMarker);
    if (!credentialMarker) {
      continue;
    }
    const fileEnvVar = normalizeOptionalString(entry.fileEnvVar);
    const fallbackPaths = normalizeTrimmedStringList(entry.fallbackPaths);
    if (!fileEnvVar && fallbackPaths.length === 0) {
      continue;
    }
    const requiresAnyEnv = normalizeTrimmedStringList(entry.requiresAnyEnv);
    const requiresAllEnv = normalizeTrimmedStringList(entry.requiresAllEnv);
    const source = normalizeOptionalString(entry.source);
    normalized.push({
      type: "local-file-with-env",
      ...(fileEnvVar ? { fileEnvVar } : {}),
      ...(fallbackPaths.length > 0 ? { fallbackPaths } : {}),
      ...(requiresAnyEnv.length > 0 ? { requiresAnyEnv } : {}),
      ...(requiresAllEnv.length > 0 ? { requiresAllEnv } : {}),
      credentialMarker,
      ...(source ? { source } : {}),
    });
  }
  return normalized.length > 0 ? normalized : undefined;
}

function normalizeManifestSetup(value: unknown): PluginManifestSetup | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const providers = normalizeManifestSetupProviders(value.providers);
  const cliBackends = normalizeTrimmedStringList(value.cliBackends);
  const configMigrations = normalizeTrimmedStringList(value.configMigrations);
  const requiresRuntime =
    typeof value.requiresRuntime === "boolean" ? value.requiresRuntime : undefined;
  const setup = {
    ...(providers ? { providers } : {}),
    ...(cliBackends.length > 0 ? { cliBackends } : {}),
    ...(configMigrations.length > 0 ? { configMigrations } : {}),
    ...(requiresRuntime !== undefined ? { requiresRuntime } : {}),
  } satisfies PluginManifestSetup;
  return Object.keys(setup).length > 0 ? setup : undefined;
}

function normalizeManifestQaRunners(value: unknown): PluginManifestQaRunner[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }
  const normalized: PluginManifestQaRunner[] = [];
  for (const entry of value) {
    if (!isRecord(entry)) {
      continue;
    }
    const commandName = normalizeOptionalString(entry.commandName) ?? "";
    if (!commandName) {
      continue;
    }
    const description = normalizeOptionalString(entry.description) ?? "";
    normalized.push({
      commandName,
      ...(description ? { description } : {}),
    });
  }
  return normalized.length > 0 ? normalized : undefined;
}

function normalizeProviderAuthChoices(
  value: unknown,
): PluginManifestProviderAuthChoice[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }
  const normalized: PluginManifestProviderAuthChoice[] = [];
  for (const entry of value) {
    if (!isRecord(entry)) {
      continue;
    }
    const provider = normalizeOptionalString(entry.provider) ?? "";
    const method = normalizeOptionalString(entry.method) ?? "";
    const choiceId = normalizeOptionalString(entry.choiceId) ?? "";
    if (!provider || !method || !choiceId) {
      continue;
    }
    const choiceLabel = normalizeOptionalString(entry.choiceLabel) ?? "";
    const choiceHint = normalizeOptionalString(entry.choiceHint) ?? "";
    const assistantPriority =
      typeof entry.assistantPriority === "number" && Number.isFinite(entry.assistantPriority)
        ? entry.assistantPriority
        : undefined;
    const assistantVisibility =
      entry.assistantVisibility === "manual-only" || entry.assistantVisibility === "visible"
        ? entry.assistantVisibility
        : undefined;
    const deprecatedChoiceIds = normalizeTrimmedStringList(entry.deprecatedChoiceIds);
    const groupId = normalizeOptionalString(entry.groupId) ?? "";
    const groupLabel = normalizeOptionalString(entry.groupLabel) ?? "";
    const groupHint = normalizeOptionalString(entry.groupHint) ?? "";
    const onboardingFeatured = entry.onboardingFeatured === true;
    const optionKey = normalizeOptionalString(entry.optionKey) ?? "";
    const cliFlag = normalizeOptionalString(entry.cliFlag) ?? "";
    const cliOption = normalizeOptionalString(entry.cliOption) ?? "";
    const cliDescription = normalizeOptionalString(entry.cliDescription) ?? "";
    const onboardingScopes = normalizeTrimmedStringList(entry.onboardingScopes).filter(
      (scope): scope is PluginManifestOnboardingScope =>
        scope === "text-inference" || scope === "image-generation" || scope === "music-generation",
    );
    normalized.push({
      provider,
      method,
      choiceId,
      ...(choiceLabel ? { choiceLabel } : {}),
      ...(choiceHint ? { choiceHint } : {}),
      ...(assistantPriority !== undefined ? { assistantPriority } : {}),
      ...(assistantVisibility ? { assistantVisibility } : {}),
      ...(deprecatedChoiceIds.length > 0 ? { deprecatedChoiceIds } : {}),
      ...(groupId ? { groupId } : {}),
      ...(groupLabel ? { groupLabel } : {}),
      ...(groupHint ? { groupHint } : {}),
      ...(onboardingFeatured ? { onboardingFeatured: true } : {}),
      ...(optionKey ? { optionKey } : {}),
      ...(cliFlag ? { cliFlag } : {}),
      ...(cliOption ? { cliOption } : {}),
      ...(cliDescription ? { cliDescription } : {}),
      ...(onboardingScopes.length > 0 ? { onboardingScopes } : {}),
    });
  }
  return normalized.length > 0 ? normalized : undefined;
}

function normalizeChannelConfigs(
  value: unknown,
): Record<string, PluginManifestChannelConfig> | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const normalized: Record<string, PluginManifestChannelConfig> = Object.create(null);
  for (const [key, rawEntry] of Object.entries(value)) {
    const channelId = normalizeOptionalString(key) ?? "";
    if (!channelId || isBlockedObjectKey(channelId) || !isRecord(rawEntry)) {
      continue;
    }
    const schema = isRecord(rawEntry.schema) ? rawEntry.schema : null;
    if (!schema) {
      continue;
    }
    const uiHints = isRecord(rawEntry.uiHints)
      ? (rawEntry.uiHints as Record<string, PluginConfigUiHint>)
      : undefined;
    const runtime =
      isRecord(rawEntry.runtime) && typeof rawEntry.runtime.safeParse === "function"
        ? (rawEntry.runtime as ChannelConfigRuntimeSchema)
        : undefined;
    const label = normalizeOptionalString(rawEntry.label) ?? "";
    const description = normalizeOptionalString(rawEntry.description) ?? "";
    const preferOver = normalizeTrimmedStringList(rawEntry.preferOver);
    const commandDefaults = normalizeManifestChannelCommandDefaults(rawEntry.commands);
    normalized[channelId] = {
      schema,
      ...(uiHints ? { uiHints } : {}),
      ...(runtime ? { runtime } : {}),
      ...(label ? { label } : {}),
      ...(description ? { description } : {}),
      ...(preferOver.length > 0 ? { preferOver } : {}),
      ...(commandDefaults ? { commands: commandDefaults } : {}),
    };
  }
  return Object.keys(normalized).length > 0 ? normalized : undefined;
}

function normalizeManifestChannelCommandDefaults(
  value: unknown,
): PluginManifestChannelCommandDefaults | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const nativeCommandsAutoEnabled =
    typeof value.nativeCommandsAutoEnabled === "boolean"
      ? value.nativeCommandsAutoEnabled
      : undefined;
  const nativeSkillsAutoEnabled =
    typeof value.nativeSkillsAutoEnabled === "boolean" ? value.nativeSkillsAutoEnabled : undefined;
  return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
    ? {
        ...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
        ...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
      }
    : undefined;
}

export function resolvePluginManifestPath(rootDir: string): string {
  for (const filename of PLUGIN_MANIFEST_FILENAMES) {
    const candidate = path.join(rootDir, filename);
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }
  return path.join(rootDir, PLUGIN_MANIFEST_FILENAME);
}

function buildPluginManifestLoadCacheKey(params: {
  manifestPath: string;
  rejectHardlinks: boolean;
  rootRealPath?: string;
  stats: fs.Stats;
}): string {
  return createPluginCacheKey([
    [
      path.resolve(params.manifestPath),
      params.rejectHardlinks,
      params.rootRealPath ?? "",
      params.stats.dev,
      params.stats.ino,
    ],
    params.stats.size,
    params.stats.mtimeMs,
    params.stats.ctimeMs,
  ]);
}

function getCachedPluginManifestLoadResult(
  key: string,
  stats: fs.Stats,
): PluginManifestLoadResult | undefined {
  const entry = pluginManifestLoadCache.get(key);
  if (
    !entry ||
    entry.size !== stats.size ||
    entry.mtimeMs !== stats.mtimeMs ||
    entry.ctimeMs !== stats.ctimeMs
  ) {
    return undefined;
  }
  return entry.result;
}

function setCachedPluginManifestLoadResult(
  key: string,
  stats: fs.Stats,
  result: PluginManifestLoadResult,
): void {
  pluginManifestLoadCache.set(key, {
    result,
    size: stats.size,
    mtimeMs: stats.mtimeMs,
    ctimeMs: stats.ctimeMs,
  });
}

function parsePluginKind(raw: unknown): PluginKind | PluginKind[] | undefined {
  if (typeof raw === "string") {
    return raw as PluginKind;
  }
  if (Array.isArray(raw) && raw.length > 0 && raw.every((k) => typeof k === "string")) {
    return raw.length === 1 ? (raw[0] as PluginKind) : (raw as PluginKind[]);
  }
  return undefined;
}

export function loadPluginManifest(
  rootDir: string,
  rejectHardlinks = true,
  rootRealPath?: string,
): PluginManifestLoadResult {
  const manifestPath = resolvePluginManifestPath(rootDir);
  const opened = openRootFileSync({
    absolutePath: manifestPath,
    rootPath: rootDir,
    ...(rootRealPath !== undefined ? { rootRealPath } : {}),
    boundaryLabel: "plugin root",
    maxBytes: MAX_PLUGIN_MANIFEST_BYTES,
    rejectHardlinks,
  });
  if (!opened.ok) {
    return matchRootFileOpenFailure(opened, {
      path: () => ({
        ok: false,
        error: `plugin manifest not found: ${manifestPath}`,
        manifestPath,
      }),
      fallback: (failure) => ({
        ok: false,
        error: `unsafe plugin manifest path: ${manifestPath} (${failure.reason})`,
        manifestPath,
      }),
    });
  }
  const stats = opened.stat;
  const cacheKey = buildPluginManifestLoadCacheKey({
    manifestPath,
    rejectHardlinks,
    ...(rootRealPath !== undefined ? { rootRealPath } : {}),
    stats,
  });
  const cached = getCachedPluginManifestLoadResult(cacheKey, stats);
  if (cached) {
    fs.closeSync(opened.fd);
    return cached;
  }
  const cacheResult = (result: PluginManifestLoadResult): PluginManifestLoadResult => {
    setCachedPluginManifestLoadResult(cacheKey, stats, result);
    return result;
  };
  let raw: unknown;
  try {
    raw = parseJsonWithJson5Fallback(fs.readFileSync(opened.fd, "utf-8"));
  } catch (err) {
    return cacheResult({
      ok: false,
      error: `failed to parse plugin manifest: ${String(err)}`,
      manifestPath,
    });
  } finally {
    fs.closeSync(opened.fd);
  }
  if (!isRecord(raw)) {
    return cacheResult({ ok: false, error: "plugin manifest must be an object", manifestPath });
  }
  const id = normalizeOptionalString(raw.id) ?? "";
  if (!id) {
    return cacheResult({ ok: false, error: "plugin manifest requires id", manifestPath });
  }
  const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null;
  if (!configSchema) {
    return cacheResult({ ok: false, error: "plugin manifest requires configSchema", manifestPath });
  }

  const requiresPlugins = normalizeTrimmedStringList(raw.requiresPlugins);
  const kind = parsePluginKind(raw.kind);
  const enabledByDefault = raw.enabledByDefault === true;
  const enabledByDefaultOnPlatforms = normalizeManifestDefaultPlatforms(
    raw.enabledByDefaultOnPlatforms,
  );
  const legacyPluginIds = normalizeTrimmedStringList(raw.legacyPluginIds);
  const autoEnableWhenConfiguredProviders = normalizeTrimmedStringList(
    raw.autoEnableWhenConfiguredProviders,
  );
  const name = normalizeOptionalString(raw.name);
  const description = normalizeOptionalString(raw.description);
  const version = normalizeOptionalString(raw.version);
  const channels = normalizeTrimmedStringList(raw.channels);
  const providers = normalizeTrimmedStringList(raw.providers);
  const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
  const providerCatalogEntry = normalizeOptionalString(raw.providerCatalogEntry);
  const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
  const modelCatalog = normalizeModelCatalog(raw.modelCatalog, {
    ownedProviders: new Set([...providers, ...cliBackends]),
  });
  const modelPricing = normalizeManifestModelPricing(raw.modelPricing, {
    ownedProviders: new Set(providers),
  });
  const modelIdNormalization = normalizeManifestModelIdNormalization(raw.modelIdNormalization, {
    ownedProviders: new Set(providers),
  });
  const providerEndpoints = normalizeManifestProviderEndpoints(raw.providerEndpoints);
  const providerRequest = normalizeManifestProviderRequest(raw.providerRequest, {
    ownedProviders: new Set(providers),
  });
  const secretProviderIntegrations = normalizeManifestSecretProviderIntegrations(
    raw.secretProviderIntegrations,
  );
  const syntheticAuthRefs = normalizeTrimmedStringList(raw.syntheticAuthRefs);
  const nonSecretAuthMarkers = normalizeTrimmedStringList(raw.nonSecretAuthMarkers);
  const commandAliases = normalizeManifestCommandAliases(raw.commandAliases);
  const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
  const providerAuthAliases = normalizeStringRecord(raw.providerAuthAliases);
  const channelEnvVars = normalizeStringListRecord(raw.channelEnvVars);
  const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
  const activation = normalizeManifestActivation(raw.activation);
  const setup = normalizeManifestSetup(raw.setup);
  const qaRunners = normalizeManifestQaRunners(raw.qaRunners);
  const skills = normalizeTrimmedStringList(raw.skills);
  const contracts = normalizeManifestContracts(raw.contracts);
  const mediaUnderstandingProviderMetadata = normalizeMediaUnderstandingProviderMetadata(
    raw.mediaUnderstandingProviderMetadata,
  );
  const imageGenerationProviderMetadata = normalizeCapabilityProviderMetadata(
    raw.imageGenerationProviderMetadata,
  );
  const videoGenerationProviderMetadata = normalizeCapabilityProviderMetadata(
    raw.videoGenerationProviderMetadata,
  );
  const musicGenerationProviderMetadata = normalizeCapabilityProviderMetadata(
    raw.musicGenerationProviderMetadata,
  );
  const toolMetadata = normalizePluginToolMetadata(raw.toolMetadata);
  const configContracts = normalizeManifestConfigContracts(raw.configContracts);
  const channelConfigs = normalizeChannelConfigs(raw.channelConfigs);

  let uiHints: Record<string, PluginConfigUiHint> | undefined;
  if (isRecord(raw.uiHints)) {
    uiHints = raw.uiHints as Record<string, PluginConfigUiHint>;
  }

  return cacheResult({
    ok: true,
    manifest: {
      id,
      configSchema,
      ...(requiresPlugins.length > 0 ? { requiresPlugins } : {}),
      ...(enabledByDefault ? { enabledByDefault } : {}),
      ...(enabledByDefaultOnPlatforms.length > 0 ? { enabledByDefaultOnPlatforms } : {}),
      ...(legacyPluginIds.length > 0 ? { legacyPluginIds } : {}),
      ...(autoEnableWhenConfiguredProviders.length > 0
        ? { autoEnableWhenConfiguredProviders }
        : {}),
      kind,
      channels,
      providers,
      providerCatalogEntry,
      modelSupport,
      modelCatalog,
      modelPricing,
      modelIdNormalization,
      providerEndpoints,
      providerRequest,
      secretProviderIntegrations,
      cliBackends,
      syntheticAuthRefs,
      nonSecretAuthMarkers,
      commandAliases,
      providerAuthEnvVars,
      providerAuthAliases,
      channelEnvVars,
      providerAuthChoices,
      activation,
      setup,
      qaRunners,
      skills,
      name,
      description,
      version,
      uiHints,
      contracts,
      mediaUnderstandingProviderMetadata,
      imageGenerationProviderMetadata,
      videoGenerationProviderMetadata,
      musicGenerationProviderMetadata,
      toolMetadata,
      configContracts,
      channelConfigs,
    },
    manifestPath,
  });
}

// package.json "openclaw" metadata (used for setup/catalog)
export type PluginPackageChannel = {
  id?: string;
  label?: string;
  selectionLabel?: string;
  detailLabel?: string;
  docsPath?: string;
  docsLabel?: string;
  blurb?: string;
  order?: number;
  aliases?: readonly string[];
  preferOver?: readonly string[];
  systemImage?: string;
  selectionDocsPrefix?: string;
  selectionDocsOmitLabel?: boolean;
  selectionExtras?: readonly string[];
  markdownCapable?: boolean;
  exposure?: {
    configured?: boolean;
    setup?: boolean;
    docs?: boolean;
  };
  showConfigured?: boolean;
  showInSetup?: boolean;
  quickstartAllowFrom?: boolean;
  forceAccountBinding?: boolean;
  preferSessionLookupForAnnounceTarget?: boolean;
  commands?: PluginManifestChannelCommandDefaults;
  configuredState?: {
    specifier?: string;
    exportName?: string;
    env?: {
      allOf?: readonly string[];
      anyOf?: readonly string[];
    };
  };
  persistedAuthState?: {
    specifier?: string;
    exportName?: string;
  };
  doctorCapabilities?: PluginPackageChannelDoctorCapabilities;
  cliAddOptions?: readonly PluginPackageChannelCliOption[];
};

export type PluginPackageChannelDoctorCapabilities = {
  dmAllowFromMode?: "topOnly" | "topOrNested" | "nestedOnly";
  groupModel?: "sender" | "route" | "hybrid";
  groupAllowFromFallbackToAllowFrom?: boolean;
  warnOnEmptyGroupSenderAllowlist?: boolean;
};

export type PluginPackageChannelCliOption = {
  flags: string;
  description: string;
  defaultValue?: boolean | string;
};

export type PluginPackageInstall = {
  clawhubSpec?: string;
  npmSpec?: string;
  localPath?: string;
  defaultChoice?: "clawhub" | "npm" | "local";
  minHostVersion?: string;
  expectedIntegrity?: string;
  allowInvalidConfigRecovery?: boolean;
};

export type OpenClawPackageStartup = {
  /**
   * Opt-in for channel plugins whose `setupEntry` fully covers the gateway
   * startup surface needed before the server starts listening.
   */
  deferConfiguredChannelFullLoadUntilAfterListen?: boolean;
};

export type OpenClawPackageSetupFeatures = {
  configPromotion?: boolean;
  legacyStateMigrations?: boolean;
  legacySessionSurfaces?: boolean;
};

export type OpenClawPackageCompat = {
  pluginApi?: string;
};

export type OpenClawPackageManifest = {
  extensions?: string[];
  runtimeExtensions?: string[];
  setupEntry?: string;
  runtimeSetupEntry?: string;
  setupFeatures?: OpenClawPackageSetupFeatures;
  plugin?: {
    id?: string;
    label?: string;
  };
  channel?: PluginPackageChannel;
  compat?: OpenClawPackageCompat;
  install?: PluginPackageInstall;
  startup?: OpenClawPackageStartup;
};

export const DEFAULT_PLUGIN_ENTRY_CANDIDATES = [
  "index.ts",
  "index.js",
  "index.mjs",
  "index.cjs",
] as const;

export type PackageExtensionResolution =
  | { status: "ok"; entries: string[] }
  | { status: "missing"; entries: [] }
  | { status: "empty"; entries: [] }
  | { status: "invalid"; entries: []; error: string };

export type ManifestKey = typeof MANIFEST_KEY;

export type PackageManifest = {
  name?: string;
  version?: string;
  description?: string;
  dependencies?: Record<string, string>;
  optionalDependencies?: Record<string, string>;
} & Partial<Record<ManifestKey, OpenClawPackageManifest>>;

export function getPackageManifestMetadata(
  manifest: PackageManifest | undefined,
): OpenClawPackageManifest | undefined {
  if (!manifest) {
    return undefined;
  }
  return manifest[MANIFEST_KEY];
}

export function resolvePackageExtensionEntries(
  manifest: PackageManifest | undefined,
): PackageExtensionResolution {
  const rawOpenClaw = manifest?.[MANIFEST_KEY] as unknown;
  if (rawOpenClaw === undefined || rawOpenClaw === null) {
    return { status: "missing", entries: [] };
  }
  if (!isRecord(rawOpenClaw)) {
    return {
      status: "invalid",
      entries: [],
      error: "package.json openclaw must be an object",
    };
  }
  const raw = rawOpenClaw.extensions;
  if (raw === undefined || raw === null) {
    return { status: "missing", entries: [] };
  }
  if (!Array.isArray(raw)) {
    return {
      status: "invalid",
      entries: [],
      error: "package.json openclaw.extensions must be an array",
    };
  }
  const entries: string[] = [];
  for (const [index, entry] of raw.entries()) {
    const normalized = normalizeOptionalString(entry);
    if (!normalized) {
      return {
        status: "invalid",
        entries: [],
        error: `package.json openclaw.extensions[${index}] must be a non-empty string`,
      };
    }
    entries.push(normalized);
  }
  if (entries.length === 0) {
    return { status: "empty", entries: [] };
  }
  return { status: "ok", entries };
}
