fix(ida): preserve registration recovery facts in readiness
Fixes #5771. Preserve the configuration owner's parsed registration settings and remediation in IDA readiness, retain caller-selected paths, and distinguish configuration selection from validity and live connection. Verified with 84 relevant tests, CLI/MCP negative-case parity, static/docs gates, independent source review and repeated actual Codex recovery tasks. No real IDA engine/GUI analysis qualification. Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
This commit is contained in:
1 parent
044f180222
commit
264862bf2e
3 files changed
+126
-26
No files matched your search
@@ -41,6 +41,15 @@ rea function /absolute/path/to/program main --json
|
||||
rea decompile /absolute/path/to/program main --json
|
||||
```
|
||||
|
||||
If the registration cannot be read or validated, provider diagnostics retain its
|
||||
selected `registration_path` and the configuration owner's setting/constraint
|
||||
facts in `configuration_details.settings`, plus `configuration_remediation`.
|
||||
`configured` means a path was selected, not that it is valid or an IDA connection
|
||||
was established; failed registrations remain unavailable. Correct
|
||||
`REA_IDA_MCP_CONFIG` in the next CLI invocation's environment or the MCP server's
|
||||
launch environment. Restart/reconnect that server after changing its environment;
|
||||
an export in another shell does not update a running server.
|
||||
|
||||
Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows. Attached profiles, including native mode, refuse universal Mach-O inputs at admission: every slice shares the recorded whole-file hash, so REA cannot verify which slice the GUI loaded. Open a thin file extracted with `lipo -thin` in both IDA and REA, or use headless mode, which analyzes the selected slice.
|
||||
|
||||
REA's close operation releases its MCP connection or proxy. It never saves or closes the existing GUI database. External edits remain visible because results are live and are not replayed from snapshots. These checks do not lock the GUI or prove that the database is unpatched.
|
||||
|
||||
@@ -1,8 +1,84 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
|
||||
import { parseConfig } from "../config/parseConfig.js";
|
||||
import { IdaProvider } from "./IdaProvider.js";
|
||||
|
||||
it.each([
|
||||
{ state: "unconfigured", selected: false, content: undefined },
|
||||
{ state: "missing", selected: true, content: undefined },
|
||||
{ state: "malformed", selected: true, content: "{" },
|
||||
{ state: "invalid", selected: true, content: "{}" },
|
||||
])(
|
||||
"preserves the owning registration setting and selection for $state IDA readiness",
|
||||
async ({ selected, content }) => {
|
||||
const root = await createTestTempDirectory("rea-ida-recovery-");
|
||||
const path = join(root, "registration.json");
|
||||
if (content !== undefined) await writeFile(path, content);
|
||||
const config = parseConfig(selected ? { REA_IDA_MCP_CONFIG: path } : {});
|
||||
if (!config.ok) throw config.error;
|
||||
const connect = vi.fn(() => {
|
||||
throw new Error("Readiness must not connect to IDA");
|
||||
});
|
||||
const provider = new IdaProvider(config.value, {}, connect);
|
||||
try {
|
||||
const snapshot = await provider.inspectReadinessSnapshot();
|
||||
expect(snapshot.availability).toMatchObject({
|
||||
status: "unavailable",
|
||||
code: selected ? "open_options_invalid" : "not_configured",
|
||||
reason: expect.stringContaining("REA_IDA_MCP_CONFIG"),
|
||||
diagnostics: {
|
||||
configured: selected,
|
||||
registration_path: selected ? path : null,
|
||||
live_connection_probed: false,
|
||||
configuration_details: {
|
||||
settings: [
|
||||
{ setting: "REA_IDA_MCP_CONFIG", constraint: expect.any(String) },
|
||||
],
|
||||
},
|
||||
configuration_remediation:
|
||||
expect.stringContaining("REA_IDA_MCP_CONFIG"),
|
||||
},
|
||||
});
|
||||
expect(connect).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
const closed = await provider.closeOwnedResources();
|
||||
if (!closed.ok) throw closed.error;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a selected valid registration distinct from a live IDA connection", async () => {
|
||||
const root = await createTestTempDirectory("rea-ida-configured-");
|
||||
const path = join(root, "registration.json");
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify({ mode: "attached", url: "http://127.0.0.1:13337/mcp" }),
|
||||
);
|
||||
const config = parseConfig({ REA_IDA_MCP_CONFIG: path });
|
||||
if (!config.ok) throw config.error;
|
||||
const connect = vi.fn(() => {
|
||||
throw new Error("Readiness must not connect to IDA");
|
||||
});
|
||||
const provider = new IdaProvider(config.value, {}, connect);
|
||||
try {
|
||||
expect(await provider.inspectAvailability()).toMatchObject({
|
||||
status: "available",
|
||||
diagnostics: {
|
||||
configured: true,
|
||||
registration_path: path,
|
||||
live_connection_probed: false,
|
||||
},
|
||||
});
|
||||
expect(connect).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
const closed = await provider.closeOwnedResources();
|
||||
if (!closed.ok) throw closed.error;
|
||||
}
|
||||
});
|
||||
|
||||
describe("IDA provider target support", () => {
|
||||
it("declines headerless raw-binary targets", () => {
|
||||
const config = parseConfig({});
|
||||
|
||||
+40
-25
@@ -7,6 +7,7 @@ import {
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type {
|
||||
AnalysisClient,
|
||||
AnalysisProviderBoundClientFactory,
|
||||
@@ -171,7 +172,31 @@ export class IdaProvider implements AnalysisProviderCandidate {
|
||||
config.value.protocol === "native",
|
||||
)
|
||||
: this.capabilities();
|
||||
if (config.ok && config.value.mode === "headless") {
|
||||
const registrationDiagnostics = {
|
||||
configured: this.config.idaMcpConfigPath !== undefined,
|
||||
registration_path: this.config.idaMcpConfigPath ?? null,
|
||||
live_connection_probed: false,
|
||||
};
|
||||
if (!config.ok) {
|
||||
const failure = projectAnalysisError(config.error);
|
||||
return {
|
||||
availability: {
|
||||
status: "unavailable",
|
||||
code:
|
||||
this.config.idaMcpConfigPath === undefined
|
||||
? "not_configured"
|
||||
: "open_options_invalid",
|
||||
reason: failure.message,
|
||||
diagnostics: {
|
||||
...registrationDiagnostics,
|
||||
configuration_details: failure.details ?? null,
|
||||
configuration_remediation: failure.remediation.action,
|
||||
},
|
||||
},
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
if (config.value.mode === "headless") {
|
||||
const privacy = privateRuntimeRootCapability();
|
||||
if (!privacy.available)
|
||||
return {
|
||||
@@ -180,9 +205,8 @@ export class IdaProvider implements AnalysisProviderCandidate {
|
||||
code: privacy.code,
|
||||
reason: privacy.reason,
|
||||
diagnostics: {
|
||||
configured: true,
|
||||
...registrationDiagnostics,
|
||||
mode: "headless",
|
||||
live_connection_probed: false,
|
||||
private_workspace_available: false,
|
||||
windows_native_failure: {
|
||||
stage: privacy.failureStage,
|
||||
@@ -193,28 +217,19 @@ export class IdaProvider implements AnalysisProviderCandidate {
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
const availability: ProviderAvailability = config.ok
|
||||
? {
|
||||
status: "available" as const,
|
||||
code: null,
|
||||
reason: null,
|
||||
diagnostics: {
|
||||
configured: true,
|
||||
mode: config.value.mode,
|
||||
transport: "command" in config.value ? "stdio" : "http",
|
||||
live_connection_probed: false,
|
||||
},
|
||||
}
|
||||
: {
|
||||
status: "unavailable" as const,
|
||||
code:
|
||||
this.config.idaMcpConfigPath === undefined
|
||||
? "not_configured"
|
||||
: "open_options_invalid",
|
||||
reason: config.error.message,
|
||||
diagnostics: { configured: false, live_connection_probed: false },
|
||||
};
|
||||
return { availability, capabilities };
|
||||
return {
|
||||
availability: {
|
||||
status: "available" as const,
|
||||
code: null,
|
||||
reason: null,
|
||||
diagnostics: {
|
||||
...registrationDiagnostics,
|
||||
mode: config.value.mode,
|
||||
transport: "command" in config.value ? "stdio" : "http",
|
||||
},
|
||||
},
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
inspectTargetSupport(target: BinaryTarget): ProviderTargetSupport {
|
||||
const diagnostics = {
|
||||
|
||||
Reference in new issue
Block a user