From 37d2ebac4c2a524eec6e32c4c57cbbb632491ad8 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:34:55 +0530 Subject: [PATCH] fix(artifacts): accept the bundle subject when extracting an app bundle (#891) * fix(artifacts): accept the bundle subject when extracting an app bundle For a .app target the session target is the bundle's executable, while the artifact provider reports the bundle itself as the subject. The subject check exempted inventory_artifact and inspect_artifact but not extract_artifact, so extracting any .app failed with execution_failure after the output tree had already been written, and the output path was never reported. * refactor(binary): rename artifact bundle subject helper isArtifactInventorySubject also covers extract_artifact for .app bundles, so name it for the shared bundle-subject exemption. --------- Co-authored-by: Cursor Agent --- .../binary/BinarySessionExecution.ts | 8 +-- tests/boundary/mcp/artifactMcp.test.ts | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/application/binary/BinarySessionExecution.ts b/src/application/binary/BinarySessionExecution.ts index ccca9eb05b..4cf4e3a8ae 100644 --- a/src/application/binary/BinarySessionExecution.ts +++ b/src/application/binary/BinarySessionExecution.ts @@ -132,7 +132,7 @@ export const bindExecutionTarget = ( if ( result.value.subject !== null && result.value.subject.sha256 !== target.sha256 && - !isArtifactInventorySubject(operation, result.value.subject, target) + !isArtifactBundleSubject(operation, result.value.subject, target) ) return err( new ProviderAdapterError( @@ -143,10 +143,12 @@ export const bindExecutionTarget = ( return ok({ ...result.value, subject: result.value.subject ?? subject }); }; -const isArtifactInventorySubject = ( +const isArtifactBundleSubject = ( operation: AnalysisOperation, subject: EvidenceSubjectTarget, target: BinaryTarget, ): boolean => - (operation === "inventory_artifact" || operation === "inspect_artifact") && + (operation === "inventory_artifact" || + operation === "inspect_artifact" || + operation === "extract_artifact") && subject.path === (target.sourcePath ?? target.path); diff --git a/tests/boundary/mcp/artifactMcp.test.ts b/tests/boundary/mcp/artifactMcp.test.ts index 88baf63ab7..8dfb3d8dd2 100644 --- a/tests/boundary/mcp/artifactMcp.test.ts +++ b/tests/boundary/mcp/artifactMcp.test.ts @@ -148,6 +148,57 @@ it("extracts an active archive through MCP when requested", async () => { } }); +it("extracts a macOS app bundle through MCP", async () => { + const root = await createTestTempDirectory("rea-artifact-extract-app-mcp-"); + const contents = join(root, "Fixture.app", "Contents"); + await mkdir(join(contents, "MacOS"), { recursive: true }); + const plist = + 'CFBundleExecutableFixture'; + await writeFile(join(contents, "Info.plist"), plist); + const header = Buffer.alloc(32); + header.writeUInt32LE(0xfeedfacf, 0); + header.writeUInt32LE(0x01000007, 4); + header.writeUInt32LE(3, 8); + header.writeUInt32LE(2, 12); + await writeFile(join(contents, "MacOS", "Fixture"), header); + + const session = createTestBinarySession(new ArtifactProvider()); + const server = createServer(session, session); + const client = new Client({ + name: "artifact-extract-app-test", + version: "1", + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + let outputRoot: string | undefined; + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const opened = await client.callTool({ + name: "open_binary", + arguments: { path: join(root, "Fixture.app") }, + }); + expect(opened.isError).not.toBe(true); + + const result = await client.callTool({ + name: "extract_artifact", + arguments: {}, + }); + expect(result.isError).not.toBe(true); + const normalized = z + .object({ output_root: z.string() }) + .parse(compactResult(result.structuredContent).result); + outputRoot = normalized.output_root; + expect( + await readFile(join(outputRoot, "Contents", "Info.plist"), "utf8"), + ).toBe(plist); + } finally { + await Promise.allSettled([client.close(), server.close(), session.close()]); + if (outputRoot !== undefined) + await rm(outputRoot, { recursive: true, force: true }); + } +}); + it("records an explicitly continued mismatch, preserves verified siblings, and never reports equivalence", async () => { const root = await createTestTempDirectory("rea-asar-continue-mcp-"); const source = join(root, "source");