chore: import upstream snapshot with attribution
Release / Check for new version (push) Has been cancelled
Release / Build macOS ARM64 (push) Has been cancelled
Release / Build macOS x64 (push) Has been cancelled
Release / Build Linux ARM64 (push) Has been cancelled
Release / Build Linux musl ARM64 (push) Has been cancelled
Release / Build Linux musl x64 (push) Has been cancelled
Release / Build Linux x64 (push) Has been cancelled
Release / Build Windows x64 (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish sandbox package to npm (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
CI / Rust (windows-latest - x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Native E2E Tests (push) Has been cancelled
CI / Windows Integration Test (push) Has been cancelled
CI / Global Install (macos-latest) (push) Has been cancelled
CI / Global Install (ubuntu-latest) (push) Has been cancelled
CI / Version Sync Check (push) Has been cancelled
CI / Rust (push) Has been cancelled
CI / Dashboard (push) Has been cancelled
CI / Sandbox Package (push) Has been cancelled
CI / Rust (macos-latest - aarch64-apple-darwin) (push) Has been cancelled
CI / Rust (macos-latest - x86_64-apple-darwin) (push) Has been cancelled
CI / Global Install (windows-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:35:58 +08:00
commit cb15c5e0d8
413 changed files with 176862 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
set -euo pipefail
# Build agent-browser for all platforms using Docker
# Usage: ./scripts/build-all-platforms.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
OUTPUT_DIR="$PROJECT_ROOT/bin"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}Building agent-browser for all platforms...${NC}"
echo ""
# Ensure output directory exists
mkdir -p "$OUTPUT_DIR"
# Build the Docker image if needed
echo -e "${YELLOW}Building Docker cross-compilation image...${NC}"
docker build --platform linux/amd64 -t agent-browser-builder -f "$PROJECT_ROOT/docker/Dockerfile.build" "$PROJECT_ROOT"
# Function to build for a target
build_target() {
local rust_target=$1
local build_target=$2
local output_name=$3
echo -e "${YELLOW}Building for ${build_target}...${NC}"
rm -f "$OUTPUT_DIR/$output_name"
docker run --rm \
--platform linux/amd64 \
-v "$PROJECT_ROOT/cli:/build" \
-v "$OUTPUT_DIR:/output" \
agent-browser-builder \
-c "set -euo pipefail
cargo zigbuild --release --target ${build_target}
source_path=/build/target/${rust_target}/release/agent-browser
if [ -f \"\$source_path.exe\" ]; then
source_path=\"\$source_path.exe\"
fi
cp \"\$source_path\" /output/${output_name}
chmod +x /output/${output_name} 2>/dev/null || true"
if [ -f "$OUTPUT_DIR/$output_name" ]; then
echo -e "${GREEN}✓ Built ${output_name}${NC}"
else
echo -e "${RED}✗ Failed to build ${output_name}${NC}"
return 1
fi
}
# Build for each platform
# Linux x64
build_target "x86_64-unknown-linux-gnu" "x86_64-unknown-linux-gnu.2.28" "agent-browser-linux-x64"
# Linux ARM64
build_target "aarch64-unknown-linux-gnu" "aarch64-unknown-linux-gnu.2.28" "agent-browser-linux-arm64"
# Windows x64
build_target "x86_64-pc-windows-gnu" "x86_64-pc-windows-gnu" "agent-browser-win32-x64.exe"
# macOS x64 (via zig for cross-compilation)
build_target "x86_64-apple-darwin" "x86_64-apple-darwin" "agent-browser-darwin-x64"
# macOS ARM64 (via zig for cross-compilation)
build_target "aarch64-apple-darwin" "aarch64-apple-darwin" "agent-browser-darwin-arm64"
# Linux musl x64 (Alpine)
build_target "x86_64-unknown-linux-musl" "x86_64-unknown-linux-musl" "agent-browser-linux-musl-x64"
# Linux musl ARM64 (Alpine)
build_target "aarch64-unknown-linux-musl" "aarch64-unknown-linux-musl" "agent-browser-linux-musl-arm64"
echo ""
echo -e "${GREEN}Build complete!${NC}"
echo ""
echo "Binaries are in: $OUTPUT_DIR"
ls -la "$OUTPUT_DIR"/agent-browser-*
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env node
/**
* Verifies that package.json and cli/Cargo.toml have the same version.
* Used in CI to catch version drift.
*/
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
// Read package.json version
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
const packageVersion = packageJson.version;
// Read Cargo.toml version
const cargoToml = readFileSync(join(rootDir, 'cli/Cargo.toml'), 'utf-8');
const cargoVersionMatch = cargoToml.match(/^version\s*=\s*"([^"]*)"/m);
if (!cargoVersionMatch) {
console.error('Could not find version in cli/Cargo.toml');
process.exit(1);
}
const cargoVersion = cargoVersionMatch[1];
// Read dashboard package.json version
const dashboardPkg = JSON.parse(readFileSync(join(rootDir, 'packages/dashboard/package.json'), 'utf-8'));
const dashboardVersion = dashboardPkg.version;
// Read sandbox package versions
const sandboxPkg = JSON.parse(readFileSync(join(rootDir, 'packages/@agent-browser/sandbox/package.json'), 'utf-8'));
const sandboxVersion = sandboxPkg.version;
const sandboxVersionSource = readFileSync(join(rootDir, 'packages/@agent-browser/sandbox/src/version.ts'), 'utf-8');
const sandboxVersionMatch = sandboxVersionSource.match(/AGENT_BROWSER_SANDBOX_VERSION\s*=\s*"([^"]*)"/);
if (!sandboxVersionMatch) {
console.error('Could not find AGENT_BROWSER_SANDBOX_VERSION in packages/@agent-browser/sandbox/src/version.ts');
process.exit(1);
}
const sandboxRuntimeVersion = sandboxVersionMatch[1];
const mismatches = [];
if (packageVersion !== cargoVersion) {
mismatches.push(` cli/Cargo.toml: ${cargoVersion}`);
}
if (packageVersion !== dashboardVersion) {
mismatches.push(` packages/dashboard: ${dashboardVersion}`);
}
if (packageVersion !== sandboxVersion) {
mismatches.push(` packages/@agent-browser/sandbox/package.json: ${sandboxVersion}`);
}
if (packageVersion !== sandboxRuntimeVersion) {
mismatches.push(` packages/@agent-browser/sandbox/src/version.ts: ${sandboxRuntimeVersion}`);
}
if (mismatches.length > 0) {
console.error('Version mismatch detected!');
console.error(` package.json: ${packageVersion}`);
for (const m of mismatches) console.error(m);
console.error('');
console.error("Run 'pnpm run version:sync' to fix this.");
process.exit(1);
}
console.log(`Versions are in sync: ${packageVersion}`);
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env node
/**
* Copies the compiled Rust binary to bin/ with platform-specific naming
*/
import { copyFileSync, existsSync, mkdirSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { platform, arch } from 'os';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '..');
const sourceExt = platform() === 'win32' ? '.exe' : '';
const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`);
const binDir = join(projectRoot, 'bin');
// Determine platform suffix
const platformKey = `${platform()}-${arch()}`;
const ext = platform() === 'win32' ? '.exe' : '';
const targetName = `agent-browser-${platformKey}${ext}`;
const targetPath = join(binDir, targetName);
if (!existsSync(sourcePath)) {
console.error(`Error: Native binary not found at ${sourcePath}`);
console.error('Run "cargo build --release --manifest-path cli/Cargo.toml" first');
process.exit(1);
}
if (!existsSync(binDir)) {
mkdirSync(binDir, { recursive: true });
}
copyFileSync(sourcePath, targetPath);
console.log(`✓ Copied native binary to ${targetPath}`);
+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env node
/**
* Postinstall script for agent-browser
*
* Downloads the platform-specific native binary if not present.
* On global installs, patches npm's bin entry to use the native binary directly:
* - Windows: Overwrites .cmd/.ps1 shims
* - Mac/Linux: Replaces symlink to point to native binary
*/
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, symlinkSync, lstatSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { platform, arch } from 'os';
import { get } from 'https';
import { execSync } from 'child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '..');
const binDir = join(projectRoot, 'bin');
// Detect if the system uses musl libc (e.g. Alpine Linux)
function isMusl() {
if (platform() !== 'linux') return false;
try {
const result = execSync('ldd --version 2>&1 || true', { encoding: 'utf8' });
return result.toLowerCase().includes('musl');
} catch {
return existsSync('/lib/ld-musl-x86_64.so.1') || existsSync('/lib/ld-musl-aarch64.so.1');
}
}
// Platform detection
const osKey = platform() === 'linux' && isMusl() ? 'linux-musl' : platform();
// Windows ARM64 falls back to x64 binary (no native ARM64 build available).
// x64 binaries run via Windows' built-in emulation on ARM64.
const effectiveArch = platform() === 'win32' && arch() === 'arm64' ? 'x64' : arch();
const platformKey = `${osKey}-${effectiveArch}`;
const ext = platform() === 'win32' ? '.exe' : '';
const binaryName = `agent-browser-${platformKey}${ext}`;
const binaryPath = join(binDir, binaryName);
// Package info
const packageJson = JSON.parse(
(await import('fs')).readFileSync(join(projectRoot, 'package.json'), 'utf8')
);
const version = packageJson.version;
// GitHub release URL
const GITHUB_REPO = 'vercel-labs/agent-browser';
const DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
async function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
const request = (url) => {
get(url, (response) => {
// Handle redirects
if (response.statusCode === 301 || response.statusCode === 302) {
request(response.headers.location);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', (err) => {
unlinkSync(dest);
reject(err);
});
};
request(url);
});
}
/**
* Detect which package manager ran this postinstall and write a marker file
* next to the binary so `agent-browser upgrade` can use the correct one
* without fragile path heuristics or slow subprocess probing.
*
* npm_config_user_agent is set by npm/pnpm/yarn/bun during lifecycle scripts,
* e.g. "pnpm/8.10.0 node/v20.10.0 linux x64"
*/
function writeInstallMethod() {
const ua = process.env.npm_config_user_agent || '';
let method = '';
if (ua.startsWith('pnpm/')) method = 'pnpm';
else if (ua.startsWith('yarn/')) method = 'yarn';
else if (ua.startsWith('bun/')) method = 'bun';
else if (ua.startsWith('npm/')) method = 'npm';
if (method) {
try {
writeFileSync(join(binDir, '.install-method'), method);
} catch {
// Non-critical — upgrade will fall back to heuristics
}
}
}
async function main() {
// Check if binary already exists
if (existsSync(binaryPath)) {
// Ensure binary is executable (npm doesn't preserve execute bit)
if (platform() !== 'win32') {
chmodSync(binaryPath, 0o755);
}
console.log(`✓ Native binary ready: ${binaryName}`);
writeInstallMethod();
// On global installs, fix npm's bin entry to use native binary directly
await fixGlobalInstallBin();
showInstallReminder();
return;
}
// Ensure bin directory exists
if (!existsSync(binDir)) {
mkdirSync(binDir, { recursive: true });
}
console.log(`Downloading native binary for ${platformKey}...`);
if (platform() === 'win32' && arch() === 'arm64') {
console.log(` Note: Using x64 binary on ARM64 Windows (runs via emulation)`);
}
console.log(`URL: ${DOWNLOAD_URL}`);
try {
await downloadFile(DOWNLOAD_URL, binaryPath);
// Make executable on Unix
if (platform() !== 'win32') {
chmodSync(binaryPath, 0o755);
}
console.log(`✓ Downloaded native binary: ${binaryName}`);
} catch (err) {
console.log(`Could not download native binary: ${err.message}`);
console.log('');
console.log('To build the native binary locally:');
console.log(' 1. Install Rust: https://rustup.rs');
console.log(' 2. Run: npm run build:native');
}
writeInstallMethod();
// On global installs, fix npm's bin entry to use native binary directly
// This avoids the /bin/sh error on Windows and provides zero-overhead execution
await fixGlobalInstallBin();
showInstallReminder();
}
function findSystemChrome() {
const os = platform();
if (os === 'darwin') {
const candidates = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
];
return candidates.find(p => existsSync(p)) || null;
}
if (os === 'linux') {
const names = ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium'];
for (const name of names) {
try {
const result = execSync(`which ${name} 2>/dev/null`, { encoding: 'utf8' }).trim();
if (result) return result;
} catch {}
}
return null;
}
if (os === 'win32') {
const candidates = [
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
];
return candidates.find(p => p && existsSync(p)) || null;
}
return null;
}
function showInstallReminder() {
const systemChrome = findSystemChrome();
if (systemChrome) {
console.log('');
console.log(` ✓ System Chrome found: ${systemChrome}`);
console.log(' agent-browser will use it automatically.');
console.log('');
return;
}
console.log('');
console.log(' ⚠ No Chrome installation detected.');
console.log(' If you plan to use a local browser, run:');
console.log('');
console.log(' agent-browser install');
if (platform() === 'linux') {
console.log('');
console.log(' On Linux, include system dependencies with:');
console.log('');
console.log(' agent-browser install --with-deps');
}
console.log('');
console.log(' You can skip this if you use --cdp, --provider, --engine, or --executable-path.');
console.log('');
}
/**
* Fix npm's bin entry on global installs to use the native binary directly.
* This provides zero-overhead CLI execution for global installs.
*/
async function fixGlobalInstallBin() {
if (platform() === 'win32') {
await fixWindowsShims();
} else {
await fixUnixSymlink();
}
}
/**
* Fix npm symlink on Mac/Linux global installs.
* Replace the symlink to the JS wrapper with a symlink to the native binary.
*/
async function fixUnixSymlink() {
// Get npm's global bin directory (npm prefix -g + /bin)
let npmBinDir;
try {
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
npmBinDir = join(prefix, 'bin');
} catch {
return; // npm not available
}
const symlinkPath = join(npmBinDir, 'agent-browser');
// Check if symlink exists (indicates global install)
try {
const stat = lstatSync(symlinkPath);
if (!stat.isSymbolicLink()) {
return; // Not a symlink, don't touch it
}
} catch {
return; // Symlink doesn't exist, not a global install
}
// Replace symlink to point directly to native binary
try {
unlinkSync(symlinkPath);
symlinkSync(binaryPath, symlinkPath);
console.log('✓ Optimized: symlink points to native binary (zero overhead)');
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize symlink: ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
}
}
/**
* Fix npm-generated shims on Windows global installs.
* npm generates shims that try to run /bin/sh, which doesn't exist on Windows.
* We overwrite them to invoke the native .exe directly.
*/
async function fixWindowsShims() {
let npmBinDir;
try {
npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
} catch {
return;
}
const cmdShim = join(npmBinDir, 'agent-browser.cmd');
const ps1Shim = join(npmBinDir, 'agent-browser.ps1');
// Shims may not exist yet during postinstall (npm creates them after
// lifecycle scripts). If missing, fall back: the JS wrapper at
// bin/agent-browser.js handles Windows correctly via child_process.spawn.
if (!existsSync(cmdShim)) {
return;
}
// Detect architecture so ARM64 Windows is handled correctly
// (falls back to x64 binary — see platform detection above)
const cpuArch = effectiveArch;
const relativeBinaryPath = `node_modules\\agent-browser\\bin\\agent-browser-win32-${cpuArch}.exe`;
const absoluteBinaryPath = join(npmBinDir, relativeBinaryPath);
// Only rewrite shims if the native binary actually exists
if (!existsSync(absoluteBinaryPath)) {
return;
}
try {
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
writeFileSync(cmdShim, cmdContent);
const ps1Content = `#!/usr/bin/env pwsh\r\n$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent\r\n& "$basedir\\${relativeBinaryPath}" $args\r\nexit $LASTEXITCODE\r\n`;
writeFileSync(ps1Shim, ps1Content);
console.log('✓ Optimized: shims point to native binary (zero overhead)');
} catch (err) {
console.log(`⚠ Could not optimize shims: ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
}
}
main().catch(console.error);
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env node
/**
* Syncs the version from package.json to all other config files.
* Run this script before building or releasing.
*/
import { execSync } from "child_process";
import { readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, "..");
const cliDir = join(rootDir, "cli");
// Read version from package.json (single source of truth)
const packageJson = JSON.parse(
readFileSync(join(rootDir, "package.json"), "utf-8")
);
const version = packageJson.version;
console.log(`Syncing version ${version} to all config files...`);
// Update Cargo.toml
const cargoTomlPath = join(cliDir, "Cargo.toml");
let cargoToml = readFileSync(cargoTomlPath, "utf-8");
const cargoVersionRegex = /^version\s*=\s*"[^"]*"/m;
const newCargoVersion = `version = "${version}"`;
let cargoTomlUpdated = false;
if (cargoVersionRegex.test(cargoToml)) {
const oldMatch = cargoToml.match(cargoVersionRegex)?.[0];
if (oldMatch !== newCargoVersion) {
cargoToml = cargoToml.replace(cargoVersionRegex, newCargoVersion);
writeFileSync(cargoTomlPath, cargoToml);
console.log(` Updated cli/Cargo.toml: ${oldMatch} -> ${newCargoVersion}`);
cargoTomlUpdated = true;
} else {
console.log(` cli/Cargo.toml already up to date`);
}
} else {
console.error(" Could not find version field in cli/Cargo.toml");
process.exit(1);
}
// Update packages/dashboard/package.json
const dashboardPkgPath = join(rootDir, "packages", "dashboard", "package.json");
const dashboardPkg = JSON.parse(readFileSync(dashboardPkgPath, "utf-8"));
if (dashboardPkg.version !== version) {
const oldVersion = dashboardPkg.version;
dashboardPkg.version = version;
writeFileSync(dashboardPkgPath, JSON.stringify(dashboardPkg, null, 2) + "\n");
console.log(` Updated packages/dashboard/package.json: ${oldVersion} -> ${version}`);
} else {
console.log(` packages/dashboard/package.json already up to date`);
}
// Update packages/@agent-browser/sandbox/package.json
const sandboxPkgPath = join(rootDir, "packages", "@agent-browser", "sandbox", "package.json");
const sandboxPkg = JSON.parse(readFileSync(sandboxPkgPath, "utf-8"));
if (sandboxPkg.version !== version) {
const oldVersion = sandboxPkg.version;
sandboxPkg.version = version;
writeFileSync(sandboxPkgPath, JSON.stringify(sandboxPkg, null, 2) + "\n");
console.log(` Updated packages/@agent-browser/sandbox/package.json: ${oldVersion} -> ${version}`);
} else {
console.log(` packages/@agent-browser/sandbox/package.json already up to date`);
}
// Update package runtime version constant
const sandboxVersionPath = join(
rootDir,
"packages",
"@agent-browser",
"sandbox",
"src",
"version.ts",
);
const sandboxVersionSource = `export const AGENT_BROWSER_SANDBOX_VERSION = "${version}";\n`;
const currentSandboxVersionSource = readFileSync(sandboxVersionPath, "utf-8");
if (currentSandboxVersionSource !== sandboxVersionSource) {
writeFileSync(sandboxVersionPath, sandboxVersionSource);
console.log(` Updated packages/@agent-browser/sandbox/src/version.ts -> ${version}`);
} else {
console.log(` packages/@agent-browser/sandbox/src/version.ts already up to date`);
}
// Update Cargo.lock to match Cargo.toml
if (cargoTomlUpdated) {
try {
execSync("cargo update -p agent-browser --offline", {
cwd: cliDir,
stdio: "pipe",
});
console.log(` Updated cli/Cargo.lock`);
} catch {
// --offline may fail if package not in cache, try without it
try {
execSync("cargo update -p agent-browser", {
cwd: cliDir,
stdio: "pipe",
});
console.log(` Updated cli/Cargo.lock`);
} catch (e) {
console.error(` Warning: Could not update Cargo.lock: ${e.message}`);
}
}
}
console.log("Version sync complete.");
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTANCE_FILE="$SCRIPT_DIR/.instance"
NAME_PREFIX="agent-browser-debug"
INSTANCE_TYPE="${INSTANCE_TYPE:-t3.xlarge}"
if [[ -f "$INSTANCE_FILE" ]]; then
echo "Error: Instance already provisioned. See $INSTANCE_FILE"
echo "Run ./scripts/windows-debug/start.sh to start it, or delete .instance to re-provision."
exit 1
fi
REGION=$(aws configure get region 2>/dev/null || echo "")
if [[ -z "$REGION" ]]; then
echo "Error: No AWS region configured. Run: aws configure set region us-east-1"
exit 1
fi
echo "Provisioning Windows debug instance in $REGION..."
# --- IAM Role for SSM ---
ROLE_NAME="${IAM_ROLE_NAME:-$NAME_PREFIX-ssm-role}"
PROFILE_NAME="${INSTANCE_PROFILE_NAME:-$NAME_PREFIX-instance-profile}"
if aws iam get-instance-profile --instance-profile-name "$PROFILE_NAME" &>/dev/null; then
echo "Instance profile $PROFILE_NAME already exists, reusing."
else
echo "Instance profile $PROFILE_NAME not found. Creating IAM resources..."
if ! aws iam get-role --role-name "$ROLE_NAME" &>/dev/null; then
echo "Creating IAM role: $ROLE_NAME"
if ! aws iam create-role \
--role-name "$ROLE_NAME" \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}' \
--no-cli-pager; then
echo ""
echo "Error: Failed to create IAM role (see error above)."
echo ""
echo "Ask an IAM admin to create the following, then re-run with:"
echo " INSTANCE_PROFILE_NAME=<name> ./scripts/windows-debug/provision.sh"
echo ""
echo "What the admin needs to create:"
echo " 1. IAM Role: $ROLE_NAME"
echo " - Trusted entity: EC2 (ec2.amazonaws.com)"
echo " - Attached policy: AmazonSSMManagedInstanceCore"
echo " 2. Instance Profile: $PROFILE_NAME"
echo " - With the above role added to it"
echo ""
echo "Or run these commands with an account that has iam:CreateRole permission:"
echo ""
echo " aws iam create-role --role-name $ROLE_NAME \\"
echo " --assume-role-policy-document '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}'"
echo ""
echo " aws iam attach-role-policy --role-name $ROLE_NAME \\"
echo " --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
echo ""
echo " aws iam create-instance-profile --instance-profile-name $PROFILE_NAME"
echo ""
echo " aws iam add-role-to-instance-profile \\"
echo " --instance-profile-name $PROFILE_NAME --role-name $ROLE_NAME"
exit 1
fi
aws iam attach-role-policy \
--role-name "$ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
else
echo "IAM role $ROLE_NAME already exists."
fi
echo "Creating instance profile: $PROFILE_NAME"
aws iam create-instance-profile --instance-profile-name "$PROFILE_NAME" --no-cli-pager
aws iam add-role-to-instance-profile \
--instance-profile-name "$PROFILE_NAME" \
--role-name "$ROLE_NAME"
echo "Waiting for instance profile propagation..."
sleep 10
fi
# --- Security Group (no inbound rules) ---
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --query "Vpcs[0].VpcId" --output text)
if [[ "$VPC_ID" == "None" || -z "$VPC_ID" ]]; then
echo "Error: No default VPC found. Create one with: aws ec2 create-default-vpc"
exit 1
fi
SG_NAME="$NAME_PREFIX-sg"
SG_ID=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=$SG_NAME" "Name=vpc-id,Values=$VPC_ID" \
--query "SecurityGroups[0].GroupId" --output text 2>/dev/null || echo "None")
if [[ "$SG_ID" == "None" || -z "$SG_ID" ]]; then
echo "Creating security group: $SG_NAME"
SG_ID=$(aws ec2 create-security-group \
--group-name "$SG_NAME" \
--description "agent-browser Windows debug instance (SSM only, no inbound)" \
--vpc-id "$VPC_ID" \
--query "GroupId" --output text)
# Revoke default egress isn't needed; SSM requires outbound HTTPS.
# No inbound rules -- SSM uses outbound connections only.
else
echo "Security group $SG_NAME ($SG_ID) already exists, reusing."
fi
# --- AMI (latest Windows Server 2022) ---
AMI_ID=$(aws ssm get-parameter \
--name "/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base" \
--query "Parameter.Value" --output text)
echo "Using AMI: $AMI_ID (Windows Server 2022)"
# --- UserData bootstrap script ---
USERDATA_FILE=$(mktemp)
trap "rm -f $USERDATA_FILE" EXIT
cat > "$USERDATA_FILE" <<'PWSH'
<powershell>
$ErrorActionPreference = "Continue"
$logFile = "C:\bootstrap.log"
function Log($msg) {
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$ts $msg" | Tee-Object -FilePath $logFile -Append
}
Log "--- Bootstrap starting ---"
# Install Git
Log "Installing Git..."
$gitInstaller = "$env:TEMP\git-installer.exe"
Invoke-WebRequest -Uri "https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1.2-64-bit.exe" -OutFile $gitInstaller
Start-Process -FilePath $gitInstaller -ArgumentList "/VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS=`"icons,ext\reg\shellhere,assoc,assoc_sh`"" -Wait
$env:PATH = "C:\Program Files\Git\cmd;$env:PATH"
[Environment]::SetEnvironmentVariable("PATH", "C:\Program Files\Git\cmd;$([Environment]::GetEnvironmentVariable('PATH', 'Machine'))", "Machine")
Log "Git installed: $(git --version)"
# Install Rust
Log "Installing Rust..."
$rustupInit = "$env:TEMP\rustup-init.exe"
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile $rustupInit
Start-Process -FilePath $rustupInit -ArgumentList "-y --default-toolchain stable" -Wait
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
[Environment]::SetEnvironmentVariable("PATH", "$env:USERPROFILE\.cargo\bin;$([Environment]::GetEnvironmentVariable('PATH', 'Machine'))", "Machine")
Log "Rust installed: $(rustc --version)"
# Install MSVC build tools (required for Rust on Windows)
Log "Installing Visual Studio Build Tools..."
$vsInstaller = "$env:TEMP\vs_buildtools.exe"
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vs_buildtools.exe" -OutFile $vsInstaller
Start-Process -FilePath $vsInstaller -ArgumentList "--quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" -Wait
Log "Build tools installed."
# Clone repo
Log "Cloning agent-browser..."
git clone https://github.com/vercel-labs/agent-browser.git C:\agent-browser
Set-Location C:\agent-browser
Log "Repo cloned."
# Build CLI
Log "Building agent-browser CLI..."
cargo build --release --manifest-path cli\Cargo.toml
Log "Build complete."
# Install Chrome
Log "Installing Chrome via agent-browser..."
.\cli\target\release\agent-browser.exe install
Log "Chrome installed."
Log "--- Bootstrap complete ---"
</powershell>
PWSH
# --- Launch instance ---
echo "Launching $INSTANCE_TYPE instance..."
INSTANCE_ID=$(aws ec2 run-instances \
--image-id "$AMI_ID" \
--instance-type "$INSTANCE_TYPE" \
--iam-instance-profile "Name=$PROFILE_NAME" \
--security-group-ids "$SG_ID" \
--user-data "file://$USERDATA_FILE" \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":80,"VolumeType":"gp3"}}]' \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$NAME_PREFIX}]" \
--metadata-options "HttpTokens=required" \
--query "Instances[0].InstanceId" --output text)
echo "Instance launched: $INSTANCE_ID"
# Save instance config
cat > "$INSTANCE_FILE" <<EOF
INSTANCE_ID=$INSTANCE_ID
REGION=$REGION
EOF
echo "Waiting for instance to enter running state..."
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"
echo "Instance is running."
echo ""
echo "Instance $INSTANCE_ID is booting and bootstrapping (Rust, Git, Chrome)."
echo "Bootstrap takes ~15-20 minutes on first boot."
echo ""
echo "Check bootstrap progress:"
echo " ./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\""
echo ""
echo "Once ready, sync your branch and start debugging:"
echo " ./scripts/windows-debug/sync.sh"
echo " ./scripts/windows-debug/run.sh \"cd C:\\agent-browser && cargo test\""
echo ""
echo "Stop when done to save costs:"
echo " ./scripts/windows-debug/stop.sh"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTANCE_FILE="$SCRIPT_DIR/.instance"
if [[ ! -f "$INSTANCE_FILE" ]]; then
echo "Error: No instance provisioned. Run ./scripts/windows-debug/provision.sh first."
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "Usage: ./scripts/windows-debug/run.sh \"<powershell-command>\""
echo ""
echo "Examples:"
echo " ./scripts/windows-debug/run.sh \"cd C:\\agent-browser && cargo test\""
echo " ./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\""
echo " ./scripts/windows-debug/run.sh \"cd C:\\agent-browser && cargo test e2e -- --ignored --test-threads=1\""
exit 1
fi
source "$INSTANCE_FILE"
export AWS_DEFAULT_REGION="$REGION"
COMMAND="$*"
PARAMS_FILE=$(mktemp)
trap "rm -f $PARAMS_FILE" EXIT
python3 -c '
import json, sys
path_setup = "$env:PATH = \"$env:USERPROFILE\\.cargo\\bin;C:\\Program Files\\Git\\cmd;$env:PATH\""
cmd = path_setup + "\n" + sys.argv[1]
json.dump({"commands": [cmd]}, open(sys.argv[2], "w"))
' "$COMMAND" "$PARAMS_FILE"
COMMAND_ID=$(aws ssm send-command \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunPowerShellScript" \
--parameters "file://$PARAMS_FILE" \
--timeout-seconds 3600 \
--query "Command.CommandId" --output text)
echo "Command sent (ID: $COMMAND_ID). Waiting..." >&2
while true; do
RESULT=$(aws ssm get-command-invocation \
--command-id "$COMMAND_ID" \
--instance-id "$INSTANCE_ID" \
--output json 2>&1) || true
STATUS=$(echo "$RESULT" | python3 -c "
import sys, json
try:
print(json.loads(sys.stdin.read()).get('Status', 'Unknown'))
except:
print('Pending')
" 2>/dev/null)
case "$STATUS" in
Success)
echo "$RESULT" | python3 -c "
import sys, json
r = json.loads(sys.stdin.read())
out = r.get('StandardOutputContent', '').rstrip()
err = r.get('StandardErrorContent', '').rstrip()
if out:
print(out)
if err:
print(err, file=sys.stderr)
"
exit 0
;;
Failed|TimedOut|Cancelled)
echo "$RESULT" | python3 -c "
import sys, json
r = json.loads(sys.stdin.read())
out = r.get('StandardOutputContent', '').rstrip()
err = r.get('StandardErrorContent', '').rstrip()
if out:
print(out)
if err:
print(err, file=sys.stderr)
"
echo "Command $STATUS." >&2
exit 1
;;
*)
sleep 3
;;
esac
done
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTANCE_FILE="$SCRIPT_DIR/.instance"
if [[ ! -f "$INSTANCE_FILE" ]]; then
echo "Error: No instance provisioned. Run ./scripts/windows-debug/provision.sh first."
exit 1
fi
source "$INSTANCE_FILE"
export AWS_DEFAULT_REGION="$REGION"
STATE=$(aws ec2 describe-instances \
--instance-ids "$INSTANCE_ID" \
--query "Reservations[0].Instances[0].State.Name" --output text)
if [[ "$STATE" == "running" ]]; then
echo "Instance $INSTANCE_ID is already running."
else
echo "Starting instance $INSTANCE_ID..."
aws ec2 start-instances --instance-ids "$INSTANCE_ID" --no-cli-pager
echo "Waiting for running state..."
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"
echo "Instance is running."
fi
echo "Waiting for SSM agent connectivity..."
for i in $(seq 1 30); do
SSM_STATUS=$(aws ssm describe-instance-information \
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
--query "InstanceInformationList[0].PingStatus" --output text 2>/dev/null || echo "None")
if [[ "$SSM_STATUS" == "Online" ]]; then
echo "SSM agent is online. Ready for commands."
echo " ./scripts/windows-debug/run.sh \"your-command-here\""
exit 0
fi
sleep 10
done
echo "Warning: SSM agent not online after 5 minutes. The instance may still be booting."
echo "Try again in a minute: ./scripts/windows-debug/run.sh \"hostname\""
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTANCE_FILE="$SCRIPT_DIR/.instance"
if [[ ! -f "$INSTANCE_FILE" ]]; then
echo "Error: No instance provisioned. Nothing to stop."
exit 1
fi
source "$INSTANCE_FILE"
export AWS_DEFAULT_REGION="$REGION"
STATE=$(aws ec2 describe-instances \
--instance-ids "$INSTANCE_ID" \
--query "Reservations[0].Instances[0].State.Name" --output text)
if [[ "$STATE" == "stopped" ]]; then
echo "Instance $INSTANCE_ID is already stopped."
exit 0
fi
echo "Stopping instance $INSTANCE_ID..."
aws ec2 stop-instances --instance-ids "$INSTANCE_ID" --no-cli-pager
echo "Waiting for stopped state..."
aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID"
echo "Instance stopped. No compute charges while stopped (storage only: ~$0.64/mo)."
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
RUN="$SCRIPT_DIR/run.sh"
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "https://github.com/vercel-labs/agent-browser.git")
echo "Syncing branch '$BRANCH' on Windows instance..."
"$RUN" "
cd C:\agent-browser
git remote set-url origin '$REMOTE_URL'
git fetch origin
git checkout -B '$BRANCH' 'origin/$BRANCH'
git log -1 --oneline
"
echo ""
echo "Branch synced. Rebuilding..."
"$RUN" "
cd C:\agent-browser
cargo build --release --manifest-path cli\Cargo.toml
Write-Host 'Build complete.'
"