mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1f5b3eac9 | ||
|
|
e88f450c77 | ||
|
|
41472c9335 |
No files matched your search
@@ -76,6 +76,12 @@ export const prepareSessionExecution = (
|
||||
active.route.binding?.identity.id ?? active.route.identity.id,
|
||||
operation,
|
||||
capability?.reason ?? "operation is not declared by this provider",
|
||||
capability?.remediation === undefined
|
||||
? undefined
|
||||
: {
|
||||
userMessage: capability.reason,
|
||||
remediationAction: capability.remediation,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,6 +221,14 @@ export const EXISTING_PROJECT_WINDOWS_UNAVAILABLE =
|
||||
export const EXISTING_PROJECT_WINDOWS_REMEDIATION =
|
||||
"Unset REA_GHIDRA_PROJECT_PATH in the environment that starts REA to import targets into an ephemeral project, or open the existing project on a supported Linux or macOS host; restart REA after changing the environment.";
|
||||
|
||||
/** Existing-project observations cannot be edited through REA. */
|
||||
export const EXISTING_PROJECT_READ_ONLY =
|
||||
"Existing Ghidra project sessions are read-only.";
|
||||
|
||||
/** An editable fresh import is a separate analysis, not the saved project. */
|
||||
export const EXISTING_PROJECT_READ_ONLY_REMEDIATION =
|
||||
"To keep the saved project analysis, continue read-only; REA cannot edit it. To use a separate editable Ghidra session, unset REA_GHIDRA_PROJECT_PATH and REA_GHIDRA_PROGRAM_NAME in the environment that starts REA, restart REA, and reopen the target. A fresh import is ephemeral and does not carry the selected project's saved annotations.";
|
||||
|
||||
/** Read-only project mode retains precise unavailable mutation capabilities. */
|
||||
export const existingProjectCapabilities = (
|
||||
platform: NodeJS.Platform,
|
||||
@@ -246,8 +254,9 @@ export const existingProjectCapabilities = (
|
||||
...capability,
|
||||
limitations,
|
||||
available: false,
|
||||
reason: "Existing Ghidra project sessions are read-only.",
|
||||
reason: EXISTING_PROJECT_READ_ONLY,
|
||||
availabilityCode: "open_options_invalid",
|
||||
remediation: EXISTING_PROJECT_READ_ONLY_REMEDIATION,
|
||||
};
|
||||
return { ...capability, limitations };
|
||||
});
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
inferredUnavailabilityReason,
|
||||
runtimeFailureReason,
|
||||
} from "../../../src/application/binary/SessionRuntimeAvailability.js";
|
||||
import {
|
||||
EXISTING_PROJECT_READ_ONLY,
|
||||
EXISTING_PROJECT_READ_ONLY_REMEDIATION,
|
||||
existingProjectCapabilities,
|
||||
} from "../../../src/ghidra/GhidraProviderCapabilities.js";
|
||||
import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
||||
import { err, ok, type Result } from "../../../src/domain/result.js";
|
||||
@@ -438,6 +443,7 @@ const connectScripted = async (options: {
|
||||
readonly respondAuxiliary?: { current: Responder };
|
||||
/** Operations the fixture declares unavailable, with their declared reason. */
|
||||
readonly declaredRefusals?: Readonly<Record<string, string>>;
|
||||
readonly declaredRemediation?: Readonly<Record<string, string>>;
|
||||
readonly families?: string;
|
||||
}) => {
|
||||
const [path] = await createBinarySessionTargets();
|
||||
@@ -448,13 +454,17 @@ const connectScripted = async (options: {
|
||||
ok({ profile: createAnalysisProfile(FIXTURE, { fixture: true }) }),
|
||||
),
|
||||
capabilities: () =>
|
||||
options.operations.map((operation) =>
|
||||
liveCapability(
|
||||
options.operations.map((operation) => {
|
||||
const descriptor = liveCapability(
|
||||
FIXTURE,
|
||||
operation,
|
||||
options.declaredRefusals?.[operation],
|
||||
),
|
||||
),
|
||||
);
|
||||
const remediation = options.declaredRemediation?.[operation];
|
||||
return descriptor.available || remediation === undefined
|
||||
? descriptor
|
||||
: { ...descriptor, remediation };
|
||||
}),
|
||||
createClient: () => ({
|
||||
execute: (operation) =>
|
||||
operation === "health"
|
||||
@@ -532,6 +542,84 @@ const DIRECT_OPERATIONS = [
|
||||
const ADDRESS = { address: "0x1000", document: "fixture" };
|
||||
const PROCEDURE = { procedure: "main" };
|
||||
|
||||
it("delivers a declared read-only recovery without changing the selected session", async () => {
|
||||
const calls: string[] = [];
|
||||
const descriptor = existingProjectCapabilities("darwin").find(
|
||||
(capability) => capability.operation === "set_address_name",
|
||||
);
|
||||
if (
|
||||
descriptor === undefined ||
|
||||
descriptor.available ||
|
||||
descriptor.remediation === undefined
|
||||
)
|
||||
throw new Error("Expected Ghidra's read-only descriptor and recovery");
|
||||
const { client } = await connectScripted({
|
||||
operations: ["address_name", "set_address_name"],
|
||||
declaredRefusals: { set_address_name: descriptor.reason },
|
||||
declaredRemediation: {
|
||||
set_address_name: descriptor.remediation,
|
||||
},
|
||||
respond: {
|
||||
current: (operation) => {
|
||||
calls.push(operation);
|
||||
return Promise.resolve(observed("original"));
|
||||
},
|
||||
},
|
||||
});
|
||||
const before = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
const refused = await client.callTool({
|
||||
name: "set_address_name",
|
||||
arguments: { address: "0x1000", name: "new_name" },
|
||||
});
|
||||
const error = parseMcpToolError(refused).error;
|
||||
expect(error.code).toBe("capability_unavailable");
|
||||
expect(error.details?.reason).toBe(EXISTING_PROJECT_READ_ONLY);
|
||||
expect(error.message).toBe(EXISTING_PROJECT_READ_ONLY);
|
||||
expect(error.remediation.action).toBe(EXISTING_PROJECT_READ_ONLY_REMEDIATION);
|
||||
expect(error.remediation.action).toContain("REA_GHIDRA_PROJECT_PATH");
|
||||
expect(error.remediation.action).toContain("REA_GHIDRA_PROGRAM_NAME");
|
||||
expect(calls).not.toContain("set_address_name");
|
||||
const after = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
const state = (reply: typeof before) =>
|
||||
toolContract("binary_session").outputSchema.parse(reply.structuredContent)
|
||||
.result.analysis_run;
|
||||
expect(state(before)).toBeDefined();
|
||||
expect(state(after)).toEqual(state(before));
|
||||
});
|
||||
|
||||
it("preserves another provider's declared recovery without interpreting its wording", async () => {
|
||||
const reason = "Fixture capability requires an explicit workspace.";
|
||||
const remediation = "Set FIXTURE_WORKSPACE to a caller-selected directory.";
|
||||
const calls: string[] = [];
|
||||
const { client } = await connectScripted({
|
||||
operations: ["set_address_name"],
|
||||
declaredRefusals: { set_address_name: reason },
|
||||
declaredRemediation: { set_address_name: remediation },
|
||||
respond: {
|
||||
current: (operation) => {
|
||||
calls.push(operation);
|
||||
return Promise.resolve(observed("unexpected"));
|
||||
},
|
||||
},
|
||||
});
|
||||
const reply = await client.callTool({
|
||||
name: "set_address_name",
|
||||
arguments: { address: "0x1000", name: "new_name" },
|
||||
});
|
||||
const error = parseMcpToolError(reply).error;
|
||||
expect(error.code).toBe("capability_unavailable");
|
||||
expect(error.message).toBe(reason);
|
||||
expect(error.details?.reason).toBe(reason);
|
||||
expect(error.remediation.action).toBe(remediation);
|
||||
expect(calls).not.toContain("set_address_name");
|
||||
});
|
||||
|
||||
it("returns provider-inferred siblings to declared availability after one success", async () => {
|
||||
let broken = false;
|
||||
const respond = {
|
||||
|
||||
Reference in new issue
Block a user