import { ghidraProjectPathSchema, ghidraProgramNameSchema, } from "./ghidraExistingProject.js"; import { z } from "zod"; import { mcpInputBudgetSchema } from "./mcpInputBudget.js"; import { isAbsolute } from "node:path"; import { ConfigurationError, type ConfigurationSettingIssue, } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; import { analysisProviderSelectorSchema } from "../domain/providerSelection.js"; import { ghidraCompilerSpecIdSchema, ghidraLanguageIdSchema, } from "./ghidraLanguageOverride.js"; import { ghidraDisabledAnalyzersSchema } from "./ghidraDisabledAnalyzers.js"; import { DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS, providerStartupTimeoutSchema, } from "./providerStartupTimeout.js"; import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js"; import { mcpOutputSchemaProfileSchema } from "./mcpOutputSchemaProfile.js"; import { mcpToolCatalogProfileSchema } from "./mcpToolCatalogProfile.js"; import { mcpResponseBudgetSchema } from "./mcpResponseBudget.js"; import { debuggerSessionLimitEnvironmentShape } from "./debuggerSessionLimits.js"; import { mcpDoctorDeadlineMsSchema, mcpFirstCatalogBudgetMsSchema, mcpInitializeBudgetMsSchema, mcpToolsListBudgetMsSchema, } from "./mcpStartupBudgets.js"; import { mcpToolFamiliesSchema } from "./mcpToolFamilies.js"; import { parseLoaderArgs, parseStringArray } from "./parsers.js"; import { processStartConfigurationSchema } from "./processStart.js"; import { parseJebMcpEndpoint } from "./jebMcpEndpoint.js"; const environmentSchema = z.object({ ...debuggerSessionLimitEnvironmentShape, REA_ANALYSIS_PROVIDER: analysisProviderSelectorSchema.default("auto"), REA_MCP_MAX_RESPONSE_BYTES: mcpResponseBudgetSchema.optional(), REA_MCP_MAX_INPUT_ELEMENTS: mcpInputBudgetSchema.optional(), REA_MCP_INITIALIZE_BUDGET_MS: mcpInitializeBudgetMsSchema.optional(), REA_MCP_TOOLS_LIST_BUDGET_MS: mcpToolsListBudgetMsSchema.optional(), REA_MCP_FIRST_CATALOG_BUDGET_MS: mcpFirstCatalogBudgetMsSchema.optional(), REA_MCP_DOCTOR_DEADLINE_MS: mcpDoctorDeadlineMsSchema.optional(), REA_MCP_INPUT_SCHEMA_PROFILE: mcpInputSchemaProfileSchema.optional(), REA_MCP_TOOL_FAMILIES: mcpToolFamiliesSchema.optional(), REA_MCP_OUTPUT_SCHEMA_PROFILE: mcpOutputSchemaProfileSchema.optional(), REA_MCP_TOOL_CATALOG_PROFILE: mcpToolCatalogProfileSchema.optional(), REA_IDA_MCP_CONFIG: z .string() .min(1) .refine(isAbsolute, "REA_IDA_MCP_CONFIG must be absolute") .optional(), REA_JEB_MCP_URL: z .string() .superRefine((value, context) => { const parsed = parseJebMcpEndpoint(value); if (!parsed.ok) context.addIssue({ code: "custom", message: typeof parsed.error.cause === "string" ? parsed.error.cause : parsed.error.message, }); }) .optional(), GHIDRA_INSTALL_DIR: z .string() .min(1) .refine(isAbsolute, "GHIDRA_INSTALL_DIR must be absolute") .optional(), JAVA_HOME: z .string() .min(1) .refine(isAbsolute, "JAVA_HOME must be absolute") .optional(), REA_GHIDRA_NATIVEAOT_JAR: z .string() .min(1) .refine(isAbsolute, "REA_GHIDRA_NATIVEAOT_JAR must be absolute") .optional(), REA_SWIFTC_COMPILE_TIMEOUT_MS: providerStartupTimeoutSchema.default( DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS, ), REA_GHIDRA_PROJECT_PATH: ghidraProjectPathSchema.optional(), REA_GHIDRA_CHECKPOINT_DIR: z .string() .min(1) .refine( (value) => value.startsWith("/") && !value.includes("\0"), "must be an absolute POSIX directory path", ) .optional(), REA_GHIDRA_PROGRAM_NAME: ghidraProgramNameSchema.optional(), REA_GHIDRA_LANGUAGE_ID: ghidraLanguageIdSchema.optional(), REA_GHIDRA_COMPILER_SPEC_ID: ghidraCompilerSpecIdSchema.optional(), REA_GHIDRA_DISABLED_ANALYZERS: ghidraDisabledAnalyzersSchema.optional(), REA_GHIDRA_SEED_FILE: z .string() .min(1) .refine(isAbsolute, "REA_GHIDRA_SEED_FILE must be absolute") .optional(), REA_ILSPY_CMD_PATH: z .string() .min(1) .refine(isAbsolute, "REA_ILSPY_CMD_PATH must be absolute") .optional(), HOPPER_LAUNCHER_PATH: z.string().min(1).optional(), HOPPER_TARGET_PATH: z.string().min(1).optional(), HOPPER_TARGET_KIND: z.enum(["executable", "database"]).default("executable"), HOPPER_LOADER_ARGS_JSON: z.string().optional(), REA_LOG_LEVEL: processStartConfigurationSchema.shape.REA_LOG_LEVEL, REA_REFERENCE_SECRET_PATTERNS_JSON: z.string().default("[]"), }); /** Copy producer-owned update facts from a Zod issue without inventing them. */ const configurationSettingFromIssue = (issue: { readonly path: readonly PropertyKey[]; readonly message: string; readonly code: string; readonly params?: Readonly> | undefined; }): ConfigurationSettingIssue => { const setting = String(issue.path[0] ?? "environment"); const constraint = issue.message; if (issue.code !== "custom" || issue.params === undefined) return { setting, constraint }; const updateMode = issue.params.update_mode; const pendingRestart = issue.params.pending_restart; return { setting, constraint, ...(updateMode === "runtime" || updateMode === "restart" ? { update_mode: updateMode } : {}), ...(typeof pendingRestart === "boolean" ? { pending_restart: pendingRestart } : {}), }; }; /** Parse the JEB-only timeout at its isolated provider-composition boundary. */ export const parseJebMcpTimeoutMs = ( value: string | undefined, ): Result => { if (value === undefined) return ok(undefined); const parsed = providerStartupTimeoutSchema.safeParse(value); if (parsed.success) return ok(parsed.data); return err( new ConfigurationError("Invalid REA environment configuration", { cause: parsed.error, settings: [ { setting: "REA_JEB_MCP_TIMEOUT_MS", constraint: parsed.error.issues[0]?.message ?? "must be a valid timeout", }, ], }), ); }; export type Environment = Omit< z.infer, "HOPPER_LOADER_ARGS_JSON" | "REA_REFERENCE_SECRET_PATTERNS_JSON" > & { readonly loaderArgs: readonly string[]; readonly referenceSecretPatterns: readonly string[]; }; /** Parse individual settings before layered registration fragments are merged. */ export const parseEnvironmentValues = ( environment: Readonly>, unresolvedSettings: ReadonlySet = new Set(), ): Result => { // Unknown client-specific entries are preserved by registration owners and // still cross the child-process boundary. Check names and byte admission before // the known-setting schema strips them; never include rejected values. // Cordis expression values stay unknown, but their literal names are known. const keys = new Set([...Object.keys(environment), ...unresolvedSettings]); const invalidEntries = [...keys] .filter( (key) => key.includes("=") || key.includes("\0") || (Object.hasOwn(environment, key) && environment[key]?.includes("\0")), ) .map((setting) => ({ setting, constraint: setting.includes("=") ? "Environment variable names must not contain '='" : "Environment variable names and values must not contain NUL bytes", })); if (invalidEntries.length > 0) return err( new ConfigurationError("Invalid REA environment configuration", { settings: invalidEntries, }), ); // An expression has no known runtime value to validate or default. Its // original text was checked for invalid bytes above and remains with its owner. const literals = Object.fromEntries( Object.entries(environment).filter(([key]) => !unresolvedSettings.has(key)), ); const parsed = environmentSchema.safeParse(literals); if (!parsed.success) { // Zod messages name the constraint, never the rejected value. // A producer may attach update_mode and pending_restart on a custom issue. const settings = parsed.error.issues.map((issue) => configurationSettingFromIssue(issue), ); return err( new ConfigurationError("Invalid REA environment configuration", { cause: parsed.error, settings, }), ); } const { HOPPER_LOADER_ARGS_JSON: encodedLoaderArgs, REA_REFERENCE_SECRET_PATTERNS_JSON: encodedSecretPatterns, ...env } = parsed.data; const loaderArgs = parseLoaderArgs(encodedLoaderArgs); if (!loaderArgs.ok) return loaderArgs; const secretPatterns = parseStringArray( encodedSecretPatterns, "REA_REFERENCE_SECRET_PATTERNS_JSON", ); if (!secretPatterns.ok) return secretPatterns; return ok({ ...env, loaderArgs: loaderArgs.value, referenceSecretPatterns: secretPatterns.value, }); }; /** Validate complete runtime semantics without probing the host or persisting defaults. */ export const parseEnvironment = ( environment: Readonly>, // Registration expressions are not evaluated; their absence is not proven. unresolvedSettings: ReadonlySet = new Set(), ): Result => { const parsed = parseEnvironmentValues(environment, unresolvedSettings); if (!parsed.ok) return parsed; const env = parsed.value; if (env.REA_GHIDRA_CHECKPOINT_DIR !== undefined) { const conflicts = [ "REA_GHIDRA_PROJECT_PATH", "REA_GHIDRA_SEED_FILE", "REA_GHIDRA_NATIVEAOT_JAR", "REA_GHIDRA_LANGUAGE_ID", "REA_GHIDRA_DISABLED_ANALYZERS", ].filter((key) => Reflect.get(env, key) !== undefined); if (conflicts.length > 0) return err( new ConfigurationError( "Ghidra checkpoints currently support stock fresh-import analysis only", { settings: conflicts.map((setting) => ({ setting, constraint: "cannot be combined with REA_GHIDRA_CHECKPOINT_DIR", })), }, ), ); } const project = env.REA_GHIDRA_PROJECT_PATH; const incompatible = project === undefined ? env.REA_GHIDRA_PROGRAM_NAME === undefined || unresolvedSettings.has("REA_GHIDRA_PROJECT_PATH") ? [] : ["REA_GHIDRA_PROGRAM_NAME"] : [ "REA_GHIDRA_LANGUAGE_ID", "REA_GHIDRA_COMPILER_SPEC_ID", "REA_GHIDRA_SEED_FILE", "REA_GHIDRA_DISABLED_ANALYZERS", "REA_GHIDRA_NATIVEAOT_JAR", ].filter( (setting) => // Blank analyzer lists parse to no override, so they cannot conflict. (setting === "REA_GHIDRA_DISABLED_ANALYZERS" ? env[setting] : environment[setting]) !== undefined, ); if (incompatible.length > 0) return err( new ConfigurationError("Invalid existing Ghidra project configuration", { settings: incompatible.map((setting) => ({ setting, constraint: project === undefined ? "requires REA_GHIDRA_PROJECT_PATH" : "Import and analysis overrides cannot be combined with an existing project", })), }), ); // Ghidra's analyzeHeadless accepts -cspec only together with -processor. if ( env.REA_GHIDRA_COMPILER_SPEC_ID !== undefined && env.REA_GHIDRA_LANGUAGE_ID === undefined && !unresolvedSettings.has("REA_GHIDRA_LANGUAGE_ID") ) return err( new ConfigurationError("Invalid REA environment configuration", { settings: [ { setting: "REA_GHIDRA_COMPILER_SPEC_ID", constraint: "requires REA_GHIDRA_LANGUAGE_ID", }, ], }), ); return parsed; };