mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 15:26:25 +00:00
refactor(provider): declare deep capabilities from source (#892)
This commit is contained in:
1 parent
4037c6545d
commit
b808e2d6bb
17 files changed
+220
-133
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
CAPABILITIES,
|
||||
windowsP0Capabilities,
|
||||
} from "./GhidraProviderCapabilities.js";
|
||||
import { HOPPER_PROVIDER_TOOL_CONTRACTS } from "../hopper/HopperProvider.js";
|
||||
import { HOPPER_OPERATIONS } from "../hopper/HopperProvider.js";
|
||||
|
||||
const result = () => {
|
||||
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
|
||||
@@ -105,10 +105,6 @@ describe("Ghidra function annotations", () => {
|
||||
(c) => c.operation === "annotate_native_function",
|
||||
),
|
||||
).toMatchObject({ available: false });
|
||||
expect(
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS.some(
|
||||
(c) => c.name === "annotate_native_function",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(HOPPER_OPERATIONS).not.toContain("annotate_native_function");
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
||||
import {
|
||||
GHIDRA_PROVIDER_IDENTITY,
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
||||
GHIDRA_OPERATIONS,
|
||||
GhidraProvider,
|
||||
type GhidraProviderClientFactory,
|
||||
} from "./GhidraProvider.js";
|
||||
@@ -140,9 +140,9 @@ describe("Ghidra provider", () => {
|
||||
const ghidra = provider(host);
|
||||
|
||||
expect(ghidra.identity()).toEqual(GHIDRA_PROVIDER_IDENTITY);
|
||||
expect(ghidra.capabilities().map(({ operation }) => operation)).toEqual(
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
expect(ghidra.capabilities().map(({ operation }) => operation)).toEqual([
|
||||
...GHIDRA_OPERATIONS,
|
||||
]);
|
||||
expect(ghidra.capabilities()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
CAPABILITIES,
|
||||
windowsP0Capabilities,
|
||||
GHIDRA_PROVIDER_IDENTITY,
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
||||
GHIDRA_OPERATIONS,
|
||||
} from "./GhidraProviderCapabilities.js";
|
||||
import {
|
||||
createGhidraProviderClient,
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
windowsNativeCapabilities,
|
||||
} from "../process/WindowsAuthority.js";
|
||||
|
||||
export { GHIDRA_PROVIDER_IDENTITY, GHIDRA_PROVIDER_TOOL_CONTRACTS };
|
||||
export { GHIDRA_PROVIDER_IDENTITY, GHIDRA_OPERATIONS };
|
||||
export type { GhidraProviderClientFactory };
|
||||
|
||||
const SUPPORTED_ARCHITECTURES = new Set(["x86", "x86_64", "arm", "arm64"]);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
hasWindowsNativeAuthority,
|
||||
windowsNativeAuthorityUnavailableReason,
|
||||
} from "../process/WindowsAuthority.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
|
||||
import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
|
||||
|
||||
@@ -17,25 +16,11 @@ export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
version: null,
|
||||
});
|
||||
|
||||
const providerContractByName = new Map(
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ kind }) => kind === "official-proxy" || kind === "enhanced",
|
||||
).map((contract) => [contract.name, contract]),
|
||||
);
|
||||
|
||||
/** Provider-neutral contracts implemented by the Ghidra adapter. */
|
||||
export const GHIDRA_PROVIDER_TOOL_CONTRACTS = Object.freeze(
|
||||
[...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].map(
|
||||
(operation) => {
|
||||
const contract = providerContractByName.get(operation);
|
||||
if (contract === undefined)
|
||||
throw new TypeError(
|
||||
`Missing provider-neutral contract for ${operation}`,
|
||||
);
|
||||
return contract;
|
||||
},
|
||||
),
|
||||
);
|
||||
/** Analyst operations implemented by the Ghidra inventory and function adapters. */
|
||||
export const GHIDRA_OPERATIONS = Object.freeze([
|
||||
...GHIDRA_INVENTORY_OPERATIONS,
|
||||
...GHIDRA_FUNCTION_OPERATIONS,
|
||||
]);
|
||||
|
||||
/** Health limitations shared by every Ghidra-backed capability. */
|
||||
export const healthLimitations = Object.freeze([
|
||||
@@ -157,10 +142,7 @@ export const limitationsFor = (operation: string): readonly string[] => {
|
||||
|
||||
/** Provider-neutral capabilities advertised by every non-Windows Ghidra session. */
|
||||
export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) => {
|
||||
const operation = contract.analysisOperation;
|
||||
if (operation === null)
|
||||
throw new TypeError(`Missing analysis operation for ${contract.name}`);
|
||||
GHIDRA_OPERATIONS.map((operation) => {
|
||||
return Object.freeze({
|
||||
provider: GHIDRA_PROVIDER_IDENTITY,
|
||||
operation,
|
||||
@@ -175,7 +157,7 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: Object.freeze(limitationsFor(contract.name)),
|
||||
limitations: Object.freeze(limitationsFor(operation)),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2,10 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseConfig } from "../config.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import {
|
||||
HopperProvider,
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS,
|
||||
} from "./HopperProvider.js";
|
||||
import { HopperProvider, HOPPER_OPERATIONS } from "./HopperProvider.js";
|
||||
|
||||
describe("Hopper provider capabilities", () => {
|
||||
it("declines DOS MZ targets with a provider-specific support reason", () => {
|
||||
@@ -35,9 +32,9 @@ describe("Hopper provider capabilities", () => {
|
||||
const provider = new HopperProvider(config.value, silentLogger);
|
||||
const capabilities = provider.capabilities();
|
||||
const published = structuredClone(capabilities);
|
||||
expect(capabilities.map(({ operation }) => operation)).toEqual(
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
expect(capabilities.map(({ operation }) => operation)).toEqual([
|
||||
...HOPPER_OPERATIONS,
|
||||
]);
|
||||
expect(new Set(capabilities.map(({ operation }) => operation)).size).toBe(
|
||||
capabilities.length,
|
||||
);
|
||||
|
||||
@@ -18,7 +18,6 @@ import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import { HopperApplicationLauncher } from "./BridgeLauncher.js";
|
||||
import {
|
||||
hopperLoaderArgsForTarget,
|
||||
@@ -26,63 +25,17 @@ import {
|
||||
} from "./HopperAnalysisProfile.js";
|
||||
import { HopperClient } from "./HopperClient.js";
|
||||
|
||||
/** Public identity committed by every Hopper-backed observation. */
|
||||
export const HOPPER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "hopper",
|
||||
name: "Hopper Disassembler",
|
||||
version: null,
|
||||
});
|
||||
import {
|
||||
CAPABILITIES,
|
||||
HOPPER_PROVIDER_IDENTITY,
|
||||
} from "./HopperProviderCapabilities.js";
|
||||
|
||||
export {
|
||||
HOPPER_PROVIDER_IDENTITY,
|
||||
HOPPER_OPERATIONS,
|
||||
} from "./HopperProviderCapabilities.js";
|
||||
|
||||
const IDENTITY = HOPPER_PROVIDER_IDENTITY;
|
||||
const MUTATING_OPERATIONS = new Set([
|
||||
"set_address_name",
|
||||
"set_addresses_names",
|
||||
"set_bookmark",
|
||||
"set_comment",
|
||||
"set_inline_comment",
|
||||
"unset_bookmark",
|
||||
]);
|
||||
|
||||
/** Tool contracts implemented directly by the Hopper adapter. */
|
||||
export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
|
||||
...GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ kind, name }) =>
|
||||
kind === "official-proxy" &&
|
||||
name !== "annotate_native_function" &&
|
||||
name !== "inspect_native_load_image" &&
|
||||
name !== "inspect_native_data_type" &&
|
||||
name !== "inspect_native_instruction" &&
|
||||
name !== "resolve_native_call_targets",
|
||||
),
|
||||
...GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ name }) => name === "analyze_function",
|
||||
),
|
||||
]);
|
||||
|
||||
const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => {
|
||||
const operation = contract.analysisOperation;
|
||||
if (operation === null)
|
||||
throw new TypeError(`Missing analysis operation for ${contract.name}`);
|
||||
return Object.freeze({
|
||||
provider: IDENTITY,
|
||||
operation,
|
||||
available: true,
|
||||
reason: null,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
|
||||
launchesProcess: true,
|
||||
mayShowUi: true,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem: MUTATING_OPERATIONS.has(contract.name),
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"Results depend on Hopper's completed static analysis.",
|
||||
]),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
/** Concrete analysis provider backed by REA's private Hopper bridge. */
|
||||
export class HopperProvider implements AnalysisProviderCandidate {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import type {
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
|
||||
|
||||
/** Public identity committed by every Hopper-backed observation. */
|
||||
export const HOPPER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "hopper",
|
||||
name: "Hopper Disassembler",
|
||||
version: null,
|
||||
});
|
||||
|
||||
/** Analyst operations implemented by REA's private Hopper bridge. */
|
||||
export const HOPPER_OPERATIONS = Object.freeze([
|
||||
"address_name",
|
||||
"comment",
|
||||
"current_address",
|
||||
"current_procedure",
|
||||
"current_document",
|
||||
"goto_address",
|
||||
"inline_comment",
|
||||
"list_bookmarks",
|
||||
"list_documents",
|
||||
"list_names",
|
||||
"list_procedures",
|
||||
"list_segments",
|
||||
"list_strings",
|
||||
"next_address",
|
||||
"prev_address",
|
||||
"procedure_address",
|
||||
"procedure_assembly",
|
||||
"procedure_callees",
|
||||
"procedure_callers",
|
||||
"procedure_info",
|
||||
"read_function_instructions",
|
||||
"read_bytes",
|
||||
"address_to_file_offset",
|
||||
"procedure_references",
|
||||
"procedure_pseudo_code",
|
||||
"resolve_containing_procedure",
|
||||
"search_procedures",
|
||||
"search_strings",
|
||||
"set_address_name",
|
||||
"set_addresses_names",
|
||||
"set_bookmark",
|
||||
"set_comment",
|
||||
"set_current_document",
|
||||
"set_inline_comment",
|
||||
"unset_bookmark",
|
||||
"xrefs",
|
||||
"analyze_function",
|
||||
] as const satisfies readonly (OfficialToolName | "analyze_function")[]);
|
||||
|
||||
const MUTATING_OPERATIONS = new Set<string>([
|
||||
"set_address_name",
|
||||
"set_addresses_names",
|
||||
"set_bookmark",
|
||||
"set_comment",
|
||||
"set_inline_comment",
|
||||
"unset_bookmark",
|
||||
]);
|
||||
|
||||
/** Source-owned capabilities without acquiring or probing a Hopper session. */
|
||||
export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
HOPPER_OPERATIONS.map((operation) =>
|
||||
Object.freeze({
|
||||
provider: HOPPER_PROVIDER_IDENTITY,
|
||||
operation,
|
||||
available: true,
|
||||
reason: null,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: MUTATING_OPERATIONS.has(operation),
|
||||
launchesProcess: true,
|
||||
mayShowUi: true,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem: MUTATING_OPERATIONS.has(operation),
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"Results depend on Hopper's completed static analysis.",
|
||||
]),
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { IDA_OPERATIONS, idaCapabilities } from "./IdaProviderCapabilities.js";
|
||||
|
||||
it("preserves live read-only capabilities when callers attempt to alter declarations", () => {
|
||||
const original = idaCapabilities("headless", true);
|
||||
Reflect.set(IDA_OPERATIONS, 0, "set_comment");
|
||||
Reflect.set(IDA_OPERATIONS, IDA_OPERATIONS.length, "set_address_name");
|
||||
|
||||
expect(idaCapabilities("headless", true)).toEqual(original);
|
||||
expect(original.every(({ cachePolicy }) => cachePolicy === "live")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(original.every(({ effects }) => !effects.mutatesArtifact)).toBe(true);
|
||||
expect(
|
||||
original.find(({ operation }) => operation === "procedure_callers"),
|
||||
).toMatchObject({ available: false });
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import type {
|
||||
AnalysisOperation,
|
||||
CapabilityDescriptor,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
|
||||
/** This version identifies REA's adapter, not IDA or the upstream Python package. */
|
||||
export const IDA_PROVIDER_IDENTITY = Object.freeze({
|
||||
@@ -9,7 +11,7 @@ export const IDA_PROVIDER_IDENTITY = Object.freeze({
|
||||
});
|
||||
|
||||
/** Read-only analyst outcomes supported by both upstream compatibility profiles. */
|
||||
export const IDA_OPERATIONS = [
|
||||
export const IDA_OPERATIONS = Object.freeze([
|
||||
"list_procedures",
|
||||
"search_procedures",
|
||||
"list_strings",
|
||||
@@ -22,7 +24,7 @@ export const IDA_OPERATIONS = [
|
||||
"read_function_instructions",
|
||||
"xrefs",
|
||||
"analyze_function",
|
||||
] as const;
|
||||
] as const satisfies readonly Exclude<AnalysisOperation, "health">[]);
|
||||
export type IdaOperation = (typeof IDA_OPERATIONS)[number];
|
||||
|
||||
/** Shared limitations of observations from an externally mutable analysis database. */
|
||||
@@ -35,24 +37,12 @@ export const IDA_LIMITATIONS = Object.freeze([
|
||||
"Complete function body ranges, typed references, unresolved indirect calls, referenced data, and basic blocks are unavailable in this adapter. Empty unsupported dossier facets are unknown, not observed absence.",
|
||||
]);
|
||||
|
||||
/** Existing REA contracts implemented by the adapter; no upstream tools are exposed directly. */
|
||||
export const IDA_PROVIDER_TOOL_CONTRACTS = Object.freeze(
|
||||
IDA_OPERATIONS.map((operation) => {
|
||||
const contract = GENERATED_MCP_TOOL_CATALOG.find(
|
||||
({ name }) => name === operation,
|
||||
);
|
||||
if (contract === undefined)
|
||||
throw new TypeError(`Missing contract for ${operation}`);
|
||||
return { ...contract, name: operation };
|
||||
}),
|
||||
);
|
||||
|
||||
/** Advertise live observations and configured lifecycle effects truthfully. */
|
||||
export const idaCapabilities = (
|
||||
mode: "attached" | "headless",
|
||||
stdio: boolean,
|
||||
): readonly CapabilityDescriptor[] =>
|
||||
IDA_PROVIDER_TOOL_CONTRACTS.map(({ name: operation }) => ({
|
||||
IDA_OPERATIONS.map((operation) => ({
|
||||
provider: IDA_PROVIDER_IDENTITY,
|
||||
operation,
|
||||
...(mode === "headless" && operation === "procedure_callers"
|
||||
|
||||
Reference in new issue
Block a user