1394 lines
50 KiB
TypeScript
1394 lines
50 KiB
TypeScript
import {
|
|
PROCESS_CAPTURE_REFERENCE,
|
|
PROCESS_CAPTURE_RECONSTRUCTION,
|
|
PROCESS_COMPARISON_EVIDENCE,
|
|
} from "../../../src/contracts/investigationExamples.js";
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import {
|
|
createEvidence,
|
|
parseEvidence,
|
|
type Evidence,
|
|
} from "../../../src/domain/evidence.js";
|
|
import { EvidenceIntegrityError } from "../../../src/domain/evidenceErrors.js";
|
|
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
|
|
|
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
|
|
import {
|
|
artifactEdgeId,
|
|
artifactGraphDigest,
|
|
artifactManifestId,
|
|
occurrenceIdForLocation,
|
|
} from "../../../src/domain/artifactIdentity.js";
|
|
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
|
|
import {
|
|
ok as resultOk,
|
|
err as resultErr,
|
|
} from "../../../src/domain/result.js";
|
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { createPackageWithOptions } from "@electron/asar";
|
|
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
|
import { expect, it, vi, type MockInstance } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import {
|
|
createSingleEntryZip,
|
|
rewriteSingleEntryZipName,
|
|
} from "../../fixtures/zipNames.js";
|
|
|
|
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
|
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
|
|
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js";
|
|
import { createServer } from "../../../src/server/createServer.js";
|
|
import { createArtifactBinarySession } from "../../../src/composition/binary.js";
|
|
import { observed } from "../../fixtures/analysisExecution.js";
|
|
import { cArchiveExecutable } from "../../../src/artifacts/pyinstaller/CArchive.fixture.js";
|
|
import { compareUnicodeCodePoints } from "../../../src/domain/unicodeCodePointOrder.js";
|
|
|
|
const integrityMessage = (unpacked: boolean): string =>
|
|
`${
|
|
unpacked
|
|
? "Declared ASAR integrity for unpacked entry main.js contradicts its companion file; packaging tools commonly sign or strip unpacked native binaries after writing the archive."
|
|
: "Artifact bytes contradict declared integrity."
|
|
} If expected, rerun inspect_artifact with integrity_policy=record-and-continue (CLI: --integrity-policy record-and-continue) to retain observed bytes as untrusted; otherwise get a fresh copy.`;
|
|
|
|
it("refuses an artifact parent and summary when its actual inventory source cannot be recorded", async () => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-artifact-source-refusal-mcp-",
|
|
);
|
|
const archive = join(root, "fixture.zip");
|
|
const zip = new ZipWriter(new Uint8ArrayWriter());
|
|
await zip.add("main.js", new TextReader("console.log('source');\n"));
|
|
await writeFile(archive, await zip.close());
|
|
const session = await createArtifactBinarySession({});
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "artifact-source-refusal", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
const attempted: Evidence[] = [];
|
|
const failure = new EvidenceIntegrityError(
|
|
"controlled inventory source recording refusal",
|
|
);
|
|
const nativeRecord = session.recordEvidence.bind(session);
|
|
let recording: MockInstance<typeof session.recordEvidence> | undefined;
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: archive },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
const before = session.exportEvidenceBundle().records;
|
|
recording = vi
|
|
.spyOn(session, "recordEvidence")
|
|
.mockImplementation((evidence) => {
|
|
attempted.push(evidence);
|
|
return evidence.operation === "inventory_artifact"
|
|
? resultErr(failure)
|
|
: nativeRecord(evidence);
|
|
});
|
|
const inspected = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: { detail: "summary" },
|
|
});
|
|
expect(inspected.isError).toBe(true);
|
|
expect(parseMcpToolError(inspected)).toMatchObject({
|
|
error: projectAnalysisError(failure),
|
|
});
|
|
expect(attempted.map(({ operation }) => operation)).toEqual([
|
|
"inventory_artifact",
|
|
]);
|
|
expect(attempted[0]?.normalized_result).toMatchObject({
|
|
occurrences: expect.arrayContaining([
|
|
expect.objectContaining({ logical_path: "main.js" }),
|
|
]),
|
|
});
|
|
expect(session.exportEvidenceBundle().records).toEqual(before);
|
|
} finally {
|
|
recording?.mockRestore();
|
|
const closed = await Promise.allSettled([
|
|
client.close(),
|
|
server.close(),
|
|
session.close(),
|
|
]);
|
|
const failures: unknown[] = [];
|
|
for (const result of closed)
|
|
if (result.status === "rejected") failures.push(result.reason);
|
|
const sessionClosed = closed[2];
|
|
if (sessionClosed.status === "fulfilled" && !sessionClosed.value.ok)
|
|
failures.push(sessionClosed.value.error);
|
|
if (failures.length > 0)
|
|
throw new AggregateError(
|
|
failures,
|
|
"Artifact recorder refusal cleanup failed",
|
|
);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
{ unpacked: false, label: "embedded" },
|
|
{ unpacked: true, label: "unpacked" },
|
|
])(
|
|
"returns actionable local details for an $label ASAR integrity error",
|
|
async ({ unpacked }) => {
|
|
const root = await createTestTempDirectory("rea-asar-integrity-mcp-");
|
|
const source = join(root, "source");
|
|
const original = "console.log('ok');\n";
|
|
const changed = "console.log('no');\n";
|
|
await mkdir(source);
|
|
await writeFile(join(source, "main.js"), original);
|
|
const archive = join(root, "fixture.asar");
|
|
await createPackageWithOptions(
|
|
source,
|
|
archive,
|
|
unpacked ? { unpack: "*.js" } : {},
|
|
);
|
|
if (unpacked) {
|
|
await writeFile(join(`${archive}.unpacked`, "main.js"), changed);
|
|
} else {
|
|
const bytes = await readFile(archive);
|
|
const contentOffset = bytes.indexOf(original);
|
|
expect(contentOffset).toBeGreaterThanOrEqual(0);
|
|
bytes.write(changed, contentOffset, "utf8");
|
|
await writeFile(archive, bytes);
|
|
}
|
|
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "asar-integrity-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: archive },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
|
|
const result = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: {},
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
expect(parseMcpToolError(result)).toEqual({
|
|
error: {
|
|
code: "artifact_integrity_mismatch",
|
|
category: "integrity_mismatch",
|
|
message: integrityMessage(unpacked),
|
|
details: {
|
|
logical_path: "main.js",
|
|
declared_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
|
calculated_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
|
unpacked,
|
|
},
|
|
retryable: false,
|
|
remediation: { action: integrityMessage(unpacked) },
|
|
},
|
|
});
|
|
} finally {
|
|
await Promise.allSettled([
|
|
client.close(),
|
|
server.close(),
|
|
session.close(),
|
|
]);
|
|
}
|
|
},
|
|
20_000,
|
|
);
|
|
|
|
it("extracts an active archive through MCP when requested", async () => {
|
|
const root = await createTestTempDirectory("rea-artifact-extract-mcp-");
|
|
const archive = join(root, "fixture.zip");
|
|
const zip = new ZipWriter(new Uint8ArrayWriter());
|
|
await zip.add("main.js", new TextReader("console.log('extract');\n"));
|
|
await writeFile(archive, await zip.close());
|
|
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "artifact-extract-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: archive },
|
|
});
|
|
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(), artifacts: z.array(z.unknown()) })
|
|
.parse(toolEvidence(result.structuredContent).normalized_result);
|
|
outputRoot = normalized.output_root;
|
|
expect(await readFile(join(outputRoot, "main.js"), "utf8")).toBe(
|
|
"console.log('extract');\n",
|
|
);
|
|
expect(normalized.artifacts).toHaveLength(1);
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
if (outputRoot !== undefined)
|
|
await rm(outputRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("preserves raw legacy ZIP name bytes and CP437 path interpretation through MCP", async () => {
|
|
const root = await createTestTempDirectory("rea-legacy-zip-name-mcp-");
|
|
const archive = join(root, "legacy-name.zip");
|
|
const rawName = Buffer.from([0xc7, 0xd1, 0xb1, 0xdb, 0x2e, 0x74, 0x78, 0x74]);
|
|
await writeFile(
|
|
archive,
|
|
rewriteSingleEntryZipName(
|
|
await createSingleEntryZip("name.txt"),
|
|
"name.txt",
|
|
rawName,
|
|
false,
|
|
),
|
|
);
|
|
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "legacy-zip-name-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: archive },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
const inspected = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: {},
|
|
});
|
|
expect(inspected.isError).not.toBe(true);
|
|
const result = toolEvidence(inspected.structuredContent);
|
|
const inspection = z
|
|
.object({ substeps: z.array(z.object({ evidence: z.unknown() })) })
|
|
.parse(result.normalized_result);
|
|
const inventory = z
|
|
.object({ normalized_result: artifactInventoryResultSchema })
|
|
.parse(inspection.substeps[0]?.evidence).normalized_result;
|
|
expect(inventory.occurrences).toContainEqual(
|
|
expect.objectContaining({
|
|
logical_path: "╟╤▒█.txt",
|
|
limitations: [
|
|
"ZIP name uses CP437 fallback; source encoding unverified; raw hex: c7d1b1db2e747874.",
|
|
],
|
|
}),
|
|
);
|
|
} finally {
|
|
await Promise.all([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
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(process.env));
|
|
const server = createServer({ kind: "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(toolEvidence(result.structuredContent).normalized_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");
|
|
await mkdir(source);
|
|
const original = "console.log('ok');\n",
|
|
changed = "console.log('no');\n",
|
|
secondOriginal = "console.log('up');\n",
|
|
secondChanged = "console.log('dn');\n";
|
|
await writeFile(join(source, "main.js"), original);
|
|
await writeFile(join(source, "second.js"), secondOriginal);
|
|
await writeFile(join(source, "sibling.js"), "console.log('sibling');\n");
|
|
const archive = join(root, "fixture.asar");
|
|
await createPackageWithOptions(source, archive, {});
|
|
const bytes = await readFile(archive);
|
|
const contentOffset = bytes.indexOf(original);
|
|
const secondOffset = bytes.indexOf(secondOriginal);
|
|
expect(contentOffset).toBeGreaterThanOrEqual(0);
|
|
expect(secondOffset).toBeGreaterThanOrEqual(0);
|
|
bytes.write(changed, contentOffset, "utf8");
|
|
bytes.write(secondChanged, secondOffset, "utf8");
|
|
await writeFile(archive, bytes);
|
|
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "asar-continue-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: archive },
|
|
});
|
|
const result = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: {
|
|
integrity_policy: "record-and-continue",
|
|
},
|
|
});
|
|
expect(result.isError, JSON.stringify(result.structuredContent)).not.toBe(
|
|
true,
|
|
);
|
|
const compact = toolEvidence(result.structuredContent);
|
|
const evidence = session.evidenceById(compact.evidence_id);
|
|
if (evidence === undefined) throw new Error("missing inventory Evidence");
|
|
const inspection = z
|
|
.object({
|
|
contradictions: z.array(z.unknown()),
|
|
substeps: z.array(
|
|
z.object({
|
|
evidence: z.object({ normalized_result: z.unknown() }),
|
|
}),
|
|
),
|
|
})
|
|
.parse(compact.normalized_result);
|
|
const inventory = artifactInventoryResultSchema.parse(
|
|
inspection.substeps[0]?.evidence.normalized_result,
|
|
);
|
|
expect(inspection.contradictions).toHaveLength(2);
|
|
expect(inventory.integrity_contradictions).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
logical_path: "main.js",
|
|
trust: "observed-untrusted",
|
|
declared_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
|
observed_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
|
}),
|
|
expect.objectContaining({ logical_path: "second.js" }),
|
|
]),
|
|
);
|
|
expect(inventory.integrity_contradictions).toHaveLength(2);
|
|
expect(inventory.occurrences).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
logical_path: "main.js",
|
|
hash_status: "mismatched",
|
|
}),
|
|
expect.objectContaining({
|
|
logical_path: "sibling.js",
|
|
hash_status: "verified",
|
|
}),
|
|
expect.objectContaining({
|
|
logical_path: "second.js",
|
|
hash_status: "mismatched",
|
|
}),
|
|
]),
|
|
);
|
|
const compared = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: {
|
|
left: evidence,
|
|
right: evidence,
|
|
},
|
|
});
|
|
expect(compared.isError).not.toBe(true);
|
|
expect(
|
|
toolEvidence(compared.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
status: "contradiction",
|
|
summary: { contradiction: 2 },
|
|
changes: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
logical_path: "main.js",
|
|
classification: "contradiction",
|
|
dimensions: ["integrity"],
|
|
}),
|
|
expect.objectContaining({
|
|
logical_path: "second.js",
|
|
classification: "contradiction",
|
|
}),
|
|
]),
|
|
});
|
|
const { coverage: _coverage, ...inventoryWithoutCoverage } = inventory;
|
|
const missingCoverage = createEvidence(
|
|
{ path: archive, sha256: inventory.manifest.root_sha256, format: "asar" },
|
|
compact.provider,
|
|
{
|
|
operation: "inventory_artifact",
|
|
parameters: {},
|
|
result: inventoryWithoutCoverage,
|
|
},
|
|
);
|
|
const retainedComparison = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: { left: evidence, right: missingCoverage },
|
|
});
|
|
expect(retainedComparison.isError, JSON.stringify(retainedComparison)).toBe(
|
|
true,
|
|
);
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
it("rejects inline artifact Evidence whose content does not match its ID", async () => {
|
|
const session = createTestBinarySession(() => ({
|
|
health: () => Promise.resolve(),
|
|
execute: () => Promise.resolve(observed(null)),
|
|
close: () => Promise.resolve(resultOk(null)),
|
|
}));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({
|
|
name: "artifact-authority-test",
|
|
version: "1",
|
|
});
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const result = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: {
|
|
...ARTIFACT_COMPARISON_EXAMPLE,
|
|
left: {
|
|
...ARTIFACT_COMPARISON_EXAMPLE.left,
|
|
limitations: ["caller altered this record"],
|
|
},
|
|
},
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
expect(
|
|
session.evidenceById(ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id),
|
|
).toBeUndefined();
|
|
expect(
|
|
session.evidenceById(ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id),
|
|
).toBeUndefined();
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
it("compares inline inventory Evidence without prior session calls", async () => {
|
|
const session = createTestBinarySession(() => ({
|
|
health: () => Promise.resolve(),
|
|
execute: () => Promise.resolve(observed(null)),
|
|
close: () => Promise.resolve(resultOk(null)),
|
|
}));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({
|
|
name: "artifact-ownership-test",
|
|
version: "1",
|
|
});
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const result = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: ARTIFACT_COMPARISON_EXAMPLE,
|
|
});
|
|
expect(result.isError, JSON.stringify(result.structuredContent)).not.toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
toolEvidence(result.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
status: "changed",
|
|
});
|
|
expect(
|
|
session.evidenceById(ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id),
|
|
).toBeDefined();
|
|
expect(
|
|
session.evidenceById(ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id),
|
|
).toBeDefined();
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
it("returns full artifact graphs inline and compares changed inventories", async () => {
|
|
const directory = await createTestTempDirectory("rea-artifact-mcp-");
|
|
const archive = join(directory, "fixture.ipa");
|
|
const changedArchive = join(directory, "changed.ipa");
|
|
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
await writer.add("Payload/Fixture.app/main.js", new TextReader("main();"));
|
|
await writeFile(archive, await writer.close());
|
|
const changedWriter = new ZipWriter(new Uint8ArrayWriter());
|
|
await changedWriter.add(
|
|
"Payload/Fixture.app/main.js",
|
|
new TextReader("changed();"),
|
|
);
|
|
await writeFile(changedArchive, await changedWriter.close());
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "artifact-mcp-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: archive },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
const inspected = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: {},
|
|
});
|
|
expect(inspected.isError).not.toBe(true);
|
|
const inspectionResult = toolEvidence(inspected.structuredContent);
|
|
const inspectionEvidence = session.evidenceById(
|
|
inspectionResult.evidence_id,
|
|
);
|
|
if (inspectionEvidence === undefined)
|
|
throw new Error("missing inspection Evidence");
|
|
const inspection = z
|
|
.object({
|
|
substeps: z.array(
|
|
z.object({
|
|
evidence_id: z.string(),
|
|
status: z.literal("completed"),
|
|
}),
|
|
),
|
|
coverage: z.object({
|
|
status: z.literal("complete-within-substeps"),
|
|
substeps_completed: z.literal(1),
|
|
}),
|
|
})
|
|
.parse(inspectionResult.normalized_result);
|
|
expect(inspection.substeps).toHaveLength(1);
|
|
expect(
|
|
session.evidenceById(inspection.substeps[0]?.evidence_id ?? ""),
|
|
).toMatchObject({ operation: "inventory_artifact" });
|
|
expect(inspectionEvidence.evidence_links).toEqual([
|
|
inspection.substeps[0]?.evidence_id,
|
|
]);
|
|
const inventory = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: {},
|
|
});
|
|
expect(inventory.isError).not.toBe(true);
|
|
const inventoryResult = toolEvidence(inventory.structuredContent);
|
|
const evidence = session.evidenceById(inventoryResult.evidence_id);
|
|
if (evidence === undefined) throw new Error("missing inventory Evidence");
|
|
expect(evidence).toMatchObject({
|
|
provider: { id: "rea-artifact-graph" },
|
|
subject: { format: "ipa" },
|
|
});
|
|
const inventoryInspection = z
|
|
.object({
|
|
substeps: z.array(z.object({ evidence: z.unknown() })),
|
|
})
|
|
.parse(inventoryResult.normalized_result);
|
|
const inventoryEvidence = z
|
|
.object({
|
|
normalized_result: z.object({
|
|
manifest: z.object({ root_format: z.literal("ipa") }),
|
|
occurrences: z.array(z.object({ logical_path: z.string() })),
|
|
}),
|
|
})
|
|
.parse(inventoryInspection.substeps[0]?.evidence);
|
|
expect(inventoryEvidence.normalized_result.occurrences).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
logical_path: "Payload/Fixture.app/main.js",
|
|
}),
|
|
]),
|
|
);
|
|
const openedChanged = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: changedArchive },
|
|
});
|
|
expect(openedChanged.isError).not.toBe(true);
|
|
const changedInventory = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: {},
|
|
});
|
|
const changedResult = toolEvidence(changedInventory.structuredContent);
|
|
const changedInspection = z
|
|
.object({
|
|
substeps: z.array(z.object({ evidence: z.unknown() })),
|
|
})
|
|
.parse(changedResult.normalized_result);
|
|
const compared = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: {
|
|
left: inventoryInspection.substeps[0]?.evidence,
|
|
right: changedInspection.substeps[0]?.evidence,
|
|
},
|
|
});
|
|
expect(compared.isError).not.toBe(true);
|
|
expect(toolEvidence(compared.structuredContent)).toMatchObject({
|
|
normalized_result: { status: "changed" },
|
|
evidence_id: expect.stringMatching(/^ev_/u),
|
|
});
|
|
const unknowns = await client.callTool({
|
|
name: "list_unknowns",
|
|
arguments: { domain: "artifact-comparison" },
|
|
});
|
|
expect(unknowns.structuredContent).toMatchObject({
|
|
result: {
|
|
items: [
|
|
expect.objectContaining({
|
|
domain: "artifact-comparison",
|
|
}),
|
|
],
|
|
},
|
|
});
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
it("compares summary Evidence through retained complete inspection parents", async () => {
|
|
const directory = await createTestTempDirectory("rea-artifact-summary-mcp-");
|
|
const archive = join(directory, "one.zip");
|
|
const changedArchive = join(directory, "two.zip");
|
|
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
await writer.add("a.txt", new TextReader("same"));
|
|
await writeFile(archive, await writer.close());
|
|
const changedWriter = new ZipWriter(new Uint8ArrayWriter());
|
|
await changedWriter.add("a.txt", new TextReader("same"));
|
|
await changedWriter.add("b.txt", new TextReader("new"));
|
|
await writeFile(changedArchive, await changedWriter.close());
|
|
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({
|
|
name: "artifact-summary-mcp-test",
|
|
version: "1",
|
|
});
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const summaries = [];
|
|
for (const path of [archive, changedArchive]) {
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
const inspected = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: { detail: "summary" },
|
|
});
|
|
expect(inspected.isError).not.toBe(true);
|
|
summaries.push(toolEvidence(inspected.structuredContent));
|
|
}
|
|
const [left, right] = summaries;
|
|
if (left === undefined || right === undefined)
|
|
throw new Error("missing artifact summary Evidence");
|
|
expect(left.operation).toBe("inspect_analysis_view");
|
|
expect(right.operation).toBe("inspect_analysis_view");
|
|
const compared = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: { left, right },
|
|
});
|
|
expect(compared.isError).not.toBe(true);
|
|
const result = toolEvidence(compared.structuredContent);
|
|
expect(result.normalized_result).toMatchObject({ status: "changed" });
|
|
const leftParentId = z
|
|
.object({ parent_evidence_id: z.string() })
|
|
.parse(left.normalized_result).parent_evidence_id;
|
|
const rightParentId = z
|
|
.object({ parent_evidence_id: z.string() })
|
|
.parse(right.normalized_result).parent_evidence_id;
|
|
expect(result.evidence_links).toEqual([leftParentId, rightParentId]);
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
const toolEvidence = (value: unknown) => parseEvidence(value);
|
|
|
|
const pyInstallerComparisonArchive = (
|
|
typeCode: "m" | "s",
|
|
includeSymlink: boolean,
|
|
includePrefix = false,
|
|
) =>
|
|
cArchiveExecutable({
|
|
layout: "2.1",
|
|
pythonVersion: 309,
|
|
members: [
|
|
...(includePrefix
|
|
? [
|
|
{
|
|
name: "prefix.bin",
|
|
typeCode: "x" as const,
|
|
payload: Buffer.from("prefix bytes"),
|
|
compress: false,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
name: "helper",
|
|
typeCode,
|
|
payload: Buffer.from("same helper bytes"),
|
|
compress: false,
|
|
},
|
|
{
|
|
name: "pyi-contents-directory _internal",
|
|
typeCode: "o",
|
|
payload: Buffer.alloc(0),
|
|
compress: false,
|
|
},
|
|
{
|
|
name: "shared-app:assets/data.bin",
|
|
typeCode: "d",
|
|
payload: Buffer.alloc(0),
|
|
compress: false,
|
|
},
|
|
...(includeSymlink
|
|
? [
|
|
{
|
|
name: "unavailable-link",
|
|
typeCode: "n" as const,
|
|
payload: Buffer.from("target"),
|
|
compress: false,
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
|
|
const inspectArtifactInventoryThroughMcp = async (
|
|
client: Client,
|
|
path: string,
|
|
) => {
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
const reply = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: {},
|
|
});
|
|
expect(reply.isError).not.toBe(true);
|
|
const inspection = artifactInspectionResultSchema.parse(
|
|
toolEvidence(reply.structuredContent).normalized_result,
|
|
);
|
|
const inventory = inspection.substeps.find(
|
|
({ operation }) => operation === "inventory_artifact",
|
|
)?.evidence;
|
|
if (inventory === undefined) throw new Error("missing inventory Evidence");
|
|
return parseEvidence(inventory);
|
|
};
|
|
|
|
const compareArtifactEvidenceThroughMcp = (
|
|
client: Client,
|
|
left: Record<string, unknown>,
|
|
right: Record<string, unknown>,
|
|
) => client.callTool({ name: "compare_artifacts", arguments: { left, right } });
|
|
|
|
const withForeignMetadataAndNestedZipMember = (
|
|
evidence: ReturnType<typeof parseEvidence>,
|
|
) => {
|
|
const inventory = artifactInventoryResultSchema.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
const helper = inventory.occurrences.find(
|
|
({ logical_path }) => logical_path === "helper",
|
|
);
|
|
if (helper === undefined) throw new Error("missing helper occurrence");
|
|
const { format_metadata: _formatMetadata, ...helperWithoutMetadata } = helper;
|
|
const foreignMetadataId = occurrenceIdForLocation({
|
|
rootArtifactId: inventory.manifest.root_artifact_id,
|
|
logicalPath: "foreign-meta",
|
|
entryKind: "metadata",
|
|
});
|
|
const foreignMetadata = {
|
|
...helperWithoutMetadata,
|
|
occurrence_id: foreignMetadataId,
|
|
logical_path: "foreign-meta",
|
|
entry_kind: "metadata" as const,
|
|
artifact_format: "zip" as const,
|
|
};
|
|
const nestedZipMember = {
|
|
...helperWithoutMetadata,
|
|
occurrence_id: occurrenceIdForLocation({
|
|
rootArtifactId: inventory.manifest.root_artifact_id,
|
|
logicalPath: "helper/member.txt",
|
|
entryKind: "file",
|
|
}),
|
|
parent_occurrence_id: helper.occurrence_id,
|
|
logical_path: "helper/member.txt",
|
|
artifact_format: "zip" as const,
|
|
};
|
|
const occurrences = inventory.occurrences
|
|
.concat(foreignMetadata, nestedZipMember)
|
|
.sort((left, right) =>
|
|
compareUnicodeCodePoints(left.logical_path, right.logical_path),
|
|
);
|
|
const edges = inventory.edges
|
|
.map((edge) => {
|
|
const occurrence = occurrences.find(
|
|
({ occurrence_id }) => occurrence_id === edge.occurrence_id,
|
|
);
|
|
return occurrence === undefined
|
|
? edge
|
|
: { ...edge, edge_id: artifactEdgeId(edge, occurrence) };
|
|
})
|
|
.sort((left, right) =>
|
|
compareUnicodeCodePoints(left.edge_id, right.edge_id),
|
|
);
|
|
const graphSha256 = artifactGraphDigest({
|
|
nodes: inventory.nodes,
|
|
occurrences,
|
|
edges,
|
|
contradictions: inventory.integrity_contradictions,
|
|
});
|
|
const manifestId = artifactManifestId(
|
|
inventory.manifest.root_artifact_id,
|
|
graphSha256,
|
|
);
|
|
const subject = evidence.subject;
|
|
if (subject === null) throw new Error("missing inventory subject");
|
|
return createEvidence(
|
|
{
|
|
path: subject.local_path,
|
|
sha256: subject.digest.sha256,
|
|
format: subject.format,
|
|
},
|
|
evidence.provider,
|
|
{
|
|
operation: "inventory_artifact",
|
|
parameters: evidence.parameters,
|
|
confidence: evidence.confidence,
|
|
authority: evidence.authority,
|
|
result: {
|
|
...inventory,
|
|
occurrences,
|
|
edges,
|
|
manifest: {
|
|
...inventory.manifest,
|
|
occurrence_count: occurrences.length,
|
|
edge_count: edges.length,
|
|
graph_sha256: graphSha256,
|
|
manifest_id: manifestId,
|
|
},
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
it("does not apply PyInstaller declaration unknowns to foreign metadata or nested ZIP members", async () => {
|
|
const root = await createTestTempDirectory("rea-pyinstaller-scope-mcp-");
|
|
const archive = join(root, "sample");
|
|
await writeFile(archive, pyInstallerComparisonArchive("m", false));
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "pyinstaller-scope-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const observed = await inspectArtifactInventoryThroughMcp(client, archive);
|
|
const foreign = withForeignMetadataAndNestedZipMember(observed);
|
|
const response = await compareArtifactEvidenceThroughMcp(
|
|
client,
|
|
foreign,
|
|
foreign,
|
|
);
|
|
expect(response.isError).not.toBe(true);
|
|
expect(
|
|
toolEvidence(response.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
status: "unchanged",
|
|
summary: { unknown: 0 },
|
|
changes: [],
|
|
});
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
it("compares PyInstaller declaration facts without inventing payload identity", async () => {
|
|
const root = await createTestTempDirectory("rea-pyinstaller-compare-mcp-");
|
|
const leftPath = join(root, "module");
|
|
const rightPath = join(root, "script");
|
|
const declarationsPath = join(root, "declarations");
|
|
const modulePath = join(root, "module-clean");
|
|
const shiftedPath = join(root, "module-shifted");
|
|
await Promise.all([
|
|
writeFile(leftPath, pyInstallerComparisonArchive("m", true)),
|
|
writeFile(rightPath, pyInstallerComparisonArchive("s", true)),
|
|
writeFile(declarationsPath, pyInstallerComparisonArchive("s", false)),
|
|
writeFile(modulePath, pyInstallerComparisonArchive("m", false)),
|
|
writeFile(shiftedPath, pyInstallerComparisonArchive("m", false, true)),
|
|
]);
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "pyinstaller-compare-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const left = await inspectArtifactInventoryThroughMcp(client, leftPath);
|
|
const right = await inspectArtifactInventoryThroughMcp(client, rightPath);
|
|
const clean = await inspectArtifactInventoryThroughMcp(
|
|
client,
|
|
declarationsPath,
|
|
);
|
|
const module = await inspectArtifactInventoryThroughMcp(client, modulePath);
|
|
const shifted = await inspectArtifactInventoryThroughMcp(
|
|
client,
|
|
shiftedPath,
|
|
);
|
|
const leftInventory = artifactInventoryResultSchema.parse(
|
|
left.normalized_result,
|
|
);
|
|
const rightInventory = artifactInventoryResultSchema.parse(
|
|
right.normalized_result,
|
|
);
|
|
const leftHelper = leftInventory.occurrences.find(
|
|
({ logical_path }) => logical_path === "helper",
|
|
);
|
|
const rightHelper = rightInventory.occurrences.find(
|
|
({ logical_path }) => logical_path === "helper",
|
|
);
|
|
expect(leftHelper?.artifact_id).toBe(rightHelper?.artifact_id);
|
|
expect(leftHelper?.format_metadata).toMatchObject({ type_code: "m" });
|
|
expect(rightHelper?.format_metadata).toMatchObject({ type_code: "s" });
|
|
expect(
|
|
leftInventory.occurrences.filter(
|
|
({ entry_kind }) => entry_kind === "metadata",
|
|
),
|
|
).toHaveLength(2);
|
|
|
|
const changed = await compareArtifactEvidenceThroughMcp(
|
|
client,
|
|
left,
|
|
right,
|
|
);
|
|
expect(changed.isError).not.toBe(true);
|
|
const changedResult = toolEvidence(
|
|
changed.structuredContent,
|
|
).normalized_result;
|
|
expect(changedResult).toMatchObject({
|
|
status: "unknown",
|
|
summary: { changed: 2, unchanged: 2, unknown: 1 },
|
|
changes: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
logical_path: ".",
|
|
classification: "changed",
|
|
dimensions: expect.arrayContaining(["content"]),
|
|
}),
|
|
expect.objectContaining({
|
|
logical_path: "helper",
|
|
classification: "changed",
|
|
dimensions: ["metadata"],
|
|
left_occurrence_id: leftHelper?.occurrence_id,
|
|
right_occurrence_id: rightHelper?.occurrence_id,
|
|
}),
|
|
expect.objectContaining({
|
|
logical_path: "unavailable-link",
|
|
classification: "unknown",
|
|
dimensions: ["availability"],
|
|
}),
|
|
]),
|
|
});
|
|
expect(
|
|
z
|
|
.object({ changes: z.array(z.object({ logical_path: z.string() })) })
|
|
.parse(changedResult)
|
|
.changes.map(({ logical_path }) => logical_path),
|
|
).not.toContain("__pyinstaller__/runtime-options/1");
|
|
expect(
|
|
z
|
|
.object({ changes: z.array(z.object({ logical_path: z.string() })) })
|
|
.parse(changedResult)
|
|
.changes.map(({ logical_path }) => logical_path),
|
|
).not.toContain("__pyinstaller__/dependencies/2");
|
|
|
|
const declarationsOnly = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: { left: clean, right: clean },
|
|
});
|
|
expect(declarationsOnly.isError).not.toBe(true);
|
|
expect(
|
|
toolEvidence(declarationsOnly.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
status: "unchanged",
|
|
summary: { unchanged: 4, unknown: 0 },
|
|
changes: [],
|
|
});
|
|
|
|
const layoutOnly = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: { left: module, right: shifted },
|
|
});
|
|
expect(layoutOnly.isError).not.toBe(true);
|
|
const layoutChanges = z
|
|
.object({ changes: z.array(z.object({ logical_path: z.string() })) })
|
|
.parse(toolEvidence(layoutOnly.structuredContent).normalized_result)
|
|
.changes.map(({ logical_path }) => logical_path);
|
|
expect(layoutChanges).toContain("prefix.bin");
|
|
expect(layoutChanges).not.toContain("helper");
|
|
|
|
const unavailableControl = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: { left, right: left },
|
|
});
|
|
expect(unavailableControl.isError).not.toBe(true);
|
|
expect(
|
|
toolEvidence(unavailableControl.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
status: "unknown",
|
|
changes: [
|
|
expect.objectContaining({
|
|
logical_path: "unavailable-link",
|
|
classification: "unknown",
|
|
}),
|
|
],
|
|
});
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
it("discovers artifact target prerequisites and preserves typed MCP refusals", async () => {
|
|
const root = await createTestTempDirectory("rea-artifact-availability-mcp-");
|
|
const zip = new ZipWriter(new Uint8ArrayWriter());
|
|
await zip.add("a.txt", new TextReader("hello"));
|
|
await writeFile(join(root, "sample.zip"), await zip.close());
|
|
await writeFile(
|
|
join(root, "plain.plist"),
|
|
"<plist><dict><key>hello</key><string>world</string></dict></plist>",
|
|
);
|
|
await writeFile(
|
|
join(root, "keyed.plist"),
|
|
"<plist><dict><key>$archiver</key><string>NSKeyedArchiver</string><key>$objects</key><array><string>$null</string><string>hello</string></array><key>$top</key><dict><key>root</key><dict><key>CF$UID</key><integer>1</integer></dict></dict></dict></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(root, "standalone"), header);
|
|
await writeFile(join(root, "Tool.app"), header);
|
|
const contents = join(root, "Fixture.app", "Contents");
|
|
await mkdir(join(contents, "MacOS"), { recursive: true });
|
|
await writeFile(join(contents, "MacOS", "Fixture"), header);
|
|
await writeFile(
|
|
join(contents, "Info.plist"),
|
|
"<plist><dict><key>CFBundleExecutable</key><string>Fixture</string></dict></plist>",
|
|
);
|
|
const session = createTestBinarySession(
|
|
new ArtifactProvider(process.env, "darwin"),
|
|
);
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({
|
|
name: "artifact-availability-test",
|
|
version: "1",
|
|
});
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
const operations = [
|
|
"decode_interface_builder",
|
|
"inspect_asset_catalog",
|
|
"inspect_keyed_archive",
|
|
"trace_dylib_resolution",
|
|
];
|
|
const availability = async () => {
|
|
const response = await client.callTool({
|
|
name: "binary_session",
|
|
arguments: {},
|
|
});
|
|
const result = toolContract("binary_session").outputSchema.parse(
|
|
response.structuredContent,
|
|
);
|
|
return result.result.tool_availability.groups;
|
|
};
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const before = await availability();
|
|
for (const name of operations)
|
|
expect(before.find((group) => group.names.includes(name))).toMatchObject({
|
|
available: false,
|
|
reason: "target_required",
|
|
});
|
|
for (const filename of [
|
|
"sample.zip",
|
|
"plain.plist",
|
|
"keyed.plist",
|
|
"standalone",
|
|
"Tool.app",
|
|
"Fixture.app",
|
|
]) {
|
|
expect(
|
|
(
|
|
await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: join(root, filename) },
|
|
})
|
|
).isError,
|
|
).not.toBe(true);
|
|
const groups = await availability();
|
|
for (const name of operations) {
|
|
const supported =
|
|
filename === "Fixture.app" ||
|
|
(filename.endsWith(".plist") && name === "inspect_keyed_archive") ||
|
|
(["standalone", "Tool.app"].includes(filename) &&
|
|
name === "trace_dylib_resolution");
|
|
const group = groups.find((entry) => entry.names.includes(name));
|
|
expect(group, `${filename} ${name}`).toMatchObject({
|
|
available: supported,
|
|
reason: supported ? "available" : "target_unsupported",
|
|
});
|
|
if (!supported) {
|
|
expect(group?.remediation).toMatch(/requires an active/);
|
|
const refused = await client.callTool({ name, arguments: {} });
|
|
expect(parseMcpToolError(refused)).toMatchObject({
|
|
error: { code: "unsupported_target" },
|
|
});
|
|
}
|
|
}
|
|
if (filename.endsWith(".plist")) {
|
|
const inspected = await client.callTool({
|
|
name: "inspect_keyed_archive",
|
|
arguments: {},
|
|
});
|
|
if (filename === "keyed.plist")
|
|
expect(inspected.isError).not.toBe(true);
|
|
else
|
|
expect(parseMcpToolError(inspected)).toMatchObject({
|
|
error: { code: "unsupported_target" },
|
|
});
|
|
}
|
|
await client.callTool({ name: "close_binary", arguments: {} });
|
|
}
|
|
await writeFile(
|
|
join(contents, "state.plist"),
|
|
await readFile(join(root, "keyed.plist")),
|
|
);
|
|
for (const format of ["raw-binary", "dos-com"] as const) {
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: {
|
|
path: join(root, "Fixture.app"),
|
|
format,
|
|
...(format === "raw-binary"
|
|
? { architecture: "x86_64", entry_point: "0x0" }
|
|
: {}),
|
|
},
|
|
});
|
|
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
|
const groups = await availability();
|
|
for (const name of operations.filter(
|
|
(name) => name !== "trace_dylib_resolution",
|
|
))
|
|
expect(
|
|
groups.find((group) => group.names.includes(name)),
|
|
`${format} ${name}`,
|
|
).toMatchObject({ available: true });
|
|
const inspected = await client.callTool({
|
|
name: "inspect_keyed_archive",
|
|
arguments: { path: "Contents/state.plist" },
|
|
});
|
|
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
|
|
await client.callTool({ name: "close_binary", arguments: {} });
|
|
}
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
it("attributes ASAR integrity contradictions to their actual inputs and withholds correlation support", async () => {
|
|
const root = await createTestTempDirectory("rea-asar-attribution-");
|
|
const source = join(root, "source");
|
|
await mkdir(source);
|
|
const original = "console.log('ok');\n";
|
|
const changed = "console.log('no');\n";
|
|
await writeFile(join(source, "main.js"), original);
|
|
const originalArchive = join(root, "original.asar");
|
|
await createPackageWithOptions(source, originalArchive, {});
|
|
const staleBytes = await readFile(originalArchive);
|
|
const offset = staleBytes.indexOf(original);
|
|
expect(offset).toBeGreaterThanOrEqual(0);
|
|
staleBytes.write(changed, offset, "utf8");
|
|
const staleArchive = join(root, "stale.asar");
|
|
await writeFile(staleArchive, staleBytes);
|
|
await writeFile(join(source, "main.js"), changed);
|
|
const currentArchive = join(root, "current.asar");
|
|
await createPackageWithOptions(source, currentArchive, {});
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "artifact-attribution", version: "1" });
|
|
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(st);
|
|
await client.connect(ct);
|
|
const observations = [];
|
|
for (const path of [originalArchive, staleArchive, currentArchive]) {
|
|
expect(
|
|
(await client.callTool({ name: "open_binary", arguments: { path } }))
|
|
.isError,
|
|
).not.toBe(true);
|
|
const inspected = await client.callTool({
|
|
name: "inspect_artifact",
|
|
arguments: { integrity_policy: "record-and-continue" },
|
|
});
|
|
expect(inspected.isError).not.toBe(true);
|
|
observations.push(toolEvidence(inspected.structuredContent));
|
|
}
|
|
for (const record of [
|
|
PROCESS_CAPTURE_REFERENCE,
|
|
PROCESS_CAPTURE_RECONSTRUCTION,
|
|
PROCESS_COMPARISON_EVIDENCE,
|
|
])
|
|
expect(session.recordEvidence(record).ok).toBe(true);
|
|
const [originalEvidence, stale, current] = observations;
|
|
if (
|
|
originalEvidence === undefined ||
|
|
stale === undefined ||
|
|
current === undefined
|
|
)
|
|
throw new Error("Expected three artifact observations");
|
|
for (const [left, right, mismatches] of [
|
|
[stale, current, [stale.evidence_id]],
|
|
[current, stale, [stale.evidence_id]],
|
|
[stale, stale, [stale.evidence_id]],
|
|
[originalEvidence, current, []],
|
|
] as const)
|
|
await assertIntegrityComparison(client, session, left, right, mismatches);
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|
|
|
|
const assertIntegrityComparison = async (
|
|
client: Client,
|
|
session: ReturnType<typeof createTestBinarySession>,
|
|
left: ReturnType<typeof toolEvidence>,
|
|
right: ReturnType<typeof toolEvidence>,
|
|
mismatches: readonly string[],
|
|
) => {
|
|
const compared = await client.callTool({
|
|
name: "compare_artifacts",
|
|
arguments: { left, right },
|
|
});
|
|
expect(compared.isError).not.toBe(true);
|
|
const comparison = toolEvidence(compared.structuredContent);
|
|
expect(comparison.normalized_result).toMatchObject({
|
|
status: mismatches.length === 0 ? "changed" : "contradiction",
|
|
integrity_mismatch_evidence_ids: mismatches,
|
|
});
|
|
const unknown = session
|
|
.listUnknowns({ domain: "artifact-comparison" })
|
|
.find((item) => item.question.includes(comparison.evidence_id));
|
|
expect(unknown).toBeDefined();
|
|
if (mismatches.length > 0) {
|
|
expect(unknown?.contradicting_evidence_ids).toEqual(mismatches);
|
|
expect(unknown?.supporting_evidence_ids).toEqual(
|
|
[...new Set([left.evidence_id, right.evidence_id])].filter(
|
|
(id) => !mismatches.includes(id),
|
|
),
|
|
);
|
|
}
|
|
const correlated = await client.callTool({
|
|
name: "correlate_static_and_runtime",
|
|
arguments: {
|
|
static_comparisons: [comparison],
|
|
runtime_comparisons: [PROCESS_COMPARISON_EVIDENCE],
|
|
mappings: [
|
|
{
|
|
static: {
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
selector: { kind: "artifact", logical_path: "main.js" },
|
|
},
|
|
runtime: {
|
|
comparison_evidence_id: PROCESS_COMPARISON_EVIDENCE.evidence_id,
|
|
dimension: "terminal",
|
|
},
|
|
side_alignment: "left_to_left",
|
|
hypothesis: {
|
|
statement: "Member bytes changed while terminal output did not.",
|
|
expected_pattern: "static_only",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(correlated.isError).not.toBe(true);
|
|
expect(
|
|
toolEvidence(correlated.structuredContent).normalized_result,
|
|
).toMatchObject(
|
|
mismatches.length > 0
|
|
? {
|
|
status: "unknown",
|
|
summary: { hypotheses: 0, unresolved: 1 },
|
|
correlations: {
|
|
items: [
|
|
{
|
|
observed_pattern: "unknown",
|
|
classification: "unresolved_branch",
|
|
static: { status: "contradiction" },
|
|
},
|
|
],
|
|
},
|
|
}
|
|
: { status: "correlated", summary: { hypotheses: 1, unresolved: 0 } },
|
|
);
|
|
};
|