mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
* fix(electron): resolve pages served from inside app.asar Packaged Electron applications load their renderer pages from file URLs inside app.asar. The host filesystem cannot stat those paths, so list_electron_targets excluded every such page as unsupported_url and inspect_electron_page could not select it. Resolve these URLs the way Electron's file protocol does: the first .asar path component that is a regular file is the archive, and the remainder must name a regular file in its header. Directories, links, and missing members stay rejected. Parsed headers are cached by archive identity. * fix(artifacts): extract and explain rewritten ASAR unpacked companions Packaging tools commonly sign or strip .asar.unpacked native binaries after writing the archive header, so their declared hashes no longer match. analyze_javascript_application already accepts integrity_policy, but extract_artifact did not, and inspect_artifact and extract_artifact still told callers to "get a fresh copy", which cannot help for a shipped app. - Accept integrity_policy on extract_artifact (MCP and CLI). The default still fails closed; record-and-continue writes the observed bytes and returns the extracted files' integrity_contradictions records. - Name the unpacked entry and the post-packaging cause in integrity failures, and point every operation that accepts integrity_policy at record-and-continue. - Report an active unpacked entry whose companion file is absent as unavailable with its logical path instead of a format failure. - Share one integrity_policy schema across the inventory, extraction, and JavaScript application inputs. * fix(electron): bound ASAR headers before allocation --------- Co-authored-by: morluto <[email protected]>
96 lines
3.9 KiB
TypeScript
96 lines
3.9 KiB
TypeScript
import { readFile, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { expect, it, onTestFinished } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { createServer } from "../../../src/server/createServer.js";
|
|
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { createStrippedAsarAddon } from "../../fixtures/strippedAsarAddon.js";
|
|
|
|
const openArchive = async (archive: string): Promise<Client> => {
|
|
const session = createTestBinarySession(new ArtifactProvider(process.env));
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "asar-unpacked-extraction", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
onTestFinished(async () => {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
});
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path: archive },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
return client;
|
|
};
|
|
|
|
it("extracts a rewritten unpacked companion only when the caller records the mismatch", async () => {
|
|
const { archive, addon, stripped } = await createStrippedAsarAddon();
|
|
const client = await openArchive(archive);
|
|
|
|
const strict = await client.callTool({
|
|
name: "extract_artifact",
|
|
arguments: {},
|
|
});
|
|
expect(parseMcpToolError(strict).error).toMatchObject({
|
|
code: "artifact_integrity_mismatch",
|
|
message: expect.stringContaining(
|
|
`Declared ASAR integrity for unpacked entry ${addon} contradicts its companion file; packaging tools commonly sign or strip unpacked native binaries after writing the archive. If expected, rerun extract_artifact with integrity_policy=record-and-continue`,
|
|
),
|
|
details: { logical_path: addon, unpacked: true },
|
|
});
|
|
|
|
const continued = await client.callTool({
|
|
name: "extract_artifact",
|
|
arguments: { integrity_policy: "record-and-continue" },
|
|
});
|
|
expect(continued.isError, JSON.stringify(continued)).not.toBe(true);
|
|
const evidence = parseEvidence(continued.structuredContent);
|
|
expect(evidence.parameters).toMatchObject({
|
|
integrity_policy: "record-and-continue",
|
|
});
|
|
const result = z
|
|
.object({
|
|
output_root: z.string(),
|
|
integrity_contradictions: z.array(
|
|
z.object({ logical_path: z.string(), trust: z.string() }),
|
|
),
|
|
limitations: z.array(z.string()),
|
|
})
|
|
.parse(evidence.normalized_result);
|
|
onTestFinished(() =>
|
|
rm(result.output_root, { recursive: true, force: true }),
|
|
);
|
|
expect(await readFile(join(result.output_root, addon))).toEqual(stripped);
|
|
expect(result.integrity_contradictions).toEqual([
|
|
{ logical_path: addon, trust: "observed-untrusted" },
|
|
]);
|
|
expect(result.limitations).toContain(
|
|
"1 extracted file(s) contradict declared integrity metadata; their bytes are observed-untrusted.",
|
|
);
|
|
});
|
|
|
|
it("names a missing unpacked companion instead of reporting a changed artifact", async () => {
|
|
const { archive, addon } = await createStrippedAsarAddon();
|
|
await rm(join(`${archive}.unpacked`, addon));
|
|
const client = await openArchive(archive);
|
|
|
|
const extracted = await client.callTool({
|
|
name: "extract_artifact",
|
|
arguments: {},
|
|
});
|
|
expect(parseMcpToolError(extracted).error).toMatchObject({
|
|
code: "artifact_operation_failed",
|
|
category: "unavailable",
|
|
message: `ASAR unpacked companion bytes are unavailable for ${addon}. Select the archive in place with its .unpacked directory beside it, then retry.`,
|
|
details: { logical_path: addon, unpacked: true },
|
|
});
|
|
});
|