Return native admission issues and an explicit Hopper timeout result from the configuration owner. Doctor consumes those parsed facts instead of raw timeout text plus a separate issue list. Reuse a successful aggregate parse; validate the individual timeout independently when other settings reject it. Preserve aggregate error precedence, JEB isolation, omission, constraints, causes and provider scope. Keep partial diagnostics when unrelated NUL, loader arguments, language selection or timeout settings are invalid. Validation: configuration and Doctor workflow regressions, mixed-invalid settings, full static/docs checks. No installed provider was exercised.
182 lines
6.8 KiB
TypeScript
182 lines
6.8 KiB
TypeScript
import { accessSync, constants, statSync } from "node:fs";
|
|
|
|
import {
|
|
ConfigurationError,
|
|
type ConfigurationSettingIssue,
|
|
} from "../domain/configurationErrors.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import { parseEnvironment } from "./environment.js";
|
|
import { providerStartupTimeoutSchema } from "./providerStartupTimeout.js";
|
|
import { homeDirectoryFromEnvironment } from "./homeDirectory.js";
|
|
import { DEFAULT_BRIDGE_MAX_REQUEST } from "./bridgeMaxRequestBytes.js";
|
|
import { DEFAULT_REGEX_MATCHING_DEADLINE } from "./regexMatchingDeadline.js";
|
|
import { DEFAULT_REGEX_WORKER_HEAP_REQUEST } from "./regexWorkerHeapRequest.js";
|
|
import type { AppConfig } from "./types.js";
|
|
import { hopperLauncherDiscovery } from "../application/HopperLauncherDiscovery.js";
|
|
import { localMacHopperRenamedLauncherCandidates } from "../application/MacHopperPaths.js";
|
|
const executableAvailable = (path: string): boolean => {
|
|
try {
|
|
accessSync(path, constants.X_OK);
|
|
return statSync(path).isFile();
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const defaultHopperLauncherPath = (
|
|
platform: NodeJS.Platform,
|
|
homeDirectory: string,
|
|
executable: (path: string) => boolean = executableAvailable,
|
|
): string => {
|
|
const discovery = hopperLauncherDiscovery(platform, homeDirectory);
|
|
for (const candidate of discovery.candidates)
|
|
if (executable(candidate)) return candidate;
|
|
if (discovery.probesMacApplicationBundles)
|
|
for (const candidate of localMacHopperRenamedLauncherCandidates(
|
|
homeDirectory,
|
|
))
|
|
if (executable(candidate)) return candidate;
|
|
return discovery.fallback;
|
|
};
|
|
|
|
/**
|
|
* Environment native analysis admits. JEB validates its own timeout during
|
|
* optional provider composition; that setting must not block unrelated
|
|
* CLI/MCP startup. Registration validation still checks it directly.
|
|
*/
|
|
const nativeAnalysisEnvironment = (
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): Readonly<Record<string, string | undefined>> => {
|
|
const { REA_JEB_MCP_TIMEOUT_MS: _jebTimeout, ...appEnvironment } =
|
|
environment;
|
|
return appEnvironment;
|
|
};
|
|
|
|
/** Native admission issues and an explicitly configured timeout's independent facts. */
|
|
export interface NativeAnalysisConfigurationInspection {
|
|
readonly issues: readonly ConfigurationSettingIssue[];
|
|
readonly configuredHopperStartupTimeout?: Result<number, ConfigurationError>;
|
|
}
|
|
|
|
/** Inspect native admission without probing launchers or losing mixed-invalid diagnostics. */
|
|
export const inspectNativeAnalysisConfiguration = (
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): NativeAnalysisConfigurationInspection => {
|
|
const parsed = parseEnvironment(nativeAnalysisEnvironment(environment));
|
|
const issues = parsed.ok ? [] : parsed.error.settings;
|
|
const configuredTimeout = environment.REA_HOPPER_STARTUP_TIMEOUT_MS;
|
|
if (configuredTimeout === undefined) return { issues };
|
|
if (parsed.ok)
|
|
return {
|
|
issues,
|
|
configuredHopperStartupTimeout: ok(
|
|
parsed.value.REA_HOPPER_STARTUP_TIMEOUT_MS,
|
|
),
|
|
};
|
|
// Another setting may reject the aggregate before this supplied timeout's
|
|
// facts are available. Its dedicated diagnostic still uses the same schema.
|
|
const timeout = providerStartupTimeoutSchema.safeParse(configuredTimeout);
|
|
return {
|
|
issues,
|
|
configuredHopperStartupTimeout: timeout.success
|
|
? ok(timeout.data)
|
|
: err(
|
|
new ConfigurationError("Invalid REA environment configuration", {
|
|
cause: timeout.error,
|
|
settings: timeout.error.issues.map(({ message }) => ({
|
|
setting: "REA_HOPPER_STARTUP_TIMEOUT_MS",
|
|
constraint: message,
|
|
})),
|
|
}),
|
|
),
|
|
};
|
|
};
|
|
|
|
/** Parse provider configuration and runtime prerequisites at the composition root. */
|
|
export const parseConfig = (
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): Result<AppConfig, ConfigurationError> => {
|
|
const parsed = parseEnvironment(nativeAnalysisEnvironment(environment));
|
|
if (!parsed.ok) return parsed;
|
|
const env = parsed.value;
|
|
return ok({
|
|
...(env.REA_MCP_MAX_RESPONSE_BYTES === undefined
|
|
? {}
|
|
: { mcpMaxResponseBytes: env.REA_MCP_MAX_RESPONSE_BYTES }),
|
|
...(env.REA_MCP_INPUT_SCHEMA_PROFILE === undefined
|
|
? {}
|
|
: { mcpInputSchemaProfile: env.REA_MCP_INPUT_SCHEMA_PROFILE }),
|
|
...(env.REA_MCP_OUTPUT_SCHEMA_PROFILE === undefined
|
|
? {}
|
|
: { mcpOutputSchemaProfile: env.REA_MCP_OUTPUT_SCHEMA_PROFILE }),
|
|
analysisProvider: env.REA_ANALYSIS_PROVIDER,
|
|
...(env.REA_IDA_MCP_CONFIG === undefined
|
|
? {}
|
|
: { idaMcpConfigPath: env.REA_IDA_MCP_CONFIG }),
|
|
ghidraInstallDir: env.GHIDRA_INSTALL_DIR,
|
|
ghidraJavaHome: env.JAVA_HOME,
|
|
ghidraStartupTimeoutMs: env.REA_GHIDRA_STARTUP_TIMEOUT_MS,
|
|
...(env.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS === undefined
|
|
? {}
|
|
: {
|
|
ghidraDecompileTimeoutSeconds:
|
|
env.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS,
|
|
}),
|
|
...(env.REA_GHIDRA_LANGUAGE_ID === undefined
|
|
? {}
|
|
: {
|
|
ghidraLanguageOverride: {
|
|
languageId: env.REA_GHIDRA_LANGUAGE_ID,
|
|
...(env.REA_GHIDRA_COMPILER_SPEC_ID === undefined
|
|
? {}
|
|
: { compilerSpecId: env.REA_GHIDRA_COMPILER_SPEC_ID }),
|
|
},
|
|
}),
|
|
...(env.REA_GHIDRA_SEED_FILE === undefined
|
|
? {}
|
|
: { ghidraSeedFile: env.REA_GHIDRA_SEED_FILE }),
|
|
...(env.REA_GHIDRA_DISABLED_ANALYZERS === undefined
|
|
? {}
|
|
: { ghidraDisabledAnalyzers: env.REA_GHIDRA_DISABLED_ANALYZERS }),
|
|
...(env.REA_GHIDRA_NATIVEAOT_JAR === undefined
|
|
? {}
|
|
: { ghidraNativeAotJar: env.REA_GHIDRA_NATIVEAOT_JAR }),
|
|
ilspyCmdPath: env.REA_ILSPY_CMD_PATH,
|
|
hopperLauncherPath:
|
|
env.HOPPER_LAUNCHER_PATH ??
|
|
defaultHopperLauncherPath(
|
|
process.platform,
|
|
homeDirectoryFromEnvironment(environment, process.platform),
|
|
),
|
|
hopperStartupTimeoutMs: env.REA_HOPPER_STARTUP_TIMEOUT_MS,
|
|
hopperTargetPath: env.HOPPER_TARGET_PATH,
|
|
hopperTargetKind: env.HOPPER_TARGET_KIND,
|
|
hopperLoaderArgs: env.loaderArgs,
|
|
logLevel: env.REA_LOG_LEVEL,
|
|
referenceSourcePolicy: {
|
|
secretPatterns: env.referenceSecretPatterns,
|
|
},
|
|
bridgeMaxRequestBytes:
|
|
env.REA_BRIDGE_MAX_REQUEST_BYTES === undefined
|
|
? DEFAULT_BRIDGE_MAX_REQUEST
|
|
: {
|
|
maximumBytes: env.REA_BRIDGE_MAX_REQUEST_BYTES,
|
|
source: "setting",
|
|
},
|
|
regexMatchingDeadline:
|
|
env.REA_REGEX_MATCHING_DEADLINE_MS === undefined
|
|
? DEFAULT_REGEX_MATCHING_DEADLINE
|
|
: {
|
|
deadlineMs: env.REA_REGEX_MATCHING_DEADLINE_MS,
|
|
source: "setting",
|
|
},
|
|
regexWorkerHeapRequest:
|
|
env.REA_REGEX_WORKER_HEAP_REQUEST_MIB === undefined
|
|
? DEFAULT_REGEX_WORKER_HEAP_REQUEST
|
|
: {
|
|
heapMiB: env.REA_REGEX_WORKER_HEAP_REQUEST_MIB,
|
|
source: "setting",
|
|
},
|
|
});
|
|
};
|