refactor(binary): move production composition outside application workflows (#965)

This commit is contained in:
Ysopwnable 2026-10-07 21:56:58 +08:00 committed by GitHub
parent b5afa5f91e
commit 33d4761cc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 359 additions and 76 deletions

View File

@ -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

View File

@ -5,6 +5,10 @@ graph TD
DISPATCH["scripts/rea.mjs<br/>Executable dispatcher<br/>routes mcp / one-shot"]
end
subgraph "Production Composition"
COMPOSITION["src/composition/<br/>fresh typed provider and session factories<br/>one-shot binding and independent optional loading"]
end
subgraph "Adapters"
SERVER["src/server/<br/>MCP request translation<br/>createServer, tool registration, toolResult"]
HOPPER_BRIDGE["bridge/hopper_bridge.py<br/>Runs inside Hopper<br/>adapts operations to Hopper Python API"]
@ -18,7 +22,7 @@ graph TD
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
RECORDS["src/application/investigation/<br/>composed InvestigationRecords + narrow ports<br/>EvidenceLedger and Unknown ownership"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots<br/>compatibility facade and post-commit notifications"]
JS_RECON["src/application/ + src/artifacts/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
JS_RECON["src/application/javascript/ + src/artifacts/javascript/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>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

View File

@ -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.

View File

@ -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";

View File

@ -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<Record<string, JsonValue>>,
@ -82,7 +79,7 @@ export const runDirectAnalysis = async (
} = {},
): Promise<JsonValue> =>
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<JsonValue> =>
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<Result<AnalysisExecution, AnalysisError>> =>
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<JsonValue> => {
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<typeof createBinarySession>,
tool: Parameters<typeof runAnalysis>[1],
session: BinarySession,
tool: Parameters<typeof runAnalysis>[2],
): boolean =>
tool !== "trace_native_ui_action" && session.allowsSnapshotReplay(tool);
const executeAnalysisTool = async (input: {
readonly session: ReturnType<typeof createBinarySession>;
readonly session: BinarySession;
readonly openedTarget: BinaryTarget;
readonly tool:
| NativeToolName
@ -372,7 +378,7 @@ const prepareSnapshot = async (options: {
};
const replayProviderFor = (
session: ReturnType<typeof createBinarySession>,
session: BinarySession,
tool:
| NativeToolName
| ArtifactAnalysisOperation
@ -384,7 +390,7 @@ const replayProviderFor = (
: session.providerIdentity(tool);
const analysisProfileForEvidence = (
session: ReturnType<typeof createBinarySession>,
session: BinarySession,
tool:
| NativeToolName
| ArtifactAnalysisOperation

View File

@ -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;
}

View File

@ -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<DirectAnalysisDependencies, "createBinarySession">,
logger: Logger = silentLogger,
environment: Readonly<Record<string, string | undefined>> = process.env,
): Promise<JsonValue> => {
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<DirectAnalysisDependencies, "createBinarySession">,
logger: Logger = silentLogger,
environment: Readonly<Record<string, string | undefined>> = 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<DirectAnalysisDependencies, "createBinarySession">,
logger: Logger = silentLogger,
environment: Readonly<Record<string, string | undefined>> = process.env,
) => runSessionStatus(logger, environment);
) => runSessionStatus(dependencies, logger, environment);

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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.

View File

@ -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,
);

View File

@ -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";

View File

@ -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. */

View File

@ -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,

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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([]);
});
});

View File

@ -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",

View File

@ -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") {

View File

@ -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";