mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
refactor(ida): require the host platform for IDA MCP connections and snapshot the child environment with it (#11073)
This commit is contained in:
1 parent
9be6a96fd6
commit
bae5d74eb0
11 files changed
+77
-14
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { readSettledHealth } from "./IdaBusyHealth.js";
|
||||
import { idaConnectionDependencies } from "../../tests/support/idaConnectionDependencies.js";
|
||||
|
||||
describe("upstream IDA tool result decoding", () => {
|
||||
it("leaves successful legacy, structured, and native evidence unchanged", () => {
|
||||
@@ -187,6 +188,7 @@ const failingHttpConnection = (
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -289,6 +291,7 @@ it("preserves the SDK request timeout as a typed IDA timeout", async () => {
|
||||
timeoutMs,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
await expect(connection.connect()).resolves.toEqual([]);
|
||||
@@ -356,6 +359,7 @@ it("bounds a stalled health poll by the remaining readiness deadline", async ()
|
||||
timeoutMs,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
await connection.connect();
|
||||
@@ -510,6 +514,7 @@ const withIdaResource = async (
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
await connection.connect();
|
||||
@@ -677,6 +682,7 @@ it("uses the actual SDK abort outcome to preserve the caller reason", async () =
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const reason = { caller: "exact opaque reason" };
|
||||
@@ -734,6 +740,7 @@ it("keeps a real SDK physical failure when abort arrives during its outer unwind
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
await connection.connect();
|
||||
@@ -879,6 +886,7 @@ const cleanupConnection = () =>
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
|
||||
it("refuses an oversized HTTP error promptly and retains its exact pending cancellation for retry", async () => {
|
||||
@@ -1126,6 +1134,7 @@ it.each(["completed", "pending", "rejected"] as const)(
|
||||
timeoutMs: 30,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
const failure = await connection
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
ArtifactResourceScope,
|
||||
type ArtifactResourceOwner,
|
||||
} from "../artifacts/ArtifactResourceScope.js";
|
||||
import { HOST_PLATFORM } from "../platform/HostPlatform.js";
|
||||
import { ResponseBodyCleanup } from "../providers/ResponseBodyCleanup.js";
|
||||
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
@@ -97,10 +96,11 @@ export interface IdaMcpConnection {
|
||||
export type IdaMcpConnectionDependencies = Readonly<{
|
||||
createDiagnosticRedactor?: typeof createIdaDiagnosticRedactor;
|
||||
/**
|
||||
* Host platform that selects the Windows launch resolution. Composition
|
||||
* passes the running host; omitted, stdio registrations launch directly.
|
||||
* Host platform that selects the Windows launch resolution and the
|
||||
* environment-name semantics of the child's environment snapshot.
|
||||
* Composition passes the running host.
|
||||
*/
|
||||
platform?: NodeJS.Platform;
|
||||
platform: NodeJS.Platform;
|
||||
/** Owned process acquisition for a stdio registration. */
|
||||
launcher?: OwnedMcpLauncher;
|
||||
/** Largest stdio JSON-RPC message accepted from the upstream server. */
|
||||
@@ -133,7 +133,7 @@ const IDA_STDERR_TAIL_BYTES = 16_384;
|
||||
*/
|
||||
const idaStdioLauncher =
|
||||
(
|
||||
platform: NodeJS.Platform | undefined,
|
||||
platform: NodeJS.Platform,
|
||||
launch: OwnedMcpLauncher,
|
||||
timeoutMs: number,
|
||||
): OwnedMcpLauncher =>
|
||||
@@ -511,7 +511,7 @@ const retainedTransportCause = (cause: unknown, message: string): Error => {
|
||||
export const createIdaMcpConnection = (
|
||||
config: IdaConfiguration,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
dependencies: IdaMcpConnectionDependencies = {},
|
||||
dependencies: IdaMcpConnectionDependencies,
|
||||
): IdaMcpConnection => {
|
||||
const client = new StructuredContentMcpClient({
|
||||
name: "rea-ida-provider",
|
||||
@@ -675,11 +675,11 @@ export const createIdaMcpConnection = (
|
||||
// Only the selected snapshot reaches the child, never REA's ambient environment.
|
||||
hostEnvironment: snapshotEnvironment(
|
||||
environment,
|
||||
HOST_PLATFORM.platform,
|
||||
dependencies.platform,
|
||||
),
|
||||
env: {
|
||||
IDA_MCP_MAX_WORKERS: "1",
|
||||
...snapshotEnvironment(config.env, HOST_PLATFORM.platform),
|
||||
...snapshotEnvironment(config.env, dependencies.platform),
|
||||
},
|
||||
// Registrations commonly launch interpreter-driven scripts.
|
||||
expectedCommand: null,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { createIdaMcpConnection } from "./IdaMcpConnection.js";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { idaConnectionDependencies } from "../../tests/support/idaConnectionDependencies.js";
|
||||
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
@@ -58,6 +59,7 @@ input.on('line', (line) => {
|
||||
timeoutMs: 5000,
|
||||
},
|
||||
selected,
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
environment.REA_SELECTED_CONTEXT = "mutated-after-construction";
|
||||
try {
|
||||
@@ -105,7 +107,7 @@ input.on('line', (line) => {
|
||||
timeoutMs: 5000,
|
||||
},
|
||||
{},
|
||||
{ maxStdioMessageBytes: 4096 },
|
||||
idaConnectionDependencies({ maxStdioMessageBytes: 4096 }),
|
||||
);
|
||||
try {
|
||||
expect(await connection.connect()).toEqual(["large"]);
|
||||
|
||||
@@ -118,6 +118,27 @@ it("reports an uninspectable Windows command candidate with its path and code, n
|
||||
expect(String(failure)).not.toContain("was not found");
|
||||
});
|
||||
|
||||
it("snapshots the child's environment with the injected platform's name semantics", async () => {
|
||||
files.present.add("C:\\Tools\\ida-mcp.EXE");
|
||||
const { launches } = await observeLaunch("ida-mcp", {
|
||||
SystemRoot: "C:\\Windows",
|
||||
Path: "C:\\Tools",
|
||||
PathExt: ".EXE",
|
||||
});
|
||||
// Windows names are case-insensitive: the snapshot folds them to one
|
||||
// spelling on whatever host runs the connection.
|
||||
expect(launches).toEqual([
|
||||
expect.objectContaining({
|
||||
command: "C:\\Tools\\ida-mcp.EXE",
|
||||
hostEnvironment: expect.objectContaining({
|
||||
SYSTEMROOT: "C:\\Windows",
|
||||
PATH: "C:\\Tools",
|
||||
PATHEXT: ".EXE",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("closes while a Windows command lookup is stalled on an unreachable PATH directory", async () => {
|
||||
files.stalled.add("\\\\offline\\tools\\ida-mcp.EXE");
|
||||
const launches: unknown[] = [];
|
||||
|
||||
@@ -90,7 +90,10 @@ export class IdaProvider implements AnalysisProviderCandidate {
|
||||
private readonly connectionFactory: (
|
||||
config: IdaConfiguration,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
) => IdaMcpConnection = createIdaMcpConnection,
|
||||
) => IdaMcpConnection = (config, environment) =>
|
||||
createIdaMcpConnection(config, environment, {
|
||||
platform: HOST_PLATFORM.platform,
|
||||
}),
|
||||
) {
|
||||
this.environment = snapshotEnvironment(environment, HOST_PLATFORM.platform);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { WindowsPrivateRuntime } from "../windows/WindowsPrivateRuntime.js";
|
||||
import { idaConnectionDependencies } from "../../tests/support/idaConnectionDependencies.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const windowsRuntimes = new Set<WindowsPrivateRuntime>();
|
||||
@@ -2331,7 +2332,7 @@ it("keeps the session cleanup owner until an ignored-abort HTTP acquisition and
|
||||
const client = new IdaSessionClient(
|
||||
config,
|
||||
target,
|
||||
createIdaMcpConnection(config, {}),
|
||||
createIdaMcpConnection(config, {}, idaConnectionDependencies()),
|
||||
);
|
||||
const executing = client.execute("health", {});
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createIdaMcpConnection } from "../../src/ida/IdaMcpConnection.js";
|
||||
import { idaConnectionDependencies } from "../support/idaConnectionDependencies.js";
|
||||
|
||||
const launcherEnvironment = (): Readonly<Record<string, string>> => {
|
||||
if (process.platform !== "win32") return {};
|
||||
@@ -24,6 +25,7 @@ describe("IDA startup diagnostics", () => {
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
await expect(connection.connect()).rejects.toMatchObject({
|
||||
@@ -52,6 +54,7 @@ describe("IDA startup diagnostics", () => {
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
await expect(connection.connect()).rejects.toMatchObject({
|
||||
@@ -82,6 +85,7 @@ it("drops a leading partial UTF-8 character from the bounded stderr tail", async
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
await expect(connection.connect()).rejects.toMatchObject({
|
||||
@@ -110,6 +114,7 @@ it("retains an in-budget undecodable leading byte without claiming tail truncati
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
try {
|
||||
const failure = await connection.connect().catch((cause: unknown) => cause);
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
IdaMcpConnection,
|
||||
IdaMcpConnectionDependencies,
|
||||
} from "../../../src/ida/IdaMcpConnection.js";
|
||||
import { idaConnectionDependencies } from "../../support/idaConnectionDependencies.js";
|
||||
|
||||
const cleanups: (() => Promise<void>)[] = [];
|
||||
afterEach(async () => {
|
||||
@@ -48,7 +49,7 @@ type IdaTransportFixtureOptions = {
|
||||
readonly toolContentType?: string;
|
||||
readonly toolResponseBody?: string | Buffer;
|
||||
readonly configurationHeaders?: Readonly<Record<string, string>>;
|
||||
readonly connectionDependencies?: IdaMcpConnectionDependencies;
|
||||
readonly connectionDependencies?: Partial<IdaMcpConnectionDependencies>;
|
||||
};
|
||||
const fixtureResponse = (
|
||||
input: z.infer<typeof requestSchema>,
|
||||
@@ -188,7 +189,7 @@ const fixture = async (options: IdaTransportFixtureOptions = {}) => {
|
||||
timeoutMs: 2000,
|
||||
},
|
||||
{},
|
||||
connectionDependencies,
|
||||
idaConnectionDependencies(connectionDependencies),
|
||||
);
|
||||
cleanups.push(() => connection.close());
|
||||
return {
|
||||
@@ -304,6 +305,7 @@ process.stdin.on("data", (chunk) => {
|
||||
timeoutMs: 10_000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
cleanups.push(() => connection.close());
|
||||
expect(await connection.connect()).toEqual(["list_strings"]);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { elf } from "../../../../src/domain/binaryTarget.fixture.js";
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
|
||||
import { createIdaMcpConnection } from "../../../../src/ida/IdaMcpConnection.js";
|
||||
import { IdaSessionClient } from "../../../../src/ida/IdaSessionClient.js";
|
||||
import { idaConnectionDependencies } from "../../../support/idaConnectionDependencies.js";
|
||||
|
||||
/**
|
||||
* A headless IDA supervisor over real stdio that answers the session
|
||||
@@ -96,7 +97,11 @@ describe("retained headless IDA connection in a long-lived host", () => {
|
||||
timeoutMs: 5000,
|
||||
workspaceRoot,
|
||||
};
|
||||
const connection = createIdaMcpConnection(config, process.env);
|
||||
const connection = createIdaMcpConnection(
|
||||
config,
|
||||
process.env,
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
// Close the supervisor even when an assertion fails before cleanup does.
|
||||
onTestFinished(() => connection.close().catch(() => undefined));
|
||||
const client = new IdaSessionClient(config, target, connection);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createVerifierHttpServer } from "../../../scripts/lib/verifier-http-ser
|
||||
import { createIdaMcpConnection } from "../../../src/ida/IdaMcpConnection.js";
|
||||
import { createStreamableHttpJebMcpConnection } from "../../../src/jeb/JebMcpConnection.js";
|
||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { idaConnectionDependencies } from "../../support/idaConnectionDependencies.js";
|
||||
|
||||
const requestSchema = z.object({
|
||||
id: z.union([z.string(), z.number()]).optional(),
|
||||
@@ -143,6 +144,7 @@ const createConnection = (provider: "ida" | "jeb", endpoint: string) => {
|
||||
timeoutMs: 1_000,
|
||||
},
|
||||
{},
|
||||
idaConnectionDependencies(),
|
||||
);
|
||||
return {
|
||||
connect: () => connection.connect(),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { IdaMcpConnectionDependencies } from "../../src/ida/IdaMcpConnection.js";
|
||||
import { HOST_PLATFORM } from "../../src/platform/HostPlatform.js";
|
||||
|
||||
/**
|
||||
* IDA connection dependencies for tests: the running host's platform, as
|
||||
* composition passes it, plus any test overrides (including `platform`).
|
||||
*/
|
||||
export const idaConnectionDependencies = (
|
||||
overrides: Partial<IdaMcpConnectionDependencies> = {},
|
||||
): IdaMcpConnectionDependencies => ({
|
||||
platform: HOST_PLATFORM.platform,
|
||||
...overrides,
|
||||
});
|
||||
Reference in new issue
Block a user