diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3894e956b..7b84c8c50 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -131,6 +131,48 @@ jobs:
- run: npm run rebuild:native
- run: npm run verify:package
+ windows-curated:
+ name: Windows Ghidra P0 and package lane
+ runs-on: windows-latest
+ timeout-minutes: 25
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+ cache: npm
+ - run: npm ci
+ - run: npm run build
+ - run: npm run typecheck
+ - name: Record named Windows host capabilities
+ run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
+ - name: Validate Windows host capability report
+ run: >-
+ node -e "const report = JSON.parse(require('node:fs').readFileSync('windows-capabilities.json', 'utf8'));
+ if (report.platform !== 'win32' || report.architecture !== 'x64')
+ throw new Error('expected a Windows x64 capability report');"
+ - name: Run curated Windows P0 tests
+ run: >-
+ npx vitest run --coverage.enabled=false
+ tests/binaryTarget.test.ts
+ tests/windowsCapabilities.test.ts
+ tests/ghidraWindowsFixture.test.ts
+ tests/ghidraInstallation.test.ts
+ tests/ghidraProvider.test.ts
+ tests/ghidraTransport.test.ts
+ tests/ghidraTargetSnapshot.test.ts
+ tests/ghidraClient.test.ts
+ tests/ghidraLauncher.test.ts
+ tests/processOwnership.test.ts
+ tests/realGhidraWindowsWorkflow.test.ts
+ - run: npm run verify:package:windows
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: windows-capabilities
+ path: windows-capabilities.json
+ retention-days: 30
+
docs:
name: Docs Generation
runs-on: ubuntu-latest
diff --git a/.github/workflows/real-ghidra-windows.yml b/.github/workflows/real-ghidra-windows.yml
new file mode 100644
index 000000000..27f9e65c7
--- /dev/null
+++ b/.github/workflows/real-ghidra-windows.yml
@@ -0,0 +1,32 @@
+name: Real Ghidra Windows E2E
+
+on:
+ repository_dispatch:
+ types: [real-ghidra-windows]
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ name: Real Ghidra 12.1.2 Windows x64 PE proof
+ if: github.ref == 'refs/heads/main'
+ environment: real-ghidra-windows
+ runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+ - run: npm ci
+ - name: Verify all admitted Windows Ghidra operations
+ env:
+ REA_ANALYSIS_PROVIDER: ghidra
+ run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: windows-ghidra-proof
+ path: windows-ghidra-proof.log
+ retention-days: 30
diff --git a/package.json b/package.json
index 6d312c5ef..9e1b1220c 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
"typecheck": "tsc --noEmit",
"verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs",
"verify:ghidra": "npm run build && node scripts/verify-real-ghidra.mjs",
+ "verify:ghidra:windows": "npm run build && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build && node scripts/verify-real-browser.mjs",
"verify:javascript-runtime": "npm run build && node scripts/verify-local-javascript-runtime.mjs",
"verify:application-workflows": "npm run build && node scripts/verify-local-application-workflows.mjs",
@@ -83,8 +84,10 @@
"verify:hopper:linux": "npm run build && node scripts/verify-real-hopper-linux.mjs",
"verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs",
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
+ "build:ghidra-windows-fixture": "node scripts/create-ghidra-windows-fixture.mjs",
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
"verify:package": "npm run build && node scripts/verify-package.mjs",
+ "verify:package:windows": "npm run build && node scripts/verify-windows-package.mjs",
"probe:windows-capabilities": "npm run build && node scripts/windows-capabilities.mjs",
"verify:published": "node scripts/verify-published-package.mjs",
"config:print": "node scripts/print-mcp-config.mjs",
diff --git a/scripts/create-ghidra-windows-fixture.mjs b/scripts/create-ghidra-windows-fixture.mjs
new file mode 100644
index 000000000..fcb1df5f2
--- /dev/null
+++ b/scripts/create-ghidra-windows-fixture.mjs
@@ -0,0 +1,52 @@
+import { mkdir, writeFile } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const output = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe");
+const bytes = Buffer.alloc(1024);
+const peOffset = 0x80;
+const optionalHeader = peOffset + 24;
+const sectionTable = optionalHeader + 0xf0;
+
+bytes.write("MZ", 0, "ascii");
+bytes.writeUInt32LE(peOffset, 0x3c);
+bytes.write("PE\0\0", peOffset, "binary");
+bytes.writeUInt16LE(0x8664, peOffset + 4);
+bytes.writeUInt16LE(1, peOffset + 6);
+bytes.writeUInt16LE(0xf0, peOffset + 20);
+bytes.writeUInt16LE(0x22, peOffset + 22);
+
+bytes.writeUInt16LE(0x20b, optionalHeader);
+bytes.writeUInt32LE(0x200, optionalHeader + 4);
+bytes.writeUInt32LE(0x1000, optionalHeader + 16);
+bytes.writeUInt32LE(0x1000, optionalHeader + 20);
+bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
+bytes.writeUInt32LE(0x1000, optionalHeader + 32);
+bytes.writeUInt32LE(0x200, optionalHeader + 36);
+bytes.writeUInt16LE(6, optionalHeader + 40);
+bytes.writeUInt16LE(6, optionalHeader + 48);
+bytes.writeUInt32LE(0x2000, optionalHeader + 56);
+bytes.writeUInt32LE(0x200, optionalHeader + 60);
+bytes.writeUInt16LE(3, optionalHeader + 68);
+bytes.writeUInt16LE(0x8160, optionalHeader + 70);
+bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72);
+bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80);
+bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88);
+bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96);
+bytes.writeUInt32LE(16, optionalHeader + 108);
+
+bytes.write(".text", sectionTable, "ascii");
+bytes.writeUInt32LE(0x16, sectionTable + 8);
+bytes.writeUInt32LE(0x1000, sectionTable + 12);
+bytes.writeUInt32LE(0x200, sectionTable + 16);
+bytes.writeUInt32LE(0x200, sectionTable + 20);
+bytes.writeUInt32LE(0x6000_0020, sectionTable + 36);
+
+// entry calls the second function, returns zero; second returns 42.
+bytes.set([0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3], 0x200);
+bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
+
+await mkdir(dirname(output), { recursive: true });
+await writeFile(output, bytes);
+process.stdout.write(`${output}\n`);
diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs
new file mode 100644
index 000000000..6442fa1f2
--- /dev/null
+++ b/scripts/verify-real-ghidra-windows.mjs
@@ -0,0 +1,293 @@
+#!/usr/bin/env node
+
+import { access } from "node:fs/promises";
+import { isAbsolute, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js";
+import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
+import { GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS } from "../dist/ghidra/GhidraDefaults.js";
+import {
+ GHIDRA_FUNCTION_OPERATIONS,
+ parseGhidraFunctionInput,
+ parseGhidraFunctionResult,
+} from "../dist/ghidra/GhidraFunctionValues.js";
+import {
+ GHIDRA_INVENTORY_OPERATIONS,
+ parseGhidraInventoryInput,
+ parseGhidraInventoryResult,
+} from "../dist/ghidra/GhidraInventoryValues.js";
+import {
+ inspectGhidraInstallation,
+ SUPPORTED_GHIDRA_VERSION,
+} from "../dist/ghidra/GhidraInstallation.js";
+import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
+import { GHIDRA_SESSION_CAPABILITIES as SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
+import { parseBinaryTarget } from "../dist/domain/binaryTarget.js";
+
+if (process.platform !== "win32" || process.arch !== "x64")
+ throw new Error(
+ "The real Windows Ghidra verifier requires a Windows x64 host",
+ );
+const installDir = process.env.GHIDRA_INSTALL_DIR;
+if (installDir === undefined || !isAbsolute(installDir))
+ throw new Error(
+ "Set GHIDRA_INSTALL_DIR to the absolute root of Ghidra 12.1.2.",
+ );
+const installation = inspectGhidraInstallation({
+ installDir,
+ ...(process.env.JAVA_HOME === undefined
+ ? {}
+ : { javaHome: process.env.JAVA_HOME }),
+});
+if (
+ !installation.available ||
+ installation.analyzeHeadlessPath === null ||
+ installation.providerVersion !== SUPPORTED_GHIDRA_VERSION
+)
+ throw new Error(
+ `Windows Ghidra installation is unavailable: ${JSON.stringify(installation)}`,
+ );
+
+const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
+const targetPath = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe");
+const target = await parseBinaryTarget(targetPath);
+if (
+ !target.ok ||
+ target.value.format !== "pe" ||
+ target.value.architecture !== "x86_64" ||
+ target.value.executableRole !== "application" ||
+ target.value.managed !== false
+)
+ throw new Error(
+ `Controlled Windows fixture failed admission: ${JSON.stringify(target)}`,
+ );
+const profile = await resolveGhidraAnalysisProfile(
+ target.value,
+ GHIDRA_PROVIDER_IDENTITY,
+ installation,
+);
+if (!profile.ok || profile.value.profile === null)
+ throw new Error("Windows Ghidra profile could not be committed");
+
+const client = new GhidraClient({
+ launcher: new GhidraHeadlessLauncher({
+ analyzeHeadlessPath: installation.analyzeHeadlessPath,
+ ...(process.env.JAVA_HOME === undefined
+ ? {}
+ : { javaHome: process.env.JAVA_HOME }),
+ bridgeScriptPath: fileURLToPath(
+ new URL("../bridge/ghidra/ReaGhidraBridge.java", import.meta.url),
+ ),
+ platform: "win32",
+ }),
+ targetPath: target.value.path,
+ targetSha256: target.value.sha256,
+ transport: "authenticated-loopback-tcp",
+ providerVersion: SUPPORTED_GHIDRA_VERSION,
+ profileDigest: profile.value.profile.digest,
+ requestTimeoutMs: GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS,
+});
+
+let coordinates;
+let report;
+const observed = new Set();
+try {
+ const started = await client.start();
+ if (!started.ok) throw started.error;
+ assertSession(
+ started.value,
+ target.value.sha256,
+ profile.value.profile.digest,
+ );
+ coordinates = client.diagnostics();
+ assertRuntimeCoordinates(coordinates, target.value.sha256);
+
+ const documents = await inventory("list_documents", {});
+ const names = await inventory("list_names", {});
+ const procedures = await inventory("list_procedures", {});
+ const segments = await inventory("list_segments", {});
+ const strings = await inventory("list_strings", {});
+ if (
+ !Array.isArray(documents) ||
+ documents.length !== 1 ||
+ !pageHasItems(procedures) ||
+ !Array.isArray(segments) ||
+ segments.length === 0 ||
+ !isPage(names) ||
+ !isPage(strings)
+ )
+ throw new Error("Windows Ghidra inventory proof was incomplete");
+ const procedure = procedures.items.find(
+ (item) => item.procedure?.external === false,
+ );
+ if (procedure === undefined)
+ throw new Error("Windows Ghidra fixture exposed no local procedure");
+
+ await inventory("address_name", { address: procedure.address });
+ const resolvedAddress = await inventory("procedure_address", {
+ procedure: procedure.value,
+ });
+ if (resolvedAddress !== procedure.address)
+ throw new Error("Windows procedure name/address resolution drifted");
+ await inventory("resolve_containing_procedure", {
+ address: procedure.address,
+ });
+ await inventory("search_procedures", { pattern: procedure.value });
+ await inventory("search_strings", { pattern: "rea" });
+
+ for (const operation of [
+ "procedure_assembly",
+ "procedure_callees",
+ "procedure_callers",
+ "procedure_info",
+ "procedure_pseudo_code",
+ "procedure_references",
+ ])
+ await functionOperation(operation, { procedure: procedure.value });
+ await functionOperation("xrefs", { address: procedure.address });
+ await functionOperation("analyze_function", {
+ procedure: procedure.value,
+ include_assembly: true,
+ });
+
+ const expected = [
+ ...GHIDRA_INVENTORY_OPERATIONS,
+ ...GHIDRA_FUNCTION_OPERATIONS,
+ ];
+ if (
+ observed.size !== expected.length ||
+ expected.some((operation) => !observed.has(operation))
+ )
+ throw new Error(
+ `Windows Ghidra operation proof was incomplete: ${JSON.stringify([...observed])}`,
+ );
+
+ report = {
+ ok: true,
+ provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION },
+ target: {
+ format: target.value.format,
+ architecture: target.value.architecture,
+ executable_role: target.value.executableRole,
+ managed: target.value.managed,
+ sha256: target.value.sha256,
+ },
+ bridge_version: started.value.bridge_version,
+ transport: "authenticated-loopback-tcp",
+ operations: [...observed].sort((left, right) => left.localeCompare(right)),
+ cleanup: "complete",
+ limitations: [
+ "approved-non-sensitive-fixtures-only",
+ "no-job-object-ownership",
+ "no-private-dacl-proof",
+ "no-reparse-point-authority",
+ ],
+ };
+} finally {
+ await client.close();
+ if (coordinates !== undefined) await assertCleanup(coordinates);
+}
+if (report === undefined)
+ throw new Error("Windows Ghidra report was not produced");
+process.stdout.write(`${JSON.stringify(report)}\n`);
+
+async function inventory(operation, parameters) {
+ const input = parseGhidraInventoryInput(operation, parameters);
+ if (!input.ok) throw input.error;
+ const called = await client.callTool(operation, input.value);
+ if (!called.ok) throw called.error;
+ const result = parseGhidraInventoryResult(operation, called.value);
+ if (!result.ok) throw result.error;
+ observed.add(operation);
+ return result.value;
+}
+
+async function functionOperation(operation, parameters) {
+ const input = parseGhidraFunctionInput(operation, parameters);
+ if (!input.ok) throw input.error;
+ const called = await client.callTool(operation, input.value, {
+ timeoutMs: GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS,
+ });
+ if (!called.ok) throw called.error;
+ const result = parseGhidraFunctionResult(operation, called.value);
+ if (!result.ok) throw result.error;
+ observed.add(operation);
+ return result.value;
+}
+
+function assertSession(session, targetSha256, profileDigest) {
+ if (
+ session.provider.id !== "ghidra" ||
+ session.provider.version !== SUPPORTED_GHIDRA_VERSION ||
+ session.profile_digest !== profileDigest ||
+ session.target.sha256 !== targetSha256 ||
+ session.read_only !== true ||
+ session.analysis_complete !== true ||
+ session.analysis_timed_out !== false ||
+ session.capabilities.join(",") !== SESSION_CAPABILITIES.join(",")
+ )
+ throw new Error(
+ `Windows Ghidra session commitment drifted: ${JSON.stringify(session)}`,
+ );
+}
+
+function assertRuntimeCoordinates(runtime, targetSha256) {
+ if (
+ runtime.transport !== "authenticated-loopback-tcp" ||
+ runtime.target_sha256 !== targetSha256 ||
+ typeof runtime.runtime_root !== "string" ||
+ typeof runtime.endpoint_path !== "string" ||
+ typeof runtime.project_root !== "string" ||
+ typeof runtime.process_id !== "number"
+ )
+ throw new Error(
+ `Windows Ghidra runtime coordinates are incomplete: ${JSON.stringify(runtime)}`,
+ );
+}
+
+async function assertCleanup(runtime) {
+ for (const name of ["runtime_root", "endpoint_path", "project_root"]) {
+ const path = runtime[name];
+ if (typeof path === "string" && (await exists(path)))
+ throw new Error(`Windows Ghidra cleanup left ${name}: ${path}`);
+ }
+ if (
+ typeof runtime.process_id === "number" &&
+ processExists(runtime.process_id)
+ )
+ throw new Error(
+ `Windows Ghidra cleanup left process ${String(runtime.process_id)}`,
+ );
+}
+
+const isPage = (value) =>
+ typeof value === "object" &&
+ value !== null &&
+ !Array.isArray(value) &&
+ Array.isArray(value.items);
+
+const pageHasItems = (value) => isPage(value) && value.items.length > 0;
+
+async function exists(path) {
+ try {
+ await access(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function processExists(pid) {
+ try {
+ process.kill(pid, 0);
+ return true;
+ } catch (cause) {
+ return !(
+ cause instanceof Error &&
+ "code" in cause &&
+ cause.code === "ESRCH"
+ );
+ }
+}
diff --git a/scripts/verify-real-ghidra.mjs b/scripts/verify-real-ghidra.mjs
index 5b03d2edd..8bf4b7507 100644
--- a/scripts/verify-real-ghidra.mjs
+++ b/scripts/verify-real-ghidra.mjs
@@ -200,10 +200,18 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
try {
const started = await client.start();
if (!started.ok) throw started.error;
- assertSession(started.value, profile.value.profile.digest);
+ assertSession(
+ started.value,
+ profile.value.profile.digest,
+ parsedTarget.value.sha256,
+ );
const pinged = await client.ping();
if (!pinged.ok) throw pinged.error;
- assertSession(pinged.value, profile.value.profile.digest);
+ assertSession(
+ pinged.value,
+ profile.value.profile.digest,
+ parsedTarget.value.sha256,
+ );
runtimeCoordinates = client.diagnostics();
assertRuntimeCoordinates(runtimeCoordinates);
@@ -1027,11 +1035,12 @@ function symbolTail(value) {
return value.split("::").at(-1);
}
-function assertSession(session, expectedDigest) {
+function assertSession(session, expectedDigest, expectedTargetSha256) {
if (
session.provider.id !== "ghidra" ||
session.provider.version !== SUPPORTED_GHIDRA_VERSION ||
session.profile_digest !== expectedDigest ||
+ session.target.sha256 !== expectedTargetSha256 ||
session.read_only !== true ||
session.analysis_complete !== true ||
session.analysis_timed_out !== false ||
@@ -1045,7 +1054,7 @@ function assertSession(session, expectedDigest) {
}
function assertRuntimeCoordinates(coordinates) {
- for (const name of ["runtime_root", "socket_path", "project_root"])
+ for (const name of ["runtime_root", "endpoint_path", "project_root"])
if (typeof coordinates[name] !== "string")
throw new Error(`Ghidra verifier did not observe ${name}`);
if (typeof coordinates.process_id !== "number")
@@ -1053,7 +1062,7 @@ function assertRuntimeCoordinates(coordinates) {
}
async function assertCleanup(coordinates) {
- for (const name of ["runtime_root", "socket_path", "project_root"]) {
+ for (const name of ["runtime_root", "endpoint_path", "project_root"]) {
const path = coordinates[name];
if (typeof path === "string" && (await exists(path)))
throw new Error(`Ghidra cleanup left ${name}: ${path}`);
diff --git a/scripts/verify-windows-package.mjs b/scripts/verify-windows-package.mjs
new file mode 100644
index 000000000..48cd5b327
--- /dev/null
+++ b/scripts/verify-windows-package.mjs
@@ -0,0 +1,119 @@
+#!/usr/bin/env node
+
+import { execFile } from "node:child_process";
+import { access, mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { promisify } from "node:util";
+
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+
+import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
+
+const exec = promisify(execFile);
+const root = process.cwd();
+const workspace = await mkdtemp(join(tmpdir(), "rea-windows-package-"));
+const prefix = join(workspace, "prefix");
+
+try {
+ const packed = JSON.parse(
+ (
+ await npm(
+ ["pack", "--json", "--silent", "--pack-destination", workspace],
+ root,
+ )
+ ).stdout,
+ );
+ const packageResult = packed[0];
+ if (
+ packageResult === undefined ||
+ typeof packageResult.filename !== "string" ||
+ !Array.isArray(packageResult.files)
+ )
+ throw new Error(
+ `npm pack returned invalid metadata: ${JSON.stringify(packed)}`,
+ );
+ const packagedPaths = new Set(packageResult.files.map(({ path }) => path));
+ for (const required of [
+ "bridge/ghidra/ReaGhidraBridge.java",
+ "dist/main.js",
+ "dist/cli.js",
+ "scripts/rea.mjs",
+ ])
+ if (!packagedPaths.has(required))
+ throw new Error(`Windows package omitted ${required}`);
+
+ const tarball = join(workspace, packageResult.filename);
+ await access(tarball);
+ await npm(
+ ["install", "--no-package-lock", "--no-save", "--prefix", prefix, tarball],
+ workspace,
+ );
+ const entry = join(
+ prefix,
+ "node_modules",
+ "rea-agents",
+ "scripts",
+ "rea.mjs",
+ );
+ const environment = {
+ ...process.env,
+ REA_ANALYSIS_PROVIDER: "auto",
+ };
+ const help = await exec(process.execPath, [entry, "--help"], {
+ env: environment,
+ windowsHide: true,
+ });
+ if (!help.stdout.includes("open") || !help.stdout.includes("providers"))
+ throw new Error("Packaged Windows CLI help omitted analysis commands");
+
+ const server = new StdioClientTransport({
+ command: process.execPath,
+ args: [entry, "mcp"],
+ env: environment,
+ stderr: "pipe",
+ });
+ const client = new Client({ name: "rea-windows-package", version: "1.0.0" });
+ try {
+ await client.connect(server);
+ const catalog = await client.listTools();
+ const expected = TOOL_CONTRACTS.map(({ name }) => name).sort(
+ (left, right) => left.localeCompare(right),
+ );
+ const actual = catalog.tools
+ .map(({ name }) => name)
+ .sort((left, right) => left.localeCompare(right));
+ if (JSON.stringify(actual) !== JSON.stringify(expected))
+ throw new Error(
+ "Packaged Windows MCP catalog drifted from TOOL_CONTRACTS",
+ );
+ } finally {
+ await client.close();
+ }
+
+ process.stdout.write(
+ `${JSON.stringify({
+ ok: true,
+ platform: process.platform,
+ package: packageResult.filename,
+ tools: TOOL_CONTRACTS.length,
+ ghidra_bridge: "present",
+ })}\n`,
+ );
+} finally {
+ await rm(workspace, { recursive: true, force: true });
+}
+
+function npm(arguments_, cwd) {
+ const npmExecPath = process.env.npm_execpath;
+ return npmExecPath === undefined
+ ? exec(process.platform === "win32" ? "npm.cmd" : "npm", arguments_, {
+ cwd,
+ windowsHide: true,
+ })
+ : exec(process.execPath, [npmExecPath, ...arguments_], {
+ cwd,
+ windowsHide: true,
+ });
+}
diff --git a/tests/binaryTarget.test.ts b/tests/binaryTarget.test.ts
index aaa1dd1ee..82dac07db 100644
--- a/tests/binaryTarget.test.ts
+++ b/tests/binaryTarget.test.ts
@@ -189,21 +189,24 @@ describe("binary target I/O", () => {
expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
});
- it("rejects an app program symlink that leaves the bundle", async () => {
- directory = await mkdtemp(join(tmpdir(), "rea-target-"));
- const app = join(directory, "Escaping.app");
- const contents = join(app, "Contents");
- const programs = join(contents, "MacOS");
- const outside = join(directory, "outside");
- await mkdir(programs, { recursive: true });
- await writeFile(outside, thinMach(0xfeedfacf, 0x0100000c));
- await writeFile(
- join(contents, "Info.plist"),
- "CFBundleExecutableEscaping",
- );
- await symlink(outside, join(programs, "Escaping"));
- expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
- });
+ it.skipIf(process.platform === "win32")(
+ "rejects an app program symlink that leaves the bundle",
+ async () => {
+ directory = await mkdtemp(join(tmpdir(), "rea-target-"));
+ const app = join(directory, "Escaping.app");
+ const contents = join(app, "Contents");
+ const programs = join(contents, "MacOS");
+ const outside = join(directory, "outside");
+ await mkdir(programs, { recursive: true });
+ await writeFile(outside, thinMach(0xfeedfacf, 0x0100000c));
+ await writeFile(
+ join(contents, "Info.plist"),
+ "CFBundleExecutableEscaping",
+ );
+ await symlink(outside, join(programs, "Escaping"));
+ expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
+ },
+ );
it("honors an explicit database kind without relying on the file suffix", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-target-"));
diff --git a/tests/ghidraWindowsFixture.test.ts b/tests/ghidraWindowsFixture.test.ts
new file mode 100644
index 000000000..4e27080a9
--- /dev/null
+++ b/tests/ghidraWindowsFixture.test.ts
@@ -0,0 +1,37 @@
+import { execFile } from "node:child_process";
+import { createHash } from "node:crypto";
+import { readFile } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { promisify } from "node:util";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+import { parseExecutableHeader } from "../src/domain/binaryTarget.js";
+
+const exec = promisify(execFile);
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const generator = resolve(root, "scripts", "create-ghidra-windows-fixture.mjs");
+const fixture = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe");
+const expectedSha256 =
+ "732ec6083ae3b63bcbda33f2d0fd7e1a6539babade07cbdae4fc20d3394faf49";
+
+describe("controlled Windows Ghidra fixture", () => {
+ it("generates one deterministic native x86-64 PE application", async () => {
+ await exec(process.execPath, [generator]);
+ const bytes = await readFile(fixture);
+
+ expect(createHash("sha256").update(bytes).digest("hex")).toBe(
+ expectedSha256,
+ );
+ expect(parseExecutableHeader(bytes, "x64")).toMatchObject({
+ ok: true,
+ value: {
+ format: "pe",
+ architecture: "x86_64",
+ executableRole: "application",
+ managed: false,
+ },
+ });
+ });
+});
diff --git a/tests/realGhidraWindowsWorkflow.test.ts b/tests/realGhidraWindowsWorkflow.test.ts
new file mode 100644
index 000000000..6aca5b5de
--- /dev/null
+++ b/tests/realGhidraWindowsWorkflow.test.ts
@@ -0,0 +1,27 @@
+import { readFile } from "node:fs/promises";
+
+import { describe, expect, it } from "vitest";
+
+describe("real Windows Ghidra workflow trust boundary", () => {
+ it("runs only default-branch code on the fixed self-hosted environment", async () => {
+ const workflow = await readFile(
+ new URL("../.github/workflows/real-ghidra-windows.yml", import.meta.url),
+ "utf8",
+ );
+
+ expect(workflow).toContain("types: [real-ghidra-windows]");
+ expect(workflow).not.toContain("workflow_dispatch:");
+ expect(workflow).toContain("contents: read");
+ expect(workflow).toContain("if: github.ref == 'refs/heads/main'");
+ expect(workflow).toContain("environment: real-ghidra-windows");
+ expect(workflow).toContain(
+ "runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]",
+ );
+ expect(workflow).not.toMatch(
+ /\$\{\{\s*(?:inputs|github\.event\.(?:inputs|client_payload))\b/u,
+ );
+ expect(workflow).not.toContain("cache: npm");
+ expect(workflow).toContain("npm run verify:ghidra:windows");
+ expect(workflow).toContain("windows-ghidra-proof.log");
+ });
+});