mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
474 lines
14 KiB
TypeScript
474 lines
14 KiB
TypeScript
import type { Client } from "@modelcontextprotocol/client";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import type { AnySchema } from "ajv";
|
|
import { expect, it } from "vitest";
|
|
import { mkdir, realpath, symlink, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { z } from "zod";
|
|
|
|
import {
|
|
createAnalysisExecution,
|
|
type AnalysisClient,
|
|
type ProviderIdentity,
|
|
} from "../application/AnalysisProvider.js";
|
|
import { InvestigationRecords } from "../application/investigation/InvestigationRecords.js";
|
|
import { BinarySession } from "../application/binary/BinarySession.js";
|
|
import { AnalysisProviderRegistry } from "../application/binary/AnalysisProviderRegistry.js";
|
|
import { SessionProviderRouter } from "../application/binary/SessionProviderRouter.js";
|
|
import { toolContract } from "../contracts/toolContracts.js";
|
|
import { createEvidence } from "../domain/evidence.js";
|
|
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
|
import { createArtifactBinarySession } from "../composition/binary.js";
|
|
import { build } from "plist";
|
|
import { createApplicationMcpHarness } from "../../tests/fixtures/applicationMcpHarness.js";
|
|
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
|
|
|
const identity: ProviderIdentity = {
|
|
id: "bundle-resource-mcp-fixture",
|
|
name: "Bundle resource MCP fixture",
|
|
version: "1",
|
|
};
|
|
|
|
it("routes bundle resource MCP calls with null executable identity and retained Evidence", async () => {
|
|
const parent = await createTestTempDirectory("rea-bundle-resource-mcp-");
|
|
const bundle = join(parent, "Fixture.app");
|
|
await mkdir(bundle);
|
|
const plistPath = join(bundle, "Info.plist");
|
|
await writeFile(plistPath, '<?xml version="1.0"?><plist><dict/></plist>');
|
|
|
|
const resourceClient: AnalysisClient = {
|
|
execute: async (operation) => {
|
|
if (operation === "health")
|
|
return { ok: true, value: createAnalysisExecution(null, identity) };
|
|
return {
|
|
ok: true,
|
|
value: createAnalysisExecution(
|
|
{
|
|
format: "xml",
|
|
value: {},
|
|
bundle: {
|
|
identifier: null,
|
|
executable: null,
|
|
name: null,
|
|
version: null,
|
|
short_version: null,
|
|
},
|
|
source_path: plistPath,
|
|
resource_root: {
|
|
caller_path: bundle,
|
|
canonical_path: bundle,
|
|
info_plist_path: plistPath,
|
|
layout: "root",
|
|
executable_sha256: null,
|
|
},
|
|
provenance: [],
|
|
limitations: [],
|
|
},
|
|
identity,
|
|
{ subject: null },
|
|
),
|
|
};
|
|
},
|
|
close: async () => ({ ok: true, value: null }),
|
|
};
|
|
const session = new BinarySession(
|
|
SessionProviderRouter.selectable(new AnalysisProviderRegistry([]), []),
|
|
new InvestigationRecords(),
|
|
{ createClient: () => ({ client: resourceClient, capabilities: [] }) },
|
|
);
|
|
const { client } = await createApplicationMcpHarness({
|
|
session,
|
|
clientIdentity: { name: "bundle-resource-test", version: "1" },
|
|
});
|
|
|
|
await expectResourceSchemaBranches(client);
|
|
|
|
const unbound = await client.callTool({
|
|
name: "inspect_plist",
|
|
arguments: { path: plistPath },
|
|
});
|
|
expect(unbound.isError).toBe(true);
|
|
const unboundError = unbound.content.find((item) => item.type === "text");
|
|
if (unboundError?.type !== "text") throw new Error("Expected error text");
|
|
expect(JSON.parse(unboundError.text)).toMatchObject({
|
|
error: { code: "target_unavailable", category: "unavailable" },
|
|
});
|
|
const unopened = await client.callTool({
|
|
name: "binary_session",
|
|
arguments: {},
|
|
});
|
|
expect(
|
|
toolContract("binary_session").outputSchema.parse(
|
|
unopened.structuredContent,
|
|
),
|
|
).toMatchObject({
|
|
result: {
|
|
open: false,
|
|
tool_availability: {
|
|
groups: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
names: expect.arrayContaining(["inspect_plist"]),
|
|
available: false,
|
|
reason: "target_required",
|
|
}),
|
|
]),
|
|
},
|
|
},
|
|
});
|
|
|
|
const opened = await client.callTool({
|
|
name: "open_bundle_resources",
|
|
arguments: { path: bundle },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
expect(
|
|
toolContract("open_bundle_resources").outputSchema.parse(
|
|
opened.structuredContent,
|
|
),
|
|
).toMatchObject({
|
|
result: {
|
|
canonical_path: bundle,
|
|
executable_sha256: null,
|
|
},
|
|
});
|
|
|
|
const status = await client.callTool({
|
|
name: "binary_session",
|
|
arguments: {},
|
|
});
|
|
expect(status.isError).not.toBe(true);
|
|
expect(
|
|
toolContract("binary_session").outputSchema.parse(status.structuredContent),
|
|
).toMatchObject({
|
|
result: {
|
|
open: true,
|
|
session_kind: "bundle_resources",
|
|
resource_root: { canonical_path: bundle, executable_sha256: null },
|
|
},
|
|
});
|
|
|
|
const inspected = await client.callTool({
|
|
name: "inspect_plist",
|
|
arguments: { path: plistPath },
|
|
});
|
|
expect(inspected.isError).not.toBe(true);
|
|
const evidence = toolContract("inspect_plist").outputSchema.parse(
|
|
inspected.structuredContent,
|
|
);
|
|
expect(evidence).toMatchObject({
|
|
subject: null,
|
|
normalized_result: {
|
|
source_path: plistPath,
|
|
resource_root: { executable_sha256: null },
|
|
},
|
|
});
|
|
const evidenceId = z
|
|
.object({ evidence_id: z.string() })
|
|
.parse(evidence).evidence_id;
|
|
expect(session.evidenceById(evidenceId)).toMatchObject({
|
|
subject: null,
|
|
operation: "inspect_plist",
|
|
normalized_result: {
|
|
resource_root: { canonical_path: bundle, executable_sha256: null },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("exposes the production resource session through the MCP SDK for a keyless bundle", async () => {
|
|
const parent = await createTestTempDirectory(
|
|
"rea-bundle-resource-production-mcp-",
|
|
);
|
|
const bundle = join(parent, "Fixture");
|
|
const alias = join(parent, "Caller");
|
|
const resourceDirectory = join(bundle, "Resources");
|
|
await mkdir(resourceDirectory, { recursive: true });
|
|
await writeFile(join(bundle, "Info.plist"), "<plist><dict/></plist>");
|
|
await writeFile(
|
|
join(resourceDirectory, "Archive.plist"),
|
|
build({
|
|
$archiver: "NSKeyedArchiver",
|
|
$version: 100000,
|
|
$objects: ["$null", "resource-only-value"],
|
|
$top: { root: { UID: 1 } },
|
|
}),
|
|
);
|
|
|
|
await symlink(bundle, alias, "junction");
|
|
const canonical = await realpath(bundle);
|
|
|
|
const session = await createArtifactBinarySession({}, process.platform);
|
|
const { client } = await createApplicationMcpHarness({
|
|
session,
|
|
clientIdentity: { name: "production-bundle-resource-test", version: "1" },
|
|
});
|
|
|
|
const opened = await client.callTool({
|
|
name: "open_bundle_resources",
|
|
arguments: { path: alias },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
expect(
|
|
toolContract("open_bundle_resources").outputSchema.parse(
|
|
opened.structuredContent,
|
|
),
|
|
).toMatchObject({
|
|
result: {
|
|
caller_path: alias,
|
|
canonical_path: canonical,
|
|
executable_sha256: null,
|
|
},
|
|
});
|
|
|
|
const decoded = await client.callTool({
|
|
name: "decode_interface_builder",
|
|
arguments: {},
|
|
});
|
|
expect(decoded.isError).not.toBe(true);
|
|
const decodedEvidence = toolContract(
|
|
"decode_interface_builder",
|
|
).outputSchema.parse(decoded.structuredContent);
|
|
expect(decodedEvidence).toMatchObject({
|
|
subject: null,
|
|
normalized_result: {
|
|
target_sha256: null,
|
|
resource_root: {
|
|
caller_path: alias,
|
|
canonical_path: canonical,
|
|
executable_sha256: null,
|
|
},
|
|
},
|
|
});
|
|
const decodedEvidenceId = z
|
|
.object({ evidence_id: z.string() })
|
|
.parse(decodedEvidence).evidence_id;
|
|
for (const view of [
|
|
{ kind: "summary" },
|
|
{
|
|
kind: "interface-builder",
|
|
collection: "documents",
|
|
offset: 0,
|
|
limit: 64,
|
|
},
|
|
] as const) {
|
|
const projected = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: {
|
|
source: { kind: "retained-evidence", evidence_id: decodedEvidenceId },
|
|
view,
|
|
},
|
|
});
|
|
expect(projected.isError).not.toBe(true);
|
|
expect(
|
|
toolContract("inspect_analysis_view").outputSchema.parse(
|
|
projected.structuredContent,
|
|
),
|
|
).toMatchObject({
|
|
normalized_result: {
|
|
artifact: { path: canonical, sha256: null },
|
|
resource_root: {
|
|
caller_path: alias,
|
|
canonical_path: canonical,
|
|
executable_sha256: null,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
const inspected = await client.callTool({
|
|
name: "inspect_keyed_archive",
|
|
arguments: { path: "Resources/Archive.plist" },
|
|
});
|
|
expect(inspected.isError).not.toBe(true);
|
|
const evidence = toolContract("inspect_keyed_archive").outputSchema.parse(
|
|
inspected.structuredContent,
|
|
);
|
|
expect(evidence).toMatchObject({
|
|
subject: null,
|
|
normalized_result: {
|
|
archive_path: "Resources/Archive.plist",
|
|
target_sha256: null,
|
|
resource_root: {
|
|
caller_path: alias,
|
|
canonical_path: canonical,
|
|
executable_sha256: null,
|
|
},
|
|
objects: expect.arrayContaining([
|
|
expect.objectContaining({ value: "resource-only-value" }),
|
|
]),
|
|
},
|
|
});
|
|
const evidenceId = z
|
|
.object({ evidence_id: z.string() })
|
|
.parse(evidence).evidence_id;
|
|
expect(session.evidenceById(evidenceId)).toMatchObject({
|
|
subject: null,
|
|
operation: "inspect_keyed_archive",
|
|
normalized_result: {
|
|
resource_root: {
|
|
caller_path: alias,
|
|
canonical_path: canonical,
|
|
executable_sha256: null,
|
|
},
|
|
},
|
|
});
|
|
const closed = await client.callTool({ name: "close_binary", arguments: {} });
|
|
expect(closed.isError).not.toBe(true);
|
|
const statusAfterClose = await client.callTool({
|
|
name: "binary_session",
|
|
arguments: {},
|
|
});
|
|
expect(
|
|
toolContract("binary_session").outputSchema.parse(
|
|
statusAfterClose.structuredContent,
|
|
),
|
|
).toMatchObject({ result: { open: false } });
|
|
});
|
|
|
|
const expectResourceSchemaBranches = async (client: Client): Promise<void> => {
|
|
const schemaCases: Array<{
|
|
readonly name:
|
|
| "decode_interface_builder"
|
|
| "inspect_asset_catalog"
|
|
| "inspect_keyed_archive";
|
|
readonly validResult: JsonValue;
|
|
}> = [
|
|
{
|
|
name: "decode_interface_builder",
|
|
validResult: jsonObjectSchema.parse({
|
|
target_sha256: "a".repeat(64),
|
|
documents: [],
|
|
graph: {
|
|
target_sha256: "a".repeat(64),
|
|
provider: { id: "fixture", version: null, tool_version: "1" },
|
|
nodes: [],
|
|
edges: [],
|
|
coverage: [],
|
|
truncated: false,
|
|
},
|
|
limitations: [],
|
|
}),
|
|
},
|
|
{
|
|
name: "inspect_asset_catalog",
|
|
validResult: jsonObjectSchema.parse({
|
|
target_sha256: "a".repeat(64),
|
|
catalogs: [],
|
|
inventory_coverage: { scope: "container-entries", status: "unknown" },
|
|
catalog_coverage: {
|
|
status: "unknown",
|
|
reason: "catalog_coverage_not_reported",
|
|
},
|
|
resource_key_coverage: {
|
|
status: "unknown",
|
|
reason: "resource_key_coverage_not_reported",
|
|
},
|
|
total_records: 0,
|
|
offset: 0,
|
|
limit: 1,
|
|
records: [],
|
|
resource_key_matches: [],
|
|
next_offset: null,
|
|
truncated: false,
|
|
limitations: [],
|
|
}),
|
|
},
|
|
{
|
|
name: "inspect_keyed_archive",
|
|
validResult: jsonObjectSchema.parse({
|
|
target_sha256: "a".repeat(64),
|
|
archive_path: "Resources/data.archive",
|
|
archive_sha256: "b".repeat(64),
|
|
archive_format: "binary-plist",
|
|
roots: {},
|
|
objects: [],
|
|
references: [],
|
|
total_objects: 0,
|
|
total_references: 0,
|
|
offset: 0,
|
|
next_offset: null,
|
|
truncated: false,
|
|
limitations: [],
|
|
}),
|
|
},
|
|
] as const;
|
|
const advertisedTools = new Map(
|
|
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
|
|
);
|
|
const resourceRoot = {
|
|
caller_path: "/tmp/Fixture.app",
|
|
canonical_path: "/tmp/Fixture.app",
|
|
info_plist_path: "/tmp/Fixture.app/Info.plist",
|
|
layout: "root",
|
|
executable_sha256: null,
|
|
} as const;
|
|
for (const { name, validResult } of schemaCases) {
|
|
const contract = toolContract(name);
|
|
const validEvidence = createEvidence(undefined, identity, {
|
|
operation: name,
|
|
parameters: {},
|
|
result: validResult,
|
|
});
|
|
expect(contract.outputSchema.safeParse(validEvidence).success, name).toBe(
|
|
true,
|
|
);
|
|
const malformedResult = structuredClone(validResult) as Record<
|
|
string,
|
|
JsonValue
|
|
>;
|
|
malformedResult.target_sha256 = null;
|
|
if (name === "decode_interface_builder") {
|
|
malformedResult.graph = {
|
|
...(malformedResult.graph as Record<string, JsonValue>),
|
|
target_sha256: null,
|
|
};
|
|
}
|
|
const malformedEvidence = createEvidence(undefined, identity, {
|
|
operation: name,
|
|
parameters: {},
|
|
result: malformedResult,
|
|
});
|
|
expect(
|
|
contract.outputSchema.safeParse(malformedEvidence).success,
|
|
`${name} canonical rejects missing resource_root`,
|
|
).toBe(false);
|
|
const advertised = advertisedTools.get(name)?.outputSchema;
|
|
expect(advertised, `${name} is advertised`).toBeDefined();
|
|
if (advertised === undefined) continue;
|
|
const validateAdvertised = new Ajv2020({
|
|
strict: false,
|
|
validateFormats: false,
|
|
inlineRefs: false,
|
|
code: { optimize: false },
|
|
}).compile(advertised as AnySchema);
|
|
const resourceResult = structuredClone(validResult) as Record<
|
|
string,
|
|
JsonValue
|
|
>;
|
|
resourceResult.target_sha256 = null;
|
|
resourceResult.resource_root = resourceRoot;
|
|
if (name === "decode_interface_builder") {
|
|
resourceResult.graph = {
|
|
...(resourceResult.graph as Record<string, JsonValue>),
|
|
target_sha256: null,
|
|
};
|
|
}
|
|
const resourceEvidence = createEvidence(undefined, identity, {
|
|
operation: name,
|
|
parameters: {},
|
|
result: resourceResult,
|
|
});
|
|
expect(
|
|
contract.outputSchema.safeParse(resourceEvidence).success,
|
|
name,
|
|
).toBe(true);
|
|
expect(
|
|
validateAdvertised(resourceEvidence),
|
|
`${name} advertised root branch`,
|
|
).toBe(true);
|
|
expect(
|
|
validateAdvertised(malformedEvidence),
|
|
`${name} advertised schema rejects missing resource_root`,
|
|
).toBe(false);
|
|
}
|
|
};
|