import { parseEnvironment, parseEnvironmentValues, type Environment, } from "../config/environment.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; import { effectiveClientServer, clientConfigurationRecordSchema, type ClientConfigurationDocument, } from "./ClientConfigurationDocument.js"; import { PRODUCT_IDENTITY } from "../identity.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { mergeEnvironmentOverrides, snapshotEnvironment, } from "../process/snapshotEnvironment.js"; import { dshDocument } from "./DshCordisPatch.js"; import type { McpToolTimeoutInputs } from "../mcpStartupPolicy.js"; import type { AnalysisProviderSelector } from "../contracts/providerSelection.js"; export type ProviderRequestBudgetResolver = ( analysisProvider: AnalysisProviderSelector, environment: Readonly>, ) => Promise>; export interface ClientRegistrationEnvironmentDetails { readonly values: Readonly>; readonly unresolvedSettings: ReadonlySet; readonly timeoutInputs: McpToolTimeoutInputs | undefined; } interface ResolvedClientRegistrationEnvironmentDetails extends ClientRegistrationEnvironmentDetails { readonly timeoutInputs: McpToolTimeoutInputs; } const TIMEOUT_INPUT_SETTINGS: ReadonlySet = new Set([ "REA_ANALYSIS_PROVIDER", "REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS", "REA_GHIDRA_STARTUP_TIMEOUT_MS", "REA_HOPPER_STARTUP_TIMEOUT_MS", "REA_JEB_MCP_TIMEOUT_MS", ]); const environmentSchema = clientConfigurationRecordSchema.transform( (values, context) => { const entries: [string, string][] = []; for (const [key, value] of Object.entries(values)) { if (typeof value === "string") entries.push([key, value]); else context.addIssue({ code: "custom", path: [key], message: "Environment values must be strings", }); } return Object.fromEntries(entries); }, ); /** Read explicit REA overrides without dropping peers when one value is invalid. */ export const clientRegistrationEnvironment = ( parsed: ClientConfigurationDocument | undefined, overrides: Readonly> = {}, validation: "fragment" | "effective" = "effective", platform: NodeJS.Platform = process.platform, ): Result>, ConfigurationError> => { const details = clientRegistrationEnvironmentDetails( parsed, overrides, validation, platform, ); return details.ok ? ok(details.value.values) : details; }; /** Retain original strings and the parser-owned timeout facts. */ export const clientRegistrationEnvironmentDetails = ( parsed: ClientConfigurationDocument | undefined, overrides: Readonly> = {}, validation: "fragment" | "effective" = "effective", platform: NodeJS.Platform = process.platform, ): Result => { if (parsed?.dialect === "dsh") { const environment = dshDocument(parsed.document).environment( overrides, validation, platform, ); if (!environment.ok) return environment; return ok( environmentDetails( environment.value.values, environment.value.configuration, environment.value.unresolvedSettings, ), ); } const key = parsed?.dialect === "opencode" || parsed?.dialect === "opencode_v2" ? "environment" : "env"; const registration = parsed === undefined ? undefined : effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey); const raw = typeof registration === "object" && registration !== null ? Reflect.get(registration, key) : undefined; const existing = environmentSchema.safeParse(raw === undefined ? {} : raw); if (!existing.success) return err( new ConfigurationError("Invalid REA server environment", { settings: existing.error.issues.map((issue) => ({ setting: [key, ...issue.path].join("."), constraint: "Server environment must be an object containing only string values", })), }), ); // Validate known settings with the runtime's parser, but retain the original // string overrides, including unknown client-specific keys and no defaults. const environment = mergeEnvironmentOverrides( existing.data, overrides, platform, ); return validatedDetails(environment, validation, platform); }; /** Resolve the async provider-specific budget through its retained reader owner. */ export const clientRegistrationEnvironmentDetailsWithBudget = async ( parsed: ClientConfigurationDocument | undefined, overrides: Readonly>, validation: "fragment" | "effective", resolveProviderRequestBudget: ProviderRequestBudgetResolver | undefined, platform: NodeJS.Platform = process.platform, ): Promise< Result > => { const details = clientRegistrationEnvironmentDetails( parsed, overrides, validation, platform, ); if (!details.ok) return details; const unresolved = [...details.value.unresolvedSettings].filter( (setting) => resolveProviderRequestBudget !== undefined || TIMEOUT_INPUT_SETTINGS.has(setting), ); if (details.value.timeoutInputs === undefined || unresolved.length > 0) return err( new ConfigurationError( "Cannot derive a provider request budget from expression-backed settings", { settings: unresolved.map((setting) => ({ setting, constraint: "Resolve this setting to a literal before deriving a request budget", })), }, ), ); const resolved = { ...details.value, timeoutInputs: details.value.timeoutInputs, }; if (resolveProviderRequestBudget === undefined) return ok(resolved); return validatedBudget(resolved, resolveProviderRequestBudget, platform); }; const validatedDetails = ( environment: Readonly>, validation: "fragment" | "effective", platform: NodeJS.Platform = process.platform, ): Result => { const runtimeEnvironment = snapshotEnvironment(environment, platform); const validated = validation === "fragment" ? parseEnvironmentValues(runtimeEnvironment) : parseEnvironment(runtimeEnvironment); if (!validated.ok) return validated; return ok(environmentDetails(environment, validated.value)); }; const environmentDetails = ( environment: Readonly>, configuration: Environment, unresolvedSettings: ReadonlySet = new Set(), ): ClientRegistrationEnvironmentDetails => ({ values: Object.fromEntries( Object.entries(environment).sort(([left], [right]) => compareUnicodeCodePoints(left, right), ), ), unresolvedSettings, timeoutInputs: [...unresolvedSettings].some((setting) => TIMEOUT_INPUT_SETTINGS.has(setting), ) ? undefined : { analysisProvider: configuration.REA_ANALYSIS_PROVIDER, ghidraStartupTimeoutMs: configuration.REA_GHIDRA_STARTUP_TIMEOUT_MS, ...(configuration.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS === undefined ? {} : { ghidraDecompileTimeoutMs: configuration.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS * 1000, }), hopperStartupTimeoutMs: configuration.REA_HOPPER_STARTUP_TIMEOUT_MS, ...(configuration.REA_JEB_MCP_TIMEOUT_MS === undefined ? {} : { jebMcpTimeoutMs: configuration.REA_JEB_MCP_TIMEOUT_MS }), }, }); const validatedBudget = async ( details: ResolvedClientRegistrationEnvironmentDetails, resolveProviderRequestBudget: ProviderRequestBudgetResolver, platform: NodeJS.Platform, ): Promise< Result > => { const providerBudget = await resolveProviderRequestBudget( details.timeoutInputs.analysisProvider, snapshotEnvironment(details.values, platform), ); if (!providerBudget.ok) return providerBudget; return ok({ ...details, timeoutInputs: { ...details.timeoutInputs, ...(providerBudget.value === undefined ? {} : { providerRequestTimeoutMs: providerBudget.value }), }, }); };