diff --git a/src/domain/analysisErrorPresentation.ts b/src/domain/analysisErrorPresentation.ts index 57cc940ea7..2967fffebd 100644 --- a/src/domain/analysisErrorPresentation.ts +++ b/src/domain/analysisErrorPresentation.ts @@ -447,6 +447,16 @@ const artifactMessage = ({ return `ASAR unpacked companion bytes are unavailable for ${details.logicalPath}. Select the archive in place with its .unpacked directory beside it, then retry.`; if (reason === "unavailable") return "Artifact processing is unavailable for the current target or host. Check artifact support and required tools."; + // integrity_policy records only declared-versus-observed digest + // contradictions, which carry their declared and observed facts. Container + // self-verification (such as a DMG checksum) and source changes fail closed + // under either policy, so offering a policy retry would be ineffective. + if ( + reason === "integrity" && + details === undefined && + INTEGRITY_POLICY_OPERATIONS.has(operation) + ) + return "Artifact failed an integrity check that integrity_policy cannot relax, such as a container checksum or a source that changed while being read. Get a valid, unchanged copy and try again."; if (reason === "integrity" && INTEGRITY_POLICY_OPERATIONS.has(operation)) return `${ details?.unpacked === true && diff --git a/tests/boundary/mcp/artifactMcp.test.ts b/tests/boundary/mcp/artifactMcp.test.ts index d84c139100..0e5023127d 100644 --- a/tests/boundary/mcp/artifactMcp.test.ts +++ b/tests/boundary/mcp/artifactMcp.test.ts @@ -22,6 +22,7 @@ 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 { execFileOutput } from "../../../src/process/ExecFileOutput.js"; import { observed } from "../../fixtures/analysisExecution.js"; const integrityMessage = (unpacked: boolean): string => @@ -104,6 +105,85 @@ it.each([ 20_000, ); +it.skipIf(process.platform !== "darwin")( + "does not offer an ineffective integrity-policy retry for a checksum-invalid DMG", + async () => { + const root = await createTestTempDirectory("rea-dmg-checksum-mcp-"); + const writableImagePath = join(root, "writable.dmg"); + const imagePath = join(root, "fixture.dmg"); + await execFileOutput("/usr/bin/hdiutil", [ + "create", + "-quiet", + "-size", + "1m", + "-layout", + "NONE", + "-type", + "UDIF", + writableImagePath, + ]); + await execFileOutput("/usr/bin/hdiutil", [ + "convert", + "-quiet", + writableImagePath, + "-format", + "UDRO", + "-o", + imagePath, + ]); + const image = await readFile(imagePath); + const footer = image.subarray(-512); + expect(footer.subarray(0, 4).toString("ascii")).toBe("koly"); + const corruptionOffset = Number(footer.readBigUInt64BE(24)) + 4096; + const originalByte = image[corruptionOffset]; + if (originalByte === undefined) + throw new Error("expected a byte in the generated image data fork"); + image[corruptionOffset] = originalByte ^ 0x01; + await writeFile(imagePath, image); + + const session = createTestBinarySession(new ArtifactProvider(process.env)); + const server = createServer({ kind: "session", session }); + const client = new Client({ name: "dmg-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: imagePath }, + }); + expect(opened.isError).not.toBe(true); + + const message = + "Artifact failed an integrity check that integrity_policy cannot relax, such as a container checksum or a source that changed while being read. Get a valid, unchanged copy and try again."; + for (const integrity_policy of ["fail", "record-and-continue"]) { + const result = await client.callTool({ + name: "inspect_artifact", + arguments: { integrity_policy }, + }); + expect(result.isError).toBe(true); + expect(parseMcpToolError(result)).toMatchObject({ + error: { + code: "artifact_operation_failed", + category: "integrity_mismatch", + message, + retryable: false, + remediation: { action: message }, + }, + }); + } + } finally { + await Promise.allSettled([ + client.close(), + server.close(), + session.close(), + ]); + } + }, + 60_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");