diff --git a/docs/capability-migration.md b/docs/capability-migration.md index 99fbfeb5e..0ef5109a3 100644 --- a/docs/capability-migration.md +++ b/docs/capability-migration.md @@ -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 diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index 4a70f8471..dfa69c07b 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -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, diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs index 3e719e600..91e357241 100644 --- a/scripts/lib/catalog-core.mjs +++ b/scripts/lib/catalog-core.mjs @@ -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", diff --git a/scripts/lib/module-boundaries.d.mts b/scripts/lib/module-boundaries.d.mts index 79f4b972b..abf8daf9b 100644 --- a/scripts/lib/module-boundaries.d.mts +++ b/scripts/lib/module-boundaries.d.mts @@ -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. */ diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index 64967db5b..81bfce577 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -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)) || diff --git a/src/ghidra/GhidraAnnotations.test.ts b/src/ghidra/GhidraAnnotations.test.ts index 4c2f61571..4107d746a 100644 --- a/src/ghidra/GhidraAnnotations.test.ts +++ b/src/ghidra/GhidraAnnotations.test.ts @@ -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"); }); }); diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 3c05e93fe..7bfddef40 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -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({ diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index 84a7c6cd8..17ef5cf20 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -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"]); diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts index 76887fdff..0bec2afa0 100644 --- a/src/ghidra/GhidraProviderCapabilities.ts +++ b/src/ghidra/GhidraProviderCapabilities.ts @@ -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)), }); }), ); diff --git a/src/hopper/HopperProvider.test.ts b/src/hopper/HopperProvider.test.ts index 24e822ad9..2fdb8e06e 100644 --- a/src/hopper/HopperProvider.test.ts +++ b/src/hopper/HopperProvider.test.ts @@ -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, ); diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts index 95eb829c8..96a71a345 100644 --- a/src/hopper/HopperProvider.ts +++ b/src/hopper/HopperProvider.ts @@ -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 { diff --git a/src/hopper/HopperProviderCapabilities.ts b/src/hopper/HopperProviderCapabilities.ts new file mode 100644 index 000000000..d42f2c7a9 --- /dev/null +++ b/src/hopper/HopperProviderCapabilities.ts @@ -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([ + "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.", + ]), + }), + ), +); diff --git a/src/ida/IdaProviderCapabilities.test.ts b/src/ida/IdaProviderCapabilities.test.ts new file mode 100644 index 000000000..cc36a62b3 --- /dev/null +++ b/src/ida/IdaProviderCapabilities.test.ts @@ -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 }); +}); diff --git a/src/ida/IdaProviderCapabilities.ts b/src/ida/IdaProviderCapabilities.ts index 3a2694789..9d8e67499 100644 --- a/src/ida/IdaProviderCapabilities.ts +++ b/src/ida/IdaProviderCapabilities.ts @@ -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[]); 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" diff --git a/tests/boundary/cli/productCatalog.test.ts b/tests/boundary/cli/productCatalog.test.ts index d98127880..f3a483ecf 100644 --- a/tests/boundary/cli/productCatalog.test.ts +++ b/tests/boundary/cli/productCatalog.test.ts @@ -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, diff --git a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts index 19bf37603..70bce0078 100644 --- a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts @@ -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); }); diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index 31fb07b01..abf88802f 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -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);