mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
fix(ida): retain registration constraints and identity refusal guidance
This commit is contained in:
1 parent
6739b3cd80
commit
75dfd1c747
6 files changed
+206
-22
No files matched your search
+34
-10
@@ -4,6 +4,7 @@ import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
|
||||
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
withOwnedRegularFile,
|
||||
@@ -132,17 +133,40 @@ export const readIdaConfiguration = (
|
||||
const envelope = z
|
||||
.object({ mcpServers: z.object({ "ida-pro-mcp": z.unknown() }) })
|
||||
.safeParse(input);
|
||||
const parsed = registrationSchema.safeParse(
|
||||
envelope.success ? envelope.data.mcpServers["ida-pro-mcp"] : input,
|
||||
const registration = envelope.success
|
||||
? envelope.data.mcpServers["ida-pro-mcp"]
|
||||
: input;
|
||||
const parsed = registrationSchema.safeParse(registration);
|
||||
if (parsed.success) return ok(parsed.data);
|
||||
// Only schema-authored correction text is admitted; Zod's aggregate
|
||||
// message can contain registration values, including credentials.
|
||||
const constraints = projectInputIssues(
|
||||
parsed.error.issues,
|
||||
registration,
|
||||
).flatMap((issue) =>
|
||||
issue.message === undefined
|
||||
? []
|
||||
: [`${issue.path.join(".") || "registration"}: ${issue.message}`],
|
||||
);
|
||||
if (constraints.length === 0)
|
||||
return err(
|
||||
idaConfigurationError(
|
||||
"REA_IDA_MCP_CONFIG must contain an IDA MCP command/args or local url registration; see docs/ida-provider.md.",
|
||||
parsed.error,
|
||||
),
|
||||
);
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
`REA_IDA_MCP_CONFIG: ${constraints.join("; ")}. See docs/ida-provider.md.`,
|
||||
{
|
||||
cause: parsed.error,
|
||||
settings: constraints.map((constraint) => ({
|
||||
setting: "REA_IDA_MCP_CONFIG",
|
||||
constraint,
|
||||
})),
|
||||
},
|
||||
),
|
||||
);
|
||||
return parsed.success
|
||||
? ok(parsed.data)
|
||||
: err(
|
||||
idaConfigurationError(
|
||||
"REA_IDA_MCP_CONFIG must contain an IDA MCP command/args or local url registration; see docs/ida-provider.md.",
|
||||
parsed.error,
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof ConfigurationError) return err(cause);
|
||||
if (cause instanceof NonRegularFileReadError)
|
||||
|
||||
@@ -620,7 +620,22 @@ describe("attached IDA function failure handling", () => {
|
||||
producer.inputSha256 = "a".repeat(64);
|
||||
const mismatch = await client.execute("health", {});
|
||||
expect(mismatch.ok).toBe(false);
|
||||
if (!mismatch.ok) expect(mismatch.error.message).toContain("SHA-256");
|
||||
if (!mismatch.ok) {
|
||||
expect(projectAnalysisError(mismatch.error)).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
retryable: false,
|
||||
message: expect.stringContaining("SHA-256"),
|
||||
remediation: { action: expect.stringContaining("headless mode") },
|
||||
details: {
|
||||
operation: "health",
|
||||
diagnostics: {
|
||||
identity_check: "attached_sha256",
|
||||
reason: expect.stringContaining(target.path),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(producer.calls.map(({ name }) => name)).toEqual(["get_metadata"]);
|
||||
producer.inputSha256 = target.sha256;
|
||||
producer.beforeCall = async (name) => {
|
||||
if (name === "decompile_function") producer.imageBase = "0x2000";
|
||||
|
||||
@@ -175,6 +175,17 @@ const isReleasedWorkerRow = (entry: {
|
||||
entry.owned === false &&
|
||||
entry.is_active === false;
|
||||
|
||||
/** Format-owner facts for a refused database/target binding, before any operation replies exist. */
|
||||
class IdaIdentityRefusal extends AnalysisProtocolError {
|
||||
constructor(
|
||||
readonly reason: string,
|
||||
readonly recovery: string,
|
||||
check: string,
|
||||
) {
|
||||
super(reason, { diagnostics: { identity_check: check, reason, recovery } });
|
||||
}
|
||||
}
|
||||
|
||||
/** Serial read-only session whose lifecycle authority excludes an attached GUI. */
|
||||
export class IdaSessionClient implements AnalysisClient {
|
||||
#tail: Promise<void> = Promise.resolve();
|
||||
@@ -432,7 +443,14 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
context: IdaOperationFailureContext,
|
||||
) {
|
||||
let failure: AnalysisError;
|
||||
if (cause instanceof AnalysisCancelledError) {
|
||||
if (cause instanceof IdaIdentityRefusal) {
|
||||
failure = new AnalysisOutputError(operation, cause.reason, {
|
||||
cause,
|
||||
userMessage: cause.reason,
|
||||
remediationAction: cause.recovery,
|
||||
diagnostics: cause.diagnostics,
|
||||
});
|
||||
} else if (cause instanceof AnalysisCancelledError) {
|
||||
failure = new AnalysisCancelledError(operation, {
|
||||
cause:
|
||||
cause.cause === undefined ? options?.signal?.reason : cause.cause,
|
||||
@@ -680,12 +698,16 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
if (
|
||||
!sameIdaHostPath(opened.session.input_path, this.#workspace.inputPath)
|
||||
)
|
||||
throw new AnalysisProtocolError(
|
||||
throw new IdaIdentityRefusal(
|
||||
"IDA opened a different input than the private admitted target copy.",
|
||||
"Check the selected input and upstream worker binding. Retry only after retained private-worker cleanup is confirmed.",
|
||||
"opened_input",
|
||||
);
|
||||
if (opened.session.session_id !== this.#workspace.sessionId)
|
||||
throw new AnalysisProtocolError(
|
||||
throw new IdaIdentityRefusal(
|
||||
"IDA returned an adopted or unexpected session instead of the requested private worker.",
|
||||
"Use an IDA supervisor that honors the requested private worker session ID. Confirm retained cleanup before reopening the target.",
|
||||
"worker_session",
|
||||
);
|
||||
const inventory = databaseListSchema.parse(
|
||||
await this.connection.call("idb_list", {}),
|
||||
@@ -708,8 +730,10 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
entry.is_active !== true ||
|
||||
!sameIdaHostPath(entry.input_path, this.#workspace.inputPath)
|
||||
)
|
||||
throw new AnalysisProtocolError(
|
||||
throw new IdaIdentityRefusal(
|
||||
"IDA did not return an active owned headless worker for the private input copy.",
|
||||
"Configure the supervisor to return an active owned worker for the private input. Confirm retained cleanup before reopening the target.",
|
||||
"owned_worker_binding",
|
||||
);
|
||||
this.#database = opened.session.session_id;
|
||||
}
|
||||
@@ -742,8 +766,10 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
this.#metadata = raw;
|
||||
onObserved?.(raw);
|
||||
if (metadata.sha256.toLowerCase() !== this.target.sha256.toLowerCase())
|
||||
throw new AnalysisProtocolError(
|
||||
throw new IdaIdentityRefusal(
|
||||
`The attached IDA input SHA-256 does not match the selected target ${this.target.path}; open that target in IDA or use headless mode.`,
|
||||
"Open the selected original input in IDA, or configure headless mode and reopen the target through REA.",
|
||||
"attached_sha256",
|
||||
);
|
||||
identity = JSON.stringify([
|
||||
metadata.path,
|
||||
@@ -752,8 +778,10 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
]);
|
||||
} else {
|
||||
if (this.#database === undefined || this.#workspace === undefined)
|
||||
throw new AnalysisProtocolError(
|
||||
throw new IdaIdentityRefusal(
|
||||
"IDA headless session has no database binding.",
|
||||
"Close the REA analysis session and reopen the selected target using the configured headless supervisor.",
|
||||
"database_binding",
|
||||
);
|
||||
const raw = await readSettledHealth(this.connection, {
|
||||
database: this.#database,
|
||||
@@ -768,8 +796,10 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
(await idaFileDigest(this.#workspace.inputPath)) !==
|
||||
this.#workspace.inputSha256
|
||||
)
|
||||
throw new AnalysisProtocolError(
|
||||
throw new IdaIdentityRefusal(
|
||||
"IDA headless input identity changed after target admission.",
|
||||
"Close the REA analysis session, confirm private-worker cleanup, then reopen the selected original target.",
|
||||
"headless_input",
|
||||
);
|
||||
if (metadata.auto_analysis_ready !== true)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
@@ -798,8 +828,10 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
this.#initialIdentity !== undefined &&
|
||||
identity !== this.#initialIdentity
|
||||
)
|
||||
throw new AnalysisProtocolError(
|
||||
"The attached IDA target or image base changed during the REA session; reopen the target.",
|
||||
throw new IdaIdentityRefusal(
|
||||
"The IDA target or image base changed during the REA session; reopen the target.",
|
||||
"Close the REA analysis session and reopen the selected target after confirming the intended input and image base in IDA.",
|
||||
"session_identity",
|
||||
);
|
||||
this.#initialIdentity = identity;
|
||||
return this.#metadata;
|
||||
|
||||
Reference in new issue
Block a user