diff --git a/docs/adr/0001-provider-selection-and-analysis-profiles.md b/docs/adr/0001-provider-selection-and-analysis-profiles.md index dfcc7f86a..07eaa659e 100644 --- a/docs/adr/0001-provider-selection-and-analysis-profiles.md +++ b/docs/adr/0001-provider-selection-and-analysis-profiles.md @@ -30,7 +30,7 @@ analyzer, or provider-version differences while appearing target-compatible. The decision addressed the earlier representations of [`CompositeProvider`](https://github.com/morluto/rea/blob/main/src/application/binary/CompositeProvider.ts), -[runtime composition](https://github.com/morluto/rea/blob/main/src/application/runtime.ts), +[runtime composition](https://github.com/morluto/rea/blob/main/src/composition/binary.ts), [`BinarySession`](https://github.com/morluto/rea/blob/main/src/application/binary/BinarySession.ts), [`BinaryTarget`](https://github.com/morluto/rea/blob/main/src/domain/binaryTarget.ts), [analysis snapshot](https://github.com/morluto/rea/blob/main/src/domain/analysisSnapshot.ts), and diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid index d3430f38e..f4835fdb2 100644 --- a/docs/architecture.mermaid +++ b/docs/architecture.mermaid @@ -5,6 +5,10 @@ graph TD DISPATCH["scripts/rea.mjs
Executable dispatcher
routes mcp / one-shot"] end + subgraph "Production Composition" + COMPOSITION["src/composition/
fresh typed provider and session factories
one-shot binding and independent optional loading"] + end + subgraph "Adapters" SERVER["src/server/
MCP request translation
createServer, tool registration, toolResult"] HOPPER_BRIDGE["bridge/hopper_bridge.py
Runs inside Hopper
adapts operations to Hopper Python API"] @@ -18,7 +22,7 @@ graph TD REGISTRY["AnalysisProviderRegistry
sorted deep candidates
deterministic selection, no fallback"] RECORDS["src/application/investigation/
composed InvestigationRecords + narrow ports
EvidenceLedger and Unknown ownership"] SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache
binary target/profile snapshots
compatibility facade and post-commit notifications"] - JS_RECON["src/application/ + src/artifacts/
JavaScript artifact reconstruction
directory/ASAR reads
AST-only bundle projection"] + JS_RECON["src/application/javascript/ + src/artifacts/javascript/
JavaScript artifact reconstruction
directory/ASAR reads
AST-only bundle projection"] end subgraph "Provider Layers" @@ -69,6 +73,10 @@ graph TD MAIN --> SERVER MAIN --> APP CLI --> APP + MAIN --> COMPOSITION + CLI --> COMPOSITION + SERVER --> COMPOSITION + COMPOSITION --> SESSION APP --> FIRMWARE FIRMWARE --> PROCESS FIRMWARE --> CONTRACTS @@ -84,9 +92,10 @@ graph TD APP -->|record callbacks| RECORDS SERVER -->|narrow record ports| RECORDS RECORDS --> DOMAIN - REGISTRY --> HOPPER - REGISTRY --> GHIDRA - REGISTRY --> IDA + COMPOSITION --> HOPPER + COMPOSITION --> GHIDRA + COMPOSITION --> IDA + COMPOSITION --> AUX IDA --> IDA_MCP IDA --> CONTRACTS IDA --> DOMAIN diff --git a/docs/capability-migration.md b/docs/capability-migration.md index 3789591d6..9363211f6 100644 --- a/docs/capability-migration.md +++ b/docs/capability-migration.md @@ -6,17 +6,17 @@ there is no new universal provider interface or fixed module manifest. ## Ownership to preserve -| Responsibility | Current owner | Migration direction | -| --------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------- | -| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral | -| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery | -| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port | -| Shared analyst workflows | application | Share between CLI and MCP | -| Concrete provider construction | binary runtime, composition, MCP startup | Consolidate one capability at a time in production composition | -| Investigation Evidence and Unknowns | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports | -| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned | -| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup | -| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts | +| Responsibility | Current owner | Migration direction | +| --------------------------------------------- | ----------------------------------- | ----------------------------------------------------- | +| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral | +| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery | +| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port | +| Shared analyst workflows | application | Share between CLI and MCP | +| Concrete provider construction | composition, MCP startup | Keep fresh typed factories at the production boundary | +| Investigation Evidence and Unknowns | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports | +| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned | +| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup | +| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts | Retain exact Evidence IDs, provenance, detached reads, atomic Evidence/Unknown mutations and optimistic revisions. Preserve current close and failed-cleanup @@ -69,8 +69,8 @@ paths removed during a move. Pass exact source paths for a focused check. Pure source and its tests keep inward dependencies. Application/server provider construction guards cover the migrated deep, Android, firmware, JavaScript recovery and observation -implementations. The existing binary runtime is an exact temporary composition -exception; shared browser capture/export helpers remain admitted while their +implementations. Application workflows cannot import composition, including the +former binary runtime entrypoint; shared browser capture/export helpers remain admitted while their ownership is reviewed. Test lane restrictions still use Oxlint. Producer-dependent browser and managed comparison tests live in boundary lanes with their original fixtures and assertions. This is a development guard, not runtime authorization. @@ -104,8 +104,8 @@ inside the provider. The existing launcher seams remain available to boundary fixtures, which now exercise the production factories. Application workflows and MCP registration must not import these concrete -providers. Composition may import their implementations. Binary composition is -still at its existing entrypoint while its direct-analysis callers are investigated. +providers. Composition may import their implementations. Binary composition also +uses this outer boundary after its direct-analysis callers were migrated. This concrete pilot does not introduce a shared provider interface or module manifest. @@ -329,10 +329,9 @@ records still have their separate owner in `src/application/investigation/`. `AnalysisProvider.ts` stays shared because its identity/execution types also serve nonbinary ports. `BinaryTargetResolver.ts` stays shared because Android, firmware, -managed and artifact workflows use its target parsing. The existing `runtime.ts` -remains the exact temporary production wiring entrypoint while direct-analysis -callers are migrated deliberately; application workflows gain no outward -composition dependency. +managed and artifact workflows use its target parsing. Production wiring lives +in `src/composition/binary.ts`; application workflows gain no outward composition +dependency. The three colocated application tests move with their owner. Composition cases remain in `tests/composition/analysis-sessions/`; filesystem and SDK cases retain @@ -385,3 +384,23 @@ Portable decoder and injected producer tests establish their format/boundary claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecycle claims require the real Apple lane. Native binary API/value-flow semantics and shared cross-domain contract aggregates keep their separate owners. + +## Binary production composition closure + +`src/composition/binary.ts` is the shared production owner for deep candidate +construction, lazy auxiliary composition and the managed-only session factory. +It preserves the existing constructor bodies, selection policy and fresh session +per invocation. Factory registration acquires no engine or target. + +`DirectAnalysis` and `DirectAnalysisStatus` receive these two existing factories +through `DirectAnalysisDependencies`. They retain configuration parsing, selected +status environment, cancellation, snapshot replay/binding, Evidence projection +and finally-close. The dependency uses the existing BinarySession type, including +its replay policy; it adds no universal backend or lifecycle contract. + +`src/composition/directAnalysis.ts` binds the production factories for CLI and +one-shot managed MCP callers. Binding creates no session and stores no retained +analysis state. MCP startup constructs its connection session through the same +binary factory. Source and compiled fixtures use these entrypoints. The former +application runtime path is removed, and the source guard no longer admits its +outward provider/composition imports. diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index ca1e4d933..a0c71738b 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -89,12 +89,14 @@ const failedBoundary = (file, target) => { ) return "pure-layer"; } - if (file === "src/application/runtime.ts") return undefined; if (layer === "application" && targetLayer === "composition") return "application-composition"; if ( (layer === "application" || layer === "server") && (MIGRATED_PROVIDER_ROOTS.has(targetLayer) || + /^src\/(?:artifacts\/ArtifactProvider|dotnet\/ManagedStaticProvider|native\/NativeMacOSProvider)\.[^/]+$/u.test( + target, + ) || /^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target)) ) return "provider-construction"; diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index 5875047bb..c273128f2 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -3,7 +3,8 @@ import { parseConfig } from "../config.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { EnhancedTools } from "./EnhancedTools.js"; import { executeFunctionAnalysisEvidence } from "./FunctionAnalysisEvidence.js"; -import { createBinarySession, createManagedBinarySession } from "./runtime.js"; +import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js"; +import type { BinarySession } from "./binary/BinarySession.js"; import { silentLogger, type Logger } from "../logger.js"; import { createEvidence } from "../domain/evidence.js"; import type { Evidence } from "../domain/evidence.js"; @@ -38,11 +39,6 @@ import { import type { AnalysisProviderSelector } from "../contracts/providerSelection.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; -export { - runCapabilityStatus, - runProviderStatus, -} from "./DirectAnalysisStatus.js"; - type DirectAnalysisTool = | "annotate_native_function" | "inspect_native_load_image" @@ -70,6 +66,7 @@ type DirectAnalysisTool = * retain a target or provider client for a subsequent command. */ export const runDirectAnalysis = async ( + dependencies: DirectAnalysisDependencies, path: string, tool: DirectAnalysisTool, arguments_: Readonly>, @@ -82,7 +79,7 @@ export const runDirectAnalysis = async ( } = {}, ): Promise => withProcessCancellation(options.signal, (signal) => - runAnalysis(path, tool, arguments_, { + runAnalysis(dependencies, path, tool, arguments_, { logger: options.logger ?? silentLogger, snapshotPath: options.snapshotPath, signal, @@ -97,6 +94,7 @@ export const runDirectAnalysis = async ( /** Execute one provider-native semantic operation with atomic provenance. */ export const runProviderAnalysis = async ( + dependencies: DirectAnalysisDependencies, ...[path, tool, arguments_, logger = silentLogger, signal]: readonly [ path: string, tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName, @@ -106,9 +104,9 @@ export const runProviderAnalysis = async ( ] ): Promise => isManagedToolName(tool) - ? runManagedProviderAnalysis(path, tool, signal) + ? runManagedProviderAnalysis(dependencies, path, tool, signal) : withProcessCancellation(signal, (operationSignal) => - runAnalysis(path, tool, arguments_, { + runAnalysis(dependencies, path, tool, arguments_, { logger, snapshotPath: undefined, signal: operationSignal, @@ -117,12 +115,13 @@ export const runProviderAnalysis = async ( /** Execute managed metadata inspection in an isolated managed-only session. */ export const runManagedProviderExecution = async ( + dependencies: DirectAnalysisDependencies, path: string, tool: ManagedToolName, signal?: AbortSignal, ): Promise> => withProcessCancellation(signal, async (operationSignal) => { - const session = createManagedBinarySession(); + const session = dependencies.createManagedBinarySession(); try { const opened = await session.open(path, { signal: operationSignal }); if (!opened.ok) return opened; @@ -133,11 +132,17 @@ export const runManagedProviderExecution = async ( }); const runManagedProviderAnalysis = async ( + dependencies: DirectAnalysisDependencies, path: string, tool: ManagedToolName, signal?: AbortSignal, ): Promise => { - const execution = await runManagedProviderExecution(path, tool, signal); + const execution = await runManagedProviderExecution( + dependencies, + path, + tool, + signal, + ); if (!execution.ok) return cliError(execution.error); const value = execution.value; return createEvidence(value.subject ?? undefined, value.provider, { @@ -151,6 +156,7 @@ const runManagedProviderAnalysis = async ( }; const runAnalysis = async ( + dependencies: DirectAnalysisDependencies, path: string, tool: | NativeToolName @@ -177,7 +183,7 @@ const runAnalysis = async ( const { logger, signal, snapshotPath } = options; const config = parseConfig(options.environment ?? process.env); if (!config.ok) return cliError(config.error); - const session = createBinarySession(config.value, logger); + const session = dependencies.createBinarySession(config.value, logger); try { const prepared = await prepareSnapshot({ path, @@ -247,13 +253,13 @@ const runAnalysis = async ( }; const canReplayDirectTool = ( - session: ReturnType, - tool: Parameters[1], + session: BinarySession, + tool: Parameters[2], ): boolean => tool !== "trace_native_ui_action" && session.allowsSnapshotReplay(tool); const executeAnalysisTool = async (input: { - readonly session: ReturnType; + readonly session: BinarySession; readonly openedTarget: BinaryTarget; readonly tool: | NativeToolName @@ -372,7 +378,7 @@ const prepareSnapshot = async (options: { }; const replayProviderFor = ( - session: ReturnType, + session: BinarySession, tool: | NativeToolName | ArtifactAnalysisOperation @@ -384,7 +390,7 @@ const replayProviderFor = ( : session.providerIdentity(tool); const analysisProfileForEvidence = ( - session: ReturnType, + session: BinarySession, tool: | NativeToolName | ArtifactAnalysisOperation diff --git a/src/application/DirectAnalysisDependencies.ts b/src/application/DirectAnalysisDependencies.ts new file mode 100644 index 000000000..606b8eaa4 --- /dev/null +++ b/src/application/DirectAnalysisDependencies.ts @@ -0,0 +1,12 @@ +import type { AppConfig } from "../config.js"; +import type { Logger } from "../logger.js"; +import type { BinarySession } from "./binary/BinarySession.js"; + +/** Existing one-shot session factories supplied by the production boundary. */ +export interface DirectAnalysisDependencies { + readonly createBinarySession: ( + config: AppConfig, + logger: Logger, + ) => BinarySession; + readonly createManagedBinarySession: () => BinarySession; +} diff --git a/src/application/DirectAnalysisStatus.ts b/src/application/DirectAnalysisStatus.ts index 142eccfff..4d75f831f 100644 --- a/src/application/DirectAnalysisStatus.ts +++ b/src/application/DirectAnalysisStatus.ts @@ -3,15 +3,16 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js"; import { createServerIdentity } from "../serverIdentity.js"; import { silentLogger, type Logger } from "../logger.js"; -import { createBinarySession } from "./runtime.js"; +import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js"; const runSessionStatus = async ( + dependencies: Pick, logger: Logger = silentLogger, environment: Readonly> = process.env, ): Promise => { const config = parseConfig(environment); if (!config.ok) return { error: projectAnalysisError(config.error) }; - const session = createBinarySession(config.value, logger); + const session = dependencies.createBinarySession(config.value, logger); try { return { ...jsonObjectSchema.parse(session.status()), @@ -26,12 +27,14 @@ const runSessionStatus = async ( /** List binary-session provider candidates and auxiliary operation availability. */ export const runProviderStatus = ( + dependencies: Pick, logger: Logger = silentLogger, environment: Readonly> = process.env, -) => runSessionStatus(logger, environment); +) => runSessionStatus(dependencies, logger, environment); /** List binary-session operation descriptors; this is not the full MCP catalog. */ export const runCapabilityStatus = ( + dependencies: Pick, logger: Logger = silentLogger, environment: Readonly> = process.env, -) => runSessionStatus(logger, environment); +) => runSessionStatus(dependencies, logger, environment); diff --git a/src/cli/artifactCommands.ts b/src/cli/artifactCommands.ts index 6eaecab5f..4681608e0 100644 --- a/src/cli/artifactCommands.ts +++ b/src/cli/artifactCommands.ts @@ -1,6 +1,6 @@ import { z } from "incur"; -import { runProviderAnalysis } from "../application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../composition/directAnalysis.js"; import { createArtifactExtractionDestination } from "../application/ArtifactExtractionDestination.js"; import { logCliCommand } from "../cliLogging.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts index 0a095cc95..6c70a2626 100644 --- a/src/cli/coreBinaryCommands.ts +++ b/src/cli/coreBinaryCommands.ts @@ -2,7 +2,7 @@ import { z } from "incur"; import { stat } from "node:fs/promises"; import { resolve } from "node:path"; -import { runDirectAnalysis } from "../application/DirectAnalysis.js"; +import { runDirectAnalysis } from "../composition/directAnalysis.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import type { Logger } from "../logger.js"; diff --git a/src/cli/coreNativeCommands.ts b/src/cli/coreNativeCommands.ts index 79e7df100..41f8f7655 100644 --- a/src/cli/coreNativeCommands.ts +++ b/src/cli/coreNativeCommands.ts @@ -1,6 +1,6 @@ import { z } from "incur"; -import { runDirectAnalysis } from "../application/DirectAnalysis.js"; +import { runDirectAnalysis } from "../composition/directAnalysis.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import type { Logger } from "../logger.js"; diff --git a/src/cli/managedCommands.ts b/src/cli/managedCommands.ts index b3c167efd..ee32fd91f 100644 --- a/src/cli/managedCommands.ts +++ b/src/cli/managedCommands.ts @@ -3,7 +3,7 @@ import { z } from "incur"; import { compareManagedMemberPaths } from "../application/managed/ManagedMemberComparisonService.js"; import { verifyManagedNativeBoundariesEvidence } from "../application/managed/ManagedNativeVerificationService.js"; import { importManagedReconstructionEvidence } from "../application/managed/ManagedReconstructionService.js"; -import { runProviderAnalysis } from "../application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../composition/directAnalysis.js"; import { parseCliJsonInput } from "../cliJsonInput.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; diff --git a/src/cli/utilityCommands.ts b/src/cli/utilityCommands.ts index c54f20589..6b03ed6a3 100644 --- a/src/cli/utilityCommands.ts +++ b/src/cli/utilityCommands.ts @@ -5,7 +5,7 @@ import { runCapabilityStatus, runProviderAnalysis, runProviderStatus, -} from "../application/DirectAnalysis.js"; +} from "../composition/directAnalysis.js"; import { importReferenceSource } from "../application/ReferenceSourceImport.js"; import { projectReferenceSourceImportError } from "../application/ReferenceSourceImportTypes.js"; import { parseConfig } from "../config.js"; diff --git a/src/cliEnvironmentSeam.test.ts b/src/cliEnvironmentSeam.test.ts index f53ef55ba..4aa57dca3 100644 --- a/src/cliEnvironmentSeam.test.ts +++ b/src/cliEnvironmentSeam.test.ts @@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest"; import { createCli } from "./cli.js"; import { createSystemDoctorHost } from "./doctorRuntime.js"; import { captureProcessScenarioFile } from "./application/ProcessCli.js"; -import { runCapabilityStatus } from "./application/DirectAnalysisStatus.js"; +import { runCapabilityStatus } from "./composition/directAnalysis.js"; import { probeProcessCaptureCapability } from "./application/ProcessHarness.js"; import { parseEvidence } from "./domain/evidence.js"; import { parseProcessCapture } from "./domain/processCapture.js"; diff --git a/src/application/runtime.ts b/src/composition/binary.ts similarity index 72% rename from src/application/runtime.ts rename to src/composition/binary.ts index 07026076a..5e49cf1ae 100644 --- a/src/application/runtime.ts +++ b/src/composition/binary.ts @@ -1,15 +1,15 @@ import type { AppConfig } from "../config.js"; -import type { BinarySession } from "./binary/BinarySession.js"; +import type { BinarySession } from "../application/binary/BinarySession.js"; import { HopperProvider } from "../hopper/HopperProvider.js"; import { GhidraProvider } from "../ghidra/GhidraProvider.js"; import { IdaProvider } from "../ida/IdaProvider.js"; import { silentLogger, type Logger } from "../logger.js"; -import { auxiliaryAnalysisProviderDeclarations } from "../composition/auxiliaryAnalysisProviders.js"; -import { AnalysisProviderRegistry } from "./binary/AnalysisProviderRegistry.js"; -import { composeBinarySession } from "./binary/BinarySessionComposition.js"; -import { LazyAnalysisProvider } from "./binary/LazyAnalysisProvider.js"; +import { auxiliaryAnalysisProviderDeclarations } from "./auxiliaryAnalysisProviders.js"; +import { AnalysisProviderRegistry } from "../application/binary/AnalysisProviderRegistry.js"; +import { composeBinarySession } from "../application/binary/BinarySessionComposition.js"; +import { LazyAnalysisProvider } from "../application/binary/LazyAnalysisProvider.js"; import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js"; -import { SessionProviderRouter } from "./binary/SessionProviderRouter.js"; +import { SessionProviderRouter } from "../application/binary/SessionProviderRouter.js"; /** * Compose the target-switching runtime shared directly by CLI and MCP adapters. diff --git a/src/composition/directAnalysis.ts b/src/composition/directAnalysis.ts new file mode 100644 index 000000000..670d3a642 --- /dev/null +++ b/src/composition/directAnalysis.ts @@ -0,0 +1,46 @@ +import { + runDirectAnalysis as executeDirectAnalysis, + runProviderAnalysis as executeProviderAnalysis, + runManagedProviderExecution as executeManagedProvider, +} from "../application/DirectAnalysis.js"; +import { + runCapabilityStatus as executeCapabilityStatus, + runProviderStatus as executeProviderStatus, +} from "../application/DirectAnalysisStatus.js"; +import type { DirectAnalysisDependencies } from "../application/DirectAnalysisDependencies.js"; +import { createBinarySession, createManagedBinarySession } from "./binary.js"; + +const dependencies: DirectAnalysisDependencies = { + createBinarySession, + createManagedBinarySession, +}; + +/** Run one isolated binary operation through the production session factory. */ +export const runDirectAnalysis = executeDirectAnalysis.bind( + undefined, + dependencies, +); + +/** Run one isolated native, artifact or managed operation. */ +export const runProviderAnalysis = executeProviderAnalysis.bind( + undefined, + dependencies, +); + +/** Execute managed metadata without constructing native provider candidates. */ +export const runManagedProviderExecution = executeManagedProvider.bind( + undefined, + dependencies, +); + +/** Report production provider candidates without opening a target. */ +export const runProviderStatus = executeProviderStatus.bind( + undefined, + dependencies, +); + +/** Report production session operations without opening a target. */ +export const runCapabilityStatus = executeCapabilityStatus.bind( + undefined, + dependencies, +); diff --git a/src/main.ts b/src/main.ts index 4f8c6491a..c3b6c74b9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import { realpathSync } from "node:fs"; import { serveStdio } from "@modelcontextprotocol/server/stdio"; import { parseConfig } from "./config.js"; -import { createBinarySession } from "./application/runtime.js"; +import { createBinarySession } from "./composition/binary.js"; import { createLogger } from "./logger.js"; import { projectAnalysisError } from "./domain/analysisErrorProjection.js"; import type { RuntimeDependencies } from "./main/types.js"; diff --git a/src/server/registerManagedTools.ts b/src/server/registerManagedTools.ts index 66d5d54c8..bf5a9108f 100644 --- a/src/server/registerManagedTools.ts +++ b/src/server/registerManagedTools.ts @@ -9,7 +9,7 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { err } from "../domain/result.js"; import type { Logger } from "../logger.js"; import { registerEvidenceTools } from "./registerEvidenceTools.js"; -import { runManagedProviderExecution } from "../application/DirectAnalysis.js"; +import { runManagedProviderExecution } from "../composition/directAnalysis.js"; import { isManagedToolName } from "../contracts/managed/managedToolContracts.js"; /** Register execution-free managed PE/CLI inspection. */ diff --git a/tests/boundary/filesystem/directAnalysisPermissions.test.ts b/tests/boundary/filesystem/directAnalysisPermissions.test.ts index 7078d7603..fcdef7e5d 100644 --- a/tests/boundary/filesystem/directAnalysisPermissions.test.ts +++ b/tests/boundary/filesystem/directAnalysisPermissions.test.ts @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { writeAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js"; -import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js"; +import { runDirectAnalysis } from "../../../src/composition/directAnalysis.js"; import type { AnalysisSnapshot } from "../../../src/domain/analysisSnapshot.js"; import { snapshotBinding, diff --git a/tests/boundary/filesystem/runtimeIdentification.test.ts b/tests/boundary/filesystem/runtimeIdentification.test.ts index b9c3c438d..7ebc2e6b9 100644 --- a/tests/boundary/filesystem/runtimeIdentification.test.ts +++ b/tests/boundary/filesystem/runtimeIdentification.test.ts @@ -11,7 +11,7 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { identifyRuntimes, diff --git a/tests/boundary/mcp/artifactEntryOrderMcp.test.ts b/tests/boundary/mcp/artifactEntryOrderMcp.test.ts index 9fe438994..a3ad28671 100644 --- a/tests/boundary/mcp/artifactEntryOrderMcp.test.ts +++ b/tests/boundary/mcp/artifactEntryOrderMcp.test.ts @@ -5,7 +5,7 @@ import { expect, it } from "vitest"; import { z } from "zod"; import { parseConfig } from "../../../src/config.js"; -import { createBinarySession } from "../../../src/application/runtime.js"; +import { createBinarySession } from "../../../src/composition/binary.js"; import { createServer } from "../../../src/server/createServer.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index b099a3825..a0bf85875 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -6,7 +6,7 @@ import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { expect, it } from "vitest"; import { z } from "zod"; import { parseConfig } from "../../../src/config.js"; -import { createBinarySession } from "../../../src/application/runtime.js"; +import { createBinarySession } from "../../../src/composition/binary.js"; import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts index c7e1ab0e7..7c758effb 100644 --- a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts +++ b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts @@ -10,7 +10,7 @@ import { } from "@zip.js/zip.js"; import { afterEach, expect, it } from "vitest"; -import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/nativeDemangleMcp.test.ts b/tests/boundary/mcp/nativeDemangleMcp.test.ts index 172600ba7..706826ca6 100644 --- a/tests/boundary/mcp/nativeDemangleMcp.test.ts +++ b/tests/boundary/mcp/nativeDemangleMcp.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { describe, expect } from "vitest"; import { z } from "zod"; -import { createBinarySession } from "../../../src/application/runtime.js"; +import { createBinarySession } from "../../../src/composition/binary.js"; import { parseConfig } from "../../../src/config.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/shutdown.test.ts b/tests/boundary/mcp/shutdown.test.ts index 82ce3bdf1..acccf5a32 100644 --- a/tests/boundary/mcp/shutdown.test.ts +++ b/tests/boundary/mcp/shutdown.test.ts @@ -2,7 +2,7 @@ import type { StdioServerHandle } from "@modelcontextprotocol/server/stdio"; import pino from "pino"; import { expect, it } from "vitest"; -import { createManagedBinarySession } from "../../../src/application/runtime.js"; +import { createManagedBinarySession } from "../../../src/composition/binary.js"; import { MCP_SHUTDOWN_FAILED } from "../../../src/main/messages.js"; import { createShutdown } from "../../../src/main/shutdown.js"; import type { RuntimeDependencies } from "../../../src/main/types.js"; diff --git a/tests/boundary/providers/managed/managedDirectAnalysis.test.ts b/tests/boundary/providers/managed/managedDirectAnalysis.test.ts index a1f2b56fa..9abcf917b 100644 --- a/tests/boundary/providers/managed/managedDirectAnalysis.test.ts +++ b/tests/boundary/providers/managed/managedDirectAnalysis.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; -import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js"; import { parseEvidence } from "../../../../src/domain/evidence.js"; import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js"; diff --git a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts index 33fd99120..f58c5032b 100644 --- a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts +++ b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts @@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; -import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js"; import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js"; import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js"; import { artifactInspectionResultSchema } from "../../../../src/domain/artifactInspection.js"; diff --git a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts index 223811110..98ab35808 100644 --- a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts +++ b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; -import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js"; import { ArtifactPathRegistry, normalizeArtifactPath, diff --git a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts index 4966c6ead..c99d2abee 100644 --- a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts +++ b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts @@ -5,7 +5,7 @@ import { createPackageWithOptions } from "@electron/asar"; import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; -import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js"; import { artifactInspectionResultSchema } from "../../../../src/domain/artifactInspection.js"; import { parseEvidence } from "../../../../src/domain/evidence.js"; diff --git a/tests/composition/analysis-sessions/appleApplication.test.ts b/tests/composition/analysis-sessions/appleApplication.test.ts index 8f870ad47..7fca0260c 100644 --- a/tests/composition/analysis-sessions/appleApplication.test.ts +++ b/tests/composition/analysis-sessions/appleApplication.test.ts @@ -17,7 +17,7 @@ import { } from "../../support/applicationSessionFixture.js"; import { projectAppleApplicationEvidence } from "../../../src/application/apple/AppleApplicationService.js"; -import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js"; import { appleApplicationProjectionResultSchema } from "../../../src/domain/apple/appleApplication.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; diff --git a/tests/composition/android/application.test.ts b/tests/composition/android/application.test.ts index d0d57be6b..fe8def8ad 100644 --- a/tests/composition/android/application.test.ts +++ b/tests/composition/android/application.test.ts @@ -16,7 +16,7 @@ import { } from "../../support/applicationSessionFixture.js"; import { projectAndroidApplicationEvidence } from "../../../src/application/android/AndroidApplicationService.js"; -import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js"; import { androidApplicationProjectionResultSchema } from "../../../src/domain/android/androidApplication.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; diff --git a/tests/composition/directAnalysisFactories.test.ts b/tests/composition/directAnalysisFactories.test.ts new file mode 100644 index 000000000..2de45c68e --- /dev/null +++ b/tests/composition/directAnalysisFactories.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from "vitest"; +import { + runDirectAnalysis, + runManagedProviderExecution, +} from "../../src/application/DirectAnalysis.js"; +import { runCapabilityStatus } from "../../src/application/DirectAnalysisStatus.js"; +import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js"; +import type { BinarySession } from "../../src/application/binary/BinarySession.js"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { observed } from "../fixtures/analysisExecution.js"; +import { + createBinarySessionTargets, + createTestBinarySession, +} from "../fixtures/binarySession.js"; + +const recordingFactories = (failure?: Error) => { + const sessions: BinarySession[] = []; + let clientsClosed = 0; + const createSession = () => { + const session = createTestBinarySession(() => ({ + execute: (operation) => { + if (failure !== undefined && operation === "read_bytes") + return Promise.reject(failure); + return Promise.resolve(observed({ operation })); + }, + close: () => { + clientsClosed += 1; + return Promise.resolve(); + }, + })); + sessions.push(session); + return session; + }; + const dependencies: DirectAnalysisDependencies = { + createBinarySession: createSession, + createManagedBinarySession: createSession, + }; + return { dependencies, sessions, clientsClosed: () => clientsClosed }; +}; + +describe("one-shot analysis factory boundary", () => { + it("keeps independent commands and releases their actual session clients", async () => { + const [path] = await createBinarySessionTargets(); + const factories = recordingFactories(); + const first = parseEvidence( + await runDirectAnalysis(factories.dependencies, path, "read_bytes", {}), + ); + const second = parseEvidence( + await runDirectAnalysis(factories.dependencies, path, "read_bytes", {}), + ); + expect(first).toMatchObject({ + operation: "read_bytes", + normalized_result: { operation: "read_bytes" }, + subject: { local_path: path }, + }); + expect(second.subject).toEqual(first.subject); + expect(factories.sessions).toHaveLength(2); + expect(factories.sessions[0]).not.toBe(factories.sessions[1]); + expect(factories.clientsClosed()).toBe(2); + for (const session of factories.sessions) { + expect(session.activeTarget()).toBeUndefined(); + expect(session.evidenceById(first.evidence_id)).toBeUndefined(); + expect(session.evidenceById(second.evidence_id)).toBeUndefined(); + } + }); + + it("preserves thrown failures while releasing clients and cancellation listeners", async () => { + const [path] = await createBinarySessionTargets(); + const failure = new Error("fixture execution failed"); + const factories = recordingFactories(failure); + const listeners = process.listenerCount("SIGINT"); + await expect( + runDirectAnalysis(factories.dependencies, path, "read_bytes", {}), + ).rejects.toBe(failure); + expect(factories.clientsClosed()).toBe(1); + expect(process.listenerCount("SIGINT")).toBe(listeners); + expect(factories.sessions[0]?.activeTarget()).toBeUndefined(); + }); + + it("does not acquire a client after cancellation before target opening", async () => { + const [path] = await createBinarySessionTargets(); + const factories = recordingFactories(); + const controller = new AbortController(); + controller.abort(); + const result = await runDirectAnalysis( + factories.dependencies, + path, + "read_bytes", + {}, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + error: "Analysis failed", + code: "cancelled", + }); + expect(factories.clientsClosed()).toBe(0); + expect(factories.sessions[0]?.activeTarget()).toBeUndefined(); + }); + + it("keeps managed execution independent of native construction", async () => { + const [path] = await createBinarySessionTargets(); + const factories = recordingFactories(); + const result = await runManagedProviderExecution( + { + ...factories.dependencies, + createBinarySession: () => { + throw new Error("native factory must stay unused"); + }, + }, + path, + "inspect_managed_artifact", + ); + expect(result).toMatchObject({ + ok: true, + value: { result: { operation: "inspect_managed_artifact" } }, + }); + expect(factories.clientsClosed()).toBe(1); + expect(factories.sessions[0]?.activeTarget()).toBeUndefined(); + }); +}); + +describe("status factory configuration", () => { + it("passes the selected environment and logger without opening a target", async () => { + const factories = recordingFactories(); + const selected: string[] = []; + await runCapabilityStatus( + { + createBinarySession: (config, logger) => { + selected.push(config.analysisProvider); + expect(logger).toBeDefined(); + return factories.dependencies.createBinarySession(config, logger); + }, + }, + undefined, + { REA_ANALYSIS_PROVIDER: "ghidra" }, + ); + expect(selected).toEqual(["ghidra"]); + expect(factories.clientsClosed()).toBe(0); + expect(factories.sessions[0]?.activeTarget()).toBeUndefined(); + }); + + it("retains configuration diagnostics before acquiring any session", async () => { + const factories = recordingFactories(); + const result = await runCapabilityStatus( + factories.dependencies, + undefined, + { REA_ANALYSIS_PROVIDER: "invalid_provider" }, + ); + expect(result).toHaveProperty("error"); + expect(factories.sessions).toEqual([]); + }); +}); diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index 4b808f331..8dca27063 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -8,6 +8,40 @@ import { describe, expect, it } from "vitest"; import { inspectModuleBoundaries } from "../../scripts/lib/module-boundaries.mjs"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +describe("binary production construction ownership", () => { + it.each([ + "hopper/HopperProvider.js", + "ghidra/GhidraProvider.js", + "ida/IdaProvider.js", + "artifacts/ArtifactProvider.js", + "dotnet/ManagedStaticProvider.js", + "native/NativeMacOSProvider.js", + ])("keeps %s in production composition", (provider) => { + const source = `import { Provider } from "../${provider}";`; + for (const file of ["src/application/runtime.ts", "src/server/probe.ts"]) + expect( + inspectModuleBoundaries(file, source, process.cwd()), + ).toMatchObject([{ boundary: "provider-construction" }]); + expect( + inspectModuleBoundaries( + "src/composition/binary.ts", + source, + process.cwd(), + ), + ).toEqual([]); + }); + + it("rejects the former runtime composition exception", () => { + expect( + inspectModuleBoundaries( + "src/application/runtime.ts", + 'export { createBinarySession } from "../composition/binary.js";', + process.cwd(), + ), + ).toMatchObject([{ boundary: "application-composition" }]); + }); +}); + const execute = promisify(execFile); const verifier = resolve("scripts/verify-module-boundaries.mjs"); @@ -93,7 +127,7 @@ describe("incremental module import boundaries", () => { "../../android/JadxProvider.js", false, ], - ["src/application/runtime.ts", "../android/JadxProvider.js", true], + ["src/application/runtime.ts", "../android/JadxProvider.js", false], [ "src/application/android/runtime.ts", "../../android/JadxProvider.js", diff --git a/tests/fixtures/contractPresentation.mjs b/tests/fixtures/contractPresentation.mjs index 6eab97d88..530f2de23 100644 --- a/tests/fixtures/contractPresentation.mjs +++ b/tests/fixtures/contractPresentation.mjs @@ -6,7 +6,7 @@ import { ELECTRON_TOOL_CONTRACTS } from "../../dist/contracts/javascript/electro import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "../../dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js"; import { TOOL_CONTRACTS } from "../../dist/contracts/toolContracts.js"; import { parseConfig } from "../../dist/config.js"; -import { createBinarySession } from "../../dist/application/runtime.js"; +import { createBinarySession } from "../../dist/composition/binary.js"; // Mutate only this child's presentation arrays, before importing registration. if (process.argv[2] === "reversed") { diff --git a/tests/support/applicationSessionFixture.ts b/tests/support/applicationSessionFixture.ts index d7a23371a..20440ae55 100644 --- a/tests/support/applicationSessionFixture.ts +++ b/tests/support/applicationSessionFixture.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { expect } from "vitest"; -import { runProviderAnalysis } from "../../src/application/DirectAnalysis.js"; +import { runProviderAnalysis } from "../../src/composition/directAnalysis.js"; import type { AnalysisError } from "../../src/domain/analysisErrorBase.js"; import { type Evidence, parseEvidence } from "../../src/domain/evidence.js"; import type { Result } from "../../src/domain/result.js";