From 0ec257587cc83704142a1569b32a39dfb05259b8 Mon Sep 17 00:00:00 2001 From: VIvidh Mahajan Date: Sat, 6 Jun 2026 00:46:21 -0400 Subject: [PATCH] providers: add BaseLayer --- scripts/run-and-publish.sh | 109 ++++++++++++++++++++++++++- src/providers/baselayer.ts | 79 +++++++++++++++++++ src/providers/index.ts | 3 + src/types.ts | 3 +- web/components/leaderboard-table.tsx | 1 + web/lib/data.ts | 6 ++ web/public/logos/baselayer.svg | 42 +++++++++++ 7 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 src/providers/baselayer.ts create mode 100644 web/public/logos/baselayer.svg diff --git a/scripts/run-and-publish.sh b/scripts/run-and-publish.sh index eac7edd..37a0851 100755 --- a/scripts/run-and-publish.sh +++ b/scripts/run-and-publish.sh @@ -15,6 +15,7 @@ REGION="" RUNS=100 DRY_RUN=0 NO_RESET=0 +BASELAYER_SELFHOST_METAL_JSON="" while [[ $# -gt 0 ]]; do case "$1" in @@ -31,7 +32,7 @@ while [[ $# -gt 0 ]]; do done case "$REGION" in - us-east) PROVIDERS="steel,kernel,kernel-headful,hyperbrowser,anchorbrowser,browser-use" ;; + us-east) PROVIDERS="steel,kernel,kernel-headful,hyperbrowser,anchorbrowser,browser-use,baselayer" ;; us-west) PROVIDERS="notte,browserbase" ;; "") echo "[ERROR] --region us-east|us-west required" >&2; exit 2 ;; *) echo "[ERROR] invalid --region: $REGION (want us-east or us-west)" >&2; exit 2 ;; @@ -40,6 +41,109 @@ esac REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" +cleanup_baselayer_selfhost() { + if [[ -z "${BASELAYER_SELFHOST_METAL_JSON:-}" || ! -f "$BASELAYER_SELFHOST_METAL_JSON" ]]; then + return + fi + node - "$BASELAYER_SELFHOST_METAL_JSON" <<'NODE' +const fs = require("fs"); +const meta = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); +if (!meta.instanceId || !meta.region) process.exit(0); +console.log(`[INFO] terminating BaseLayer self-host metal ${meta.instanceId} in ${meta.region}`); +const { spawnSync } = require("child_process"); +spawnSync("aws", [ + "ec2", + "terminate-instances", + "--profile", + process.env.BASELAYER_AWS_PROFILE || "baselayer", + "--region", + meta.region, + "--instance-ids", + meta.instanceId, +], { stdio: "inherit" }); +NODE +} +trap cleanup_baselayer_selfhost EXIT + +provider_enabled() { + local needle="$1" + IFS=',' read -ra enabled <<< "$PROVIDERS" + for p in "${enabled[@]}"; do + [[ "$p" == "$needle" ]] && return 0 + done + return 1 +} + +setup_baselayer_selfhost() { + if ! provider_enabled "baselayer"; then + return + fi + if [[ -n "${BASELAYER_API_URL:-}" ]]; then + echo "[INFO] BASELAYER_API_URL already set; using existing BaseLayer endpoint" + return + fi + if [[ "${BASELAYER_AUTO_SELFHOST:-1}" != "1" ]]; then + echo "[ERROR] baselayer provider is enabled but BASELAYER_API_URL is unset." >&2 + echo "[ERROR] Set BASELAYER_API_URL or set BASELAYER_AUTO_SELFHOST=1 to provision from the BaseLayer repo." >&2 + exit 1 + fi + if [[ $DRY_RUN -eq 1 && "${BASELAYER_DRY_RUN_SELFHOST:-0}" != "1" ]]; then + echo "[INFO] --dry-run: skipping BaseLayer auto-selfhost provisioning." + echo "[INFO] Set BASELAYER_DRY_RUN_SELFHOST=1 to allow dry-run provisioning." + return + fi + + local ps + ps="$(command -v pwsh || command -v powershell || true)" + if [[ -z "$ps" ]]; then + echo "[ERROR] BaseLayer auto-selfhost requires PowerShell 7+ (pwsh) because the BaseLayer AWS wrapper is PowerShell." >&2 + exit 1 + fi + if ! command -v aws >/dev/null 2>&1; then + echo "[ERROR] BaseLayer auto-selfhost requires AWS CLI v2 on the BrowserArena runner." >&2 + exit 1 + fi + + local base_repo="${BASELAYER_REPO:-https://github.com/Lasdw6/BaseLayer.git}" + local base_ref="${BASELAYER_REF:-main}" + local base_dir="${BASELAYER_SELFHOST_REPO_DIR:-$REPO_ROOT/.tmp/baselayer-selfhost-repo}" + local out_dir="${BASELAYER_SELFHOST_OUT_DIR:-$REPO_ROOT/.tmp/baselayer-selfhost}" + local aws_region="${BASELAYER_AWS_REGION:-us-east-2}" + local smoke_runs="${BASELAYER_SELFHOST_SMOKE_RUNS:-1}" + local smoke_concurrency="${BASELAYER_SELFHOST_SMOKE_CONCURRENCY:-1}" + local runtime_profile="${BASELAYER_RUNTIME_PROFILE:-baselayer-firecracker-headless-shell}" + + echo "[INFO] provisioning BaseLayer self-host from $base_repo ref=$base_ref region=$aws_region" + rm -rf "$base_dir" + git clone --depth 1 --branch "$base_ref" "$base_repo" "$base_dir" + + "$ps" -NoProfile -ExecutionPolicy Bypass -File "$base_dir/scripts/bench/run-browserarena-selfhosted.ps1" \ + -Mode local \ + -AwsProfile "${BASELAYER_AWS_PROFILE:-baselayer}" \ + -Region "$aws_region" \ + -BaseLayerRepo "$base_repo" \ + -BaseLayerRef "$base_ref" \ + -BrowserArenaPath "$REPO_ROOT" \ + -Target "${BASELAYER_SELFHOST_TARGET:-https://example.com}" \ + -RuntimeProfile "$runtime_profile" \ + -Concurrency "$smoke_concurrency" \ + -Runs "$smoke_runs" \ + -Repeats 1 \ + -KeepMetal \ + -OutDir "$out_dir" + + BASELAYER_SELFHOST_METAL_JSON="$(find "$out_dir" -path '*/repeat-1/metal.json' -type f | sort | tail -n 1)" + if [[ -z "$BASELAYER_SELFHOST_METAL_JSON" || ! -f "$BASELAYER_SELFHOST_METAL_JSON" ]]; then + echo "[ERROR] BaseLayer self-host setup did not produce metal.json under $out_dir" >&2 + exit 1 + fi + export BASELAYER_API_URL + BASELAYER_API_URL="$(node -e 'const fs=require("fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(`http://${m.publicIp}:3000`);' "$BASELAYER_SELFHOST_METAL_JSON")" + export BASELAYER_RUNTIME_PROFILE="$runtime_profile" + rm -rf "results/hello-browser/baselayer/$(date -u +%F)" + echo "[INFO] BaseLayer self-host ready at $BASELAYER_API_URL" +} + mkdir -p logs LOG_FILE="logs/cron-$(date -u +%Y-%m-%dT%H-%M-%SZ)-${REGION}.log" exec > >(tee -a "$LOG_FILE") 2>&1 @@ -65,6 +169,8 @@ else echo "[WARN] no .env found; provider env vars must already be exported" fi +setup_baselayer_selfhost + # Sanity: at least one provider in this region has its env var set. Mirrors # the env-var map in src/providers/index.ts. have_any=0 @@ -76,6 +182,7 @@ for p in "${PROV_LIST[@]}"; do hyperbrowser) v="${HYPERBROWSER_API_KEY:-}" ;; anchorbrowser) v="${ANCHORBROWSER_API_KEY:-}" ;; browser-use) v="${BROWSER_USE_API_KEY:-}" ;; + baselayer) v="${BASELAYER_API_URL:-}" ;; notte) v="${NOTTE_API_KEY:-}" ;; browserbase) v="${BROWSERBASE_API_KEY:-}" ;; *) echo "[ERROR] unknown provider in region map: $p" >&2; exit 2 ;; diff --git a/src/providers/baselayer.ts b/src/providers/baselayer.ts new file mode 100644 index 0000000..3e8d1b1 --- /dev/null +++ b/src/providers/baselayer.ts @@ -0,0 +1,79 @@ +import type { ProviderClient, ProviderSession } from "../types.js"; +import { requireEnv } from "../utils/env.js"; + +type BaseLayerSessionResponse = { + id?: string; + sessionId?: string; + cdpUrl?: string; + connectUrl?: string; + webSocketDebuggerUrl?: string; +}; + +function optionalEnv(name: string): string | undefined { + const value = process.env[name]?.trim(); + return value ? value : undefined; +} + +export class BaseLayerProvider implements ProviderClient { + readonly name = "BASELAYER"; + + computeCost(): number { + return 0; + } + + private apiUrl(): string { + return requireEnv("BASELAYER_API_URL").replace(/\/$/, ""); + } + + private authHeaders(): Record { + const headers: Record = {}; + const apiKey = optionalEnv("BASELAYER_API_KEY"); + if (apiKey) headers.Authorization = `Bearer ${apiKey}`; + return headers; + } + + private jsonHeaders(): Record { + return { + ...this.authHeaders(), + "Content-Type": "application/json", + }; + } + + async create(): Promise { + const runtimeProfile = optionalEnv("BASELAYER_RUNTIME_PROFILE"); + const body: Record = { browser: "chromium" }; + if (runtimeProfile) body.runtimeProfile = runtimeProfile; + + const response = await fetch(`${this.apiUrl()}/v1/sessions`, { + method: "POST", + headers: this.jsonHeaders(), + body: JSON.stringify(body), + signal: AbortSignal.timeout(120_000), + }); + + if (!response.ok) { + throw new Error(`BaseLayer create failed: HTTP ${response.status} - ${await response.text()}`); + } + + const data = (await response.json()) as BaseLayerSessionResponse; + const id = data.id ?? data.sessionId; + const cdpUrl = data.cdpUrl ?? data.connectUrl ?? data.webSocketDebuggerUrl; + if (!id || !cdpUrl) { + throw new Error(`Invalid BaseLayer session response: ${JSON.stringify(data)}`); + } + + return { id, cdpUrl }; + } + + async release(id: string): Promise { + const response = await fetch(`${this.apiUrl()}/v1/sessions/${encodeURIComponent(id)}`, { + method: "DELETE", + headers: this.authHeaders(), + signal: AbortSignal.timeout(30_000), + }); + + if (!response.ok && response.status !== 404) { + throw new Error(`BaseLayer release failed: HTTP ${response.status} - ${await response.text()}`); + } + } +} diff --git a/src/providers/index.ts b/src/providers/index.ts index bfe8284..efc4f2e 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -6,6 +6,7 @@ import { HyperbrowserProvider } from "./hyperbrowser.js"; import { KernelProvider, KernelHeadfulProvider } from "./kernel.js"; import { NotteProvider } from "./notte.js"; import { BrowserUseProvider } from "./browser-use.js"; +import { BaseLayerProvider } from "./baselayer.js"; export function getRequiredEnvVarsForProvider(name: string): string[] { const key = name.trim().toLowerCase(); @@ -20,6 +21,7 @@ export function getRequiredEnvVarsForProvider(name: string): string[] { if (key === "notte") return ["NOTTE_API_KEY"]; if (key === "browser-use" || key === "browseruse" || key === "bu") return ["BROWSER_USE_API_KEY"]; + if (key === "baselayer" || key === "base-layer") return ["BASELAYER_API_URL"]; throw new Error(`Unknown provider: ${name}`); } @@ -42,5 +44,6 @@ export function resolveProvider(name: string): ProviderClient { if (key === "notte") return new NotteProvider(); if (key === "browser-use" || key === "browseruse" || key === "bu") return new BrowserUseProvider(); + if (key === "baselayer" || key === "base-layer") return new BaseLayerProvider(); throw new Error(`Unknown provider: ${name}`); } diff --git a/src/types.ts b/src/types.ts index 0fb4f1c..40dfc1a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,8 @@ export type ProviderName = | "KERNEL" | "KERNEL_HEADFUL" | "NOTTE" - | "BROWSER_USE"; + | "BROWSER_USE" + | "BASELAYER"; export type ProviderSession = { id: string; diff --git a/web/components/leaderboard-table.tsx b/web/components/leaderboard-table.tsx index 9bc08b4..3d81b08 100644 --- a/web/components/leaderboard-table.tsx +++ b/web/components/leaderboard-table.tsx @@ -53,6 +53,7 @@ const PROVIDER_LOGOS: Record = { KERNEL: "/logos/kernel.png", STEEL: "/logos/steel.png", BROWSER_USE: "/logos/browseruse.png", + BASELAYER: "/logos/baselayer.svg", }; type SortDirection = "asc" | "desc"; diff --git a/web/lib/data.ts b/web/lib/data.ts index 0dc8012..0a277a8 100644 --- a/web/lib/data.ts +++ b/web/lib/data.ts @@ -82,6 +82,12 @@ const PROVIDER_META: Record< }, STEEL: { displayName: "Steel", url: "https://www.steel.dev", browserRegion: "us-east-1" }, BROWSER_USE: { displayName: "Browser Use", url: "https://www.browser-use.com", browserRegion: "us-east-1" }, + BASELAYER: { + displayName: "BaseLayer", + url: "https://github.com/Lasdw6/BaseLayer", + browserRegion: "self-hosted AWS m5zn.metal", + disclaimer: "Self-hosted on AWS m5zn.metal; not a managed cloud provider.", + }, }; function median(values: number[]): number { diff --git a/web/public/logos/baselayer.svg b/web/public/logos/baselayer.svg new file mode 100644 index 0000000..88606b1 --- /dev/null +++ b/web/public/logos/baselayer.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +