fix(setup): include registered IDA budgets in client deadlines

This commit is contained in:
morluto
2026-10-15 07:36:34 +08:00
parent 9a44fea347
commit fa318995f5
10 changed files with 278 additions and 16 deletions
+7 -4
View File
@@ -148,8 +148,10 @@ startup; an explicit Ghidra startup timeout of 2700000 ms produces a 2820000 ms
tool timeout. The startup allowance follows an explicitly selected Ghidra or
Hopper provider, and otherwise uses the larger known Ghidra/Hopper deadline.
An explicit `REA_JEB_MCP_TIMEOUT_MS` request budget also contributes to that
maximum. Setup forwards these explicitly selected REA deadlines and the input
schema profile into the registration; unrelated process values are omitted.
maximum. For `ida` or `auto` selection, a valid IDA registration's parsed
`timeoutMs` also contributes to that maximum; its default is 300000 ms.
Setup forwards these explicitly selected REA deadlines and the input schema
profile into the registration; unrelated process values are omitted.
This headroom is a practical default, not a guarantee for every analysis
duration. Setup preserves positive timeout values already in the `rea` entry.
You can edit `startupTimeoutMs` and `toolTimeoutMs` there; doctor keeps positive
@@ -241,8 +243,9 @@ the default Ghidra allowance; an explicit Ghidra startup timeout of 2700
seconds yields 2820 seconds. Setup preserves positive caller-selected timeout
values in the REA entry, and doctor reports a non-blocking advisory when a tool
timeout is shorter than the known provider allowance. An explicitly selected
JEB request budget also contributes to the allowance. These values are
client-side deadlines, so increase them in `~/.codex/config.toml` if a
JEB request budget also contributes to the allowance. A configured IDA
registration's parsed `timeoutMs` contributes for `ida` or `auto` selection.
These values are client-side deadlines, so increase them in `~/.codex/config.toml` if a
workload needs more time. Devin honors `XDG_CONFIG_HOME` for its
`devin/mcp_config.json` on non-Windows systems.
+1 -1
View File
@@ -133,7 +133,7 @@ Use a package version that actually includes this adapter; repository main can b
Opening an MCP target starts its selected provider session; headless opening waits for auto-analysis. Set the agent's MCP request deadline long enough for the target. The registration's optional `timeoutMs` defaults to 300000 for upstream requests. An upstream SDK request that reaches this limit returns `provider_timeout` with its operation and actual request budget. A freshly opened headless worker can briefly report `status: "busy"` from `server_health`; REA re-reads health every 100 ms until the worker answers, the caller cancels, or `timeoutMs` has elapsed since the first read. Each health request is capped by the time remaining in that overall wait, so a stalled read cannot extend the readiness deadline by another full request timeout. Expiry while the worker is still busy produces a retryable `provider_timeout`; no further read is started after the deadline. That error names the busy tool from the last observation, or `unknown` when the worker reported none or a value that is not tool-name-shaped. If a health request itself consumes the remaining wait budget, the same last-busy timeout is returned with the stalled request's captured output. Cancellation during the wait produces `cancelled`. Any other unexpected health payload, including a busy payload with an invalid shape, remains `unreadable_output`. Cancellation drains the current upstream request before cleanup; it does not force-stop GUI analysis. An open request that times out without a completed response retains the private workspace because worker inventory alone cannot prove the request has stopped. `binary_session` reports actual tool availability. Repeated doctor/setup calls are unnecessary when the registration already works.
For guided registration, set `REA_IDA_MCP_CONFIG` before `rea setup --client <client>`. Setup includes that file reference in its reviewed plan and preserves it after approval; it does not copy upstream credentials into the agent configuration. `rea doctor --provider ida --json` validates the registration without starting IDA and explicitly leaves runtime compatibility and target binding for analysis.
For guided registration, set `REA_IDA_MCP_CONFIG` before `rea setup --client <client>`. Setup includes that file reference in its reviewed plan and preserves it after approval; it does not copy upstream credentials into the agent configuration. For `ida` or `auto` selection, Codex/Kimi setup and doctor use the effective client registration's parsed `timeoutMs` (default 300000 ms) when recommending the agent's per-tool deadline. Existing positive client deadlines remain caller choices. `rea doctor --provider ida --json` validates the registration without starting IDA and explicitly leaves runtime compatibility and target binding for analysis.
## Coverage and evidence
@@ -14,6 +14,12 @@ import { PRODUCT_IDENTITY } from "../identity.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.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<Record<string, string>>,
) => Result<number | undefined, ConfigurationError>;
export interface ClientRegistrationEnvironmentDetails {
readonly values: Readonly<Record<string, string>>;
@@ -41,6 +47,7 @@ export const clientRegistrationEnvironmentDetails = (
parsed: ClientConfigurationDocument | undefined,
overrides: Readonly<Record<string, string>> = {},
validation: "fragment" | "effective" = "effective",
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
): Result<ClientRegistrationEnvironmentDetails, ConfigurationError> => {
if (parsed?.dialect === "dsh") {
const environment = dshDocument(parsed.document).environment(
@@ -48,7 +55,11 @@ export const clientRegistrationEnvironmentDetails = (
validation,
);
if (!environment.ok) return environment;
return validatedDetails(environment.value, validation);
return validatedDetails(
environment.value,
validation,
resolveProviderRequestBudget,
);
}
const key =
parsed?.dialect === "opencode" || parsed?.dialect === "opencode_v2"
@@ -78,18 +89,29 @@ export const clientRegistrationEnvironmentDetails = (
// Validate known settings with the runtime's parser, but retain the original
// string overrides, including unknown client-specific keys and no defaults.
const environment = { ...existing.data, ...overrides };
return validatedDetails(environment, validation);
return validatedDetails(
environment,
validation,
resolveProviderRequestBudget,
);
};
const validatedDetails = (
environment: Readonly<Record<string, string>>,
validation: "fragment" | "effective",
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
): Result<ClientRegistrationEnvironmentDetails, ConfigurationError> => {
const validated =
validation === "fragment"
? parseEnvironmentValues(environment)
: parseEnvironment(environment);
if (!validated.ok) return validated;
const providerRequestBudget =
resolveProviderRequestBudget?.(
validated.value.REA_ANALYSIS_PROVIDER,
environment,
) ?? ok(undefined);
if (!providerRequestBudget.ok) return providerRequestBudget;
return ok({
values: Object.fromEntries(
Object.entries(environment).sort(([left], [right]) =>
@@ -103,6 +125,9 @@ const validatedDetails = (
...(validated.value.REA_JEB_MCP_TIMEOUT_MS === undefined
? {}
: { jebMcpTimeoutMs: validated.value.REA_JEB_MCP_TIMEOUT_MS }),
...(providerRequestBudget.value === undefined
? {}
: { providerRequestTimeoutMs: providerRequestBudget.value }),
},
});
};
+21 -3
View File
@@ -35,6 +35,7 @@ import type { SetupClient } from "./SupportedClients.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { dshDocument } from "./DshCordisPatch.js";
import { clientRegistrationEnvironmentDetails } from "./ClientRegistrationEnvironment.js";
import type { ProviderRequestBudgetResolver } from "./ClientRegistrationEnvironment.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
interface ClientRegistrationStatusBase {
@@ -98,6 +99,7 @@ interface ClientRegistrationReadOptions {
readonly platform?: NodeJS.Platform;
readonly environment?: NodeJS.ProcessEnv;
readonly resources?: ArtifactResourceScope;
readonly resolveProviderRequestBudget?: ProviderRequestBudgetResolver;
}
/** Registration facts shared by diagnostics and update maintenance. */
@@ -227,10 +229,26 @@ export const readClientRegistrationInspections = async (
const registration = parseRegistration(raw, effectiveClient);
const registrationEnvironment =
client.name === "kimi" || client.name === "codex"
? clientRegistrationEnvironmentDetails(parsed)
? clientRegistrationEnvironmentDetails(
parsed,
{},
"effective",
options.resolveProviderRequestBudget,
)
: undefined;
if (registrationEnvironment !== undefined && !registrationEnvironment.ok)
throw registrationEnvironment.error;
if (
registrationEnvironment !== undefined &&
!registrationEnvironment.ok
) {
record({
client: client.name,
config_path: configPath,
command: [],
state: "invalid",
remediation: registrationEnvironment.error.message,
});
continue;
}
const command: RegistrationCommand = [
registration.command,
...registration.args,
+13
View File
@@ -36,6 +36,7 @@ import {
type ClientRegistrationStatus,
type UnhealthyClientRegistrationStatus,
} from "./ClientRegistrationStatus.js";
import type { ProviderRequestBudgetResolver } from "./ClientRegistrationEnvironment.js";
import {
collectDoctorDiagnostics,
doctorHealthy,
@@ -105,6 +106,8 @@ export interface DoctorHost {
brewHopperPath(): Promise<string | undefined>;
manualHopperPaths(): Promise<readonly string[]>;
providerInspections?(): Promise<readonly DoctorProviderInspection[]>;
/** Resolve a parsed upstream request budget from the effective client environment. */
resolveProviderRequestBudget?: ProviderRequestBudgetResolver;
/** Validated provider settings preserved by setup, independent of readiness. */
providerRegistrationEnvironment?(): Readonly<Record<string, string>>;
installationPaths?(): Promise<readonly string[]>;
@@ -431,6 +434,7 @@ export interface SystemDoctorHostOptions {
readonly providerRegistrationEnvironment?: () => Readonly<
Record<string, string>
>;
readonly resolveProviderRequestBudget?: ProviderRequestBudgetResolver;
readonly linuxDemoRuntimeCheck?: () => Promise<DoctorCheck>;
}
@@ -547,6 +551,9 @@ export const systemDoctorHost = (
providerRegistrationEnvironment:
options.providerRegistrationEnvironment,
}),
...(options.resolveProviderRequestBudget === undefined
? {}
: { resolveProviderRequestBudget: options.resolveProviderRequestBudget }),
async ilspyCmdVersion(path) {
try {
await access(path, constants.X_OK);
@@ -599,6 +606,12 @@ export const systemDoctorHost = (
platform,
environment,
resources,
...(options.resolveProviderRequestBudget === undefined
? {}
: {
resolveProviderRequestBudget:
options.resolveProviderRequestBudget,
}),
}),
};
};
+29 -1
View File
@@ -6,6 +6,7 @@ import {
type ClientConfigurationSource,
} from "./ClientConfigurationFiles.js";
import { err, ok, type Result } from "../domain/result.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import {
clientRegistrationEntry,
clientConfigurationValuesEqual,
@@ -26,6 +27,7 @@ import writeFileAtomic from "write-file-atomic";
import {
clientRegistrationEnvironment,
clientRegistrationEnvironmentDetails,
type ProviderRequestBudgetResolver,
} from "./ClientRegistrationEnvironment.js";
import { PRODUCT_IDENTITY } from "../identity.js";
@@ -81,16 +83,23 @@ export const configureClientConfiguration = (
client: SetupClient,
environment: SetupProviderEnvironment = {},
command: readonly string[] = npxRegistrationCommand(),
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
): Promise<ClientConfigurationResult> => {
if (client.configPathError !== undefined)
return Promise.resolve({ status: "failed", reason: "path" });
return configureClientDocument(client, environment, command);
return configureClientDocument(
client,
environment,
command,
resolveProviderRequestBudget,
);
};
const configureClientDocument = async (
client: SetupClient,
environment: SetupProviderEnvironment,
command: readonly string[],
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
): Promise<ClientConfigurationResult> => {
const format = client.format;
if (format === undefined || format === "unsupported")
@@ -118,6 +127,7 @@ const configureClientDocument = async (
command,
existing,
files.value,
resolveProviderRequestBudget,
),
);
if (!policy.ok) {
@@ -156,6 +166,7 @@ const configureClientDocument = async (
command,
parsed,
files.value,
resolveProviderRequestBudget,
),
);
if (!policy.ok) {
@@ -184,6 +195,7 @@ const configureClientDocument = async (
servers: { ...fallback.value, ...parsed.servers },
},
files.value,
resolveProviderRequestBudget,
);
if (registrationCurrent(parsed, desired)) return { status: "unchanged" };
return persistClientConfiguration({
@@ -353,6 +365,7 @@ export const clientConfigurationAligned = async (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
command: readonly string[],
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
): Promise<boolean> => {
if (client.configPathError !== undefined) return false;
try {
@@ -372,6 +385,7 @@ export const clientConfigurationAligned = async (
command,
parsed,
files.value,
resolveProviderRequestBudget,
),
);
if (!policy.ok) {
@@ -388,6 +402,7 @@ export const clientConfigurationAligned = async (
command,
parsed,
files.value,
resolveProviderRequestBudget,
),
)
);
@@ -404,11 +419,13 @@ export const inspectClientConfiguration = async (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
command: readonly string[],
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
): Promise<ClientConfigurationInspection> => {
const inspection = await inspectClientDocument(
client,
providerEnvironment,
command,
resolveProviderRequestBudget,
);
if (inspection.status !== "create" && inspection.status !== "update")
return inspection;
@@ -423,6 +440,7 @@ const inspectClientDocument = async (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
command: readonly string[],
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
): Promise<ClientConfigurationInspection> => {
if (client.configPathError !== undefined)
return { status: "invalid", remediation: client.configPathError };
@@ -456,6 +474,7 @@ const inspectClientDocument = async (
command,
existing,
files.value,
resolveProviderRequestBudget,
),
);
if (!policy.ok) {
@@ -505,6 +524,7 @@ const inspectClientDocument = async (
command,
parsed,
files.value,
resolveProviderRequestBudget,
),
);
if (!policy.ok) {
@@ -522,6 +542,7 @@ const inspectClientDocument = async (
command,
parsed,
files.value,
resolveProviderRequestBudget,
);
if (registrationCurrent(parsed, desired))
return { status: "already_current" };
@@ -532,6 +553,8 @@ const inspectClientDocument = async (
}
} catch (cause: unknown) {
if (cause instanceof RegularFileCleanupFailure) throw cause;
if (cause instanceof ConfigurationError)
return { status: "invalid", remediation: cause.message };
// Malformed configuration is reported with the invalid remediation.
void cause;
return {
@@ -660,6 +683,7 @@ const clientConfigurationDesired = (
command: readonly string[],
parsed: ClientConfigurationDocument | undefined,
sources: readonly ClientConfigurationSource[],
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
) => {
const effective = effectiveClientConfiguration(sources, {
client,
@@ -685,6 +709,10 @@ const clientConfigurationDesired = (
const validated = clientRegistrationEnvironmentDetails(
effective,
providerEnvironment,
"effective",
client.name === "codex" || client.name === "kimi"
? resolveProviderRequestBudget
: undefined,
);
if (!validated.ok) throw validated.error;
// Preserve explicitly configured server settings, never the ambient process
+21 -4
View File
@@ -244,7 +244,10 @@ export const systemSetupHost = (
detectedClients: () => detectClients(homeDirectory, platform, environment),
supportedClients: () =>
Promise.resolve(supportedClients(homeDirectory, platform, environment)),
...clientConfigurationOperations(resources),
...clientConfigurationOperations(
resources,
doctorHost.resolveProviderRequestBudget,
),
skillNeedsInstall: (clientIds) =>
canonicalSkillNeedsInstall(
homeDirectory,
@@ -288,7 +291,10 @@ const setupInstallerDiagnostic = (
: `${normalized.slice(0, 397).trimEnd()}...`;
};
const clientConfigurationOperations = (resources: ArtifactResourceScope) => ({
const clientConfigurationOperations = (
resources: ArtifactResourceScope,
resolveProviderRequestBudget: DoctorHost["resolveProviderRequestBudget"],
) => ({
configureClient: (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
@@ -297,7 +303,12 @@ const clientConfigurationOperations = (resources: ArtifactResourceScope) => ({
runClientConfigurationOperation(resources, async () =>
client.format === "unsupported"
? { status: "skipped" as const }
: configureClientConfiguration(client, providerEnvironment, command),
: configureClientConfiguration(
client,
providerEnvironment,
command,
resolveProviderRequestBudget,
),
),
clientNeedsConfigure: (
client: SetupClient,
@@ -311,6 +322,7 @@ const clientConfigurationOperations = (resources: ArtifactResourceScope) => ({
client,
providerEnvironment,
command,
resolveProviderRequestBudget,
)),
),
inspectClientConfiguration: (
@@ -319,7 +331,12 @@ const clientConfigurationOperations = (resources: ArtifactResourceScope) => ({
command: readonly string[],
) =>
runClientConfigurationOperation(resources, () =>
inspectClientConfiguration(client, providerEnvironment, command),
inspectClientConfiguration(
client,
providerEnvironment,
command,
resolveProviderRequestBudget,
),
),
});
+17
View File
@@ -7,8 +7,10 @@ import { fileURLToPath } from "node:url";
import { inspectJadxProvider } from "./android/JadxDoctor.js";
import { inspectSystemGhidraProvider } from "./ghidra/GhidraDoctor.js";
import { inspectIdaRegistration } from "./ida/IdaDoctor.js";
import { readIdaConfiguration } from "./ida/IdaConfiguration.js";
import { parseJebMcpEndpoint } from "./jeb/JebMcpEndpoint.js";
import { selectLinuxPrivateDisplayStrategy } from "./hopper/LinuxPrivateDisplayProbe.js";
import { ok } from "./domain/result.js";
/** Compose provider diagnostics at the outer CLI adapter boundary. */
export const createSystemDoctorHost = (
@@ -24,6 +26,21 @@ export const createSystemDoctorHost = (
return systemDoctorHost({
environment,
providerRegistrationEnvironment: () => selectedJebRegistrationEnvironment,
resolveProviderRequestBudget: (
analysisProvider,
registrationEnvironment,
) => {
if (analysisProvider !== "ida" && analysisProvider !== "auto")
return ok(undefined);
const path = registrationEnvironment.REA_IDA_MCP_CONFIG;
if (path === undefined) return ok(undefined);
const registration = readIdaConfiguration(path);
if (registration.ok) return ok(registration.value.timeoutMs);
// An invalid IDA registration cannot be selected automatically. Keep it
// out of recommendations for another available provider, but surface the
// parser-owned error when IDA itself was explicitly selected.
return analysisProvider === "ida" ? registration : ok(undefined);
},
providerInspections: async () => [
await inspectSystemGhidraProvider(environment),
inspectIdaRegistration(environment.REA_IDA_MCP_CONFIG),
+6 -1
View File
@@ -24,6 +24,7 @@ export interface McpToolTimeoutInputs {
readonly ghidraStartupTimeoutMs: number;
readonly hopperStartupTimeoutMs: number;
readonly jebMcpTimeoutMs?: number;
readonly providerRequestTimeoutMs?: number;
}
/** Allow for known startup phases and caller-selected upstream request budgets. */
@@ -40,7 +41,11 @@ export const mcpProviderDeadlineAllowanceMs = (
inputs.ghidraStartupTimeoutMs,
inputs.hopperStartupTimeoutMs,
);
return Math.max(selectedTimeout, inputs.jebMcpTimeoutMs ?? 0);
return Math.max(
selectedTimeout,
inputs.jebMcpTimeoutMs ?? 0,
inputs.providerRequestTimeoutMs ?? 0,
);
};
/** Add operation headroom to the parsed provider deadline allowance. */
@@ -322,6 +322,142 @@ cliTest(
120_000,
);
for (const [client, provider] of [
["codex", "ida"],
["kimi", "ida"],
["kimi", "auto"],
] as const)
cliTest(
`public ${client} setup and doctor use the effective IDA registration request budget with ${provider} selection`,
async ({ cli }) => {
const home = await createTestTempDirectory(
"rea-ida-registration-budget-",
);
const clientHome = join(home, client);
await mkdir(clientHome, { recursive: true });
const idaConfig = join(home, "ida.json");
const writeRegistration = (timeoutMs: number) =>
writeFile(
idaConfig,
JSON.stringify({
url: "http://127.0.0.1:12345/mcp",
headers: { Authorization: "Bearer fixture-credential" },
timeoutMs,
}),
);
await writeRegistration(1_800_001);
const environment = {
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
CODEX_HOME: clientHome,
KIMI_CODE_HOME: clientHome,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
};
const setupArguments = [
"setup",
"--client",
client,
"--skill=false",
"--yes",
"--json",
];
const configured = await cli.run({
arguments: setupArguments,
cwd: home,
environment: {
...environment,
REA_ANALYSIS_PROVIDER: provider,
REA_IDA_MCP_CONFIG: idaConfig,
},
});
expect(configured.exitCode, configured.stdout).toBe(0);
const config = join(
clientHome,
client === "codex" ? "config.toml" : "mcp.json",
);
const text = await readFile(config, "utf8");
expect(
effectiveClientServer(
parseClientConfiguration(text, client === "codex" ? "toml" : "kimi"),
"rea",
),
).toMatchObject({
...(client === "codex"
? { tool_timeout_sec: 1921 }
: { toolTimeoutMs: 1_920_001 }),
env: { REA_ANALYSIS_PROVIDER: provider, REA_IDA_MCP_CONFIG: idaConfig },
});
expect(text).not.toContain("fixture-credential");
const run = (arguments_: readonly string[]) =>
cli.run({ arguments: arguments_, cwd: home, environment });
const healthy = await run(["doctor", "--client", client, "--json"]);
expect(healthy.exitCode, healthy.stdout).toBe(0);
expect(JSON.stringify(healthy.json)).not.toContain('"advisory"');
// Only the stored client's provider path identifies this changed budget;
// the doctor/setup process has no IDA configuration in its environment.
await writeRegistration(2_400_001);
const doctor = await run(["doctor", "--client", client, "--json"]);
expect(doctor.exitCode, doctor.stdout).toBe(0);
expect(doctor.json).toMatchObject({
identity: {
registrations: expect.arrayContaining([
expect.objectContaining({
client,
state: "aligned",
advisory: expect.stringContaining(
client === "codex"
? "tool_timeout_sec = 2521 seconds"
: "toolTimeoutMs = 2520001 ms",
),
}),
]),
},
});
const repeated = await run(setupArguments);
expect(repeated.exitCode, repeated.stdout).toBe(0);
expect(repeated.json).toMatchObject({
status: "ready",
plannedActions: [],
appliedActions: [],
});
expect(await readFile(config, "utf8")).toBe(text);
await expect(access(`${config}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
if (provider === "ida") {
await writeRegistration(0);
const invalidDoctor = await run([
"doctor",
"--client",
client,
"--json",
]);
expect(invalidDoctor.exitCode).toBe(1);
expect(invalidDoctor.json).toMatchObject({
identity: {
registrations: expect.arrayContaining([
expect.objectContaining({
client,
state: "invalid",
remediation: expect.stringContaining("REA_IDA_MCP_CONFIG"),
}),
]),
},
});
const invalidSetup = await run(setupArguments);
expect(invalidSetup.exitCode).toBe(1);
expect(invalidSetup.stdout).toContain("REA_IDA_MCP_CONFIG");
expect(await readFile(config, "utf8")).toBe(text);
await expect(access(`${config}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
}
},
120_000,
);
for (const invalidSetting of [
{ setting: "REA_MCP_INPUT_SCHEMA_PROFILE", value: "unknown" },
{ setting: "REA_GHIDRA_STARTUP_TIMEOUT_MS", value: "0" },