refactor(provider): declare deep capabilities from source (#892)
This commit is contained in:
parent
4037c6545d
commit
b808e2d6bb
|
|
@ -225,6 +225,27 @@ explicit downloader and fixed manifest belong to `scripts/fixtures/android/`.
|
|||
JADX CLI/MCP parity requires the fixed APK and existing audited JAR; synthetic
|
||||
protocol/cancellation success does not establish that engine or an unverified host.
|
||||
|
||||
## Deep-provider source declarations
|
||||
|
||||
Hopper, Ghidra and IDA declare their implemented analyst operations in their
|
||||
own `*ProviderCapabilities.ts` modules. Hopper's inventory is typed against the
|
||||
existing direct-operation contracts; Ghidra reuses its inventory/function adapter
|
||||
operations; IDA retains its read-only operation family. Frozen operation arrays
|
||||
feed the existing capability builders without importing generated catalog data.
|
||||
|
||||
The catalog generator reads these source declarations and joins operation names
|
||||
to canonical `TOOL_CONTRACTS`. A missing contract identifies the declaring provider
|
||||
and operation. Public schemas and descriptions remain owned by contracts; the
|
||||
declarations describe implementation coverage. Provider adapters must not consume
|
||||
the generated MCP catalog that is derived from them. The resolved module guard
|
||||
checks this edge while admitting canonical source contracts, generated package
|
||||
metadata and the existing application snapshot-cache consumer.
|
||||
|
||||
Exact provider effects and lifecycle differences remain in their adapters:
|
||||
Hopper's GUI and mutation effects, Ghidra's experimental Windows authority and
|
||||
mutation restrictions, and IDA's attached/headless effects and live observations.
|
||||
This source increment introduces no common provider lifecycle or new interface.
|
||||
|
||||
## Binary application ownership
|
||||
|
||||
`src/application/binary/` owns the active session, deep-provider registry and
|
||||
|
|
|
|||
|
|
@ -96,6 +96,21 @@ export const toolFamilyCatalog = (sources) => {
|
|||
|
||||
/** Build provider identities with their sorted capability names. */
|
||||
export const providerCatalog = (sources) => {
|
||||
const contractsByName = new Map(
|
||||
sources.toolContracts.TOOL_CONTRACTS.map((contract) => [
|
||||
contract.name,
|
||||
contract,
|
||||
]),
|
||||
);
|
||||
const declaredContracts = (providerId, operations) =>
|
||||
operations.map((operation) => {
|
||||
const contract = contractsByName.get(operation);
|
||||
if (contract === undefined)
|
||||
throw new TypeError(
|
||||
`Provider ${providerId} declares an unknown analyst operation: ${operation}`,
|
||||
);
|
||||
return contract;
|
||||
});
|
||||
const {
|
||||
applicationContracts,
|
||||
reconciliationContracts,
|
||||
|
|
@ -105,15 +120,21 @@ export const providerCatalog = (sources) => {
|
|||
return [
|
||||
{
|
||||
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
|
||||
contracts: sources.hopperProvider.HOPPER_PROVIDER_TOOL_CONTRACTS,
|
||||
contracts: declaredContracts(
|
||||
"hopper",
|
||||
sources.hopperProvider.HOPPER_OPERATIONS,
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY,
|
||||
contracts: sources.ghidraProvider.GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
||||
contracts: declaredContracts(
|
||||
"ghidra",
|
||||
sources.ghidraProvider.GHIDRA_OPERATIONS,
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.idaProvider.IDA_PROVIDER_IDENTITY,
|
||||
contracts: sources.idaProvider.IDA_PROVIDER_TOOL_CONTRACTS,
|
||||
contracts: declaredContracts("ida", sources.idaProvider.IDA_OPERATIONS),
|
||||
},
|
||||
{
|
||||
identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ const SOURCE_PATHS = {
|
|||
"dist/contracts/javascriptRecoveryToolContracts.js",
|
||||
javascriptRecoveryProvider: "dist/javascript/recovery/WakaruRelease.js",
|
||||
supportedClients: "dist/application/SupportedClients.js",
|
||||
hopperProvider: "dist/hopper/HopperProvider.js",
|
||||
ghidraProvider: "dist/ghidra/GhidraProvider.js",
|
||||
hopperProvider: "dist/hopper/HopperProviderCapabilities.js",
|
||||
ghidraProvider: "dist/ghidra/GhidraProviderCapabilities.js",
|
||||
idaProvider: "dist/ida/IdaProviderCapabilities.js",
|
||||
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
|
||||
artifactProviders: "dist/application/InvestigationProviders.js",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ export interface ModuleBoundaryViolation {
|
|||
readonly boundary:
|
||||
| "pure-layer"
|
||||
| "application-composition"
|
||||
| "provider-construction";
|
||||
| "provider-construction"
|
||||
| "provider-generated-catalog";
|
||||
}
|
||||
|
||||
/** Check resolved source ownership, including type imports and reexports. */
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
|||
"inspector",
|
||||
"javascript",
|
||||
]);
|
||||
const PROVIDER_ROOTS = new Set([
|
||||
...MIGRATED_PROVIDER_ROOTS,
|
||||
"artifacts",
|
||||
"browser",
|
||||
"dotnet",
|
||||
"native",
|
||||
]);
|
||||
|
||||
const repositoryPath = (root, path) =>
|
||||
relative(root, path).replaceAll("\\", "/");
|
||||
|
|
@ -53,6 +60,11 @@ const failedBoundary = (file, target) => {
|
|||
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
|
||||
const layer = file.split("/")[1];
|
||||
const targetLayer = target.split("/")[1];
|
||||
if (
|
||||
PROVIDER_ROOTS.has(layer) &&
|
||||
/^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target)
|
||||
)
|
||||
return "provider-generated-catalog";
|
||||
if (PURE_LAYERS.has(layer)) {
|
||||
if (
|
||||
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ import { JADX_PROVIDER_IDENTITY } from "../../../src/android/JadxRelease.js";
|
|||
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
|
||||
import {
|
||||
HOPPER_PROVIDER_IDENTITY,
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS,
|
||||
HOPPER_OPERATIONS,
|
||||
} from "../../../src/hopper/HopperProvider.js";
|
||||
import {
|
||||
GHIDRA_PROVIDER_IDENTITY,
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
||||
GHIDRA_OPERATIONS,
|
||||
} from "../../../src/ghidra/GhidraProvider.js";
|
||||
import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../../../src/native/NativeMacOSProvider.js";
|
||||
import { IDA_PROVIDER_IDENTITY } from "../../../src/ida/IdaProvider.js";
|
||||
|
|
@ -130,11 +130,11 @@ describe("canonical product catalog", () => {
|
|||
expect(
|
||||
catalog.providers.find(({ id }) => id === HOPPER_PROVIDER_IDENTITY.id)
|
||||
?.capabilities,
|
||||
).toEqual(HOPPER_PROVIDER_TOOL_CONTRACTS.map(({ name }) => name).sort());
|
||||
).toEqual([...HOPPER_OPERATIONS].sort());
|
||||
expect(
|
||||
catalog.providers.find(({ id }) => id === GHIDRA_PROVIDER_IDENTITY.id)
|
||||
?.capabilities,
|
||||
).toEqual(GHIDRA_PROVIDER_TOOL_CONTRACTS.map(({ name }) => name).sort());
|
||||
).toEqual([...GHIDRA_OPERATIONS].sort());
|
||||
expect(
|
||||
catalog.providers.find(
|
||||
({ id }) => id === CDP_ELECTRON_PROVIDER_IDENTITY.id,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||
|
||||
import { HOPPER_PROVIDER_TOOL_CONTRACTS } from "../../../../src/hopper/HopperProvider.js";
|
||||
import { HOPPER_OPERATIONS } from "../../../../src/hopper/HopperProvider.js";
|
||||
import { HopperClient } from "../../../../src/hopper/HopperClient.js";
|
||||
import type { HopperDiagnostic } from "../../../../src/hopper/HopperDiagnostics.js";
|
||||
import { providerCleanupFailure } from "../../../../src/hopper/HopperDiagnostics.js";
|
||||
|
|
@ -74,11 +74,9 @@ describe("HopperClient protocol", () => {
|
|||
it("routes every established operation through the authenticated bridge", async () => {
|
||||
const client = await startClient();
|
||||
const results = await Promise.all(
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS.map(({ name }) =>
|
||||
client.callTool(name, {}),
|
||||
),
|
||||
HOPPER_OPERATIONS.map((name) => client.callTool(name, {})),
|
||||
);
|
||||
expect(results).toHaveLength(HOPPER_PROVIDER_TOOL_CONTRACTS.length);
|
||||
expect(results).toHaveLength(HOPPER_OPERATIONS.length);
|
||||
expect(results.every((result) => result.ok)).toBe(true);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,18 @@ describe("incremental module import boundaries", () => {
|
|||
["src/domain/probe.test.ts", "../dotnet/ManagedMemberInspector.js", false],
|
||||
["src/server/probe.ts", "../composition/android.js", true],
|
||||
["src/application/probe.ts", "../browser/CdpCaptureValues.js", true],
|
||||
["src/hopper/probe.ts", "../generatedMcpToolCatalog.js", false],
|
||||
["src/ghidra/probe.ts", "../generatedMcpToolCatalog.ts", false],
|
||||
["src/ida/probe.ts", "../generatedMcpToolCatalog.js", false],
|
||||
["src/artifacts/probe.ts", "../generatedMcpToolCatalog.js", false],
|
||||
["src/browser/probe.ts", "../generatedMcpToolCatalog.js", false],
|
||||
["src/ghidra/probe.ts", "../generatedPackageMetadata.js", true],
|
||||
["src/hopper/probe.ts", "../contracts/officialToolContracts.js", true],
|
||||
[
|
||||
"src/application/binary/probe.ts",
|
||||
"../../generatedMcpToolCatalog.js",
|
||||
true,
|
||||
],
|
||||
])("checks %s importing %s", async (file, dependency, allowed) => {
|
||||
const temporary = await createTestTempDirectory("rea-module-boundary-");
|
||||
const fixture = join(temporary, file);
|
||||
|
|
|
|||
Loading…
Reference in New Issue