Remove the unused React Native Markdown renderer. Pin safe YAML, shell quoting, tar, and cache dependencies; upgrade npm within the 11 series and apply upstream fixes to its hidden bundled Undici and cache copies. Add actual-consumer security regressions and an opt-in real npm/ACP upgrade verifier. Typechecks, Web build, Android APK/signature checks, and 2,997 tests passed (23 skipped). Real old-to-new npm installs, ACP initialize, rollback, restart, and fallback retention passed under the desktop Electron runtime. Android USB-device validation and full desktop automatic-update release gates remain pending. Dependency audit is reduced from 85 to 74 unique advisories; this is not a complete security clearance.
463 lines
19 KiB
JavaScript
463 lines
19 KiB
JavaScript
/**
|
|
* ACP Adapter Installer Real End-to-End Cross-Version Verification (Linux x86_64)
|
|
*
|
|
* Exercises an authentic old-to-new upgrade lifecycle using official Google Antigravity
|
|
* binary distributions:
|
|
* - Antigravity v1.2.1 (333,590,110 bytes archive, ~1.05 GB uncompressed)
|
|
* - Antigravity v1.3.0 (333,727,150 bytes archive, ~1.05 GB uncompressed)
|
|
*
|
|
* Verifies:
|
|
* 1. Fresh installation of v1.2.1 with real archive extraction and live stdio ACP handshake.
|
|
* 2. Real rollback protection when an update to v1.3.0 fails validation, confirming that
|
|
* v1.2.1 remains intact and operable, and temporary staging directories are unlinked.
|
|
* 3. Real cross-version upgrade to v1.3.0 with live ACP handshake validation, atomic swap,
|
|
* manifest persistence, and immediate fallback retention (v1.2.1 kept on disk).
|
|
* 4. Version pruning lifecycle, confirming retention of active and fallback versions.
|
|
* 5. Official archives exceed the former 300 MiB download limit; extracted payloads remain
|
|
* below the former 1 GiB limit, with the 2 GiB limit providing room for future releases.
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { createReadStream, existsSync, mkdtempSync, statSync } from "node:fs";
|
|
import { mkdir, open, readFile, readdir, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { Readable } from "node:stream";
|
|
import { createAcpAdapterManager } from "../apps/desktop/src/main/acp-adapter-manager.mjs";
|
|
|
|
const OFFICIAL_RELEASES = {
|
|
"1.2.1": {
|
|
id: "antigravity-acp",
|
|
version: "1.2.1",
|
|
distribution: {
|
|
binary: {
|
|
"linux-x86_64": {
|
|
archive: "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.2.1-linux-x86_64.zip",
|
|
cmd: "./agy_acp_server.par",
|
|
args: ["--uid="],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"1.3.0": {
|
|
id: "antigravity-acp",
|
|
version: "1.3.0",
|
|
distribution: {
|
|
binary: {
|
|
"linux-x86_64": {
|
|
archive: "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.3.0-linux-x86_64.zip",
|
|
cmd: "./agy_acp_server.par",
|
|
args: ["--uid="],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Executes a real ACP protocol handshake over stdio against an agy_acp_server binary.
|
|
*/
|
|
export async function executeRealAcpHandshake(commandSpec, { timeoutMs = 25_000 } = {}) {
|
|
const binaryPath = commandSpec.command;
|
|
const args = commandSpec.args ?? [];
|
|
|
|
if (!existsSync(binaryPath)) {
|
|
throw new Error(`binary_not_found: ${binaryPath}`);
|
|
}
|
|
|
|
const child = spawn(binaryPath, args, {
|
|
env: { ...process.env, ...commandSpec.env },
|
|
stdio: ["pipe", "pipe", "ignore"],
|
|
windowsHide: true,
|
|
shell: false,
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let resolved = false;
|
|
let stdoutBuffer = "";
|
|
|
|
const timer = setTimeout(() => {
|
|
cleanup(new Error(`acp_handshake_timeout (${timeoutMs}ms)`));
|
|
}, timeoutMs);
|
|
timer.unref?.();
|
|
|
|
function cleanup(error) {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
try { child.stdin?.destroy(); } catch {}
|
|
try { child.kill("SIGTERM"); } catch {}
|
|
const killTimer = setTimeout(() => {
|
|
try { child.kill("SIGKILL"); } catch {}
|
|
}, 1000);
|
|
killTimer.unref?.();
|
|
if (error) reject(error);
|
|
}
|
|
|
|
child.on("error", (err) => cleanup(err));
|
|
child.on("exit", (code) => {
|
|
if (!resolved) {
|
|
cleanup(new Error(`process_exited_prematurely (code=${code})`));
|
|
}
|
|
});
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
stdoutBuffer += chunk.toString();
|
|
const lines = stdoutBuffer.split("\n");
|
|
stdoutBuffer = lines.pop() ?? "";
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
try {
|
|
const message = JSON.parse(trimmed);
|
|
if (message.id === 1 && message.result) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
try { child.stdin?.destroy(); } catch {}
|
|
try { child.kill("SIGTERM"); } catch {}
|
|
resolve({
|
|
state: "available",
|
|
protocolVersion: message.result.protocolVersion,
|
|
promptCapabilities: message.result.agentCapabilities?.promptCapabilities ?? {},
|
|
authMethods: message.result.authMethods ?? [],
|
|
rawResult: message.result,
|
|
});
|
|
return;
|
|
}
|
|
} catch {
|
|
// Non-JSON or partial line, keep reading
|
|
}
|
|
}
|
|
});
|
|
|
|
// Send ACP initialize request
|
|
const initRequest = JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "initialize",
|
|
params: {
|
|
protocolVersion: 1,
|
|
clientCapabilities: {
|
|
fs: { readTextFile: false, writeTextFile: false },
|
|
terminal: false,
|
|
},
|
|
clientInfo: { name: "edgeever-e2e-verifier", version: "1.100.0" },
|
|
},
|
|
}) + "\n";
|
|
|
|
child.stdin.write(initRequest);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Downloads a file to destination if not already present, computing SHA-256 and size.
|
|
*/
|
|
async function ensureArtifactCached(url, cachePath, { log = console.log } = {}) {
|
|
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
|
|
if (existsSync(cachePath)) {
|
|
const stat = statSync(cachePath);
|
|
if (stat.size > 100 * 1024 * 1024) {
|
|
log(` Using cached artifact: ${path.basename(cachePath)} (${(stat.size / 1024 / 1024).toFixed(2)} MiB)`);
|
|
return stat.size;
|
|
}
|
|
}
|
|
|
|
log(` Downloading ${url} -> ${cachePath}...`);
|
|
const response = await fetch(url);
|
|
if (!response.ok || !response.body) {
|
|
throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
|
|
}
|
|
|
|
const handle = await open(cachePath, "w", 0o600);
|
|
let bytesWritten = 0;
|
|
try {
|
|
for await (const chunk of Readable.fromWeb(response.body)) {
|
|
await handle.write(chunk, 0, chunk.length);
|
|
bytesWritten += chunk.length;
|
|
}
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
|
|
log(` Downloaded ${path.basename(cachePath)}: ${(bytesWritten / 1024 / 1024).toFixed(2)} MiB`);
|
|
return bytesWritten;
|
|
}
|
|
|
|
/**
|
|
* Calculates SHA-256 digest of a file.
|
|
*/
|
|
async function computeSha256(filePath) {
|
|
const hash = createHash("sha256");
|
|
for await (const chunk of createReadStream(filePath)) {
|
|
hash.update(chunk);
|
|
}
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
/**
|
|
* Runs the full real Linux E2E verification workflow.
|
|
*/
|
|
export async function runRealAcpAdapterCrossVersionVerification({
|
|
cacheDir = path.join(tmpdir(), "edgeever-e2e-cache"),
|
|
log = console.log,
|
|
} = {}) {
|
|
assert.equal(process.platform, "linux", "Verification requires Linux host");
|
|
assert.equal(process.arch, "x64", "Verification requires x86_64 CPU architecture");
|
|
|
|
const startTime = Date.now();
|
|
const root = mkdtempSync(path.join(tmpdir(), "edgeever-acp-real-e2e-"));
|
|
const record = {
|
|
platform: "linux",
|
|
arch: "x64",
|
|
storeRoot: root,
|
|
cacheDir,
|
|
phases: [],
|
|
artifacts: {},
|
|
};
|
|
|
|
log("=================================================================");
|
|
log("ACP Adapter Installer Real Linux Cross-Version Verification");
|
|
log("=================================================================");
|
|
log(`Store Root: ${root}`);
|
|
log(`Cache Dir: ${cacheDir}\n`);
|
|
|
|
try {
|
|
// Step 0: Ensure official artifacts are downloaded
|
|
log("[0/4] Downloading / verifying official distribution archives from Google CDN...");
|
|
const url121 = OFFICIAL_RELEASES["1.2.1"].distribution.binary["linux-x86_64"].archive;
|
|
const url130 = OFFICIAL_RELEASES["1.3.0"].distribution.binary["linux-x86_64"].archive;
|
|
const cache121 = path.join(cacheDir, "agy-acp-server-1.2.1-linux-x86_64.zip");
|
|
const cache130 = path.join(cacheDir, "agy-acp-server-1.3.0-linux-x86_64.zip");
|
|
|
|
const size121 = await ensureArtifactCached(url121, cache121, { log });
|
|
const size130 = await ensureArtifactCached(url130, cache130, { log });
|
|
|
|
const sha121 = await computeSha256(cache121);
|
|
const sha130 = await computeSha256(cache130);
|
|
|
|
record.artifacts["1.2.1"] = { url: url121, sizeBytes: size121, sha256: sha121 };
|
|
record.artifacts["1.3.0"] = { url: url130, sizeBytes: size130, sha256: sha130 };
|
|
|
|
log(` Artifact v1.2.1: ${size121} bytes (~${(size121 / 1024 / 1024).toFixed(2)} MiB), SHA256: ${sha121}`);
|
|
log(` Artifact v1.3.0: ${size130} bytes (~${(size130 / 1024 / 1024).toFixed(2)} MiB), SHA256: ${sha130}\n`);
|
|
|
|
// Setup custom fetchImpl that streams the real cached zip bytes and serves registry entries
|
|
let activeRegistryVersion = "1.2.1";
|
|
|
|
const customFetch = async (url) => {
|
|
if (url.endsWith(".zip")) {
|
|
const is121 = url.includes("1.2.1");
|
|
const filePath = is121 ? cache121 : cache130;
|
|
const fileSize = is121 ? size121 : size130;
|
|
const nodeStream = createReadStream(filePath);
|
|
return new Response(Readable.toWeb(nodeStream), {
|
|
status: 200,
|
|
headers: { "content-length": String(fileSize) },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ agents: [OFFICIAL_RELEASES[activeRegistryVersion]] }), {
|
|
status: 200,
|
|
});
|
|
};
|
|
|
|
const manager = createAcpAdapterManager({
|
|
root,
|
|
platform: "linux",
|
|
arch: "x64",
|
|
fetchImpl: customFetch,
|
|
});
|
|
|
|
const versionRoot = path.join(root, "antigravity");
|
|
|
|
// Phase 1: Real Baseline Install (v1.2.1)
|
|
log("[1/4] Phase 1: Installing official Antigravity v1.2.1 distribution...");
|
|
let p1ValidationPayload = null;
|
|
const install121Result = await manager.install("antigravity", async (command) => {
|
|
log(` Validating staged v1.2.1 command: ${command.command} ${command.args.join(" ")}`);
|
|
assert.ok(command.command.includes(".stage-1.2.1"), "Staged binary path must be inspected during validation");
|
|
assert.deepEqual(command.args, ["--uid="], "Linux arguments must include --uid=");
|
|
const handshake = await executeRealAcpHandshake(command);
|
|
log(` Real ACP handshake on staged v1.2.1 SUCCESS! Capabilities:`, JSON.stringify(handshake.promptCapabilities));
|
|
p1ValidationPayload = handshake;
|
|
return handshake;
|
|
});
|
|
|
|
assert.equal(install121Result.updated, true, "v1.2.1 install must report updated: true");
|
|
assert.equal(install121Result.version, "1.2.1", "Installed version must be 1.2.1");
|
|
|
|
const installed121 = manager.get("antigravity");
|
|
assert.ok(installed121, "Manager must return installed antigravity entry");
|
|
assert.equal(installed121.version, "1.2.1");
|
|
const binary121 = path.join(versionRoot, "1.2.1", "agy_acp_server.par");
|
|
assert.equal(installed121.command.command, binary121);
|
|
assert.ok(existsSync(binary121), "v1.2.1 binary must exist on disk");
|
|
|
|
const statBinary121 = statSync(binary121);
|
|
const statHarness121 = statSync(path.join(versionRoot, "1.2.1", "localharness_external"));
|
|
const totalExtracted121 = statBinary121.size + statHarness121.size;
|
|
log(` v1.2.1 files extracted: agy_acp_server.par (${(statBinary121.size / 1024 / 1024).toFixed(2)} MiB), localharness_external (${(statHarness121.size / 1024 / 1024).toFixed(2)} MiB)`);
|
|
log(` Total extracted bytes: ${totalExtracted121} bytes (~${(totalExtracted121 / 1024 / 1024 / 1024).toFixed(2)} GiB, below the former 1 GiB cap)`);
|
|
|
|
// Verify ACP handshake against committed install path
|
|
const liveHandshake121 = await executeRealAcpHandshake(installed121.command);
|
|
assert.equal(liveHandshake121.state, "available");
|
|
log(` Verified committed v1.2.1 ACP handshake operational.\n`);
|
|
|
|
record.phases.push({
|
|
phase: 1,
|
|
name: "Fresh Install v1.2.1",
|
|
version: "1.2.1",
|
|
extractedBytes: totalExtracted121,
|
|
handshake: p1ValidationPayload,
|
|
});
|
|
|
|
// Phase 2: Failed Upgrade to v1.3.0 & Rollback Verification
|
|
log("[2/4] Phase 2: Simulating failed upgrade to v1.3.0 and verifying rollback behavior...");
|
|
activeRegistryVersion = "1.3.0";
|
|
|
|
let updateFailedThrew = false;
|
|
let stageDirObserved = null;
|
|
|
|
try {
|
|
await manager.install("antigravity", async (command) => {
|
|
log(` Validation callback triggered for staged v1.3.0: ${command.command}`);
|
|
assert.ok(command.command.includes(".stage-1.3.0"), "Must stage v1.3.0 binary before commit");
|
|
stageDirObserved = path.dirname(command.command);
|
|
assert.ok(existsSync(stageDirObserved), "Stage directory must exist during validation");
|
|
// Verify staged files are real 1.3.0 binaries
|
|
const statStaged = statSync(command.command);
|
|
assert.ok(statStaged.size > 800 * 1024 * 1024, "Staged binary must be real >800 MiB binary");
|
|
// Intentionally reject validation to test rollback
|
|
log(` Intentionally rejecting validation to test atomic rollback...`);
|
|
return { state: "failed", detail: "simulated_handshake_rejection" };
|
|
}, { updateOnly: true });
|
|
} catch (error) {
|
|
updateFailedThrew = true;
|
|
assert.equal(error?.message, "simulated_handshake_rejection");
|
|
}
|
|
|
|
assert.ok(updateFailedThrew, "manager.install must reject when validation fails");
|
|
|
|
// Verify rollback guarantees
|
|
const managerStateAfterFailure = manager.get("antigravity");
|
|
assert.equal(managerStateAfterFailure?.version, "1.2.1", "Manager must retain active version 1.2.1");
|
|
assert.equal(managerStateAfterFailure?.command.command, binary121, "Active command must point to v1.2.1");
|
|
|
|
const manifestAfterFailure = JSON.parse(await readFile(path.join(root, "installed.json"), "utf8"));
|
|
assert.equal(manifestAfterFailure.antigravity?.version, "1.2.1", "installed.json must preserve v1.2.1");
|
|
assert.ok(existsSync(binary121), "v1.2.1 binary must remain intact");
|
|
|
|
// Verify staged directory was cleanly purged
|
|
const versionDirContents = await readdir(versionRoot);
|
|
const lingeringStages = versionDirContents.filter((name) => name.startsWith(".stage-"));
|
|
assert.equal(lingeringStages.length, 0, `No staging directories must remain, found: ${lingeringStages.join(", ")}`);
|
|
|
|
// Verify that the original v1.2.1 adapter still performs live ACP handshake
|
|
const fallbackHandshake = await executeRealAcpHandshake(managerStateAfterFailure.command);
|
|
assert.equal(fallbackHandshake.state, "available", "v1.2.1 adapter must remain fully functional after aborted update");
|
|
log(` Rollback protection verified: v1.2.1 retained, staging artifacts purged, and adapter functional.\n`);
|
|
|
|
record.phases.push({
|
|
phase: 2,
|
|
name: "Failed Upgrade Rollback",
|
|
targetVersion: "1.3.0",
|
|
retainedVersion: "1.2.1",
|
|
stagePurged: true,
|
|
fallbackHandshakeSuccess: true,
|
|
});
|
|
|
|
// Phase 3: Successful Cross-Version Upgrade to v1.3.0
|
|
log("[3/4] Phase 3: Executing successful cross-version upgrade to official Antigravity v1.3.0...");
|
|
let p3ValidationPayload = null;
|
|
|
|
const update130Result = await manager.install("antigravity", async (command) => {
|
|
log(` Validating staged v1.3.0 command: ${command.command}`);
|
|
assert.ok(command.command.includes(".stage-1.3.0"));
|
|
assert.deepEqual(command.args, ["--uid="]);
|
|
const handshake = await executeRealAcpHandshake(command);
|
|
log(` Real ACP handshake on staged v1.3.0 SUCCESS! Protocol Version: ${handshake.protocolVersion}, Capabilities:`, JSON.stringify(handshake.promptCapabilities));
|
|
p3ValidationPayload = handshake;
|
|
return handshake;
|
|
}, { updateOnly: true });
|
|
|
|
assert.equal(update130Result.updated, true, "Update must report updated: true");
|
|
assert.equal(update130Result.version, "1.3.0", "Update version must be 1.3.0");
|
|
|
|
const installed130 = manager.get("antigravity");
|
|
assert.equal(installed130?.version, "1.3.0", "Active version must now be 1.3.0");
|
|
const binary130 = path.join(versionRoot, "1.3.0", "agy_acp_server.par");
|
|
assert.equal(installed130?.command.command, binary130, "Active command must point to 1.3.0 binary");
|
|
assert.ok(existsSync(binary130), "v1.3.0 binary must exist on disk");
|
|
|
|
const statBinary130 = statSync(binary130);
|
|
const statHarness130 = statSync(path.join(versionRoot, "1.3.0", "localharness_external"));
|
|
const totalExtracted130 = statBinary130.size + statHarness130.size;
|
|
log(` v1.3.0 files extracted: agy_acp_server.par (${(statBinary130.size / 1024 / 1024).toFixed(2)} MiB), localharness_external (${(statHarness130.size / 1024 / 1024).toFixed(2)} MiB)`);
|
|
log(` Total extracted bytes: ${totalExtracted130} bytes (~${(totalExtracted130 / 1024 / 1024 / 1024).toFixed(2)} GiB)`);
|
|
|
|
// Verify installed.json
|
|
const manifestUpdated = JSON.parse(await readFile(path.join(root, "installed.json"), "utf8"));
|
|
assert.equal(manifestUpdated.antigravity?.version, "1.3.0", "installed.json must record v1.3.0");
|
|
|
|
// Verify immediate fallback retention: v1.2.1 directory must still be intact on disk
|
|
assert.ok(existsSync(binary121), "Previous v1.2.1 binary must be retained alongside v1.3.0 as immediate rollback fallback");
|
|
|
|
// Verify live ACP handshake against updated v1.3.0 adapter
|
|
const liveHandshake130 = await executeRealAcpHandshake(installed130.command);
|
|
assert.equal(liveHandshake130.state, "available");
|
|
log(` Verified committed v1.3.0 ACP handshake operational.\n`);
|
|
|
|
record.phases.push({
|
|
phase: 3,
|
|
name: "Successful Upgrade v1.3.0",
|
|
version: "1.3.0",
|
|
extractedBytes: totalExtracted130,
|
|
fallbackRetained: true,
|
|
handshake: p3ValidationPayload,
|
|
});
|
|
|
|
// Phase 4: Prune Lifecycle & Cleanup
|
|
log("[4/4] Phase 4: Verifying prune lifecycle and cleanup...");
|
|
await manager.prune();
|
|
|
|
assert.ok(existsSync(path.join(versionRoot, "1.3.0")), "Active v1.3.0 version must be kept");
|
|
assert.ok(existsSync(path.join(versionRoot, "1.2.1")), "Immediate previous v1.2.1 fallback must be kept");
|
|
|
|
const postPruneStages = (await readdir(versionRoot)).filter((name) => name.startsWith(".stage-"));
|
|
assert.equal(postPruneStages.length, 0, "No stage directories after prune");
|
|
|
|
// Final live probe
|
|
const finalHandshake = await executeRealAcpHandshake(manager.get("antigravity").command);
|
|
assert.equal(finalHandshake.state, "available");
|
|
log(` Pruning complete: v1.3.0 and v1.2.1 retained, stage clean, adapter verified.\n`);
|
|
|
|
const elapsedMs = Date.now() - startTime;
|
|
log("=================================================================");
|
|
log(`ACP Adapter Real Linux Cross-Version Verification: PASSED (${(elapsedMs / 1000).toFixed(2)}s)`);
|
|
log("=================================================================");
|
|
|
|
record.elapsedMs = elapsedMs;
|
|
record.success = true;
|
|
return record;
|
|
} finally {
|
|
log(`Cleaning up test root: ${root}`);
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
if (import.meta.main || process.argv[1] === new URL(import.meta.url).pathname) {
|
|
runRealAcpAdapterCrossVersionVerification()
|
|
.then((record) => {
|
|
console.log("\n[VERIFICATION SUMMARY JSON]");
|
|
console.log(JSON.stringify(record, null, 2));
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
console.error("\n[REAL E2E VERIFICATION FAILED]:", error);
|
|
process.exit(1);
|
|
});
|
|
}
|