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 <cursoragent@cursor.com>
This commit is contained in:
Jay HemnaniandCursor Agent authored and GitHub committed 2026-10-07 19:04:55 +08:00
1 parent 45fcd28b40
commit 37d2ebac4c
2 files changed
+56 -3

No files matched your search

@@ -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);
+51
View File
@@ -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 =
'<?xml version="1.0"?><plist><dict><key>CFBundleExecutable</key><string>Fixture</string></dict></plist>';
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");