// One-off dependency build, not part of normal app packaging. After native // Windows verification, publish these bytes and pin their digest in the app. import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { executableTarget } from "./prepare-cloudflared.mjs"; import { bundleInventory, releaseBytes } from "./prepare-browser.mjs"; export const WINDOWS_VENDOR_VERSION = "0.36.0-omb.2"; export const WINDOWS_VENDOR_TARGET = "x86_64-pc-windows-gnu"; export const WINDOWS_VENDOR_SOURCE = { commit: "eb05921bad874cd2a1b4fa5d1149f1ed26576cae", asset: "agent-browser-eb05921bad874cd2a1b4fa5d1149f1ed26576cae.tar.gz", url: "https://codeload.github.com/vercel-labs/agent-browser/tar.gz/eb05921bad874cd2a1b4fa5d1149f1ed26576cae", bytes: 1904718, sha256: "ed24a72a5260d9c1ea454cd849c44159bac570a6939ac645e4c5bdb98a421646", }; export const WINDOWS_VENDOR_PATCH_SHA256 = "a08f3fdfca63db5358c5354716e4216c365d1d37c08b5f32215062f937c35819"; export const WINDOWS_VENDOR_RUST = "1.97.1"; export const WINDOWS_VENDOR_PNPM = "11.1.3"; const repository = fileURLToPath(new URL("../", import.meta.url)); const patchPath = join(repository, "third_party/browser/agent-browser-windows-stdio.patch"); const digest = (bytes) => createHash("sha256").update(bytes).digest("hex"); export function parseVendorBuildArgs(args) { if (args.length !== 2 || args[0] !== "--output" || !isAbsolute(args[1])) { throw new Error("Usage: node scripts/build-windows-browser-vendor.mjs --output ABSOLUTE_NEW_DIRECTORY"); } return resolve(args[1]); } export function verifyVendorPatch(bytes) { assert.equal(digest(bytes), WINDOWS_VENDOR_PATCH_SHA256, "Windows vendor patch failed pinned SHA-256 verification"); } // Candidate artifacts are tested before their executable digest is committed // to the normal release resolver. This verifies build identity and transport // integrity, not publication approval; normal packaging never uses this path. export function verifyVendorCandidate(provenance, bytes) { assert.equal(provenance.version, WINDOWS_VENDOR_VERSION); assert.equal(provenance.target, WINDOWS_VENDOR_TARGET); assert.deepEqual(provenance.source, WINDOWS_VENDOR_SOURCE); assert.equal(provenance.patch.sha256, WINDOWS_VENDOR_PATCH_SHA256); assert.equal(provenance.executable.asset, "agent-browser-win32-x64.exe"); assert.equal(provenance.executable.bytes, bytes.length, "Candidate executable size differs from build provenance"); assert.equal(provenance.executable.sha256, digest(bytes), "Candidate executable SHA-256 differs from build provenance"); } function command(file, args, cwd, { input, capture = false } = {}) { const result = spawnSync(file, args, { cwd, input, encoding: "utf8", timeout: 30 * 60_000, stdio: capture || input !== undefined ? ["pipe", "pipe", "pipe"] : "inherit", env: { ...process.env, NEXT_TELEMETRY_DISABLED: "1", CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: "x86_64-w64-mingw32-gcc" }, }); if (result.error || result.status !== 0) throw new Error(`${file} ${args.join(" ")} failed: ${result.error?.message ?? result.stderr ?? result.status}`); return result.stdout?.trim() ?? ""; } export async function buildWindowsBrowserVendor(output) { assert.equal(process.platform, "linux", "Build this dependency on the reviewed Linux GNU cross-compiler runner"); assert.equal(process.arch, "x64", "The reviewed build runner is Linux x64"); assert(Number(process.versions.node.split(".")[0]) >= 24, "Node 24 or later is required"); assert(isAbsolute(output) && !existsSync(output), "Output must be a new absolute directory; existing artifacts are never overwritten"); const patch = readFileSync(patchPath); verifyVendorPatch(patch); const rust = command("rustc", ["--version"], repository, { capture: true }); const cargo = command("cargo", ["--version"], repository, { capture: true }); const mingw = command("x86_64-w64-mingw32-gcc", ["--version"], repository, { capture: true }).split("\n")[0]; const linker = command("x86_64-w64-mingw32-ld", ["--version"], repository, { capture: true }).split("\n")[0]; assert(rust.startsWith(`rustc ${WINDOWS_VENDOR_RUST} `), `Expected Rust ${WINDOWS_VENDOR_RUST}, got ${rust}`); assert(cargo.startsWith(`cargo ${WINDOWS_VENDOR_RUST} `), `Expected Cargo ${WINDOWS_VENDOR_RUST}, got ${cargo}`); const scratch = mkdtempSync(join(tmpdir(), "omb-browser-vendor-")); try { const source = join(scratch, "source"); mkdirSync(source); const archive = join(scratch, WINDOWS_VENDOR_SOURCE.asset); writeFileSync(archive, await releaseBytes(WINDOWS_VENDOR_SOURCE)); command("tar", ["-xzf", archive, "--strip-components=1", "-C", source], scratch); command("git", ["apply", "--check", "-"], source, { input: patch }); command("git", ["apply", "--whitespace=error-all", "-"], source, { input: patch }); const pnpm = command("pnpm", ["--version"], source, { capture: true }); assert.equal(pnpm, WINDOWS_VENDOR_PNPM, `Expected pnpm ${WINDOWS_VENDOR_PNPM}`); // Keep the real upstream dashboard: build.rs otherwise silently embeds a // placeholder. The frozen pnpm lock and Cargo --locked retain dependencies. command("pnpm", ["install", "--frozen-lockfile", "--ignore-scripts"], source); command("pnpm", ["--filter", "dashboard", "build"], source); const dashboard = join(source, "packages/dashboard/out"); assert(!readFileSync(join(dashboard, "index.html"), "utf8").includes("Dashboard not built")); command("cargo", ["build", "--locked", "--release", "--manifest-path", "cli/Cargo.toml", "--target", WINDOWS_VENDOR_TARGET], source); const binary = join(source, "cli/target", WINDOWS_VENDOR_TARGET, "release/agent-browser.exe"); const bytes = readFileSync(binary); assert.equal(executableTarget(bytes), "win32-x64", "Vendor build did not produce a Windows x64 executable"); mkdirSync(output, { recursive: true }); const asset = "agent-browser-win32-x64.exe"; copyFileSync(binary, join(output, asset)); const testArtifacts = command("cargo", ["test", "--bin", "agent-browser", "--locked", "--release", "--no-run", "--message-format=json", "--manifest-path", "cli/Cargo.toml", "--target", WINDOWS_VENDOR_TARGET], source, { capture: true }) .split("\n").map((line) => JSON.parse(line)).filter((entry) => entry.reason === "compiler-artifact" && entry.profile?.test && entry.executable); assert.equal(testArtifacts.length, 1, "Expected one native vendor regression-test binary"); copyFileSync(testArtifacts[0].executable, join(output, "agent-browser-tests.exe")); for (const [from, to] of [ ["LICENSE", "agent-browser-LICENSE.txt"], ["cli/src/native/a11y/LICENSE-axe-core.txt", "LICENSE-axe-core.txt"], ["cli/src/native/a11y/LICENSE-axe-core-THIRD-PARTY.txt", "LICENSE-axe-core-THIRD-PARTY.txt"], ]) copyFileSync(join(source, from), join(output, to)); copyFileSync(patchPath, join(output, "agent-browser-windows-stdio.patch")); copyFileSync(join(repository, "third_party/browser/README.md"), join(output, "README.md")); const provenance = { schemaVersion: 1, version: WINDOWS_VENDOR_VERSION, target: WINDOWS_VENDOR_TARGET, source: WINDOWS_VENDOR_SOURCE, patch: { upstream: "https://github.com/vercel-labs/agent-browser/pull/1781", commit: "81a98c349d04195396ffae6898bd375ad64280de", sha256: WINDOWS_VENDOR_PATCH_SHA256, scope: "Windows handle inheritance and console-free Chrome children; no MCP reader rewrite" }, toolchain: { rust, cargo, pnpm, node: process.version, mingw, linker }, cargoLockSha256: digest(readFileSync(join(source, "cli/Cargo.lock"))), pnpmLockSha256: digest(readFileSync(join(source, "pnpm-lock.yaml"))), dashboardInventorySha256: digest(JSON.stringify(bundleInventory(dashboard))), executable: { asset, bytes: bytes.length, sha256: digest(bytes) }, }; writeFileSync(join(output, "provenance.json"), `${JSON.stringify(provenance, null, 2)}\n`); console.log(JSON.stringify(provenance, null, 2)); return provenance; } finally { rmSync(scratch, { recursive: true, force: true }); } } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { await buildWindowsBrowserVendor(parseVendorBuildArgs(process.argv.slice(2))); }