From 66dcd94d092c1cb3d9ee92fcbd733f0c61c584c1 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:58:12 +0800 Subject: [PATCH] fix(cli): route static artifact analysis without native engines Static .app decoding failed under auto selection when multiple deep engines were installed. Compose the existing artifact provider without native candidates and reuse the shared one-shot target, Evidence and cleanup lifecycle. Preserve the selected provider environment. Validation: real NIB filesystem and built CLI cases, artifact direct workflow and direct-analysis lifecycle tests pass (57 tests with MCP controls). --- src/application/DirectAnalysis.ts | 51 ++++++++++++++----- src/application/DirectAnalysisDependencies.ts | 4 ++ src/composition/binary.ts | 14 +++++ src/composition/directAnalysis.ts | 4 ++ .../directAnalysisSnapshotReplay.test.ts | 2 + .../filesystem/nibNativeHierarchy.test.ts | 2 +- tests/boundary/mcp/officialArguments.test.ts | 1 + .../mcp/workflowSnapshotReplayMcp.test.ts | 1 + .../artifactProvider.directAnalysis.test.ts | 40 +++++++++++++++ .../composition/directAnalysisCleanup.test.ts | 1 + .../directAnalysisFactories.test.ts | 1 + tests/composition/directAnalysisXrefs.test.ts | 1 + 12 files changed, 107 insertions(+), 15 deletions(-) diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index 5e3799b0d1..d1d631d5aa 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -10,7 +10,10 @@ import type { Logger } from "pino"; import { createEvidence } from "../domain/evidence.js"; import type { Evidence } from "../domain/evidence.js"; import type { NativeToolName } from "../contracts/native/nativeToolContracts.js"; -import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js"; +import { + ARTIFACT_ANALYSIS_OPERATIONS, + type ArtifactAnalysisOperation, +} from "../contracts/artifactToolContracts.js"; import { isManagedToolName, type ManagedToolName, @@ -79,6 +82,10 @@ type DirectAnalysisTool = | "trace_native_ui_action" | "trace_native_values"; +const ARTIFACT_ANALYSIS_OPERATION_SET: ReadonlySet = new Set( + ARTIFACT_ANALYSIS_OPERATIONS, +); + /** * Open one binary, execute one tool, and always release provider resources. * Unlike MCP mode, every CLI invocation is intentionally isolated and does not @@ -129,16 +136,26 @@ export const runProviderAnalysis = async ( logger?: Logger, signal?: AbortSignal, ] -): Promise => - isManagedToolName(tool) - ? runManagedProviderAnalysis(dependencies, path, tool, arguments_, signal) - : withProcessCancellation(signal, (operationSignal) => - runAnalysis(dependencies, path, tool, arguments_, { - logger, - snapshotPath: undefined, - signal: operationSignal, - }), - ); +): Promise => { + if (isManagedToolName(tool)) + return runManagedProviderAnalysis( + dependencies, + path, + tool, + arguments_, + signal, + ); + return withProcessCancellation(signal, (operationSignal) => + runAnalysis(dependencies, path, tool, arguments_, { + logger, + snapshotPath: undefined, + signal: operationSignal, + ...(ARTIFACT_ANALYSIS_OPERATION_SET.has(tool) + ? { createSession: dependencies.createArtifactBinarySession } + : {}), + }), + ); +}; /** Execute managed metadata inspection in an isolated managed-only session. */ export const runManagedProviderExecution = async ( @@ -205,6 +222,7 @@ const runAnalysis = async ( readonly logger: Logger; readonly snapshotPath: string | undefined; readonly signal: AbortSignal; + readonly createSession?: () => BinarySession | Promise; readonly providerId?: AnalysisProviderSelector; readonly formatHint?: ExecutableFormatHint; readonly architecture?: BinaryArchitecture; @@ -212,9 +230,14 @@ const runAnalysis = async ( }, ): Promise => { const { logger, signal, snapshotPath } = options; - const config = dependencies.readConfiguration(); - if (!config.ok) return cliError(config.error); - const session = await dependencies.createBinarySession(config.value, logger); + let session: BinarySession; + if (options.createSession !== undefined) { + session = await options.createSession(); + } else { + const config = dependencies.readConfiguration(); + if (!config.ok) return cliError(config.error); + session = await dependencies.createBinarySession(config.value, logger); + } return withSessionCleanup( session, async () => { diff --git a/src/application/DirectAnalysisDependencies.ts b/src/application/DirectAnalysisDependencies.ts index df49dc2fe8..ae34ea4046 100644 --- a/src/application/DirectAnalysisDependencies.ts +++ b/src/application/DirectAnalysisDependencies.ts @@ -11,6 +11,10 @@ export interface DirectAnalysisDependencies { config: AppConfig, logger: Logger, ) => BinarySession | Promise; + /** Create an artifact-only session without selecting native analysis engines. */ + readonly createArtifactBinarySession: () => + | BinarySession + | Promise; readonly createManagedBinarySession: () => | BinarySession | Promise; diff --git a/src/composition/binary.ts b/src/composition/binary.ts index b4b86f6379..0cb438cb36 100644 --- a/src/composition/binary.ts +++ b/src/composition/binary.ts @@ -41,3 +41,17 @@ export const createManagedBinarySession = (): BinarySession => composeBinarySession(new AnalysisProviderRegistry([]), [ new ManagedStaticProvider(), ]); + +/** Compose the artifact operations without candidates for deep analysis engines. */ +export const createArtifactBinarySession = ( + selectedEnvironment: Readonly, + platform: NodeJS.Platform = process.platform, +): Promise => + import("../artifacts/ArtifactProvider.js").then(({ ArtifactProvider }) => + composeBinarySession(new AnalysisProviderRegistry([]), [ + new ArtifactProvider( + snapshotEnvironment(selectedEnvironment, platform), + platform, + ), + ]), + ); diff --git a/src/composition/directAnalysis.ts b/src/composition/directAnalysis.ts index c920626c44..0955c6f589 100644 --- a/src/composition/directAnalysis.ts +++ b/src/composition/directAnalysis.ts @@ -28,6 +28,10 @@ export const createDirectAnalysis = ( const { createManagedBinarySession } = await import("./binary.js"); return createManagedBinarySession(); }, + createArtifactBinarySession: async () => { + const { createArtifactBinarySession } = await import("./binary.js"); + return createArtifactBinarySession(environment); + }, }; const runDirectAnalysis: BoundOperation< DirectAnalysisRuntime["runDirectAnalysis"] diff --git a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts index 7adf193f4a..2ba32af0b3 100644 --- a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts +++ b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts @@ -243,6 +243,7 @@ const createSnapshotReplayFixture = async ( return { readConfiguration: () => parseConfig({}), createBinarySession: () => createTestBinarySession(provider), + createArtifactBinarySession: () => createTestBinarySession(provider), createManagedBinarySession: () => createTestBinarySession(provider), }; }, @@ -924,6 +925,7 @@ describe("workflow snapshot profile and cancellation binding", () => { const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: () => session, + createArtifactBinarySession: () => session, createManagedBinarySession: () => session, }; const result = await runDirectAnalysis( diff --git a/tests/boundary/filesystem/nibNativeHierarchy.test.ts b/tests/boundary/filesystem/nibNativeHierarchy.test.ts index e2b948f571..5f7c07b3ed 100644 --- a/tests/boundary/filesystem/nibNativeHierarchy.test.ts +++ b/tests/boundary/filesystem/nibNativeHierarchy.test.ts @@ -220,7 +220,7 @@ describe.skipIf(process.platform !== "darwin")( REA_ANALYSIS_PROVIDER: "auto", }, }); - expect(output.exitCode).toBe(0); + expect(output.exitCode, JSON.stringify(output)).toBe(0); const result = interfaceBuilderAnalysisSchema.parse( parseEvidence(output.json).normalized_result, ); diff --git a/tests/boundary/mcp/officialArguments.test.ts b/tests/boundary/mcp/officialArguments.test.ts index a5fe0f2b11..ec4ff3c47b 100644 --- a/tests/boundary/mcp/officialArguments.test.ts +++ b/tests/boundary/mcp/officialArguments.test.ts @@ -70,6 +70,7 @@ describe("official operation arguments", () => { const dependencies = { readConfiguration: () => parseConfig({}), createBinarySession: createSession, + createArtifactBinarySession: createSession, createManagedBinarySession: createSession, }; const client = await connect({ execute }); diff --git a/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts b/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts index b5a66df42e..59437a193d 100644 --- a/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts +++ b/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts @@ -330,6 +330,7 @@ describe("MCP composed workflow snapshot replay", () => { const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: () => createWorkflowSession(provider), + createArtifactBinarySession: () => createWorkflowSession(provider), createManagedBinarySession: () => createWorkflowSession(provider), }; const replay = await runDirectAnalysis( diff --git a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts index 2b79fce06d..c614f7d75e 100644 --- a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts +++ b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts @@ -6,11 +6,51 @@ import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { createDirectAnalysis } from "../../../../src/composition/directAnalysis.js"; +import { createArtifactBinarySession } from "../../../../src/composition/binary.js"; +import { thinMach } from "../../../../src/domain/binaryTarget.fixture.js"; +import { interfaceBuilderAnalysisSchema } from "../../../../src/domain/apple/interfaceBuilderGraph.js"; import { artifactInspectionResultSchema } from "../../../../src/domain/artifactInspection.js"; import { parseEvidence } from "../../../../src/domain/evidence.js"; const { runProviderAnalysis } = createDirectAnalysis({}); +it("runs static .app decoding without registering native analysis engines", async () => { + const root = await createTestTempDirectory("rea-artifact-only-direct-"); + const app = join(root, "Fixture.app"); + const contents = join(app, "Contents"); + await mkdir(join(contents, "MacOS"), { recursive: true }); + await mkdir(join(contents, "Resources"), { recursive: true }); + await writeFile( + join(contents, "Info.plist"), + "CFBundleExecutableFixture", + ); + await writeFile( + join(contents, "MacOS", "Fixture"), + thinMach(0xfeedfacf, 0x0100000c), + ); + + const staticSession = await createArtifactBinarySession({ + REA_ANALYSIS_PROVIDER: "auto", + }); + expect(staticSession.status()).toMatchObject({ + analysis_provider_candidates: [], + }); + await staticSession.close(); + + const evidence = parseEvidence( + await createDirectAnalysis({ + REA_ANALYSIS_PROVIDER: "auto", + }).runProviderAnalysis(app, "decode_interface_builder", {}), + ); + expect(evidence.operation).toBe("decode_interface_builder"); + expect(evidence.provider.id).toBe("rea-artifact-graph"); + const result = interfaceBuilderAnalysisSchema.parse( + evidence.normalized_result, + ); + expect(result.documents).toEqual([]); + expect(result.graph.target_sha256).toBe(evidence.subject?.digest.sha256); +}); + it("inspects an artifact through the direct CLI workflow without an REA grant", async () => { const root = await createTestTempDirectory("rea-artifact-direct-"); const source = join(root, "source"); diff --git a/tests/composition/directAnalysisCleanup.test.ts b/tests/composition/directAnalysisCleanup.test.ts index 51992d0696..6e5bc24efa 100644 --- a/tests/composition/directAnalysisCleanup.test.ts +++ b/tests/composition/directAnalysisCleanup.test.ts @@ -60,6 +60,7 @@ const factories = ( const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: () => session, + createArtifactBinarySession: () => session, createManagedBinarySession: () => session, }; return { dependencies, session }; diff --git a/tests/composition/directAnalysisFactories.test.ts b/tests/composition/directAnalysisFactories.test.ts index c1f294b61e..16d899b87c 100644 --- a/tests/composition/directAnalysisFactories.test.ts +++ b/tests/composition/directAnalysisFactories.test.ts @@ -34,6 +34,7 @@ const recordingFactories = (failure?: Error) => { const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: createSession, + createArtifactBinarySession: createSession, createManagedBinarySession: createSession, }; return { dependencies, sessions, clientsClosed: () => clientsClosed }; diff --git a/tests/composition/directAnalysisXrefs.test.ts b/tests/composition/directAnalysisXrefs.test.ts index b68832afa7..3591c41088 100644 --- a/tests/composition/directAnalysisXrefs.test.ts +++ b/tests/composition/directAnalysisXrefs.test.ts @@ -82,6 +82,7 @@ const fixture = ( const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: createSession, + createArtifactBinarySession: createSession, createManagedBinarySession: createSession, }; return { dependencies, closed: () => closed };