mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(setup): include registered IDA budgets in client deadlines
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user