9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
608 lines
22 KiB
Python
608 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""One-command benchmark: bring up the Buzz stack in Docker and run it.
|
|
|
|
``just benchmark`` wraps this script. Defaults are leaderboard-eligible out
|
|
of the box (Terminal-Bench 2.1, 5 attempts per problem, the Sonnet+Haiku
|
|
team); every ``run_leaderboard.py`` selector passes through unchanged. The
|
|
script owns everything around the run:
|
|
|
|
- A dedicated ``buzz-benchmark`` compose project reusing the production
|
|
bundle (``deploy/compose/compose.yml``) plus the benchmark port overlay,
|
|
on its own ports (relay :3600, Postgres :5633, metrics :9602) so it never
|
|
collides with a dev stack. Secrets and identities are generated once into
|
|
the gitignored ``.benchmark/`` state dir and reused across runs.
|
|
- One pinned *user* identity for the whole benchmark environment: it owns
|
|
every trial channel and posts every task, like one human running many
|
|
teams. Channels are kept (not archived) after each trial.
|
|
- ``--gui`` adds that user to the relay membership list and opens the Buzz
|
|
desktop app logged in as them, so a human can watch the teams work live.
|
|
|
|
Run inside the testbed environment (the just recipe does this):
|
|
|
|
uv run --project benchmarks/harbor-buzz-orchestra/testbed \
|
|
benchmarks/harbor-buzz-orchestra/scripts/benchmark.py [--gui] [...]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import secrets
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
PACKAGE_ROOT = Path(__file__).resolve().parent.parent
|
|
REPO_ROOT = PACKAGE_ROOT.parents[1]
|
|
STATE_DIR = PACKAGE_ROOT / ".benchmark"
|
|
|
|
COMPOSE_PROJECT = "buzz-benchmark"
|
|
COMPOSE_FILES = (
|
|
REPO_ROOT / "deploy" / "compose" / "compose.yml",
|
|
PACKAGE_ROOT / "testbed" / "compose.benchmark.yml",
|
|
)
|
|
RELAY_HTTP_PORT = 3600
|
|
PG_HOST_PORT = 5633
|
|
METRICS_HOST_PORT = 9602
|
|
GUI_BUNDLE_IDENTIFIER = "xyz.block.buzz.app.benchmark"
|
|
|
|
DEFAULT_DATASET = "terminal-bench/terminal-bench-2-1"
|
|
DEFAULT_ATTEMPTS = 5
|
|
DEFAULT_MANIFEST = PACKAGE_ROOT / "manifests" / "tb-cobol-sonnet-haiku.yaml"
|
|
DEFAULT_ENDPOINTS = PACKAGE_ROOT / "testbed" / "endpoints" / "anthropic-live.json"
|
|
SCHEMA_SQL = PACKAGE_ROOT / "testbed" / "sql" / "benchmark_schema.sql"
|
|
|
|
# Linux builds of the production agent stack, uploaded into each task
|
|
# container per trial. Built once in a rust:alpine container (musl → fully
|
|
# static, runs on any Linux task image of the same architecture) and cached.
|
|
AGENT_BINARIES = ("buzz-acp", "buzz-agent", "buzz-dev-mcp")
|
|
# Std-only loopback forwarder (not a workspace crate): agents dial the
|
|
# relay's canonical localhost address inside the task container and the
|
|
# forwarder bridges to the Docker host gateway. Compiled with plain rustc
|
|
# in the same cross-build step.
|
|
FORWARDER_SOURCE = PACKAGE_ROOT / "forwarder" / "relay_forwarder.rs"
|
|
FORWARDER_BINARY = "relay-forwarder"
|
|
LINUX_TARGET_DIR = STATE_DIR / "linux-target"
|
|
RUST_IMAGE = "rust:1.95-alpine"
|
|
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"run_leaderboard", Path(__file__).resolve().parent / "run_leaderboard.py"
|
|
)
|
|
run_leaderboard = importlib.util.module_from_spec(_spec)
|
|
sys.modules.setdefault("run_leaderboard", run_leaderboard)
|
|
_spec.loader.exec_module(run_leaderboard)
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description=__doc__.splitlines()[0],
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
problems = parser.add_mutually_exclusive_group()
|
|
problems.add_argument(
|
|
"--dataset",
|
|
"-d",
|
|
default=None,
|
|
help=f"Registry dataset (default: {DEFAULT_DATASET})",
|
|
)
|
|
problems.add_argument(
|
|
"--path", "-p", type=Path, help="Local task or dataset directory"
|
|
)
|
|
parser.add_argument(
|
|
"--include-task",
|
|
"-i",
|
|
action="append",
|
|
default=[],
|
|
help="Task name to include (glob, repeatable)",
|
|
)
|
|
parser.add_argument(
|
|
"--exclude-task",
|
|
"-x",
|
|
action="append",
|
|
default=[],
|
|
help="Task name to exclude (glob, repeatable)",
|
|
)
|
|
parser.add_argument(
|
|
"--attempts",
|
|
"-k",
|
|
type=int,
|
|
default=DEFAULT_ATTEMPTS,
|
|
help=f"Runs per problem (default: {DEFAULT_ATTEMPTS}, the leaderboard requirement)",
|
|
)
|
|
parser.add_argument(
|
|
"--manifest",
|
|
type=Path,
|
|
default=DEFAULT_MANIFEST,
|
|
help=f"Team manifest YAML (default: {DEFAULT_MANIFEST.name})",
|
|
)
|
|
parser.add_argument(
|
|
"--endpoint-config",
|
|
type=Path,
|
|
default=DEFAULT_ENDPOINTS,
|
|
help=f"Endpoint provider/API-key mapping (default: {DEFAULT_ENDPOINTS.name})",
|
|
)
|
|
parser.add_argument(
|
|
"--n-concurrent", "-n", type=int, default=4, help="Concurrent trials"
|
|
)
|
|
parser.add_argument(
|
|
"--jobs-dir", type=Path, default=PACKAGE_ROOT / "jobs", help="Job output root"
|
|
)
|
|
parser.add_argument(
|
|
"--job-name", default=None, help="Job name (default: lb-<condition>-<UTC>)"
|
|
)
|
|
parser.add_argument(
|
|
"--upload",
|
|
action="store_true",
|
|
help="Upload to Harbor Hub when the job finishes",
|
|
)
|
|
parser.add_argument(
|
|
"--gui",
|
|
action="store_true",
|
|
help="Open the Buzz desktop app as the benchmark user to watch the run live",
|
|
)
|
|
parser.add_argument(
|
|
"--fresh",
|
|
action="store_true",
|
|
help="Reset first: drop the stack's Docker volumes and the benchmark "
|
|
"GUI's app state (keys in state.json are kept)",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print the underlying harbor command and exit (no stack bring-up)",
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
# -- state: secrets and identities, generated once --------------------------
|
|
|
|
|
|
def load_state() -> dict[str, str]:
|
|
"""Generate-or-load the benchmark environment's keys and secrets."""
|
|
from harbor_buzz_testbed.keys import generate_keypair, keypair_from_secret
|
|
|
|
STATE_DIR.mkdir(mode=0o700, exist_ok=True)
|
|
state_path = STATE_DIR / "state.json"
|
|
if state_path.is_file():
|
|
state = json.loads(state_path.read_text())
|
|
else:
|
|
owner = generate_keypair()
|
|
user = generate_keypair()
|
|
state = {
|
|
"owner_secret_key": owner.secret_key,
|
|
"user_secret_key": user.secret_key,
|
|
"postgres_password": secrets.token_urlsafe(24),
|
|
"redis_password": secrets.token_urlsafe(24),
|
|
"s3_access_key": secrets.token_hex(10),
|
|
"s3_secret_key": secrets.token_hex(20),
|
|
"git_hook_hmac_secret": secrets.token_hex(32),
|
|
"relay_private_key": generate_keypair().secret_key,
|
|
}
|
|
state_path.touch(mode=0o600)
|
|
state_path.write_text(json.dumps(state, indent=2))
|
|
state["owner_pubkey"] = keypair_from_secret(state["owner_secret_key"]).pubkey
|
|
state["user_pubkey"] = keypair_from_secret(state["user_secret_key"]).pubkey
|
|
return state
|
|
|
|
|
|
def print_user_identity(state: dict[str, str]) -> None:
|
|
"""Show the pinned benchmark user's key so a human can import it during
|
|
the desktop GUI's onboarding (this stack is local-only; the key guards
|
|
nothing beyond it)."""
|
|
from harbor_buzz_testbed.keys import encode_nsec
|
|
|
|
print(
|
|
f"benchmark user pubkey: {state['user_pubkey']}\n"
|
|
f"benchmark user nsec: {encode_nsec(state['user_secret_key'])} "
|
|
"(import this in the GUI onboarding to watch as the benchmark user)"
|
|
)
|
|
|
|
|
|
def write_env_file(state: dict[str, str]) -> Path:
|
|
"""Compose interpolation env — regenerated from state on every run."""
|
|
env_path = STATE_DIR / ".env"
|
|
lines = {
|
|
"BUZZ_IMAGE": os.environ.get("BUZZ_IMAGE", "ghcr.io/block/buzz:main"),
|
|
"BUZZ_DOMAIN": "localhost",
|
|
"RELAY_URL": f"ws://localhost:{RELAY_HTTP_PORT}",
|
|
"BUZZ_MEDIA_BASE_URL": f"http://localhost:{RELAY_HTTP_PORT}/media",
|
|
"BUZZ_MEDIA_SERVER_DOMAIN": "localhost",
|
|
"BUZZ_CORS_ORIGINS": f"http://localhost:{RELAY_HTTP_PORT}",
|
|
"BUZZ_REQUIRE_AUTH_TOKEN": "true",
|
|
"BUZZ_REQUIRE_RELAY_MEMBERSHIP": "true",
|
|
"BUZZ_ALLOW_NIP_OA_AUTH": "true",
|
|
"BUZZ_AUTO_MIGRATE": "true",
|
|
"BUZZ_GIT_CONFORMANCE_PROBE": "true",
|
|
"RUST_LOG": "buzz_relay=info,buzz_db=info,buzz_auth=info",
|
|
"RELAY_OWNER_PUBKEY": state["owner_pubkey"],
|
|
"BUZZ_RELAY_PRIVATE_KEY": state["relay_private_key"],
|
|
"BUZZ_GIT_HOOK_HMAC_SECRET": state["git_hook_hmac_secret"],
|
|
"POSTGRES_DB": "buzz",
|
|
"POSTGRES_USER": "buzz",
|
|
"POSTGRES_PASSWORD": state["postgres_password"],
|
|
"REDIS_PASSWORD": state["redis_password"],
|
|
"BUZZ_S3_ACCESS_KEY": state["s3_access_key"],
|
|
"BUZZ_S3_SECRET_KEY": state["s3_secret_key"],
|
|
"BUZZ_S3_BUCKET": "buzz-media",
|
|
"BUZZ_HTTP_PORT": str(RELAY_HTTP_PORT),
|
|
"BUZZ_PG_HOST_PORT": str(PG_HOST_PORT),
|
|
"BUZZ_METRICS_HOST_PORT": str(METRICS_HOST_PORT),
|
|
}
|
|
env_path.touch(mode=0o600)
|
|
env_path.write_text("".join(f"{k}={v}\n" for k, v in lines.items()))
|
|
return env_path
|
|
|
|
|
|
def postgres_dsn(state: dict[str, str]) -> str:
|
|
return (
|
|
f"postgresql://buzz:{state['postgres_password']}@127.0.0.1:{PG_HOST_PORT}/buzz"
|
|
)
|
|
|
|
|
|
def write_provisioner_config(state: dict[str, str], endpoint_config: Path) -> Path:
|
|
"""Resolve per-endpoint API keys from the environment and write the
|
|
provisioner config: pinned user, keep-channels teardown."""
|
|
endpoints = json.loads(endpoint_config.read_text())
|
|
llm_api_keys: dict[str, str] = {}
|
|
for name, entry in endpoints.items():
|
|
env_var = entry["api_key_env"]
|
|
key = os.environ.get(env_var)
|
|
if not key:
|
|
raise SystemExit(
|
|
f"endpoint {name!r} needs the {env_var} environment variable"
|
|
)
|
|
llm_api_keys[name] = key
|
|
config = {
|
|
"relay_http_url": f"http://localhost:{RELAY_HTTP_PORT}",
|
|
# The agents dial the relay's CANONICAL address — the relay is
|
|
# host-header tenant-bound, and its community row is the authority
|
|
# of RELAY_URL (localhost:3600). Inside the task container that
|
|
# loopback address is served by a tiny forwarder bridging to the
|
|
# Docker host gateway (see --relay-gateway below).
|
|
"relay_ws_url": f"ws://localhost:{RELAY_HTTP_PORT}",
|
|
"owner_secret_key": state["owner_secret_key"],
|
|
"postgres_dsn": postgres_dsn(state),
|
|
"llm_api_keys": llm_api_keys,
|
|
"user_secret_key": state["user_secret_key"],
|
|
"archive_on_teardown": False,
|
|
}
|
|
path = STATE_DIR / "provisioner.json"
|
|
path.touch(mode=0o600)
|
|
path.write_text(json.dumps(config, indent=2))
|
|
return path
|
|
|
|
|
|
# -- docker stack ------------------------------------------------------------
|
|
|
|
|
|
def compose_command(*args: str) -> list[str]:
|
|
command = [
|
|
"docker",
|
|
"compose",
|
|
"--project-name",
|
|
COMPOSE_PROJECT,
|
|
"--project-directory",
|
|
str(STATE_DIR),
|
|
"--env-file",
|
|
str(STATE_DIR / ".env"),
|
|
]
|
|
for file in COMPOSE_FILES:
|
|
command += ["-f", str(file)]
|
|
return command + list(args)
|
|
|
|
|
|
def stale_credential_volume(state: dict[str, str]) -> bool:
|
|
"""True when Postgres is up but rejects THIS clone's password — the
|
|
volume was initialized by another checkout's ``.benchmark/`` state
|
|
(compose project name is machine-global, state dir is per-clone)."""
|
|
import psycopg
|
|
|
|
try:
|
|
psycopg.connect(postgres_dsn(state), connect_timeout=5).close()
|
|
except psycopg.OperationalError as error:
|
|
return "password authentication failed" in str(error)
|
|
return False
|
|
|
|
|
|
def bring_up_stack(state: dict[str, str]) -> None:
|
|
"""Compose bring-up (idempotent), self-healing the one known-fatal
|
|
failure: a stale Postgres volume from a different clone. Nothing in
|
|
that volume is usable (we can't even authenticate to it), so drop the
|
|
volumes and retry once rather than aborting with instructions."""
|
|
try:
|
|
subprocess.run(compose_command("up", "-d", "--wait"), check=True)
|
|
except subprocess.CalledProcessError:
|
|
if not stale_credential_volume(state):
|
|
raise
|
|
print(
|
|
"benchmark Postgres volume was initialized by a different "
|
|
"checkout's .benchmark/ state — dropping the stale volumes and "
|
|
"retrying..."
|
|
)
|
|
subprocess.run(compose_command("down", "-v"), check=True)
|
|
subprocess.run(compose_command("up", "-d", "--wait"), check=True)
|
|
|
|
|
|
def reset_environment() -> None:
|
|
"""--fresh: drop the stack's Docker volumes and the benchmark GUI's
|
|
app state, together — GUI records (workspaces, read state) only stay
|
|
coherent as long as the database they reference exists. Keys in
|
|
``state.json`` are kept, so the same nsec works after the reset."""
|
|
subprocess.run(compose_command("down", "-v"), check=True)
|
|
if sys.platform == "darwin":
|
|
for domain in ("WebKit", "Caches", "Application Support"):
|
|
shutil.rmtree(
|
|
Path.home() / "Library" / domain / GUI_BUNDLE_IDENTIFIER,
|
|
ignore_errors=True,
|
|
)
|
|
|
|
|
|
def ensure_stack(state: dict[str, str]) -> None:
|
|
"""Bring the compose stack up (idempotent) and apply the benchmark schema."""
|
|
import psycopg
|
|
|
|
bring_up_stack(state)
|
|
|
|
deadline = time.monotonic() + 60
|
|
last_error: Exception | None = None
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
with psycopg.connect(postgres_dsn(state)) as conn:
|
|
conn.execute(SCHEMA_SQL.read_text())
|
|
conn.commit()
|
|
return
|
|
except psycopg.Error as error: # containers healthy but PG settling
|
|
last_error = error
|
|
time.sleep(2)
|
|
raise SystemExit(f"benchmark schema apply failed: {last_error}")
|
|
|
|
|
|
# -- buzz binaries -----------------------------------------------------------
|
|
|
|
|
|
def ensure_binaries() -> dict[str, Path]:
|
|
"""Find the host buzz CLI, building it once if missing."""
|
|
try:
|
|
return run_leaderboard.find_binaries(None)
|
|
except SystemExit:
|
|
print("host buzz CLI missing — building (cargo build, first run only)...")
|
|
cargo = REPO_ROOT / "bin" / "cargo"
|
|
subprocess.run(
|
|
[str(cargo), "build", "-p", "buzz-cli"],
|
|
cwd=REPO_ROOT,
|
|
check=True,
|
|
)
|
|
return run_leaderboard.find_binaries(None)
|
|
|
|
|
|
def linux_triple() -> str:
|
|
"""The musl triple matching the Docker engine that runs task containers."""
|
|
arch = subprocess.run(
|
|
["docker", "version", "--format", "{{.Server.Arch}}"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout.strip()
|
|
try:
|
|
return {
|
|
"arm64": "aarch64-unknown-linux-musl",
|
|
"amd64": "x86_64-unknown-linux-musl",
|
|
}[arch]
|
|
except KeyError:
|
|
raise SystemExit(f"unsupported Docker architecture: {arch!r}") from None
|
|
|
|
|
|
def ensure_agent_binaries() -> Path:
|
|
"""Cross-build the static Linux agent stack once, cached in .benchmark/.
|
|
|
|
The agents run *inside* each Harbor task container as the real
|
|
buzz-acp → buzz-agent → buzz-dev-mcp stack, so the binaries must be
|
|
Linux ELF for the task image architecture. musl-static means they run
|
|
on any Linux base image (glibc or not). The relay loopback forwarder
|
|
is compiled in the same step with plain rustc (std-only, no deps).
|
|
"""
|
|
triple = linux_triple()
|
|
bin_dir = LINUX_TARGET_DIR / triple / "release"
|
|
targets = AGENT_BINARIES + (FORWARDER_BINARY,)
|
|
if all((bin_dir / name).is_file() for name in targets):
|
|
return bin_dir
|
|
print(
|
|
f"Linux agent binaries missing — cross-building for {triple} "
|
|
f"in {RUST_IMAGE} (first run only, ~2 min)..."
|
|
)
|
|
LINUX_TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
|
(STATE_DIR / "cargo-registry").mkdir(exist_ok=True)
|
|
packages = [arg for name in AGENT_BINARIES for arg in ("-p", name)]
|
|
forwarder_src = FORWARDER_SOURCE.relative_to(REPO_ROOT)
|
|
subprocess.run(
|
|
[
|
|
"docker",
|
|
"run",
|
|
"--rm",
|
|
"-v",
|
|
f"{REPO_ROOT}:/src:ro",
|
|
"-v",
|
|
f"{LINUX_TARGET_DIR}:/target",
|
|
"-v",
|
|
f"{STATE_DIR / 'cargo-registry'}:/usr/local/cargo/registry",
|
|
"-e",
|
|
"CARGO_TARGET_DIR=/target",
|
|
"-w",
|
|
"/src",
|
|
RUST_IMAGE,
|
|
"sh",
|
|
"-c",
|
|
"apk add --no-cache musl-dev >/dev/null && "
|
|
f"cargo build --release --locked --target {triple} "
|
|
+ " ".join(packages)
|
|
+ f" && rustc --edition 2021 -O --target {triple}"
|
|
f" -o /target/{triple}/release/{FORWARDER_BINARY}"
|
|
f" /src/{forwarder_src}",
|
|
],
|
|
check=True,
|
|
)
|
|
missing = [n for n in targets if not (bin_dir / n).is_file()]
|
|
if missing:
|
|
raise SystemExit(f"cross-build produced no {', '.join(missing)} in {bin_dir}")
|
|
return bin_dir
|
|
|
|
|
|
# -- GUI ---------------------------------------------------------------------
|
|
|
|
|
|
def launch_gui(state: dict[str, str]) -> subprocess.Popen:
|
|
"""Open the Buzz desktop app logged in as the benchmark user.
|
|
|
|
The relay runs closed (membership required), so the user pubkey is first
|
|
added to the relay membership list via buzz-admin inside the container —
|
|
NIP-OA auth tags cover the agents, but the GUI authenticates as a plain
|
|
member, exactly like a human.
|
|
"""
|
|
subprocess.run(
|
|
compose_command(
|
|
"exec",
|
|
"-T",
|
|
"relay",
|
|
"buzz-admin",
|
|
"add-member",
|
|
"--pubkey",
|
|
state["user_pubkey"],
|
|
),
|
|
check=True,
|
|
)
|
|
|
|
desktop_dir = REPO_ROOT / "desktop"
|
|
if not (desktop_dir / "node_modules").is_dir():
|
|
subprocess.run(["pnpm", "install"], cwd=desktop_dir, check=True)
|
|
|
|
# tauri dev needs sidecar files present; stub them and drop in the real
|
|
# CLI binary (mirrors the just staging recipe).
|
|
target = subprocess.run(
|
|
["rustc", "-vV"], capture_output=True, text=True, check=True
|
|
).stdout
|
|
triple = next(
|
|
line.split(": ", 1)[1]
|
|
for line in target.splitlines()
|
|
if line.startswith("host: ")
|
|
)
|
|
sidecar_dir = desktop_dir / "src-tauri" / "binaries"
|
|
sidecar_dir.mkdir(parents=True, exist_ok=True)
|
|
binaries = ensure_binaries()
|
|
for name in (
|
|
"buzz-acp",
|
|
"buzz-agent",
|
|
"buzz-dev-mcp",
|
|
"git-credential-nostr",
|
|
"buzz",
|
|
):
|
|
stub = sidecar_dir / f"{name}-{triple}"
|
|
if not stub.exists():
|
|
stub.touch()
|
|
real_cli = sidecar_dir / f"buzz-{triple}"
|
|
real_cli.write_bytes(binaries["buzz"].read_bytes())
|
|
real_cli.chmod(0o755)
|
|
|
|
print(
|
|
f"Opening Buzz GUI as the benchmark user ({state['user_pubkey'][:16]}…).\n"
|
|
"Watch, don't type — a message from you mid-trial would taint the run."
|
|
)
|
|
# Distinct bundle identifier: the desktop app persists workspaces (incl.
|
|
# their relay URLs) in per-identifier WebKit localStorage, and a stored
|
|
# workspace's relay URL overrides BUZZ_RELAY_URL by design. Reusing the
|
|
# default identifier means any past local-dev session's ws://localhost:3000
|
|
# workspace silently shadows the benchmark relay. An identifier of our own
|
|
# keeps that state isolated both ways.
|
|
tauri_config = json.dumps(
|
|
{"identifier": GUI_BUNDLE_IDENTIFIER, "productName": "Buzz Benchmark"}
|
|
)
|
|
return subprocess.Popen(
|
|
["pnpm", "exec", "tauri", "dev", "--config", tauri_config],
|
|
cwd=desktop_dir,
|
|
env={
|
|
**os.environ,
|
|
"BUZZ_RELAY_URL": f"ws://localhost:{RELAY_HTTP_PORT}",
|
|
"BUZZ_PRIVATE_KEY": state["user_secret_key"],
|
|
},
|
|
)
|
|
|
|
|
|
# -- main ---------------------------------------------------------------------
|
|
|
|
|
|
def leaderboard_argv(
|
|
args: argparse.Namespace, provisioner_config: Path, agent_bin_dir: Path
|
|
) -> list[str]:
|
|
argv: list[str] = []
|
|
if args.path:
|
|
argv += ["--path", str(args.path)]
|
|
else:
|
|
argv += ["--dataset", args.dataset or DEFAULT_DATASET]
|
|
for pattern in args.include_task:
|
|
argv += ["--include-task", pattern]
|
|
for pattern in args.exclude_task:
|
|
argv += ["--exclude-task", pattern]
|
|
argv += [
|
|
"--attempts",
|
|
str(args.attempts),
|
|
"--manifest",
|
|
str(args.manifest),
|
|
"--endpoint-config",
|
|
str(args.endpoint_config),
|
|
"--provisioner-config",
|
|
str(provisioner_config),
|
|
"--agent-bin-dir",
|
|
str(agent_bin_dir),
|
|
# The relay as reachable from inside a task container: Docker's
|
|
# host alias, bridged to the canonical localhost address by the
|
|
# uploaded forwarder. Override the alias with
|
|
# BUZZ_BENCHMARK_DOCKER_HOST if your engine exposes the host
|
|
# differently.
|
|
"--relay-gateway",
|
|
(
|
|
f"{os.environ.get('BUZZ_BENCHMARK_DOCKER_HOST', 'host.docker.internal')}"
|
|
f":{RELAY_HTTP_PORT}"
|
|
),
|
|
"--n-concurrent",
|
|
str(args.n_concurrent),
|
|
"--jobs-dir",
|
|
str(args.jobs_dir),
|
|
]
|
|
if args.job_name:
|
|
argv += ["--job-name", args.job_name]
|
|
if args.upload:
|
|
argv.append("--upload")
|
|
if args.dry_run:
|
|
argv.append("--dry-run")
|
|
return argv
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
state = load_state()
|
|
print_user_identity(state)
|
|
write_env_file(state)
|
|
provisioner_config = write_provisioner_config(state, args.endpoint_config)
|
|
|
|
if args.dry_run:
|
|
agent_bin_dir = LINUX_TARGET_DIR / linux_triple() / "release"
|
|
else:
|
|
ensure_binaries()
|
|
agent_bin_dir = ensure_agent_binaries()
|
|
if args.fresh:
|
|
reset_environment()
|
|
ensure_stack(state)
|
|
if args.gui:
|
|
launch_gui(state)
|
|
|
|
return run_leaderboard.main(
|
|
leaderboard_argv(args, provisioner_config, agent_bin_dir)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|