feat: import Chinese-localized Buzz source snapshot
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
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>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.benchmark/
|
||||
jobs/
|
||||
@@ -0,0 +1,129 @@
|
||||
# Harbor Buzz Orchestra
|
||||
|
||||
A stock-Harbor custom agent that runs a manifest-defined team through the real
|
||||
Buzz stack. Harbor sees one `BuzzOrchestraAgent`; behind that adapter, one
|
||||
orchestrator and N workers coordinate over the production relay/Postgres.
|
||||
Each agent runs *inside* the Harbor task container as the same
|
||||
`buzz-acp` → `buzz-agent` → `buzz-dev-mcp` process tree the desktop app
|
||||
launches: the production MCP toolset (shell, file tools, todo) with the
|
||||
`buzz` CLI on the shell's PATH. No Harbor fork or patch is required.
|
||||
|
||||
## Define the team
|
||||
|
||||
The manifest is the benchmark condition. Each roster entry selects an agent
|
||||
class's count, model endpoint, byte-pinned system prompt, generation settings,
|
||||
and budget:
|
||||
|
||||
```yaml
|
||||
condition: my-team
|
||||
roster:
|
||||
- id: orch
|
||||
kind: orchestrator
|
||||
role: lead
|
||||
count: 1
|
||||
endpoint: databricks/frontier
|
||||
prompt: {path: personas/orchestrator.md, sha256: <sha256>}
|
||||
generation: {max_output_tokens: 4096, context_window_tokens: 128000}
|
||||
- id: worker
|
||||
kind: worker
|
||||
role: implementer
|
||||
count: 4
|
||||
endpoint: databricks/fast-worker
|
||||
prompt: {path: personas/worker.md, sha256: <sha256>}
|
||||
generation: {max_output_tokens: 4096, context_window_tokens: 128000}
|
||||
```
|
||||
|
||||
`endpoint_config` maps those endpoint names to providers, URLs, and API-key
|
||||
environment variables. The adapter contains no fixed roster or model.
|
||||
|
||||
## Run
|
||||
|
||||
With the production compose stack and model endpoints already running, execute
|
||||
one task (`-p`), a directory of tasks, or replace `-p` with Harbor's dataset and
|
||||
task selectors:
|
||||
|
||||
```bash
|
||||
uv run --project benchmarks/harbor-buzz-orchestra/testbed harbor run --yes -p <TASK_OR_DIRECTORY> --agent harbor_buzz_orchestra:BuzzOrchestraAgent --agent-kwarg manifest=<CONDITION.yaml> --agent-kwarg provisioner_factory=harbor_buzz_testbed:provisioner_from_dict --agent-kwarg provisioner_config=<PROVISIONER.json> --agent-kwarg endpoint_config=<ENDPOINTS.json> --agent-kwarg artifact_root=benchmarks/harbor-buzz-orchestra --agent-kwarg buzz_acp_binary=<LINUX_BIN>/buzz-acp --agent-kwarg buzz_agent_binary=<LINUX_BIN>/buzz-agent --agent-kwarg buzz_dev_mcp_binary=<LINUX_BIN>/buzz-dev-mcp --agent-kwarg buzz_cli_binary=target/debug/buzz --agent-kwarg run_id="bench-$(date -u +%Y%m%dT%H%M%SZ)" --agent-timeout-multiplier 15 --n-concurrent 1
|
||||
```
|
||||
|
||||
`buzz_acp_binary`/`buzz_agent_binary`/`buzz_dev_mcp_binary` must be **Linux**
|
||||
builds matching the task image architecture — they are uploaded into each task
|
||||
container (`just benchmark` cross-builds them automatically; musl-static, so
|
||||
any Linux base image works). `buzz_cli_binary` is the **host** CLI the harness
|
||||
uses to act as the trial user.
|
||||
|
||||
`--n-concurrent 1` is the safe laptop setting for a serialized local model; it
|
||||
is not an orchestration requirement. Some TB graders install dependencies from
|
||||
public package registries at verification time — run benchmarks off networks
|
||||
that block those installs (e.g. corporate VPNs).
|
||||
|
||||
Each trial gets fresh keys and a private Buzz channel. The provisioner archives
|
||||
rather than deletes that channel, leaving the relay/Postgres event timeline
|
||||
and the per-agent acp/agent logs (downloaded into the trial's `buzz/`
|
||||
artifacts) available for analysis.
|
||||
|
||||
## Leaderboard runs
|
||||
|
||||
`just benchmark` is the one-command path: it stands up a dedicated Docker
|
||||
stack (`buzz-benchmark` compose project — relay :3600, Postgres :5633, secrets
|
||||
generated once into the gitignored `.benchmark/`), applies the benchmark
|
||||
schema, and defaults to leaderboard-eligible settings (Terminal-Bench 2.1,
|
||||
5 attempts per problem, the Sonnet+Haiku team). All selectors pass through:
|
||||
|
||||
```bash
|
||||
just benchmark # full TB 2.1, k=5
|
||||
just benchmark --path <TASK_DIR> -k 1 # one local task, one attempt
|
||||
just benchmark -i "cobol*" --attempts 3 # dataset subset
|
||||
just benchmark --gui # watch the run live
|
||||
```
|
||||
|
||||
One pinned user identity fronts the whole benchmark environment: it owns
|
||||
every trial channel (named after the task) and posts every task prompt, and
|
||||
trial channels are kept rather than archived. `--gui` adds that user to the
|
||||
relay membership list and opens the Buzz desktop app logged in as them, so
|
||||
channels fill the sidebar as the run progresses — watch, don't type; a human
|
||||
message mid-trial would taint the run. `just benchmark-down` stops the stack.
|
||||
|
||||
Networking: the relay is host-header tenant-bound, so agents must dial its
|
||||
canonical address (`ws://localhost:3600`) even from inside a task container.
|
||||
`just benchmark` uploads a tiny std-only loopback forwarder
|
||||
([`forwarder/relay_forwarder.rs`](forwarder/relay_forwarder.rs)) with the
|
||||
agent stack; it listens on the container's loopback and bridges the byte
|
||||
stream to the Docker host gateway (`host.docker.internal`, overridable via
|
||||
`BUZZ_BENCHMARK_DOCKER_HOST`).
|
||||
|
||||
`scripts/run_leaderboard.py` is the layer underneath, for running against an
|
||||
already-provisioned stack. It wraps the invocation above with only
|
||||
leaderboard-legal settings — it does not accept or forward timeout or resource
|
||||
overrides, so the job directory it produces passes Harbor's static validation
|
||||
as-is. Give it a problem set, attempts per problem, and a team manifest:
|
||||
|
||||
```bash
|
||||
uv run --project benchmarks/harbor-buzz-orchestra/testbed \
|
||||
benchmarks/harbor-buzz-orchestra/scripts/run_leaderboard.py \
|
||||
--dataset terminal-bench/terminal-bench-2-1 \
|
||||
--attempts 5 \
|
||||
--manifest benchmarks/harbor-buzz-orchestra/manifests/<TEAM>.yaml \
|
||||
--endpoint-config benchmarks/harbor-buzz-orchestra/testbed/endpoints/<ENDPOINTS>.json \
|
||||
--provisioner-config <PROVISIONER.json>
|
||||
```
|
||||
|
||||
`--path` replaces `--dataset` for local task directories; `--include-task` /
|
||||
`--exclude-task` filter by glob; `--dry-run` prints the underlying `harbor run`
|
||||
command. After the job finishes the script derives a `metadata.yaml` from the
|
||||
manifest roster (validated schema; review the display names before submitting)
|
||||
and prints the `harbor upload` / `harbor leaderboard submit` commands.
|
||||
|
||||
## Validate
|
||||
|
||||
```bash
|
||||
cd benchmarks/harbor-buzz-orchestra
|
||||
uv run --extra dev pytest -q
|
||||
uv run --extra dev ruff check .
|
||||
cd testbed
|
||||
uv run --extra dev pytest -q
|
||||
uv run --extra dev ruff check .
|
||||
```
|
||||
|
||||
Live provisioner tests require the benchmark compose stack and opt-in
|
||||
environment described in `testbed/tests/test_provisioner_live.py`.
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Loopback TCP forwarder for the benchmark task container.
|
||||
//!
|
||||
//! The benchmark relay is host-header tenant-bound: its community row is the
|
||||
//! authority of its own `RELAY_URL` (e.g. `localhost:3600`), and a request
|
||||
//! presenting any other `Host` fails closed. Agents inside a Harbor task
|
||||
//! container can only reach the host-published relay via the Docker host
|
||||
//! alias (`host.docker.internal`), which would present the wrong `Host`.
|
||||
//!
|
||||
//! So the container runtime uploads this forwarder next to the agent stack:
|
||||
//! agents dial `ws://localhost:<port>` — presenting the exact `Host` the
|
||||
//! community row expects — and the forwarder bridges the byte stream to the
|
||||
//! host gateway. Transparent to everything above TCP (WebSocket, the buzz
|
||||
//! CLI, git-over-HTTP). std-only; compiled with plain `rustc` against the
|
||||
//! musl target, so it runs on any Linux task image.
|
||||
//!
|
||||
//! Usage: `relay-forwarder <listen-addr> <target-addr>`
|
||||
|
||||
use std::io::{self, Read, Write};
|
||||
use std::net::{Shutdown, TcpListener, TcpStream};
|
||||
use std::thread;
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let (listen, target) = match (args.next(), args.next()) {
|
||||
(Some(listen), Some(target)) => (listen, target),
|
||||
_ => {
|
||||
eprintln!("usage: relay-forwarder <listen-addr> <target-addr>");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
let listener = TcpListener::bind(&listen)?;
|
||||
// Readiness marker: the container runtime polls the log for this line
|
||||
// before launching any agent.
|
||||
println!("forwarding {listen} -> {target}");
|
||||
for client in listener.incoming() {
|
||||
let Ok(client) = client else { continue };
|
||||
let target = target.clone();
|
||||
thread::spawn(move || bridge(client, &target));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Connect upstream and pump bytes both ways until either side closes.
|
||||
fn bridge(client: TcpStream, target: &str) {
|
||||
let Ok(upstream) = TcpStream::connect(target) else {
|
||||
let _ = client.shutdown(Shutdown::Both);
|
||||
return;
|
||||
};
|
||||
let (Ok(client_read), Ok(upstream_read)) = (client.try_clone(), upstream.try_clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let downstream = thread::spawn(move || pipe(upstream_read, client));
|
||||
pipe(client_read, upstream);
|
||||
let _ = downstream.join();
|
||||
}
|
||||
|
||||
/// Copy until EOF or error, then half-close the write side so protocols
|
||||
/// layered on TCP (WebSocket close handshakes) terminate cleanly.
|
||||
fn pipe(mut from: TcpStream, mut to: TcpStream) {
|
||||
let mut buf = [0u8; 16 * 1024];
|
||||
loop {
|
||||
match from.read(&mut buf) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
if to.write_all(&buf[..n]).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = to.shutdown(Shutdown::Write);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# M1 gate manifest: 1 orchestrator + 1 worker over one Buzz channel solving
|
||||
# Harbor's examples/tasks/hello-world via vanilla `harbor run`.
|
||||
#
|
||||
# Endpoints are LOCAL PLACEHOLDERS for the M1 wiring proof; the pilot/2.1
|
||||
# manifests will carry exact Databricks serving endpoint revisions and the
|
||||
# frozen price table per the canonical plan.
|
||||
schema_version: "1"
|
||||
condition: M1-hello-world
|
||||
roster:
|
||||
- id: orch
|
||||
kind: orchestrator
|
||||
role: lead
|
||||
count: 1
|
||||
endpoint: local/placeholder-orchestrator
|
||||
model_revision: m1-placeholder
|
||||
prompt:
|
||||
path: personas/orchestrator-m1.md
|
||||
sha256: af64b515de914c99fbdfc5a50830909ac10ab3a4f37e8352a354019a6be74151
|
||||
generation:
|
||||
max_output_tokens: 4096
|
||||
context_window_tokens: 128000
|
||||
- id: worker
|
||||
kind: worker
|
||||
role: implementer
|
||||
count: 1
|
||||
endpoint: local/placeholder-worker
|
||||
model_revision: m1-placeholder
|
||||
prompt:
|
||||
path: personas/worker-m1.md
|
||||
sha256: f3fef3d2c42105c256ff019d71ef99506b31a23e8cb252cac727c8bc470eb304
|
||||
generation:
|
||||
max_output_tokens: 4096
|
||||
context_window_tokens: 128000
|
||||
prices:
|
||||
local/placeholder-orchestrator:
|
||||
input_per_million_usd: 0
|
||||
cached_input_per_million_usd: 0
|
||||
output_per_million_usd: 0
|
||||
local/placeholder-worker:
|
||||
input_per_million_usd: 0
|
||||
cached_input_per_million_usd: 0
|
||||
output_per_million_usd: 0
|
||||
trial_budget:
|
||||
timeout_seconds: 1800
|
||||
@@ -0,0 +1,42 @@
|
||||
# Terminal-Bench trial: 1 Sonnet 4.6 orchestrator + 2 Haiku 4.5 workers over
|
||||
# one Buzz channel. Endpoint names are exact Anthropic model IDs (the runtime
|
||||
# passes the endpoint name to the provider as the model).
|
||||
# Prices: Anthropic list $/Mtok as of 2026-07 (Sonnet 3/15, Haiku 1/5).
|
||||
schema_version: "1"
|
||||
condition: tb-cobol-sonnet46-2haiku45
|
||||
roster:
|
||||
- id: orch
|
||||
kind: orchestrator
|
||||
role: lead
|
||||
count: 1
|
||||
endpoint: claude-sonnet-4-6
|
||||
model_revision: claude-sonnet-4-6
|
||||
prompt:
|
||||
path: personas/orchestrator-tb.md
|
||||
sha256: 206829331cdd85fb277266901b9489e190ba23097600a8bb69142d044b9a42e6
|
||||
generation:
|
||||
max_output_tokens: 4096
|
||||
context_window_tokens: 200000
|
||||
- id: worker
|
||||
kind: worker
|
||||
role: implementer
|
||||
count: 2
|
||||
endpoint: claude-haiku-4-5
|
||||
model_revision: claude-haiku-4-5
|
||||
prompt:
|
||||
path: personas/worker-tb.md
|
||||
sha256: 664a994ae3ba93f02ebbb8f61f781f6a59ea3327b21e26e7ffc2d06827ed3e45
|
||||
generation:
|
||||
max_output_tokens: 4096
|
||||
context_window_tokens: 200000
|
||||
prices:
|
||||
claude-sonnet-4-6:
|
||||
input_per_million_usd: 3
|
||||
cached_input_per_million_usd: 0.3
|
||||
output_per_million_usd: 15
|
||||
claude-haiku-4-5:
|
||||
input_per_million_usd: 1
|
||||
cached_input_per_million_usd: 0.1
|
||||
output_per_million_usd: 5
|
||||
trial_budget:
|
||||
timeout_seconds: 900
|
||||
@@ -0,0 +1,34 @@
|
||||
# Orchestrator — M1 hello-world
|
||||
|
||||
You are the orchestrator of a small team solving a terminal task. You do not
|
||||
run commands yourself; workers do. You coordinate over a Buzz channel.
|
||||
|
||||
Your `shell` tool has the `buzz` CLI on PATH, already authenticated as
|
||||
you. Nothing you write is visible to anyone unless you publish it: every
|
||||
message — step assignments, verification requests, the final `DONE:`
|
||||
— must be sent with
|
||||
`buzz messages send --channel <channel-id> --content <text>`. Your team, your
|
||||
channel id, and the user you report to are listed in the "Your team"
|
||||
section below. Your turn is not complete until you have published your
|
||||
message.
|
||||
|
||||
Rules:
|
||||
1. Read the task instruction. Break it into the smallest concrete steps.
|
||||
2. Assign each step to a worker with an @mention. One step per message.
|
||||
State the exact goal and the success check, not just the command to run.
|
||||
The task runs in the worker's terminal working directory. Unless the task
|
||||
instruction itself names a path, refer to files by bare relative name
|
||||
(`hello.txt`) — never invent an absolute path. More generally, relay the
|
||||
task's requirements verbatim and do not add constraints the task does not
|
||||
state (paths, encodings, byte-level rules such as forbidding a trailing
|
||||
newline). Where the task is silent, let standard tool defaults apply.
|
||||
3. Wait for the worker's report before assigning the next dependent step.
|
||||
4. When a worker reports output, verify it against the task's success
|
||||
criteria yourself before moving on.
|
||||
5. When the task is complete, report back to the user: publish a final
|
||||
message starting with `DONE:` that @mentions the user and summarizes
|
||||
what was produced and how you verified it. The task is not finished
|
||||
until this message is published — never conclude silently.
|
||||
|
||||
Keep messages short. Never fabricate command output. If a worker's report is
|
||||
ambiguous, ask them to re-run with the exact verification command.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Orchestrator — Terminal-Bench team
|
||||
|
||||
You are the orchestrator of a small team solving a terminal task. You do not
|
||||
run commands yourself; your workers do. You coordinate over a Buzz channel.
|
||||
Your team, your channel id, and the user you report to are listed in the
|
||||
"Your team" section below.
|
||||
|
||||
Your `shell` tool has the `buzz` CLI on PATH, already authenticated as
|
||||
you. Nothing you write is visible to anyone unless you publish it: every
|
||||
message — step assignments, verification requests, the final `DONE:`
|
||||
report — must be sent with
|
||||
`buzz messages send --channel <channel-id> --content <text>`. Your turn is
|
||||
not complete until you have published your message. Do not use the shell
|
||||
for task work — that is your workers' job.
|
||||
|
||||
Tasks arrive as a channel message from the user @mentioning you. Address
|
||||
each assignment to a specific worker by @mention, exactly one worker per
|
||||
step. You may assign independent steps to different workers, but never give
|
||||
two workers overlapping or conflicting work — they share one task
|
||||
environment and one filesystem.
|
||||
|
||||
Rules:
|
||||
1. Read the task instruction. Break it into the smallest concrete steps.
|
||||
2. Assign each step to a worker with an @mention. One step per message.
|
||||
State the exact goal and the success check, not just the command to run.
|
||||
Relay the task's requirements verbatim — use the paths the task states,
|
||||
and do not add constraints the task does not state (paths, encodings,
|
||||
byte-level rules). Where the task is silent, let standard tool defaults
|
||||
apply.
|
||||
3. Wait for the worker's report before assigning the next dependent step.
|
||||
4. When a worker reports output, verify it against the task's success
|
||||
criteria before moving on: assign a verification step that runs the
|
||||
task's own success check and shows real output. Assign each verification
|
||||
step to a different worker than the one whose work is being verified —
|
||||
independent verification, never self-review. Do not report completion on
|
||||
a worker's claim alone.
|
||||
5. When the task is complete and verified, report back to the user: publish
|
||||
a final message starting with `DONE:` that @mentions the user and
|
||||
summarizes what was produced and how it was verified. The task is not
|
||||
finished until this message is published — never conclude silently.
|
||||
|
||||
Keep messages short. Never fabricate command output. If a worker's report is
|
||||
ambiguous, ask them to re-run with the exact verification command.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Worker — M1 hello-world
|
||||
|
||||
You are a worker agent with terminal access, coordinating over a Buzz channel.
|
||||
|
||||
You work directly in the task environment: your `shell` tool runs
|
||||
commands in it, and your file tools read and edit its files. The same
|
||||
shell has the `buzz` CLI on PATH, already authenticated as you; reports go
|
||||
to the channel with
|
||||
`buzz messages send --channel <channel-id> --content <text>`. Your team and your
|
||||
channel id are listed in the "Your team" section below. Your turn is not
|
||||
complete until you have published your report.
|
||||
|
||||
The orchestrator only wakes for messages that @mention it. Every report
|
||||
you publish must start with an @mention of the agent that assigned you the
|
||||
step (use their name exactly as it appears in their message). If the
|
||||
assignment's event id is visible in your context, also pass
|
||||
`--reply-to <event-id>` to thread the report. A report that mentions
|
||||
nobody is invisible and the task will stall.
|
||||
|
||||
Rules:
|
||||
1. Act only on steps assigned to you by the orchestrator's @mention.
|
||||
2. Execute the requested step in the terminal. Prefer the smallest command
|
||||
that achieves the stated goal. Create files in your terminal's current
|
||||
working directory unless the task itself names a path. If an assignment
|
||||
gives an absolute path your working directory does not contain, report
|
||||
the mismatch instead of creating the directory — `mkdir -p` on an
|
||||
invented path silently puts the file where the grader will never look.
|
||||
3. Report back in one message: the command you ran, its exit code, and the
|
||||
relevant output (trimmed, never invented).
|
||||
4. If a command fails, report the failure verbatim and stop — do not
|
||||
improvise a different approach without the orchestrator's direction.
|
||||
5. Never claim success without showing the verifying output.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Worker — Terminal-Bench team
|
||||
|
||||
You are a worker agent with terminal access, coordinating over a Buzz
|
||||
channel. Your team, your channel id, and your orchestrator are listed in
|
||||
the "Your team" section below.
|
||||
|
||||
You work directly in the task environment: your `shell` tool runs
|
||||
commands in it, and your file tools read and edit its files. The same
|
||||
shell has the `buzz` CLI on PATH, already authenticated as you; reports go
|
||||
to the channel with
|
||||
`buzz messages send --channel <channel-id> --content <text>`. Your turn is
|
||||
not complete until you have published your report.
|
||||
|
||||
The orchestrator only wakes for messages that @mention it. Every report
|
||||
you publish must start with an @mention of the agent that assigned you the
|
||||
step (use their name exactly as it appears in their message). If the
|
||||
assignment's event id is visible in your context, also pass
|
||||
`--reply-to <event-id>` to thread the report. A report that mentions
|
||||
nobody is invisible and the task will stall.
|
||||
|
||||
Rules:
|
||||
1. Act only on steps assigned to you by the orchestrator's @mention. If a
|
||||
message assigns a step to a different worker, ignore it.
|
||||
2. Execute the requested step in the terminal BEFORE writing any report.
|
||||
Never describe output you have not yet produced. Prefer the smallest
|
||||
command that achieves the stated goal. Use the paths the task or the
|
||||
assignment states; do not invent paths.
|
||||
3. Report back in one message: the command you ran, its exit code, and the
|
||||
relevant output (trimmed, never invented).
|
||||
4. If a command fails, report the failure verbatim and stop — do not
|
||||
improvise a different approach without the orchestrator's direction.
|
||||
5. Never claim success without showing the verifying output.
|
||||
@@ -0,0 +1,29 @@
|
||||
[project]
|
||||
name = "harbor-buzz-orchestra"
|
||||
version = "0.1.0"
|
||||
description = "Harbor custom agent for Buzz-coordinated multi-agent benchmarks"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"harbor>=0.16.1,<0.18",
|
||||
"pydantic>=2.11",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.4",
|
||||
"pytest-asyncio>=1.2",
|
||||
"ruff>=0.15",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 88
|
||||
@@ -0,0 +1,607 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run a problem set with a team manifest and produce leaderboard-ready results.
|
||||
|
||||
One command wraps ``harbor run`` with only leaderboard-legal settings — no
|
||||
timeout or resource overrides are accepted or forwarded, so the resulting job
|
||||
directory passes Harbor's static validation as produced. After the run it
|
||||
writes a ``metadata.yaml`` template derived from the manifest and prints the
|
||||
exact upload/submit commands.
|
||||
|
||||
Run inside the testbed environment so ``harbor`` and the adapter are
|
||||
importable:
|
||||
|
||||
uv run --project benchmarks/harbor-buzz-orchestra/testbed \
|
||||
benchmarks/harbor-buzz-orchestra/scripts/run_leaderboard.py \
|
||||
--dataset terminal-bench/terminal-bench-2-1 \
|
||||
--attempts 5 \
|
||||
--manifest benchmarks/harbor-buzz-orchestra/manifests/<TEAM>.yaml \
|
||||
--endpoint-config benchmarks/harbor-buzz-orchestra/testbed/endpoints/<ENDPOINTS>.json \
|
||||
--provisioner-config <PROVISIONER.json> \
|
||||
--agent-bin-dir <DIR with Linux buzz-acp/buzz-agent/buzz-dev-mcp>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
PACKAGE_ROOT = Path(__file__).resolve().parent.parent
|
||||
AGENT_IMPORT = "harbor_buzz_orchestra:BuzzOrchestraAgent"
|
||||
PROVISIONER_FACTORY = "harbor_buzz_testbed:provisioner_from_dict"
|
||||
# Host-side: the harness speaks to the relay as the trial user via this CLI.
|
||||
BINARIES = ("buzz",)
|
||||
# Container-side: the production stack uploaded into each task container.
|
||||
# These must be Linux builds matching the task image architecture.
|
||||
AGENT_BINARIES = ("buzz-acp", "buzz-agent", "buzz-dev-mcp")
|
||||
# Uploaded alongside the stack when --relay-gateway is set: bridges the
|
||||
# agents' canonical relay address to the host gateway (the relay is
|
||||
# host-header tenant-bound, so agents must present its canonical Host).
|
||||
FORWARDER_BINARY = "relay-forwarder"
|
||||
|
||||
PROVIDER_ORGS = {
|
||||
"anthropic": "Anthropic",
|
||||
"openai": "OpenAI",
|
||||
"databricks": "Databricks",
|
||||
}
|
||||
|
||||
|
||||
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(required=True)
|
||||
problems.add_argument(
|
||||
"--dataset",
|
||||
"-d",
|
||||
help="Registry dataset (e.g. terminal-bench/terminal-bench-2-1)",
|
||||
)
|
||||
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 from the dataset (glob, repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude-task",
|
||||
"-x",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Task name to exclude from the dataset (glob, repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--attempts",
|
||||
"-k",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Runs per problem (leaderboards require 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest", type=Path, required=True, help="Team manifest YAML"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--endpoint-config",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="JSON mapping manifest endpoint names to providers/API keys",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--provisioner-config",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="JSON config for the Buzz relay/Postgres provisioner",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--buzz-bin-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory with the host buzz CLI (default: repo target/release, then target/debug)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--agent-bin-dir",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Directory with Linux builds of buzz-acp/buzz-agent/buzz-dev-mcp "
|
||||
"to upload into each task container",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--relay-gateway",
|
||||
default="",
|
||||
help="host:port of the benchmark relay as reachable from inside the "
|
||||
"task container (e.g. host.docker.internal:3600). When set, a "
|
||||
"loopback forwarder from --agent-bin-dir bridges the canonical "
|
||||
"relay address to this gateway",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-concurrent", "-n", type=int, default=4, help="Concurrent trials"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jobs-dir", type=Path, default=Path("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(
|
||||
"--dry-run", action="store_true", help="Print the harbor command and exit"
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def find_binaries(bin_dir: Path | None) -> dict[str, Path]:
|
||||
candidates = (
|
||||
[bin_dir]
|
||||
if bin_dir is not None
|
||||
else [
|
||||
PACKAGE_ROOT.parents[1] / "target" / kind for kind in ("release", "debug")
|
||||
]
|
||||
)
|
||||
for candidate in candidates:
|
||||
found = {name: candidate / name for name in BINARIES}
|
||||
if all(path.is_file() for path in found.values()):
|
||||
return found
|
||||
searched = ", ".join(str(c) for c in candidates)
|
||||
raise SystemExit(
|
||||
f"buzz binaries not found (need {', '.join(BINARIES)}; searched {searched}). "
|
||||
"Build them with `cargo build` or pass --buzz-bin-dir."
|
||||
)
|
||||
|
||||
|
||||
def find_agent_binaries(bin_dir: Path, with_forwarder: bool = False) -> dict[str, Path]:
|
||||
"""The Linux agent stack uploaded into each task container."""
|
||||
names = AGENT_BINARIES + ((FORWARDER_BINARY,) if with_forwarder else ())
|
||||
found = {name: bin_dir / name for name in names}
|
||||
missing = [name for name, path in found.items() if not path.is_file()]
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
f"Linux agent binaries not found in {bin_dir}: {', '.join(missing)}. "
|
||||
"`just benchmark` builds them; or cross-compile with "
|
||||
"cargo --target <arch>-unknown-linux-musl and pass --agent-bin-dir."
|
||||
)
|
||||
return found
|
||||
|
||||
|
||||
def build_command(
|
||||
args: argparse.Namespace,
|
||||
binaries: dict[str, Path],
|
||||
agent_binaries: dict[str, Path],
|
||||
) -> list[str]:
|
||||
"""Compose the harbor invocation. Standard settings only: any timeout or
|
||||
resource override would fail leaderboard static validation, so none are
|
||||
accepted or forwarded."""
|
||||
command = [
|
||||
"harbor",
|
||||
"run",
|
||||
"--yes",
|
||||
"--job-name",
|
||||
args.job_name,
|
||||
"--jobs-dir",
|
||||
str(args.jobs_dir),
|
||||
"-k",
|
||||
str(args.attempts),
|
||||
"--n-concurrent",
|
||||
str(args.n_concurrent),
|
||||
]
|
||||
if args.dataset:
|
||||
command += ["--dataset", args.dataset]
|
||||
else:
|
||||
command += ["--path", str(args.path)]
|
||||
for pattern in args.include_task:
|
||||
command += ["--include-task-name", pattern]
|
||||
for pattern in args.exclude_task:
|
||||
command += ["--exclude-task-name", pattern]
|
||||
if args.upload:
|
||||
command.append("--upload")
|
||||
command += ["--agent", AGENT_IMPORT]
|
||||
kwargs = {
|
||||
"manifest": args.manifest,
|
||||
"provisioner_factory": PROVISIONER_FACTORY,
|
||||
"provisioner_config": args.provisioner_config,
|
||||
"artifact_root": PACKAGE_ROOT,
|
||||
"endpoint_config": args.endpoint_config,
|
||||
"buzz_acp_binary": agent_binaries["buzz-acp"],
|
||||
"buzz_agent_binary": agent_binaries["buzz-agent"],
|
||||
"buzz_dev_mcp_binary": agent_binaries["buzz-dev-mcp"],
|
||||
"buzz_cli_binary": binaries["buzz"],
|
||||
"run_id": args.job_name,
|
||||
}
|
||||
if args.relay_gateway:
|
||||
kwargs["relay_gateway"] = args.relay_gateway
|
||||
kwargs["forwarder_binary"] = agent_binaries[FORWARDER_BINARY]
|
||||
for key, value in kwargs.items():
|
||||
command += ["--agent-kwarg", f"{key}={value}"]
|
||||
return command
|
||||
|
||||
|
||||
def write_metadata_template(args: argparse.Namespace, job_dir: Path) -> Path:
|
||||
"""Derive a metadata.yaml template (harbor.leaderboard.metadata schema)
|
||||
from the manifest roster; display-name placeholders are for the submitter
|
||||
to confirm."""
|
||||
manifest = yaml.safe_load(args.manifest.read_text())
|
||||
endpoints = json.loads(args.endpoint_config.read_text())
|
||||
models, seen = [], set()
|
||||
for entry in manifest.get("roster", []):
|
||||
model = entry.get("model_revision") or entry["endpoint"]
|
||||
if model in seen:
|
||||
continue
|
||||
seen.add(model)
|
||||
provider = endpoints.get(entry["endpoint"], {}).get("provider", "FILL_ME")
|
||||
models.append(
|
||||
{
|
||||
"model_name": model,
|
||||
"model_provider": provider,
|
||||
"model_display_name": model,
|
||||
"model_org_display_name": PROVIDER_ORGS.get(provider, "FILL_ME"),
|
||||
}
|
||||
)
|
||||
metadata = {
|
||||
"agent_url": "https://github.com/block/buzz",
|
||||
"agent_display_name": f"Buzz Orchestra ({manifest.get('condition', 'team')})",
|
||||
"agent_org_display_name": "Block",
|
||||
"models": models,
|
||||
}
|
||||
path = job_dir / "metadata.yaml"
|
||||
path.write_text(yaml.safe_dump(metadata, sort_keys=False))
|
||||
return path
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
for label, path in (
|
||||
("manifest", args.manifest),
|
||||
("endpoint config", args.endpoint_config),
|
||||
("provisioner config", args.provisioner_config),
|
||||
):
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"{label} not found: {path}")
|
||||
if args.job_name is None:
|
||||
condition = yaml.safe_load(args.manifest.read_text()).get("condition", "team")
|
||||
stamp = dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ")
|
||||
args.job_name = f"lb-{condition}-{stamp}"
|
||||
|
||||
if args.dry_run:
|
||||
# Dry runs print the command without requiring built binaries.
|
||||
bin_dir = args.buzz_bin_dir or PACKAGE_ROOT.parents[1] / "target" / "release"
|
||||
binaries = {name: bin_dir / name for name in BINARIES}
|
||||
agent_binaries = {
|
||||
name: args.agent_bin_dir / name
|
||||
for name in AGENT_BINARIES + (FORWARDER_BINARY,)
|
||||
}
|
||||
print(" ".join(build_command(args, binaries, agent_binaries)))
|
||||
return 0
|
||||
binaries = find_binaries(args.buzz_bin_dir)
|
||||
agent_binaries = find_agent_binaries(
|
||||
args.agent_bin_dir, with_forwarder=bool(args.relay_gateway)
|
||||
)
|
||||
command = build_command(args, binaries, agent_binaries)
|
||||
if shutil.which("harbor") is None:
|
||||
raise SystemExit(
|
||||
"harbor not on PATH — run via: uv run --project "
|
||||
f"{PACKAGE_ROOT / 'testbed'} {Path(__file__).resolve()} ..."
|
||||
)
|
||||
|
||||
result = subprocess.run(command, check=False)
|
||||
job_dir = args.jobs_dir / args.job_name
|
||||
if result.returncode != 0:
|
||||
print(f"harbor run failed (exit {result.returncode}); job dir: {job_dir}")
|
||||
return result.returncode
|
||||
|
||||
metadata_path = write_metadata_template(args, job_dir)
|
||||
print("\nLeaderboard-ready job complete.")
|
||||
print(f" 1. Review submitter details in {metadata_path}")
|
||||
print(f" 2. harbor upload {job_dir}")
|
||||
print(
|
||||
" 3. harbor leaderboard submit -l terminal-bench/terminal-bench-2-1 "
|
||||
f"-j <job UUID from upload> -m {metadata_path}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Buzz orchestra custom agent for Harbor."""
|
||||
|
||||
from .agent import BuzzOrchestraAgent
|
||||
from .container_runtime import (
|
||||
BuzzContainerRuntime,
|
||||
EndpointLaunchConfig,
|
||||
RuntimeLaunchError,
|
||||
)
|
||||
from .manifest import ExperimentManifest, ManifestError
|
||||
from .provisioning import AgentCredential, TrialHandle, TrialProvisioner
|
||||
from .runtime import OrchestraRuntime, RuntimeResult
|
||||
|
||||
__all__ = [
|
||||
"AgentCredential",
|
||||
"BuzzContainerRuntime",
|
||||
"BuzzOrchestraAgent",
|
||||
"EndpointLaunchConfig",
|
||||
"ExperimentManifest",
|
||||
"ManifestError",
|
||||
"OrchestraRuntime",
|
||||
"RuntimeLaunchError",
|
||||
"RuntimeResult",
|
||||
"TrialHandle",
|
||||
"TrialProvisioner",
|
||||
]
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Harbor custom-agent entry point for Buzz orchestration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from harbor.agents.base import BaseAgent
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
from harbor.models.agent.context import AgentContext
|
||||
|
||||
from .container_runtime import BuzzContainerRuntime, EndpointLaunchConfig
|
||||
from .manifest import ExperimentManifest
|
||||
from .provisioning import TrialProvisioner
|
||||
from .runtime import OrchestraRuntime
|
||||
|
||||
|
||||
class BuzzOrchestraAgent(BaseAgent):
|
||||
"""Coordinate an arbitrary manifest-defined team through a Buzz trial."""
|
||||
|
||||
# Set True only once the runtime writes a validated agent/trajectory.json.
|
||||
SUPPORTS_ATIF = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logs_dir: Path,
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
manifest: str | Path | dict[str, Any],
|
||||
provisioner: TrialProvisioner | None = None,
|
||||
runtime: OrchestraRuntime | None = None,
|
||||
provisioner_factory: str | None = None,
|
||||
provisioner_config: str | Path | dict[str, Any] | None = None,
|
||||
artifact_root: str | Path | None = None,
|
||||
endpoint_config: str | Path | dict[str, Any] | None = None,
|
||||
buzz_acp_binary: str = "buzz-acp",
|
||||
buzz_agent_binary: str = "buzz-agent",
|
||||
buzz_dev_mcp_binary: str = "buzz-dev-mcp",
|
||||
buzz_cli_binary: str = "buzz",
|
||||
relay_gateway: str = "",
|
||||
forwarder_binary: str = "relay-forwarder",
|
||||
run_id: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
|
||||
self.manifest = ExperimentManifest.load(manifest)
|
||||
self.provisioner = provisioner or self._build_provisioner(
|
||||
provisioner_factory, provisioner_config
|
||||
)
|
||||
self.runtime = runtime or self._build_runtime(
|
||||
logs_dir,
|
||||
artifact_root,
|
||||
endpoint_config,
|
||||
buzz_acp_binary,
|
||||
buzz_agent_binary,
|
||||
buzz_dev_mcp_binary,
|
||||
buzz_cli_binary,
|
||||
relay_gateway,
|
||||
forwarder_binary,
|
||||
)
|
||||
self.run_id = run_id
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "buzz-orchestra"
|
||||
|
||||
def version(self) -> str:
|
||||
return "0.1.0"
|
||||
|
||||
@staticmethod
|
||||
def _load_mapping(
|
||||
source: str | Path | dict[str, Any] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
if source is None:
|
||||
return None
|
||||
if isinstance(source, dict):
|
||||
return source
|
||||
import json
|
||||
|
||||
path = Path(source).expanduser()
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"cannot load JSON config {path}: {error}") from error
|
||||
if not isinstance(value, dict):
|
||||
raise TypeError(f"JSON config {path} must contain an object")
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def _build_provisioner(
|
||||
cls,
|
||||
factory_path: str | None,
|
||||
config_source: str | Path | dict[str, Any] | None,
|
||||
) -> TrialProvisioner | None:
|
||||
config = cls._load_mapping(config_source)
|
||||
if factory_path is None and config is None:
|
||||
return None
|
||||
if factory_path is None or config is None:
|
||||
raise ValueError(
|
||||
"provisioner_factory and provisioner_config must be provided together"
|
||||
)
|
||||
from harbor.utils.import_path import import_symbol
|
||||
|
||||
factory = import_symbol(factory_path)
|
||||
return factory(config)
|
||||
|
||||
@classmethod
|
||||
def _build_runtime(
|
||||
cls,
|
||||
logs_dir: Path,
|
||||
artifact_root: str | Path | None,
|
||||
endpoint_source: str | Path | dict[str, Any] | None,
|
||||
buzz_acp_binary: str,
|
||||
buzz_agent_binary: str,
|
||||
buzz_dev_mcp_binary: str,
|
||||
buzz_cli_binary: str,
|
||||
relay_gateway: str,
|
||||
forwarder_binary: str,
|
||||
) -> OrchestraRuntime | None:
|
||||
endpoint_data = cls._load_mapping(endpoint_source)
|
||||
if endpoint_data is None and artifact_root is None:
|
||||
return None
|
||||
if endpoint_data is None or artifact_root is None:
|
||||
raise ValueError(
|
||||
"artifact_root and endpoint_config must be provided together"
|
||||
)
|
||||
endpoints = {
|
||||
name: EndpointLaunchConfig(
|
||||
provider=value["provider"],
|
||||
api_key_env=value["api_key_env"],
|
||||
env=value.get("env", {}),
|
||||
)
|
||||
for name, value in endpoint_data.items()
|
||||
}
|
||||
return BuzzContainerRuntime(
|
||||
logs_dir=logs_dir,
|
||||
artifact_root=Path(artifact_root),
|
||||
endpoints=endpoints,
|
||||
buzz_acp_binary=buzz_acp_binary,
|
||||
buzz_agent_binary=buzz_agent_binary,
|
||||
buzz_dev_mcp_binary=buzz_dev_mcp_binary,
|
||||
buzz_cli_binary=buzz_cli_binary,
|
||||
relay_gateway=relay_gateway,
|
||||
forwarder_binary=forwarder_binary,
|
||||
)
|
||||
|
||||
async def setup(self, environment: BaseEnvironment) -> None:
|
||||
"""Fail fast when the provisioner is configured but its stack is unhealthy."""
|
||||
if self.provisioner is not None:
|
||||
self.provisioner.healthcheck()
|
||||
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
if self.provisioner is None or self.runtime is None:
|
||||
raise RuntimeError(
|
||||
"BuzzOrchestraAgent requires provisioner and runtime integrations; "
|
||||
"the adapter contract is installed but M1 wiring is incomplete"
|
||||
)
|
||||
|
||||
context_id = self.context_id or environment.context_id
|
||||
if context_id is None:
|
||||
raise RuntimeError("Harbor context_id is required as the trial join key")
|
||||
trial_id = str(context_id)
|
||||
run_id = self.run_id or trial_id
|
||||
# Human-readable channel label: the task short name, so a spectator
|
||||
# GUI shows one recognisable channel per problem per attempt.
|
||||
channel_label = getattr(environment, "environment_name", None)
|
||||
handle = self.provisioner.create_trial(
|
||||
run_id, trial_id, self.manifest, channel_label=channel_label
|
||||
)
|
||||
if handle.trial_id != trial_id:
|
||||
raise RuntimeError("provisioner returned a handle for a different trial_id")
|
||||
if handle.manifest_hash != self.manifest.sha256:
|
||||
raise RuntimeError("provisioner returned a handle for a different manifest")
|
||||
try:
|
||||
result = await self.runtime.run(
|
||||
instruction=instruction,
|
||||
environment=environment,
|
||||
manifest=self.manifest,
|
||||
trial=handle,
|
||||
)
|
||||
finally:
|
||||
self.provisioner.teardown(handle)
|
||||
|
||||
context.n_input_tokens = result.input_tokens
|
||||
context.n_cache_tokens = result.cached_input_tokens
|
||||
context.n_output_tokens = result.output_tokens
|
||||
context.cost_usd = result.cost_usd
|
||||
context.metadata = {
|
||||
**result.metadata,
|
||||
"manifest_sha256": self.manifest.sha256,
|
||||
"condition": self.manifest.condition,
|
||||
"buzz_channel_id": handle.channel_id,
|
||||
"run_id": run_id,
|
||||
"trial_id": trial_id,
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
"""Run the production Buzz agent stack inside the Harbor task container.
|
||||
|
||||
Each provisioned identity is a full ``buzz-acp`` → ``buzz-agent`` →
|
||||
``buzz-dev-mcp`` process tree launched *inside* the task container — the same
|
||||
binaries and the same MCP toolset (shell, file tools, the ``buzz`` CLI on
|
||||
PATH) that the desktop app gives a Buzz agent. The harness stays outside:
|
||||
it provisions, uploads the pinned binaries, posts the task as the trial
|
||||
user, and observes the channel until the orchestrator publishes DONE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
|
||||
from .manifest import AgentClass, ExperimentManifest
|
||||
from .provisioning import AgentCredential, TrialHandle
|
||||
from .runtime import RuntimeResult
|
||||
|
||||
DEFAULT_MAX_AGENT_ROUNDS = 0 # 0 = unbounded (BUZZ_AGENT_MAX_ROUNDS=0); the trial budget is the clock
|
||||
# Container-side layout for the uploaded Buzz stack.
|
||||
REMOTE_ROOT = "/opt/buzz"
|
||||
REMOTE_BIN = f"{REMOTE_ROOT}/bin"
|
||||
REMOTE_PROMPTS = f"{REMOTE_ROOT}/prompts"
|
||||
REMOTE_LOGS = f"{REMOTE_ROOT}/logs"
|
||||
# The relay is host-header tenant-bound (its community row is the authority
|
||||
# of its own RELAY_URL), so agents must present that exact Host. When the
|
||||
# relay actually lives outside the container, this forwarder listens on the
|
||||
# canonical loopback address and bridges the byte stream to the gateway.
|
||||
FORWARDER = f"{REMOTE_BIN}/relay-forwarder"
|
||||
FORWARDER_LOG = f"{REMOTE_LOGS}/relay-forwarder.log"
|
||||
# How many done-poll iterations between in-container liveness probes.
|
||||
LIVENESS_EVERY = 10
|
||||
|
||||
|
||||
class RuntimeLaunchError(RuntimeError):
|
||||
"""Raised when a Buzz agent process cannot be launched or exits early."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EndpointLaunchConfig:
|
||||
"""Deployment-specific environment needed to launch one manifest endpoint."""
|
||||
|
||||
provider: str
|
||||
api_key_env: str
|
||||
env: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _Agent:
|
||||
credential: AgentCredential
|
||||
pid: int
|
||||
stdout_log: str # container path
|
||||
stderr_log: str # container path
|
||||
|
||||
|
||||
class BuzzContainerRuntime:
|
||||
"""Launch one production Buzz agent stack per identity in the container."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
logs_dir: Path,
|
||||
artifact_root: Path,
|
||||
endpoints: dict[str, EndpointLaunchConfig],
|
||||
buzz_acp_binary: str = "buzz-acp",
|
||||
buzz_agent_binary: str = "buzz-agent",
|
||||
buzz_dev_mcp_binary: str = "buzz-dev-mcp",
|
||||
buzz_cli_binary: str = "buzz",
|
||||
relay_gateway: str = "",
|
||||
forwarder_binary: str = "relay-forwarder",
|
||||
max_agent_rounds: int = DEFAULT_MAX_AGENT_ROUNDS,
|
||||
readiness_timeout_seconds: float = 60.0,
|
||||
poll_seconds: float = 1.0,
|
||||
) -> None:
|
||||
if max_agent_rounds < 0:
|
||||
raise ValueError("max_agent_rounds must be >= 0 (0 = unbounded)")
|
||||
if readiness_timeout_seconds <= 0:
|
||||
raise ValueError("readiness_timeout_seconds must be positive")
|
||||
self.logs_dir = Path(logs_dir)
|
||||
self.artifact_root = Path(artifact_root)
|
||||
self.endpoints = endpoints
|
||||
# Linux builds uploaded into the task container:
|
||||
self.buzz_acp_binary = buzz_acp_binary
|
||||
self.buzz_agent_binary = buzz_agent_binary
|
||||
self.buzz_dev_mcp_binary = buzz_dev_mcp_binary
|
||||
# Host build used for user/provisioning operations only:
|
||||
self.buzz_cli_binary = buzz_cli_binary
|
||||
# Where the relay actually lives, as seen from inside the task
|
||||
# container (e.g. host.docker.internal:3600). When set, a loopback
|
||||
# forwarder bridges the agents' canonical relay address — the Host
|
||||
# the relay's community row is bound to — to this gateway.
|
||||
self.relay_gateway = relay_gateway
|
||||
self.forwarder_binary = forwarder_binary
|
||||
self.max_agent_rounds = max_agent_rounds
|
||||
self.readiness_timeout_seconds = readiness_timeout_seconds
|
||||
self.poll_seconds = poll_seconds
|
||||
|
||||
async def run(
|
||||
self,
|
||||
*,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
manifest: ExperimentManifest,
|
||||
trial: TrialHandle,
|
||||
) -> RuntimeResult:
|
||||
classes = self._classes_by_agent_id(manifest, trial.credentials)
|
||||
orchestrator = next(c for c in trial.credentials if c.role == "orchestrator")
|
||||
workers = [c for c in trial.credentials if c.agent_id != orchestrator.agent_id]
|
||||
if not workers:
|
||||
raise RuntimeLaunchError("Buzz orchestration requires at least one worker")
|
||||
trial_dir = self.logs_dir / "buzz"
|
||||
trial_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
agents: list[_Agent] = []
|
||||
infra: list[_Agent] = []
|
||||
try:
|
||||
await self._install_stack(environment)
|
||||
forwarder = await self._start_forwarder(environment, trial)
|
||||
if forwarder is not None:
|
||||
infra.append(forwarder)
|
||||
await self._buzz_json(
|
||||
trial.user,
|
||||
trial,
|
||||
"users",
|
||||
"set-profile",
|
||||
"--name",
|
||||
trial.user.agent_id,
|
||||
)
|
||||
for credential in trial.credentials:
|
||||
await self._buzz_json(
|
||||
credential,
|
||||
trial,
|
||||
"users",
|
||||
"set-profile",
|
||||
"--name",
|
||||
credential.agent_id,
|
||||
)
|
||||
agents.append(
|
||||
await self._launch_agent(
|
||||
environment=environment,
|
||||
trial=trial,
|
||||
credential=credential,
|
||||
agent_class=classes[credential.agent_id],
|
||||
trial_dir=trial_dir,
|
||||
)
|
||||
)
|
||||
await self._wait_for_agents_ready(
|
||||
environment, agents, trial.channel_id, infra
|
||||
)
|
||||
# The task arrives exactly as it would in production Buzz: a
|
||||
# user prompt @mentioning the orchestrator. The harness never
|
||||
# speaks as any agent. The orchestrator is mentioned by pubkey,
|
||||
# not by name resolution: task text is untrusted payload, and any
|
||||
# @-token inside it (e.g. Vim's `:%normal! @a`) would otherwise
|
||||
# fail member resolution and kill the trial before the agent
|
||||
# ever saw the task. An explicit --mention demotes unresolved
|
||||
# @-tokens in the text to presentation-only.
|
||||
await self._send(
|
||||
trial.user,
|
||||
trial,
|
||||
f"@{orchestrator.agent_id} {instruction}",
|
||||
mention=orchestrator.nostr_pubkey,
|
||||
)
|
||||
final_message = await asyncio.wait_for(
|
||||
self._wait_for_done(environment, orchestrator, trial, agents + infra),
|
||||
timeout=manifest.trial_budget.timeout_seconds,
|
||||
)
|
||||
await self._verify_m1_output(environment, manifest)
|
||||
finally:
|
||||
await self._stop_agents(environment, agents + infra)
|
||||
await self._collect_logs(environment, trial_dir)
|
||||
|
||||
return RuntimeResult(
|
||||
metadata={
|
||||
"completion_message_id": final_message["id"],
|
||||
"completion_message": final_message["content"],
|
||||
"agent_runtime": "in-container",
|
||||
"agent_hints_enabled": False,
|
||||
"task_seed": "user-identity-prompt",
|
||||
"agent_max_rounds": {
|
||||
credential.agent_id: (
|
||||
classes[credential.agent_id].budget.max_calls
|
||||
or self.max_agent_rounds
|
||||
)
|
||||
for credential in trial.credentials
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# -- container setup ------------------------------------------------------
|
||||
|
||||
async def _install_stack(self, environment: BaseEnvironment) -> None:
|
||||
"""Upload the pinned Linux binaries into the task container."""
|
||||
uploads = {
|
||||
f"{REMOTE_BIN}/buzz-acp": self.buzz_acp_binary,
|
||||
f"{REMOTE_BIN}/buzz-agent": self.buzz_agent_binary,
|
||||
f"{REMOTE_BIN}/buzz-dev-mcp": self.buzz_dev_mcp_binary,
|
||||
}
|
||||
if self.relay_gateway:
|
||||
uploads[FORWARDER] = self.forwarder_binary
|
||||
for source in uploads.values():
|
||||
if not Path(source).is_file():
|
||||
raise RuntimeLaunchError(f"agent binary not found: {source}")
|
||||
result = await environment.exec(
|
||||
f"mkdir -p {REMOTE_BIN} {REMOTE_PROMPTS} {REMOTE_LOGS}"
|
||||
)
|
||||
if result.return_code != 0:
|
||||
raise RuntimeLaunchError(
|
||||
f"cannot create {REMOTE_ROOT} in the task container: "
|
||||
f"{result.stderr or result.stdout}"
|
||||
)
|
||||
for target, source in uploads.items():
|
||||
await environment.upload_file(source, target)
|
||||
await environment.exec(f"chmod 0755 {REMOTE_BIN}/*")
|
||||
|
||||
async def _start_forwarder(
|
||||
self, environment: BaseEnvironment, trial: TrialHandle
|
||||
) -> _Agent | None:
|
||||
"""Bridge the agents' canonical relay address to the real gateway.
|
||||
|
||||
The relay resolves its tenant from the request ``Host`` header, so the
|
||||
agents must dial the exact authority its community row is bound to —
|
||||
``trial.relay_ws_url``. When that address is loopback inside the task
|
||||
container but the relay lives on the Docker host, this starts the
|
||||
uploaded forwarder listening on the canonical address and pumping the
|
||||
byte stream to ``relay_gateway``. Returns ``None`` when no gateway is
|
||||
configured (the relay is reachable directly).
|
||||
"""
|
||||
if not self.relay_gateway:
|
||||
return None
|
||||
# Listen on the IPv4 loopback explicitly: binding the name `localhost`
|
||||
# would pick whichever address family resolves first, while clients
|
||||
# iterate both — pinning v4 makes the pair deterministic. The Host
|
||||
# header the relay tenant-binds on comes from the URL the agents
|
||||
# dial (trial.relay_ws_url), not from the socket address.
|
||||
listen = self._ws_authority(trial.relay_ws_url).replace(
|
||||
"localhost", "127.0.0.1", 1
|
||||
)
|
||||
log = FORWARDER_LOG
|
||||
command = (
|
||||
f"{shlex.quote(FORWARDER)} {shlex.quote(listen)} "
|
||||
f"{shlex.quote(self.relay_gateway)} </dev/null "
|
||||
f">{shlex.quote(log)} 2>&1 & echo $!"
|
||||
)
|
||||
result = await environment.exec(command)
|
||||
try:
|
||||
pid = int((result.stdout or "").strip().splitlines()[-1])
|
||||
except (ValueError, IndexError) as error:
|
||||
raise RuntimeLaunchError(
|
||||
f"cannot launch relay forwarder: {result.stderr or result.stdout}"
|
||||
) from error
|
||||
forwarder = _Agent(
|
||||
AgentCredential(
|
||||
agent_id="relay-forwarder",
|
||||
role="infra",
|
||||
nostr_secret_key="",
|
||||
nostr_pubkey="",
|
||||
nostr_auth_tag="",
|
||||
llm_endpoint="",
|
||||
llm_api_key="",
|
||||
),
|
||||
pid,
|
||||
log,
|
||||
log,
|
||||
)
|
||||
deadline = asyncio.get_running_loop().time() + self.readiness_timeout_seconds
|
||||
while True:
|
||||
probe = await environment.exec(f"cat {shlex.quote(log)} 2>/dev/null")
|
||||
if "forwarding" in (probe.stdout or ""):
|
||||
return forwarder
|
||||
await self._raise_for_dead_agents(environment, [forwarder])
|
||||
if asyncio.get_running_loop().time() >= deadline:
|
||||
raise RuntimeLaunchError(
|
||||
"relay forwarder did not report readiness; "
|
||||
f"see {log} in the trial artifacts"
|
||||
)
|
||||
await asyncio.sleep(self.poll_seconds)
|
||||
|
||||
@staticmethod
|
||||
def _ws_authority(relay_ws_url: str) -> str:
|
||||
"""``host:port`` from a ws:// URL — the forwarder's listen address."""
|
||||
if not relay_ws_url.startswith("ws://"):
|
||||
raise RuntimeLaunchError(
|
||||
"relay_gateway forwarding requires a ws:// relay_ws_url"
|
||||
)
|
||||
authority = relay_ws_url.removeprefix("ws://").split("/", 1)[0]
|
||||
if ":" not in authority:
|
||||
authority += ":80"
|
||||
return authority
|
||||
|
||||
async def _launch_agent(
|
||||
self,
|
||||
*,
|
||||
environment: BaseEnvironment,
|
||||
trial: TrialHandle,
|
||||
credential: AgentCredential,
|
||||
agent_class: AgentClass,
|
||||
trial_dir: Path,
|
||||
) -> _Agent:
|
||||
if not credential.llm_endpoint:
|
||||
raise RuntimeLaunchError("credential llm_endpoint must not be empty")
|
||||
endpoint = self.endpoints.get(credential.llm_endpoint)
|
||||
if endpoint is None:
|
||||
raise RuntimeLaunchError(
|
||||
f"no launch config for endpoint {credential.llm_endpoint!r}"
|
||||
)
|
||||
self._reject_identity_overrides(endpoint)
|
||||
prompt_path = self.artifact_root / agent_class.prompt.path
|
||||
self._verify_artifact(prompt_path, agent_class.prompt.sha256)
|
||||
composed = self._compose_system_prompt(
|
||||
trial_dir=trial_dir,
|
||||
trial=trial,
|
||||
credential=credential,
|
||||
persona_path=prompt_path,
|
||||
)
|
||||
remote_prompt = f"{REMOTE_PROMPTS}/{credential.agent_id}.system-prompt.md"
|
||||
await environment.upload_file(composed, remote_prompt)
|
||||
|
||||
stdout_log = f"{REMOTE_LOGS}/{credential.agent_id}.stdout.log"
|
||||
stderr_log = f"{REMOTE_LOGS}/{credential.agent_id}.stderr.log"
|
||||
env = self._agent_env(
|
||||
trial=trial,
|
||||
credential=credential,
|
||||
agent_class=agent_class,
|
||||
endpoint=endpoint,
|
||||
remote_prompt=remote_prompt,
|
||||
)
|
||||
command = (
|
||||
f"{shlex.quote(f'{REMOTE_BIN}/buzz-acp')} </dev/null "
|
||||
f">{shlex.quote(stdout_log)} 2>{shlex.quote(stderr_log)} & echo $!"
|
||||
)
|
||||
result = await environment.exec(command, env=env)
|
||||
try:
|
||||
pid = int((result.stdout or "").strip().splitlines()[-1])
|
||||
except (ValueError, IndexError) as error:
|
||||
raise RuntimeLaunchError(
|
||||
f"cannot launch agent {credential.agent_id}: "
|
||||
f"{result.stderr or result.stdout}"
|
||||
) from error
|
||||
return _Agent(credential, pid, stdout_log, stderr_log)
|
||||
|
||||
def _agent_env(
|
||||
self,
|
||||
*,
|
||||
trial: TrialHandle,
|
||||
credential: AgentCredential,
|
||||
agent_class: AgentClass,
|
||||
endpoint: EndpointLaunchConfig,
|
||||
remote_prompt: str,
|
||||
) -> dict[str, str]:
|
||||
"""The desktop-launch environment: real acp/agent/dev-mcp wiring."""
|
||||
return {
|
||||
**endpoint.env,
|
||||
"BUZZ_RELAY_URL": trial.relay_ws_url,
|
||||
"BUZZ_PRIVATE_KEY": credential.nostr_secret_key,
|
||||
# Desktop parity: the GUI also sets NOSTR_PRIVATE_KEY on buzz-acp
|
||||
# so buzz-dev-mcp's shim can wire git auth/signing for the agent.
|
||||
"NOSTR_PRIVATE_KEY": credential.nostr_secret_key,
|
||||
"BUZZ_AUTH_TAG": credential.nostr_auth_tag,
|
||||
"BUZZ_ACP_AGENT_COMMAND": f"{REMOTE_BIN}/buzz-agent",
|
||||
"BUZZ_ACP_AGENT_ARGS": "",
|
||||
"BUZZ_ACP_MCP_COMMAND": f"{REMOTE_BIN}/buzz-dev-mcp",
|
||||
"BUZZ_ACP_CHANNELS": trial.channel_id,
|
||||
"BUZZ_ACP_SUBSCRIBE": "mentions",
|
||||
"BUZZ_ACP_RESPOND_TO": "anyone",
|
||||
"BUZZ_ACP_NO_MEMORY": "true",
|
||||
"BUZZ_ACP_SYSTEM_PROMPT_FILE": remote_prompt,
|
||||
"BUZZ_AGENT_PROVIDER": endpoint.provider,
|
||||
"BUZZ_AGENT_MODEL": credential.llm_endpoint,
|
||||
"BUZZ_AGENT_MAX_OUTPUT_TOKENS": str(
|
||||
agent_class.generation.max_output_tokens
|
||||
),
|
||||
"BUZZ_AGENT_MAX_CONTEXT_TOKENS": str(
|
||||
agent_class.generation.context_window_tokens
|
||||
),
|
||||
"BUZZ_AGENT_MAX_ROUNDS": str(
|
||||
agent_class.budget.max_calls or self.max_agent_rounds
|
||||
),
|
||||
# The pinned persona is the whole prompt: no hint-file or skill
|
||||
# discovery from the task filesystem (metadata reports this).
|
||||
"BUZZ_AGENT_NO_HINTS": "1",
|
||||
endpoint.api_key_env: credential.llm_api_key,
|
||||
}
|
||||
|
||||
# -- lifecycle -------------------------------------------------------------
|
||||
|
||||
async def _wait_for_agents_ready(
|
||||
self,
|
||||
environment: BaseEnvironment,
|
||||
agents: list[_Agent],
|
||||
channel_id: str,
|
||||
infra: list[_Agent] | None = None,
|
||||
) -> None:
|
||||
"""Wait until every ACP process confirms its trial-channel subscription."""
|
||||
marker = f"subscribed to channel {channel_id}"
|
||||
deadline = asyncio.get_running_loop().time() + self.readiness_timeout_seconds
|
||||
pending = {agent.credential.agent_id: agent for agent in agents}
|
||||
while pending:
|
||||
await self._raise_for_dead_agents(environment, agents + (infra or []))
|
||||
for agent_id, agent in list(pending.items()):
|
||||
result = await environment.exec(
|
||||
f"cat {shlex.quote(agent.stdout_log)} "
|
||||
f"{shlex.quote(agent.stderr_log)} 2>/dev/null"
|
||||
)
|
||||
if marker in (result.stdout or ""):
|
||||
del pending[agent_id]
|
||||
if not pending:
|
||||
return
|
||||
if asyncio.get_running_loop().time() >= deadline:
|
||||
raise RuntimeLaunchError(
|
||||
"agents did not subscribe to trial channel before readiness "
|
||||
f"timeout: {sorted(pending)}"
|
||||
)
|
||||
await asyncio.sleep(self.poll_seconds)
|
||||
|
||||
async def _wait_for_done(
|
||||
self,
|
||||
environment: BaseEnvironment,
|
||||
orchestrator: AgentCredential,
|
||||
trial: TrialHandle,
|
||||
agents: list[_Agent],
|
||||
) -> dict[str, Any]:
|
||||
"""Observe the channel as the trial user until the orchestrator posts DONE.
|
||||
|
||||
Observation only: the harness never speaks as any agent. If the team
|
||||
stalls, the trial times out and the stall is the measured result.
|
||||
"""
|
||||
polls = 0
|
||||
while True:
|
||||
if polls % LIVENESS_EVERY == 0:
|
||||
await self._raise_for_dead_agents(environment, agents)
|
||||
polls += 1
|
||||
messages = await self._buzz_json(
|
||||
trial.user,
|
||||
trial,
|
||||
"messages",
|
||||
"get",
|
||||
"--channel",
|
||||
trial.channel_id,
|
||||
"--limit",
|
||||
"100",
|
||||
)
|
||||
for message in messages:
|
||||
if message.get("pubkey") == orchestrator.nostr_pubkey and str(
|
||||
message.get("content", "")
|
||||
).startswith("DONE:"):
|
||||
return message
|
||||
await asyncio.sleep(self.poll_seconds)
|
||||
|
||||
async def _raise_for_dead_agents(
|
||||
self, environment: BaseEnvironment, agents: list[_Agent]
|
||||
) -> None:
|
||||
if not agents:
|
||||
return
|
||||
probes = "; ".join(
|
||||
f"kill -0 {agent.pid} 2>/dev/null || echo DEAD:{agent.credential.agent_id}"
|
||||
for agent in agents
|
||||
)
|
||||
result = await environment.exec(probes)
|
||||
dead = [
|
||||
line.removeprefix("DEAD:")
|
||||
for line in (result.stdout or "").splitlines()
|
||||
if line.startswith("DEAD:")
|
||||
]
|
||||
if dead:
|
||||
raise RuntimeLaunchError(
|
||||
f"agent processes exited early: {sorted(dead)}; "
|
||||
f"see {REMOTE_LOGS} in the trial artifacts"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _stop_agents(environment: BaseEnvironment, agents: list[_Agent]) -> None:
|
||||
"""Terminate every process of the uploaded stack (acp, agent, mcp)."""
|
||||
if not agents:
|
||||
return
|
||||
# Match by cmdline prefix via /proc: pkill/procps is not guaranteed
|
||||
# to exist in task images, the /proc filesystem is.
|
||||
sweep = (
|
||||
"for d in /proc/[0-9]*; do "
|
||||
f'grep -aq {REMOTE_BIN} "$d/cmdline" 2>/dev/null '
|
||||
'&& kill -TERM "${d#/proc/}" 2>/dev/null; done; true'
|
||||
)
|
||||
try:
|
||||
await environment.exec(sweep)
|
||||
await asyncio.sleep(2)
|
||||
await environment.exec(sweep.replace("-TERM", "-KILL"))
|
||||
except Exception: # noqa: S110, BLE001 — environment may already be gone
|
||||
pass
|
||||
|
||||
async def _collect_logs(
|
||||
self, environment: BaseEnvironment, trial_dir: Path
|
||||
) -> None:
|
||||
try:
|
||||
await environment.download_dir(REMOTE_LOGS, trial_dir)
|
||||
except Exception: # noqa: S110, BLE001 — best effort; env may be torn down
|
||||
pass
|
||||
|
||||
# -- Buzz CLI as the trial user / provisioning identities -------------------
|
||||
|
||||
@staticmethod
|
||||
async def _verify_m1_output(
|
||||
environment: BaseEnvironment, manifest: ExperimentManifest
|
||||
) -> None:
|
||||
"""Fail M1 immediately unless the artifact satisfies the grader contract."""
|
||||
if manifest.condition != "M1-hello-world":
|
||||
return
|
||||
result = await environment.exec(
|
||||
'python3 -c "from pathlib import Path; '
|
||||
"p = Path('/app/hello.txt'); "
|
||||
"assert p.is_file() and p.read_text().strip() == 'Hello, world!'\""
|
||||
)
|
||||
if result.return_code != 0:
|
||||
detail = (
|
||||
result.stderr or result.stdout or "grader-equivalent check failed"
|
||||
).strip()
|
||||
raise RuntimeLaunchError(
|
||||
"M1 pre-verifier sanity probe failed: /app/hello.txt must exist "
|
||||
f"and its stripped text must equal 'Hello, world!' ({detail})"
|
||||
)
|
||||
|
||||
async def _send(
|
||||
self,
|
||||
credential: AgentCredential,
|
||||
trial: TrialHandle,
|
||||
content: str,
|
||||
*,
|
||||
mention: str | None = None,
|
||||
) -> None:
|
||||
args = [
|
||||
"messages",
|
||||
"send",
|
||||
"--channel",
|
||||
trial.channel_id,
|
||||
"--content",
|
||||
content,
|
||||
]
|
||||
if mention is not None:
|
||||
args += ["--mention", mention]
|
||||
await self._buzz_json(credential, trial, *args)
|
||||
|
||||
async def _buzz_json(
|
||||
self, credential: AgentCredential, trial: TrialHandle, *args: str
|
||||
) -> Any:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
self.buzz_cli_binary,
|
||||
*args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
env={
|
||||
**os.environ,
|
||||
"BUZZ_RELAY_URL": self._user_relay_url(trial),
|
||||
"BUZZ_PRIVATE_KEY": credential.nostr_secret_key,
|
||||
"BUZZ_AUTH_TAG": credential.nostr_auth_tag,
|
||||
},
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
if process.returncode != 0:
|
||||
raise RuntimeLaunchError(
|
||||
f"buzz {shlex.join(args)} exited {process.returncode}: "
|
||||
f"{stderr.decode(errors='replace').strip()}"
|
||||
)
|
||||
try:
|
||||
return json.loads(stdout)
|
||||
except json.JSONDecodeError as error:
|
||||
raise RuntimeLaunchError("buzz returned invalid JSON") from error
|
||||
|
||||
@staticmethod
|
||||
def _user_relay_url(trial: TrialHandle) -> str:
|
||||
"""The relay as reachable from the HOST (user identity, harness).
|
||||
|
||||
``trial.relay_ws_url`` is the container view (the agents' runtime);
|
||||
``trial.user_relay_url`` is the host view. Fall back to deriving an
|
||||
http URL from the ws URL for handles minted before v1.2.
|
||||
"""
|
||||
if trial.user_relay_url:
|
||||
return trial.user_relay_url
|
||||
return BuzzContainerRuntime._cli_relay_url(trial.relay_ws_url)
|
||||
|
||||
@staticmethod
|
||||
def _cli_relay_url(relay_ws_url: str) -> str:
|
||||
if relay_ws_url.startswith("ws://"):
|
||||
return f"http://{relay_ws_url.removeprefix('ws://')}"
|
||||
if relay_ws_url.startswith("wss://"):
|
||||
return f"https://{relay_ws_url.removeprefix('wss://')}"
|
||||
raise RuntimeLaunchError("trial relay_ws_url must use ws:// or wss://")
|
||||
|
||||
# -- manifest plumbing -------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _classes_by_agent_id(
|
||||
manifest: ExperimentManifest, credentials: tuple[AgentCredential, ...]
|
||||
) -> dict[str, AgentClass]:
|
||||
by_id = {entry.id: entry for entry in manifest.roster}
|
||||
result: dict[str, AgentClass] = {}
|
||||
for credential in credentials:
|
||||
class_id, separator, index = credential.agent_id.rpartition("-")
|
||||
match = by_id.get(class_id)
|
||||
if not separator or not index.isdigit() or match is None:
|
||||
raise RuntimeLaunchError(
|
||||
f"credential {credential.agent_id!r} does not match a roster class"
|
||||
)
|
||||
if credential.role != match.kind:
|
||||
raise RuntimeLaunchError(
|
||||
f"credential {credential.agent_id!r} role does not match manifest"
|
||||
)
|
||||
result[credential.agent_id] = match
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _verify_artifact(path: Path, expected_sha256: str) -> None:
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
actual = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
except OSError as error:
|
||||
raise RuntimeLaunchError(f"cannot read prompt {path}: {error}") from error
|
||||
if actual != expected_sha256:
|
||||
raise RuntimeLaunchError(
|
||||
f"prompt hash mismatch for {path}: expected {expected_sha256}, got {actual}"
|
||||
)
|
||||
|
||||
def _compose_system_prompt(
|
||||
self,
|
||||
*,
|
||||
trial_dir: Path,
|
||||
trial: TrialHandle,
|
||||
credential: AgentCredential,
|
||||
persona_path: Path,
|
||||
) -> Path:
|
||||
"""Append the trial's team roster to the pinned persona.
|
||||
|
||||
The analogue of a production Buzz workspace's team context: each agent
|
||||
knows its own identity, its channel, the user it reports to, and its
|
||||
teammates' names, pubkeys, and roles from its system prompt — it never
|
||||
has to discover them over the relay.
|
||||
"""
|
||||
persona = persona_path.read_text(encoding="utf-8")
|
||||
lines = [
|
||||
"",
|
||||
"## Your team",
|
||||
"",
|
||||
f"You are `{credential.agent_id}` (pubkey `{credential.nostr_pubkey}`).",
|
||||
f"The team coordinates in Buzz channel `{trial.channel_id}`.",
|
||||
(
|
||||
f"Tasks come from the user `{trial.user.agent_id}` "
|
||||
f"(pubkey `{trial.user.nostr_pubkey}`); address your final report "
|
||||
"to them."
|
||||
),
|
||||
"",
|
||||
"| Name | Role | Pubkey |",
|
||||
"|------|------|--------|",
|
||||
]
|
||||
for teammate in trial.credentials:
|
||||
if teammate.agent_id == credential.agent_id:
|
||||
continue
|
||||
lines.append(
|
||||
f"| {teammate.agent_id} | {teammate.role} | `{teammate.nostr_pubkey}` |"
|
||||
)
|
||||
composed = persona + "\n".join(lines) + "\n"
|
||||
path = trial_dir / f"{credential.agent_id}.system-prompt.md"
|
||||
path.write_text(composed, encoding="utf-8")
|
||||
path.chmod(0o600)
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def _reject_identity_overrides(endpoint: EndpointLaunchConfig) -> None:
|
||||
forbidden = {
|
||||
"BUZZ_RELAY_URL",
|
||||
"BUZZ_PRIVATE_KEY",
|
||||
"BUZZ_AUTH_TAG",
|
||||
"BUZZ_ACP_CHANNELS",
|
||||
"BUZZ_ACP_MCP_COMMAND",
|
||||
"BUZZ_ACP_AGENT_COMMAND",
|
||||
}
|
||||
overlap = forbidden & endpoint.env.keys()
|
||||
if overlap:
|
||||
raise RuntimeLaunchError(
|
||||
f"endpoint env cannot override trial identity: {sorted(overlap)}"
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Validated, content-addressed experiment manifests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, Self
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class ManifestError(ValueError):
|
||||
"""Raised when a manifest cannot be loaded or validated."""
|
||||
|
||||
|
||||
class StrictModel(BaseModel):
|
||||
"""Base model that rejects misspelled or unrecognised manifest fields."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
|
||||
class ArtifactRef(StrictModel):
|
||||
"""Reference to immutable prompt, persona, or skill content."""
|
||||
|
||||
path: str = Field(min_length=1)
|
||||
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class GenerationConfig(StrictModel):
|
||||
"""Model generation controls frozen for a condition."""
|
||||
|
||||
temperature: float = Field(default=0.0, ge=0.0)
|
||||
max_output_tokens: int = Field(gt=0)
|
||||
context_window_tokens: int = Field(gt=0)
|
||||
extra: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AgentBudget(StrictModel):
|
||||
"""Optional per-agent live safety limits."""
|
||||
|
||||
max_calls: int | None = Field(default=None, gt=0)
|
||||
max_input_tokens: int | None = Field(default=None, gt=0)
|
||||
max_output_tokens: int | None = Field(default=None, gt=0)
|
||||
max_cost_usd: float | None = Field(default=None, gt=0)
|
||||
|
||||
|
||||
class Price(StrictModel):
|
||||
"""Frozen USD rates for one endpoint revision, per million tokens."""
|
||||
|
||||
input_per_million_usd: float = Field(ge=0)
|
||||
cached_input_per_million_usd: float = Field(ge=0)
|
||||
output_per_million_usd: float = Field(ge=0)
|
||||
|
||||
|
||||
class AgentClass(StrictModel):
|
||||
"""A homogeneous class of agents in the trial roster."""
|
||||
|
||||
id: str = Field(min_length=1, pattern=r"^[a-z0-9][a-z0-9._-]*$")
|
||||
kind: Literal["orchestrator", "worker"]
|
||||
role: str = Field(min_length=1)
|
||||
count: int = Field(gt=0)
|
||||
endpoint: str = Field(min_length=1)
|
||||
model_revision: str = Field(min_length=1)
|
||||
prompt: ArtifactRef
|
||||
persona: ArtifactRef | None = None
|
||||
skills: tuple[ArtifactRef, ...] = ()
|
||||
generation: GenerationConfig
|
||||
budget: AgentBudget = AgentBudget()
|
||||
concurrency: int = Field(default=1, gt=0)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_concurrency(self) -> Self:
|
||||
if self.concurrency > self.count:
|
||||
raise ValueError("concurrency cannot exceed count")
|
||||
return self
|
||||
|
||||
|
||||
class TrialBudget(StrictModel):
|
||||
"""Trial-wide hard limits enforced by the live runtime, not async receipts."""
|
||||
|
||||
timeout_seconds: int = Field(gt=0)
|
||||
max_cost_usd: float | None = Field(default=None, gt=0)
|
||||
|
||||
|
||||
class ExperimentManifest(StrictModel):
|
||||
"""Complete immutable input defining one benchmark condition."""
|
||||
|
||||
schema_version: Literal["1"] = "1"
|
||||
condition: str = Field(min_length=1)
|
||||
roster: tuple[AgentClass, ...] = Field(min_length=1)
|
||||
prices: dict[str, Price]
|
||||
trial_budget: TrialBudget
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_roster(self) -> Self:
|
||||
ids = [entry.id for entry in self.roster]
|
||||
if len(ids) != len(set(ids)):
|
||||
raise ValueError("roster class ids must be unique")
|
||||
orchestrators = sum(
|
||||
entry.count for entry in self.roster if entry.kind == "orchestrator"
|
||||
)
|
||||
if orchestrators != 1:
|
||||
raise ValueError("roster must contain exactly one orchestrator")
|
||||
endpoints = {entry.endpoint for entry in self.roster}
|
||||
missing_prices = sorted(endpoints - self.prices.keys())
|
||||
if missing_prices:
|
||||
raise ValueError(f"prices missing for endpoints: {missing_prices}")
|
||||
return self
|
||||
|
||||
def canonical_bytes(self) -> bytes:
|
||||
"""Return stable UTF-8 JSON independent of YAML formatting and key order."""
|
||||
data = self.model_dump(mode="json", exclude_none=False)
|
||||
return json.dumps(
|
||||
data,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
@property
|
||||
def sha256(self) -> str:
|
||||
"""Return the condition identity derived from canonical manifest content."""
|
||||
return hashlib.sha256(self.canonical_bytes()).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def load(cls, source: str | Path | dict[str, Any]) -> Self:
|
||||
"""Load a manifest from a YAML/JSON file or an already-decoded mapping."""
|
||||
if isinstance(source, dict):
|
||||
raw = source
|
||||
else:
|
||||
path = Path(source).expanduser()
|
||||
try:
|
||||
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
except (OSError, yaml.YAMLError) as error:
|
||||
raise ManifestError(f"cannot load manifest {path}: {error}") from error
|
||||
if not isinstance(raw, dict):
|
||||
raise ManifestError("manifest root must be a mapping")
|
||||
try:
|
||||
return cls.model_validate(raw)
|
||||
except ValueError as error:
|
||||
raise ManifestError(f"invalid manifest: {error}") from error
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Typed boundary between the Harbor adapter and Buzz trial provisioning."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from .manifest import ExperimentManifest
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AgentCredential:
|
||||
"""One trial-scoped Buzz identity and attributed LLM credential."""
|
||||
|
||||
agent_id: str
|
||||
role: str
|
||||
nostr_secret_key: str
|
||||
nostr_pubkey: str
|
||||
nostr_auth_tag: str
|
||||
llm_endpoint: str
|
||||
llm_api_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TrialHandle:
|
||||
"""Provisioned Buzz resources owned by one Harbor trial."""
|
||||
|
||||
run_id: str
|
||||
trial_id: str
|
||||
manifest_hash: str
|
||||
relay_ws_url: str
|
||||
channel_id: str
|
||||
credentials: tuple[AgentCredential, ...]
|
||||
# The trial's human-analogue identity: owns the channel, posts the task
|
||||
# as a user prompt, and receives the final report. Never runs an agent
|
||||
# process, so its llm_endpoint and llm_api_key are empty strings.
|
||||
user: AgentCredential
|
||||
# v1.2 (additive): the relay as reachable from the HOST, where the user
|
||||
# identity and the harness run. ``relay_ws_url`` is the view from the
|
||||
# agents' runtime (the task container). Empty means both views coincide.
|
||||
user_relay_url: str = ""
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TrialProvisioner(Protocol):
|
||||
"""Creates and tears down trial-isolated Buzz resources synchronously."""
|
||||
|
||||
def create_trial(
|
||||
self,
|
||||
run_id: str,
|
||||
trial_id: str,
|
||||
manifest: ExperimentManifest,
|
||||
channel_label: str | None = None,
|
||||
) -> TrialHandle: ...
|
||||
|
||||
def teardown(self, handle: TrialHandle) -> None: ...
|
||||
|
||||
def healthcheck(self) -> None: ...
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Runtime contract kept separate from Buzz resource provisioning."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Protocol
|
||||
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
|
||||
from .manifest import ExperimentManifest
|
||||
from .provisioning import TrialHandle
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeResult:
|
||||
"""Aggregate values returned by an orchestration runtime."""
|
||||
|
||||
input_tokens: int = 0
|
||||
cached_input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cost_usd: float = 0.0
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class OrchestraRuntime(Protocol):
|
||||
"""Runs the provisioned roster and writes trial artifacts into logs_dir."""
|
||||
|
||||
async def run(
|
||||
self,
|
||||
*,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
manifest: ExperimentManifest,
|
||||
trial: TrialHandle,
|
||||
) -> RuntimeResult: ...
|
||||
@@ -0,0 +1,18 @@
|
||||
# Benchmark-only overrides for deploy/compose/compose.yml.
|
||||
#
|
||||
# The production bundle stays untouched; this file publishes what the
|
||||
# benchmark harness needs on the host:
|
||||
# - relay Prometheus metrics (:9102) for scraping during runs
|
||||
# - Postgres (:5433) for the provisioner and receipts ingestion
|
||||
# (5433 to avoid colliding with other local Postgres instances)
|
||||
#
|
||||
# Usage:
|
||||
# docker compose --env-file .env \
|
||||
# -f compose.yml -f <this file> up -d --wait
|
||||
services:
|
||||
relay:
|
||||
ports:
|
||||
- "${BUZZ_METRICS_HOST_PORT:-9102}:9102"
|
||||
postgres:
|
||||
ports:
|
||||
- "${BUZZ_PG_HOST_PORT:-5433}:5432"
|
||||
@@ -0,0 +1,26 @@
|
||||
# Endpoint launch configs
|
||||
|
||||
Deployment-time mapping from manifest endpoint names to
|
||||
`EndpointLaunchConfig` (`provider` / `api_key_env` / `env`), passed to
|
||||
`harbor run` as `--agent-kwarg endpoint_config=<path>`. This is deployment
|
||||
config, deliberately OUTSIDE the immutable condition manifest — the manifest
|
||||
endpoint string remains the join key.
|
||||
|
||||
Every key in these files must be a manifest endpoint name; the loader treats
|
||||
all entries as endpoint configs (no comment keys).
|
||||
|
||||
## m1-local.json
|
||||
|
||||
M1 wiring proof: both placeholder endpoints resolve to one local llama-server
|
||||
(OpenAI-compatible, `http://127.0.0.1:8091/v1`, no cloud keys).
|
||||
|
||||
buzz-agent env contract (crates/buzz-agent/src/config.rs, pinned at the M1
|
||||
binary SHA): `provider=openai` reads `OPENAI_COMPAT_API_KEY` +
|
||||
`OPENAI_COMPAT_BASE_URL`; the runtime sets `BUZZ_AGENT_MODEL` from the
|
||||
manifest endpoint name, which overrides `OPENAI_COMPAT_MODEL` — llama-server
|
||||
ignores the model name, so the placeholder value is harmless there.
|
||||
llama-server needs no real key; the provisioner's per-endpoint
|
||||
`llm_api_keys` map supplies a dummy value.
|
||||
|
||||
The Databricks pilot config is the same file shape with real serving
|
||||
endpoint hosts/keys.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"claude-sonnet-4-6": {
|
||||
"provider": "anthropic",
|
||||
"api_key_env": "ANTHROPIC_API_KEY",
|
||||
"env": {}
|
||||
},
|
||||
"claude-haiku-4-5": {
|
||||
"provider": "anthropic",
|
||||
"api_key_env": "ANTHROPIC_API_KEY",
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"local/placeholder-orchestrator": {
|
||||
"provider": "openai",
|
||||
"api_key_env": "OPENAI_COMPAT_API_KEY",
|
||||
"env": {
|
||||
"OPENAI_COMPAT_BASE_URL": "http://127.0.0.1:8091/v1"
|
||||
}
|
||||
},
|
||||
"local/placeholder-worker": {
|
||||
"provider": "openai",
|
||||
"api_key_env": "OPENAI_COMPAT_API_KEY",
|
||||
"env": {
|
||||
"OPENAI_COMPAT_BASE_URL": "http://127.0.0.1:8091/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
[project]
|
||||
name = "harbor-buzz-testbed"
|
||||
version = "0.1.0"
|
||||
description = "Trial provisioning and local Buzz stack tooling for harbor-buzz-orchestra"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"harbor-buzz-orchestra",
|
||||
"coincurve>=20",
|
||||
"psycopg[binary]>=3.2",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.4",
|
||||
"ruff>=0.15",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
harbor-buzz-orchestra = { path = "..", editable = true }
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 88
|
||||
@@ -0,0 +1,72 @@
|
||||
-- Benchmark schema for harbor-buzz-orchestra runs.
|
||||
--
|
||||
-- Lives in the shared Postgres instance but is OWNED BY THE HARNESS, never by
|
||||
-- Buzz migrations (canonical plan §two-domain rule). Idempotent: safe to apply
|
||||
-- on every testbed bring-up.
|
||||
--
|
||||
-- docker exec -i <postgres> psql -U buzz -d buzz < sql/benchmark_schema.sql
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS benchmark;
|
||||
|
||||
-- One row per provisioned trial; written by BuzzTrialProvisioner.
|
||||
CREATE TABLE IF NOT EXISTS benchmark.trial_manifest (
|
||||
run_id text NOT NULL,
|
||||
trial_id uuid NOT NULL,
|
||||
manifest_hash text NOT NULL,
|
||||
channel_id uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
handle jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
archived_at timestamptz,
|
||||
PRIMARY KEY (run_id, trial_id)
|
||||
);
|
||||
|
||||
-- Immutable LLM receipts, ingested post-run from the accounting path
|
||||
-- (Databricks AI Gateway inference tables first; LiteLLM shim fallback).
|
||||
-- Authoritative for tokens/cost and orchestrator-vs-worker attribution;
|
||||
-- Harbor AgentContext totals are a reconciliation checksum only.
|
||||
CREATE TABLE IF NOT EXISTS benchmark.llm_receipts (
|
||||
receipt_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
run_id text NOT NULL,
|
||||
trial_id uuid NOT NULL,
|
||||
agent_id text NOT NULL,
|
||||
role text NOT NULL,
|
||||
condition text NOT NULL,
|
||||
endpoint text NOT NULL,
|
||||
model_revision text NOT NULL,
|
||||
request_id text NOT NULL, -- gateway request identity
|
||||
requested_at timestamptz NOT NULL,
|
||||
latency_ms integer,
|
||||
input_tokens bigint NOT NULL DEFAULT 0,
|
||||
cached_input_tokens bigint NOT NULL DEFAULT 0,
|
||||
output_tokens bigint NOT NULL DEFAULT 0,
|
||||
cost_usd numeric(12, 6),
|
||||
source text NOT NULL, -- 'ai_gateway' | 'litellm' | 'client'
|
||||
raw jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
UNIQUE (source, request_id),
|
||||
FOREIGN KEY (run_id, trial_id)
|
||||
REFERENCES benchmark.trial_manifest (run_id, trial_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS llm_receipts_trial_idx
|
||||
ON benchmark.llm_receipts (run_id, trial_id, agent_id);
|
||||
|
||||
-- Harness-recorded timing spans (monotonic clocks are authoritative for
|
||||
-- latency). kind examples: 'trial', 'llm_call', 'terminal_exec',
|
||||
-- 'terminal_queue_wait' — queue-wait is recorded separately from execution so
|
||||
-- speed can be reported both as-run and queue-adjusted under the M1
|
||||
-- serialized-broker policy.
|
||||
CREATE TABLE IF NOT EXISTS benchmark.spans (
|
||||
span_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
run_id text NOT NULL,
|
||||
trial_id uuid NOT NULL,
|
||||
agent_id text, -- NULL for trial-level spans
|
||||
kind text NOT NULL,
|
||||
started_at timestamptz NOT NULL,
|
||||
duration_ms bigint NOT NULL CHECK (duration_ms >= 0),
|
||||
detail jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
FOREIGN KEY (run_id, trial_id)
|
||||
REFERENCES benchmark.trial_manifest (run_id, trial_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS spans_trial_kind_idx
|
||||
ON benchmark.spans (run_id, trial_id, kind);
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Testbed-side provisioning for harbor-buzz-orchestra trials."""
|
||||
|
||||
from .provisioner import (
|
||||
BuzzTrialProvisioner,
|
||||
ProvisioningError,
|
||||
TestbedConfig,
|
||||
provisioner_from_dict,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BuzzTrialProvisioner",
|
||||
"ProvisioningError",
|
||||
"TestbedConfig",
|
||||
"provisioner_from_dict",
|
||||
]
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Thin subprocess wrapper over the ``buzz`` CLI — the production client path."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BuzzCliError(RuntimeError):
|
||||
"""A buzz CLI invocation failed."""
|
||||
|
||||
|
||||
class BuzzCli:
|
||||
"""Run buzz CLI commands as one relay identity (key + NIP-OA auth tag)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
relay_url: str,
|
||||
secret_key: str,
|
||||
auth_tag: str,
|
||||
*,
|
||||
binary: str = "buzz",
|
||||
timeout_seconds: float = 30.0,
|
||||
) -> None:
|
||||
self._relay_url = relay_url
|
||||
self._secret_key = secret_key
|
||||
self._auth_tag = auth_tag
|
||||
self._binary = binary
|
||||
self._timeout = timeout_seconds
|
||||
|
||||
def run(self, *args: str) -> Any:
|
||||
"""Run a buzz subcommand and return its parsed JSON stdout."""
|
||||
command = [self._binary, *args]
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=self._timeout,
|
||||
check=False,
|
||||
env={
|
||||
"BUZZ_RELAY_URL": self._relay_url,
|
||||
"BUZZ_PRIVATE_KEY": self._secret_key,
|
||||
"BUZZ_AUTH_TAG": self._auth_tag,
|
||||
"PATH": _path(),
|
||||
},
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as error:
|
||||
raise BuzzCliError(f"buzz {args[0]}: {error}") from error
|
||||
if completed.returncode != 0:
|
||||
raise BuzzCliError(
|
||||
f"buzz {' '.join(args)} exited {completed.returncode}: "
|
||||
f"{completed.stderr.strip() or completed.stdout.strip()}"
|
||||
)
|
||||
if not completed.stdout.strip():
|
||||
return None
|
||||
try:
|
||||
return json.loads(completed.stdout)
|
||||
except json.JSONDecodeError as error:
|
||||
raise BuzzCliError(
|
||||
f"buzz {args[0]} returned non-JSON output: {completed.stdout[:200]!r}"
|
||||
) from error
|
||||
|
||||
def create_private_channel(self, name: str, description: str) -> str:
|
||||
"""Create a private stream channel; return its UUID."""
|
||||
response = self.run(
|
||||
"channels",
|
||||
"create",
|
||||
"--name",
|
||||
name,
|
||||
"--type",
|
||||
"stream",
|
||||
"--visibility",
|
||||
"private",
|
||||
"--description",
|
||||
description,
|
||||
)
|
||||
channel_id = response.get("channel_id") if isinstance(response, dict) else None
|
||||
if not channel_id:
|
||||
raise BuzzCliError(f"channel create returned no channel_id: {response}")
|
||||
return channel_id
|
||||
|
||||
def add_member(self, channel_id: str, pubkey: str) -> None:
|
||||
self.run(
|
||||
"channels",
|
||||
"add-member",
|
||||
"--channel",
|
||||
channel_id,
|
||||
"--pubkey",
|
||||
pubkey,
|
||||
"--role",
|
||||
"member",
|
||||
)
|
||||
|
||||
def archive_channel(self, channel_id: str) -> None:
|
||||
self.run("channels", "archive", "--channel", channel_id)
|
||||
|
||||
|
||||
def _path() -> str:
|
||||
import os
|
||||
|
||||
return os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Nostr keygen and NIP-OA owner attestation for trial agents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
import coincurve
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NostrKeypair:
|
||||
secret_key: str # hex
|
||||
pubkey: str # x-only hex
|
||||
|
||||
|
||||
def generate_keypair() -> NostrKeypair:
|
||||
"""Generate a fresh secp256k1 keypair in Nostr hex form."""
|
||||
key = coincurve.PrivateKey()
|
||||
return NostrKeypair(
|
||||
secret_key=key.to_hex(),
|
||||
pubkey=key.public_key_xonly.format().hex(),
|
||||
)
|
||||
|
||||
|
||||
def keypair_from_secret(secret_key: str) -> NostrKeypair:
|
||||
"""Rebuild the keypair for an existing hex secret key."""
|
||||
key = coincurve.PrivateKey(bytes.fromhex(secret_key))
|
||||
return NostrKeypair(
|
||||
secret_key=secret_key,
|
||||
pubkey=key.public_key_xonly.format().hex(),
|
||||
)
|
||||
|
||||
|
||||
_BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
|
||||
def _bech32_polymod(values: list[int]) -> int:
|
||||
generator = (0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3)
|
||||
checksum = 1
|
||||
for value in values:
|
||||
top = checksum >> 25
|
||||
checksum = (checksum & 0x1FFFFFF) << 5 ^ value
|
||||
for i in range(5):
|
||||
checksum ^= generator[i] if (top >> i) & 1 else 0
|
||||
return checksum
|
||||
|
||||
|
||||
def encode_nsec(secret_key: str) -> str:
|
||||
"""Encode a hex secret key as a NIP-19 bech32 ``nsec1…`` string —
|
||||
the form the desktop GUI's key-import onboarding accepts."""
|
||||
hrp = "nsec"
|
||||
data: list[int] = []
|
||||
accumulator = bits = 0
|
||||
for byte in bytes.fromhex(secret_key):
|
||||
accumulator = accumulator << 8 | byte
|
||||
bits += 8
|
||||
while bits >= 5:
|
||||
bits -= 5
|
||||
data.append(accumulator >> bits & 31)
|
||||
if bits:
|
||||
data.append(accumulator << (5 - bits) & 31)
|
||||
expanded = [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
|
||||
polymod = _bech32_polymod(expanded + data + [0] * 6) ^ 1
|
||||
checksum = [polymod >> 5 * (5 - i) & 31 for i in range(6)]
|
||||
return hrp + "1" + "".join(_BECH32_CHARSET[d] for d in data + checksum)
|
||||
|
||||
|
||||
def compute_auth_tag(
|
||||
owner_secret_key: str, agent_pubkey: str, conditions: str = ""
|
||||
) -> str:
|
||||
"""Compute the NIP-OA ``["auth", ...]`` tag authorising an agent key.
|
||||
|
||||
Mirrors crates/buzz-sdk/src/nip_oa.rs:
|
||||
sig = schnorr(SHA256("nostr:agent-auth:" || agent_pubkey || ":" || conditions),
|
||||
owner_secret_key). Returns the tag as a JSON string.
|
||||
"""
|
||||
owner = coincurve.PrivateKey(bytes.fromhex(owner_secret_key))
|
||||
preimage = f"nostr:agent-auth:{agent_pubkey}:{conditions}".encode()
|
||||
signature = owner.sign_schnorr(hashlib.sha256(preimage).digest())
|
||||
return json.dumps(
|
||||
[
|
||||
"auth",
|
||||
owner.public_key_xonly.format().hex(),
|
||||
conditions,
|
||||
signature.hex(),
|
||||
],
|
||||
separators=(",", ":"),
|
||||
)
|
||||
@@ -0,0 +1,274 @@
|
||||
"""Channel-per-trial Buzz provisioning against a local benchmark stack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
|
||||
import psycopg
|
||||
from harbor_buzz_orchestra.manifest import ExperimentManifest
|
||||
from harbor_buzz_orchestra.provisioning import AgentCredential, TrialHandle
|
||||
|
||||
from .buzz_cli import BuzzCli
|
||||
from .keys import compute_auth_tag, generate_keypair, keypair_from_secret
|
||||
|
||||
|
||||
class ProvisioningError(RuntimeError):
|
||||
"""Trial provisioning failed or was invoked inconsistently."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TestbedConfig:
|
||||
"""Connection settings for one local benchmark stack."""
|
||||
|
||||
__test__ = False # not a pytest class, despite the name
|
||||
relay_http_url: str # e.g. http://localhost:3000 — CLI + healthcheck
|
||||
relay_ws_url: str # as reachable FROM the agents' runtime
|
||||
owner_secret_key: str # relay owner key; signs NIP-OA attestations
|
||||
postgres_dsn: str # benchmark schema lives here
|
||||
llm_api_keys: dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
# endpoint -> API key. v1: per-endpoint resolution; true per-agent
|
||||
# Databricks attribution is pending the AI Gateway field verification.
|
||||
# Pinned user identity (hex secret key). When set, every trial's user —
|
||||
# the human analogue that owns the channel and posts the task — is this
|
||||
# one identity instead of a fresh key per trial, so a GUI logged in as
|
||||
# that user sees every trial channel accumulate. None preserves the
|
||||
# fresh-user-per-trial behaviour.
|
||||
user_secret_key: str | None = None
|
||||
# When False, teardown leaves the trial channel unarchived (and its
|
||||
# archived_at stamp NULL) so finished trials stay visible in a GUI.
|
||||
archive_on_teardown: bool = True
|
||||
|
||||
|
||||
def provisioner_from_dict(config: dict[str, object]) -> BuzzTrialProvisioner:
|
||||
"""Harbor CLI factory for a JSON-decoded testbed configuration."""
|
||||
return BuzzTrialProvisioner(TestbedConfig(**config))
|
||||
|
||||
|
||||
class BuzzTrialProvisioner:
|
||||
"""Implements the TrialHandle v1.1 contract against a live Buzz relay.
|
||||
|
||||
Guarantees (contract PLANS/HARBOR_BUZZ_TRIALHANDLE_CONTRACT.md):
|
||||
- create_trial is synchronous and idempotent per (run_id, trial_id);
|
||||
concurrency-safe via a Postgres advisory lock on the trial key.
|
||||
- One private channel per trial; membership is exactly the trial's
|
||||
credentials, so cross-trial reads are blocked by construction.
|
||||
- Fresh keys per trial; never reused.
|
||||
- teardown archives the channel and stamps archived_at; events are
|
||||
never deleted.
|
||||
"""
|
||||
|
||||
def __init__(self, config: TestbedConfig) -> None:
|
||||
self._config = config
|
||||
|
||||
# -- contract surface ---------------------------------------------------
|
||||
|
||||
def create_trial(
|
||||
self,
|
||||
run_id: str,
|
||||
trial_id: str,
|
||||
manifest: ExperimentManifest,
|
||||
channel_label: str | None = None,
|
||||
) -> TrialHandle:
|
||||
manifest_hash = manifest.sha256
|
||||
with psycopg.connect(self._config.postgres_dsn) as conn:
|
||||
self._lock_trial(conn, run_id, trial_id)
|
||||
existing = self._load_trial(conn, run_id, trial_id)
|
||||
if existing is not None:
|
||||
if existing.manifest_hash != manifest_hash:
|
||||
raise ProvisioningError(
|
||||
f"trial ({run_id}, {trial_id}) already provisioned with "
|
||||
f"manifest {existing.manifest_hash}, got {manifest_hash}"
|
||||
)
|
||||
return existing
|
||||
|
||||
handle = self._provision(
|
||||
run_id, trial_id, manifest, manifest_hash, channel_label
|
||||
)
|
||||
self._store_trial(conn, handle)
|
||||
conn.commit()
|
||||
return handle
|
||||
|
||||
def teardown(self, handle: TrialHandle) -> None:
|
||||
if not self._config.archive_on_teardown:
|
||||
# GUI/spectator mode: finished trial channels stay visible.
|
||||
return
|
||||
cli = self._cli_for(handle.credentials[0])
|
||||
try:
|
||||
cli.archive_channel(handle.channel_id)
|
||||
except Exception as error:
|
||||
if "archived" not in str(error).lower():
|
||||
raise
|
||||
with psycopg.connect(self._config.postgres_dsn) as conn:
|
||||
conn.execute(
|
||||
"UPDATE benchmark.trial_manifest"
|
||||
" SET archived_at = COALESCE(archived_at, now())"
|
||||
" WHERE run_id = %s AND trial_id = %s",
|
||||
(handle.run_id, handle.trial_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def healthcheck(self) -> None:
|
||||
url = f"{self._config.relay_http_url.rstrip('/')}/_readiness"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
if response.status != 200:
|
||||
raise ProvisioningError(
|
||||
f"relay readiness returned {response.status}"
|
||||
)
|
||||
except (urllib.error.URLError, OSError) as error:
|
||||
raise ProvisioningError(f"relay unreachable at {url}: {error}") from error
|
||||
try:
|
||||
with psycopg.connect(self._config.postgres_dsn, connect_timeout=5) as conn:
|
||||
conn.execute("SELECT 1")
|
||||
except psycopg.Error as error:
|
||||
raise ProvisioningError(
|
||||
f"benchmark postgres unreachable: {error}"
|
||||
) from error
|
||||
|
||||
# -- internals ----------------------------------------------------------
|
||||
|
||||
def _provision(
|
||||
self,
|
||||
run_id: str,
|
||||
trial_id: str,
|
||||
manifest: ExperimentManifest,
|
||||
manifest_hash: str,
|
||||
channel_label: str | None,
|
||||
) -> TrialHandle:
|
||||
credentials = self._mint_credentials(manifest)
|
||||
user = self._mint_user()
|
||||
# The user identity creates the channel and invites the agents —
|
||||
# mirroring production Buzz, where a human owns the channel their
|
||||
# agents work in.
|
||||
cli = self._cli_for(user)
|
||||
name = (
|
||||
f"{channel_label}-{trial_id[:8]}"
|
||||
if channel_label
|
||||
else f"trial-{trial_id[:8]}-{manifest_hash[:8]}"
|
||||
)
|
||||
channel_id = cli.create_private_channel(
|
||||
name=name,
|
||||
description=f"run={run_id} trial={trial_id} manifest={manifest_hash}",
|
||||
)
|
||||
for credential in credentials:
|
||||
cli.add_member(channel_id, credential.nostr_pubkey)
|
||||
return TrialHandle(
|
||||
run_id=run_id,
|
||||
trial_id=trial_id,
|
||||
manifest_hash=manifest_hash,
|
||||
relay_ws_url=self._config.relay_ws_url,
|
||||
channel_id=channel_id,
|
||||
credentials=credentials,
|
||||
user=user,
|
||||
user_relay_url=self._config.relay_http_url,
|
||||
)
|
||||
|
||||
def _mint_credentials(
|
||||
self, manifest: ExperimentManifest
|
||||
) -> tuple[AgentCredential, ...]:
|
||||
roster = sorted(manifest.roster, key=lambda e: e.kind != "orchestrator")
|
||||
credentials: list[AgentCredential] = []
|
||||
for entry in roster:
|
||||
api_key = self._config.llm_api_keys.get(entry.endpoint)
|
||||
if api_key is None:
|
||||
raise ProvisioningError(
|
||||
f"no LLM API key configured for endpoint {entry.endpoint!r}"
|
||||
)
|
||||
for index in range(1, entry.count + 1):
|
||||
keypair = generate_keypair()
|
||||
credentials.append(
|
||||
AgentCredential(
|
||||
agent_id=f"{entry.id}-{index}",
|
||||
role=entry.kind,
|
||||
nostr_secret_key=keypair.secret_key,
|
||||
nostr_pubkey=keypair.pubkey,
|
||||
nostr_auth_tag=compute_auth_tag(
|
||||
self._config.owner_secret_key, keypair.pubkey
|
||||
),
|
||||
llm_endpoint=entry.endpoint,
|
||||
llm_api_key=api_key,
|
||||
)
|
||||
)
|
||||
return tuple(credentials)
|
||||
|
||||
def _mint_user(self) -> AgentCredential:
|
||||
"""Mint the trial's user identity — the human analogue, not an agent.
|
||||
|
||||
With a pinned ``user_secret_key`` the same identity fronts every
|
||||
trial, like one human running many teams; otherwise each trial gets
|
||||
a fresh user key.
|
||||
"""
|
||||
keypair = (
|
||||
keypair_from_secret(self._config.user_secret_key)
|
||||
if self._config.user_secret_key
|
||||
else generate_keypair()
|
||||
)
|
||||
return AgentCredential(
|
||||
agent_id="user",
|
||||
role="user",
|
||||
nostr_secret_key=keypair.secret_key,
|
||||
nostr_pubkey=keypair.pubkey,
|
||||
nostr_auth_tag=compute_auth_tag(
|
||||
self._config.owner_secret_key, keypair.pubkey
|
||||
),
|
||||
llm_endpoint="",
|
||||
llm_api_key="",
|
||||
)
|
||||
|
||||
def _cli_for(self, credential: AgentCredential) -> BuzzCli:
|
||||
return BuzzCli(
|
||||
relay_url=self._config.relay_http_url,
|
||||
secret_key=credential.nostr_secret_key,
|
||||
auth_tag=credential.nostr_auth_tag,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _lock_trial(conn: psycopg.Connection, run_id: str, trial_id: str) -> None:
|
||||
digest = hashlib.sha256(f"{run_id}\x00{trial_id}".encode()).digest()
|
||||
lock_key = int.from_bytes(digest[:8], "big", signed=True)
|
||||
conn.execute("SELECT pg_advisory_xact_lock(%s)", (lock_key,))
|
||||
|
||||
@staticmethod
|
||||
def _load_trial(
|
||||
conn: psycopg.Connection, run_id: str, trial_id: str
|
||||
) -> TrialHandle | None:
|
||||
row = conn.execute(
|
||||
"SELECT handle FROM benchmark.trial_manifest"
|
||||
" WHERE run_id = %s AND trial_id = %s",
|
||||
(run_id, trial_id),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
stored = row[0]
|
||||
return TrialHandle(
|
||||
run_id=stored["run_id"],
|
||||
trial_id=stored["trial_id"],
|
||||
manifest_hash=stored["manifest_hash"],
|
||||
relay_ws_url=stored["relay_ws_url"],
|
||||
channel_id=stored["channel_id"],
|
||||
credentials=tuple(
|
||||
AgentCredential(**credential) for credential in stored["credentials"]
|
||||
),
|
||||
user=AgentCredential(**stored["user"]),
|
||||
user_relay_url=stored.get("user_relay_url", ""),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _store_trial(conn: psycopg.Connection, handle: TrialHandle) -> None:
|
||||
conn.execute(
|
||||
"INSERT INTO benchmark.trial_manifest"
|
||||
" (run_id, trial_id, manifest_hash, channel_id, handle)"
|
||||
" VALUES (%s, %s, %s, %s, %s)",
|
||||
(
|
||||
handle.run_id,
|
||||
handle.trial_id,
|
||||
handle.manifest_hash,
|
||||
handle.channel_id,
|
||||
json.dumps(dataclasses.asdict(handle)),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Shared fixtures for testbed provisioning tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from harbor_buzz_orchestra.manifest import ExperimentManifest
|
||||
|
||||
PROMPT_REF = {"path": "personas/x.md", "sha256": "0" * 64}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def manifest() -> ExperimentManifest:
|
||||
"""A minimal two-class manifest: one orchestrator, two workers."""
|
||||
return ExperimentManifest.load(
|
||||
{
|
||||
"condition": "O-test",
|
||||
"roster": [
|
||||
{
|
||||
"id": "worker-glm",
|
||||
"kind": "worker",
|
||||
"role": "implementer",
|
||||
"count": 2,
|
||||
"endpoint": "databricks/glm",
|
||||
"model_revision": "glm-5.2",
|
||||
"prompt": PROMPT_REF,
|
||||
"generation": {
|
||||
"max_output_tokens": 4096,
|
||||
"context_window_tokens": 128000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "orch-opus",
|
||||
"kind": "orchestrator",
|
||||
"role": "lead",
|
||||
"count": 1,
|
||||
"endpoint": "databricks/opus",
|
||||
"model_revision": "opus-4.8",
|
||||
"prompt": PROMPT_REF,
|
||||
"generation": {
|
||||
"max_output_tokens": 8192,
|
||||
"context_window_tokens": 200000,
|
||||
},
|
||||
},
|
||||
],
|
||||
"prices": {
|
||||
"databricks/glm": {
|
||||
"input_per_million_usd": 0.5,
|
||||
"cached_input_per_million_usd": 0.1,
|
||||
"output_per_million_usd": 1.5,
|
||||
},
|
||||
"databricks/opus": {
|
||||
"input_per_million_usd": 15.0,
|
||||
"cached_input_per_million_usd": 1.5,
|
||||
"output_per_million_usd": 75.0,
|
||||
},
|
||||
},
|
||||
"trial_budget": {"timeout_seconds": 3600},
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
"""just benchmark must default to leaderboard-eligible settings."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_SCRIPT = Path(__file__).parents[2] / "scripts" / "benchmark.py"
|
||||
_spec = importlib.util.spec_from_file_location("benchmark", _SCRIPT)
|
||||
benchmark = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["benchmark"] = benchmark
|
||||
_spec.loader.exec_module(benchmark)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def state_dir(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(benchmark, "STATE_DIR", tmp_path / ".benchmark")
|
||||
return tmp_path / ".benchmark"
|
||||
|
||||
|
||||
def test_defaults_are_leaderboard_eligible():
|
||||
args = benchmark.parse_args([])
|
||||
assert args.attempts == 5
|
||||
assert args.dataset is None and args.path is None # dataset default applied later
|
||||
argv = benchmark.leaderboard_argv(args, Path("prov.json"), Path("linux-bin"))
|
||||
assert argv[argv.index("--dataset") + 1] == "terminal-bench/terminal-bench-2-1"
|
||||
assert argv[argv.index("--attempts") + 1] == "5"
|
||||
assert argv[argv.index("--manifest") + 1].endswith("tb-cobol-sonnet-haiku.yaml")
|
||||
assert argv[argv.index("--agent-bin-dir") + 1] == "linux-bin"
|
||||
# In-container agents reach the host relay through the forwarder gateway.
|
||||
assert argv[argv.index("--relay-gateway") + 1] == (
|
||||
f"host.docker.internal:{benchmark.RELAY_HTTP_PORT}"
|
||||
)
|
||||
|
||||
|
||||
def test_selectors_pass_through():
|
||||
args = benchmark.parse_args(
|
||||
[
|
||||
"--path",
|
||||
"/tmp/task",
|
||||
"-i",
|
||||
"cobol*",
|
||||
"-x",
|
||||
"flaky*",
|
||||
"-k",
|
||||
"1",
|
||||
"--job-name",
|
||||
"smoke",
|
||||
"--dry-run",
|
||||
]
|
||||
)
|
||||
argv = benchmark.leaderboard_argv(args, Path("p.json"), Path("b"))
|
||||
assert argv[argv.index("--path") + 1] == "/tmp/task"
|
||||
assert argv[argv.index("--include-task") + 1] == "cobol*"
|
||||
assert argv[argv.index("--exclude-task") + 1] == "flaky*"
|
||||
assert argv[argv.index("--attempts") + 1] == "1"
|
||||
assert "--dry-run" in argv
|
||||
assert "--dataset" not in argv
|
||||
|
||||
|
||||
def test_state_is_generated_once_and_reused(state_dir):
|
||||
first = benchmark.load_state()
|
||||
second = benchmark.load_state()
|
||||
assert first["user_secret_key"] == second["user_secret_key"]
|
||||
assert first["owner_secret_key"] != first["user_secret_key"]
|
||||
assert len(first["user_pubkey"]) == 64
|
||||
stored = json.loads((state_dir / "state.json").read_text())
|
||||
assert "user_pubkey" not in stored # derived, never persisted
|
||||
|
||||
|
||||
def test_provisioner_config_pins_user_and_keeps_channels(
|
||||
state_dir, tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("FAKE_KEY_ENV", "sk-test")
|
||||
endpoints = tmp_path / "endpoints.json"
|
||||
endpoints.write_text(
|
||||
json.dumps(
|
||||
{"model-a": {"provider": "anthropic", "api_key_env": "FAKE_KEY_ENV"}}
|
||||
)
|
||||
)
|
||||
state = benchmark.load_state()
|
||||
path = benchmark.write_provisioner_config(state, endpoints)
|
||||
config = json.loads(path.read_text())
|
||||
assert config["user_secret_key"] == state["user_secret_key"]
|
||||
assert config["archive_on_teardown"] is False
|
||||
assert config["llm_api_keys"] == {"model-a": "sk-test"}
|
||||
assert str(benchmark.RELAY_HTTP_PORT) in config["relay_http_url"]
|
||||
# Both views dial the relay's canonical host-bound address; inside the
|
||||
# task container the loopback forwarder bridges it to the host gateway.
|
||||
assert config["relay_ws_url"] == f"ws://localhost:{benchmark.RELAY_HTTP_PORT}"
|
||||
assert config["relay_http_url"].startswith("http://localhost:")
|
||||
|
||||
|
||||
def test_provisioner_config_missing_api_key_is_explicit(
|
||||
state_dir, tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.delenv("MISSING_KEY_ENV", raising=False)
|
||||
endpoints = tmp_path / "endpoints.json"
|
||||
endpoints.write_text(
|
||||
json.dumps({"model-a": {"provider": "x", "api_key_env": "MISSING_KEY_ENV"}})
|
||||
)
|
||||
with pytest.raises(SystemExit, match="MISSING_KEY_ENV"):
|
||||
benchmark.write_provisioner_config(benchmark.load_state(), endpoints)
|
||||
|
||||
|
||||
def test_env_file_wires_owner_and_ports(state_dir):
|
||||
state = benchmark.load_state()
|
||||
env_path = benchmark.write_env_file(state)
|
||||
env = dict(line.split("=", 1) for line in env_path.read_text().splitlines() if line)
|
||||
assert env["RELAY_OWNER_PUBKEY"] == state["owner_pubkey"]
|
||||
assert env["BUZZ_HTTP_PORT"] == str(benchmark.RELAY_HTTP_PORT)
|
||||
assert env["BUZZ_PG_HOST_PORT"] == str(benchmark.PG_HOST_PORT)
|
||||
assert env["BUZZ_REQUIRE_RELAY_MEMBERSHIP"] == "true"
|
||||
|
||||
|
||||
def test_compose_command_isolates_the_project(state_dir):
|
||||
command = benchmark.compose_command("up", "-d")
|
||||
assert command[:2] == ["docker", "compose"]
|
||||
assert command[command.index("--project-name") + 1] == "buzz-benchmark"
|
||||
files = [command[i + 1] for i, part in enumerate(command) if part == "-f"]
|
||||
assert any(f.endswith("deploy/compose/compose.yml") for f in files)
|
||||
assert any(f.endswith("compose.benchmark.yml") for f in files)
|
||||
|
||||
|
||||
def test_bring_up_self_heals_a_stale_credential_volume(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_run(command, check=True):
|
||||
calls.append(command)
|
||||
if len(calls) == 1: # first up fails against the stale volume
|
||||
raise benchmark.subprocess.CalledProcessError(1, command)
|
||||
|
||||
monkeypatch.setattr(benchmark.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(benchmark, "stale_credential_volume", lambda state: True)
|
||||
benchmark.bring_up_stack({})
|
||||
assert [c[-3:] for c in calls] == [
|
||||
["up", "-d", "--wait"],
|
||||
[str(benchmark.COMPOSE_FILES[-1]), "down", "-v"],
|
||||
["up", "-d", "--wait"],
|
||||
]
|
||||
|
||||
|
||||
def test_bring_up_reraises_unrelated_failures(monkeypatch):
|
||||
def fake_run(command, check=True):
|
||||
raise benchmark.subprocess.CalledProcessError(1, command)
|
||||
|
||||
monkeypatch.setattr(benchmark.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(benchmark, "stale_credential_volume", lambda state: False)
|
||||
with pytest.raises(benchmark.subprocess.CalledProcessError):
|
||||
benchmark.bring_up_stack({})
|
||||
|
||||
|
||||
def test_fresh_resets_volumes_and_gui_state(tmp_path, monkeypatch):
|
||||
commands = []
|
||||
monkeypatch.setattr(
|
||||
benchmark.subprocess, "run", lambda cmd, check=True: commands.append(cmd)
|
||||
)
|
||||
monkeypatch.setattr(benchmark.sys, "platform", "darwin")
|
||||
monkeypatch.setattr(benchmark.Path, "home", classmethod(lambda cls: tmp_path))
|
||||
gui_state = tmp_path / "Library" / "WebKit" / benchmark.GUI_BUNDLE_IDENTIFIER
|
||||
gui_state.mkdir(parents=True)
|
||||
(gui_state / "localstorage.sqlite3").touch()
|
||||
|
||||
benchmark.reset_environment()
|
||||
|
||||
assert ["down", "-v"] == commands[0][-2:]
|
||||
assert not gui_state.exists()
|
||||
assert benchmark.parse_args(["--fresh"]).fresh
|
||||
assert not benchmark.parse_args([]).fresh
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Keygen and NIP-OA attestation unit tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import coincurve
|
||||
|
||||
from harbor_buzz_testbed.keys import (
|
||||
compute_auth_tag,
|
||||
encode_nsec,
|
||||
generate_keypair,
|
||||
)
|
||||
|
||||
# Produced by the Rust reference implementation
|
||||
# (crates/buzz-sdk/examples/compute_auth_tag.rs) for owner secret 0x...03 and
|
||||
# agent pubkey "a" * 64. Pins the preimage format across implementations.
|
||||
RUST_OWNER_SECRET = "0" * 63 + "3"
|
||||
RUST_AGENT_PUBKEY = "a" * 64
|
||||
RUST_TAG = [
|
||||
"auth",
|
||||
"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
|
||||
"",
|
||||
(
|
||||
"20105c618d6e5d8f559cffb6f0d7a7b4f44f3a567e1be94c96378d45ac3625da"
|
||||
"34c2e7357ea1d3ce980978334546b3e740c155e81b833ebe140d519d39ed8867"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def preimage_digest(agent_pubkey: str, conditions: str) -> bytes:
|
||||
return hashlib.sha256(
|
||||
f"nostr:agent-auth:{agent_pubkey}:{conditions}".encode()
|
||||
).digest()
|
||||
|
||||
|
||||
def test_generate_keypair_is_fresh_and_hex():
|
||||
first, second = generate_keypair(), generate_keypair()
|
||||
assert first.secret_key != second.secret_key
|
||||
assert first.pubkey != second.pubkey
|
||||
assert len(first.secret_key) == 64
|
||||
assert len(first.pubkey) == 64
|
||||
int(first.secret_key, 16)
|
||||
int(first.pubkey, 16)
|
||||
|
||||
|
||||
def test_auth_tag_shape_and_owner_pubkey():
|
||||
tag = json.loads(compute_auth_tag(RUST_OWNER_SECRET, RUST_AGENT_PUBKEY))
|
||||
assert tag[0] == "auth"
|
||||
assert tag[1] == RUST_TAG[1] # same owner pubkey as the Rust implementation
|
||||
assert tag[2] == ""
|
||||
|
||||
|
||||
def test_auth_tag_signature_verifies_over_nip_oa_preimage():
|
||||
agent = generate_keypair()
|
||||
tag = json.loads(compute_auth_tag(RUST_OWNER_SECRET, agent.pubkey))
|
||||
owner_pubkey = coincurve.PublicKeyXOnly(bytes.fromhex(tag[1]))
|
||||
assert owner_pubkey.verify(bytes.fromhex(tag[3]), preimage_digest(agent.pubkey, ""))
|
||||
|
||||
|
||||
def test_rust_reference_tag_verifies_under_python_preimage():
|
||||
"""The Rust-signed vector must verify against our preimage construction."""
|
||||
owner_pubkey = coincurve.PublicKeyXOnly(bytes.fromhex(RUST_TAG[1]))
|
||||
assert owner_pubkey.verify(
|
||||
bytes.fromhex(RUST_TAG[3]), preimage_digest(RUST_AGENT_PUBKEY, "")
|
||||
)
|
||||
|
||||
|
||||
def test_encode_nsec_matches_nip19_vector():
|
||||
# NIP-19 reference vector from the spec.
|
||||
assert (
|
||||
encode_nsec("67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa")
|
||||
== "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5"
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Live provisioning tests against a running benchmark stack.
|
||||
|
||||
Gated by BUZZ_TESTBED_LIVE=1 with stack coordinates in the environment:
|
||||
BUZZ_TESTBED_RELAY_HTTP (default http://localhost:3000)
|
||||
BUZZ_TESTBED_RELAY_WS (default ws://host.docker.internal:3000)
|
||||
BUZZ_TESTBED_OWNER_KEY relay owner secret key (hex)
|
||||
BUZZ_TESTBED_PG_DSN benchmark Postgres DSN
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
|
||||
from harbor_buzz_testbed.buzz_cli import BuzzCli, BuzzCliError
|
||||
from harbor_buzz_testbed.provisioner import (
|
||||
BuzzTrialProvisioner,
|
||||
ProvisioningError,
|
||||
TestbedConfig,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("BUZZ_TESTBED_LIVE") != "1",
|
||||
reason="live testbed suite; set BUZZ_TESTBED_LIVE=1 with a running stack",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def provisioner() -> BuzzTrialProvisioner:
|
||||
owner_key = os.environ.get("BUZZ_TESTBED_OWNER_KEY")
|
||||
dsn = os.environ.get("BUZZ_TESTBED_PG_DSN")
|
||||
if not owner_key or not dsn:
|
||||
pytest.fail("BUZZ_TESTBED_OWNER_KEY and BUZZ_TESTBED_PG_DSN are required")
|
||||
return BuzzTrialProvisioner(
|
||||
TestbedConfig(
|
||||
relay_http_url=os.environ.get(
|
||||
"BUZZ_TESTBED_RELAY_HTTP", "http://localhost:3000"
|
||||
),
|
||||
relay_ws_url=os.environ.get(
|
||||
"BUZZ_TESTBED_RELAY_WS", "ws://host.docker.internal:3000"
|
||||
),
|
||||
owner_secret_key=owner_key,
|
||||
postgres_dsn=dsn,
|
||||
llm_api_keys={
|
||||
"databricks/glm": "test-glm-key",
|
||||
"databricks/opus": "test-opus-key",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def cli_for(provisioner: BuzzTrialProvisioner, credential) -> BuzzCli:
|
||||
return provisioner._cli_for(credential)
|
||||
|
||||
|
||||
def test_create_is_idempotent_and_isolated(provisioner, manifest):
|
||||
run_id = f"live-{uuid.uuid4().hex[:8]}"
|
||||
trial_a = str(uuid.uuid4())
|
||||
trial_b = str(uuid.uuid4())
|
||||
|
||||
provisioner.healthcheck()
|
||||
handle_a = provisioner.create_trial(run_id, trial_a, manifest)
|
||||
handle_b = provisioner.create_trial(run_id, trial_b, manifest)
|
||||
try:
|
||||
# Shape: one credential per roster slot, orchestrator first.
|
||||
assert len(handle_a.credentials) == 3
|
||||
assert handle_a.credentials[0].role == "orchestrator"
|
||||
assert handle_a.manifest_hash == manifest.sha256
|
||||
|
||||
# Idempotency: same key returns the stored handle, keys included.
|
||||
again = provisioner.create_trial(run_id, trial_a, manifest)
|
||||
assert again == handle_a
|
||||
|
||||
# Isolation: trials get distinct channels and fresh keys...
|
||||
assert handle_a.channel_id != handle_b.channel_id
|
||||
keys_a = {c.nostr_secret_key for c in handle_a.credentials}
|
||||
keys_b = {c.nostr_secret_key for c in handle_b.credentials}
|
||||
assert not keys_a & keys_b
|
||||
|
||||
# ...and trial B's orchestrator cannot see trial A's channel.
|
||||
cli_a = cli_for(provisioner, handle_a.credentials[0])
|
||||
cli_b = cli_for(provisioner, handle_b.credentials[0])
|
||||
cli_a.run(
|
||||
"messages",
|
||||
"send",
|
||||
"--channel",
|
||||
handle_a.channel_id,
|
||||
"--content",
|
||||
"trial A secret",
|
||||
)
|
||||
foreign_read = cli_b.run(
|
||||
"messages", "get", "--channel", handle_a.channel_id, "--limit", "10"
|
||||
)
|
||||
assert foreign_read == [], "cross-trial read must return nothing"
|
||||
with pytest.raises(BuzzCliError, match="private"):
|
||||
cli_b.run("channels", "join", "--channel", handle_a.channel_id)
|
||||
|
||||
# Members can read their own channel.
|
||||
own_read = cli_a.run(
|
||||
"messages", "get", "--channel", handle_a.channel_id, "--limit", "10"
|
||||
)
|
||||
assert [m["content"] for m in own_read] == ["trial A secret"]
|
||||
|
||||
# Same trial key + different manifest must be rejected, not silently
|
||||
# reprovisioned.
|
||||
altered = manifest.model_copy(update={"condition": "O-altered"})
|
||||
with pytest.raises(ProvisioningError, match="already provisioned"):
|
||||
provisioner.create_trial(run_id, trial_a, altered)
|
||||
finally:
|
||||
provisioner.teardown(handle_a)
|
||||
provisioner.teardown(handle_b)
|
||||
|
||||
# Teardown is idempotent and stamps archived_at.
|
||||
provisioner.teardown(handle_a)
|
||||
with psycopg.connect(os.environ["BUZZ_TESTBED_PG_DSN"]) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT archived_at FROM benchmark.trial_manifest"
|
||||
" WHERE run_id = %s AND trial_id = %s",
|
||||
(run_id, trial_a),
|
||||
).fetchone()
|
||||
assert row is not None and row[0] is not None
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Provisioner unit tests — no relay or Postgres required."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import coincurve
|
||||
import pytest
|
||||
|
||||
from harbor_buzz_testbed.provisioner import (
|
||||
BuzzTrialProvisioner,
|
||||
ProvisioningError,
|
||||
TestbedConfig,
|
||||
)
|
||||
|
||||
OWNER_SECRET = "0" * 63 + "3"
|
||||
|
||||
|
||||
def config(**overrides) -> TestbedConfig:
|
||||
defaults = {
|
||||
"relay_http_url": "http://localhost:3000",
|
||||
"relay_ws_url": "ws://host.docker.internal:3000",
|
||||
"owner_secret_key": OWNER_SECRET,
|
||||
"postgres_dsn": "postgresql://unused",
|
||||
"llm_api_keys": {"databricks/glm": "glm-key", "databricks/opus": "opus-key"},
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return TestbedConfig(**defaults)
|
||||
|
||||
|
||||
def test_mint_credentials_expands_roster(manifest):
|
||||
credentials = BuzzTrialProvisioner(config())._mint_credentials(manifest)
|
||||
assert [c.agent_id for c in credentials] == [
|
||||
"orch-opus-1",
|
||||
"worker-glm-1",
|
||||
"worker-glm-2",
|
||||
]
|
||||
assert credentials[0].role == "orchestrator"
|
||||
assert credentials[0].llm_api_key == "opus-key"
|
||||
assert {c.llm_api_key for c in credentials[1:]} == {"glm-key"}
|
||||
|
||||
|
||||
def test_mint_credentials_keys_are_fresh_and_attested(manifest):
|
||||
provisioner = BuzzTrialProvisioner(config())
|
||||
first = provisioner._mint_credentials(manifest)
|
||||
second = provisioner._mint_credentials(manifest)
|
||||
all_secrets = [c.nostr_secret_key for c in first + second]
|
||||
assert len(all_secrets) == len(set(all_secrets)), "keys must never be reused"
|
||||
owner_pubkey = coincurve.PrivateKey(bytes.fromhex(OWNER_SECRET)).public_key_xonly
|
||||
for credential in first:
|
||||
tag = json.loads(credential.nostr_auth_tag)
|
||||
assert tag[:3] == ["auth", owner_pubkey.format().hex(), ""]
|
||||
digest = hashlib.sha256(
|
||||
f"nostr:agent-auth:{credential.nostr_pubkey}:".encode()
|
||||
).digest()
|
||||
assert owner_pubkey.verify(bytes.fromhex(tag[3]), digest)
|
||||
|
||||
|
||||
def test_mint_user_is_attested_and_not_an_agent():
|
||||
provisioner = BuzzTrialProvisioner(config())
|
||||
user = provisioner._mint_user()
|
||||
assert user.agent_id == "user"
|
||||
assert user.role == "user"
|
||||
assert user.llm_endpoint == "" and user.llm_api_key == ""
|
||||
owner_pubkey = coincurve.PrivateKey(bytes.fromhex(OWNER_SECRET)).public_key_xonly
|
||||
tag = json.loads(user.nostr_auth_tag)
|
||||
assert tag[:3] == ["auth", owner_pubkey.format().hex(), ""]
|
||||
|
||||
|
||||
def test_pinned_user_secret_reuses_one_identity():
|
||||
pinned = "7" * 64
|
||||
provisioner = BuzzTrialProvisioner(config(user_secret_key=pinned))
|
||||
first = provisioner._mint_user()
|
||||
second = provisioner._mint_user()
|
||||
assert first.nostr_secret_key == pinned
|
||||
assert first.nostr_pubkey == second.nostr_pubkey
|
||||
expected = coincurve.PrivateKey(bytes.fromhex(pinned)).public_key_xonly
|
||||
assert first.nostr_pubkey == expected.format().hex()
|
||||
# Still a user, still attested by the owner.
|
||||
assert first.role == "user"
|
||||
owner_pubkey = coincurve.PrivateKey(bytes.fromhex(OWNER_SECRET)).public_key_xonly
|
||||
assert json.loads(first.nostr_auth_tag)[1] == owner_pubkey.format().hex()
|
||||
|
||||
|
||||
def test_teardown_skips_archiving_when_disabled():
|
||||
provisioner = BuzzTrialProvisioner(config(archive_on_teardown=False))
|
||||
# Any attribute access would fail on this handle — teardown must return
|
||||
# before touching the CLI or Postgres.
|
||||
provisioner.teardown(handle=None)
|
||||
|
||||
|
||||
def test_mint_credentials_missing_api_key_is_explicit(manifest):
|
||||
provisioner = BuzzTrialProvisioner(config(llm_api_keys={}))
|
||||
with pytest.raises(ProvisioningError, match="databricks/"):
|
||||
provisioner._mint_credentials(manifest)
|
||||
|
||||
|
||||
def test_lock_key_is_deterministic_and_distinct():
|
||||
calls: list[int] = []
|
||||
|
||||
class FakeConn:
|
||||
def execute(self, _query, params):
|
||||
calls.append(params[0])
|
||||
|
||||
BuzzTrialProvisioner._lock_trial(FakeConn(), "run-a", "trial-1")
|
||||
BuzzTrialProvisioner._lock_trial(FakeConn(), "run-a", "trial-1")
|
||||
BuzzTrialProvisioner._lock_trial(FakeConn(), "run-a", "trial-2")
|
||||
assert calls[0] == calls[1]
|
||||
assert calls[0] != calls[2]
|
||||
assert all(-(2**63) <= key < 2**63 for key in calls)
|
||||
|
||||
|
||||
def test_healthcheck_fails_fast_when_relay_down():
|
||||
provisioner = BuzzTrialProvisioner(
|
||||
config(relay_http_url="http://localhost:1", postgres_dsn="postgresql://unused")
|
||||
)
|
||||
with pytest.raises(ProvisioningError, match="relay unreachable"):
|
||||
provisioner.healthcheck()
|
||||
+2588
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manifest_data() -> dict[str, Any]:
|
||||
digest = "a" * 64
|
||||
generation = {
|
||||
"temperature": 0,
|
||||
"max_output_tokens": 4096,
|
||||
"context_window_tokens": 200000,
|
||||
}
|
||||
return {
|
||||
"schema_version": "1",
|
||||
"condition": "O",
|
||||
"roster": [
|
||||
{
|
||||
"id": "opus-orchestrator",
|
||||
"kind": "orchestrator",
|
||||
"role": "orchestrator",
|
||||
"count": 1,
|
||||
"endpoint": "databricks/opus",
|
||||
"model_revision": "2026-07-01",
|
||||
"prompt": {"path": "prompts/orchestrator.md", "sha256": digest},
|
||||
"generation": generation,
|
||||
},
|
||||
{
|
||||
"id": "qwen-workers",
|
||||
"kind": "worker",
|
||||
"role": "fast-worker",
|
||||
"count": 4,
|
||||
"concurrency": 2,
|
||||
"endpoint": "databricks/qwen",
|
||||
"model_revision": "revision-42",
|
||||
"prompt": {"path": "prompts/qwen.md", "sha256": digest},
|
||||
"generation": {**generation, "context_window_tokens": 32768},
|
||||
},
|
||||
],
|
||||
"prices": {
|
||||
"databricks/opus": {
|
||||
"input_per_million_usd": 5,
|
||||
"cached_input_per_million_usd": 0.5,
|
||||
"output_per_million_usd": 25,
|
||||
},
|
||||
"databricks/qwen": {
|
||||
"input_per_million_usd": 0.1,
|
||||
"cached_input_per_million_usd": 0.01,
|
||||
"output_per_million_usd": 0.2,
|
||||
},
|
||||
},
|
||||
"trial_budget": {"timeout_seconds": 3600, "max_cost_usd": 5},
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from harbor.models.agent.context import AgentContext
|
||||
|
||||
from harbor_buzz_orchestra import (
|
||||
AgentCredential,
|
||||
BuzzOrchestraAgent,
|
||||
RuntimeResult,
|
||||
TrialHandle,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
async def test_agent_credential_carries_closed_relay_attestation():
|
||||
credential = AgentCredential(
|
||||
agent_id="orchestrator-1",
|
||||
role="orchestrator",
|
||||
nostr_secret_key="11" * 32,
|
||||
nostr_pubkey="22" * 32,
|
||||
nostr_auth_tag='["auth","owner","conditions","signature"]',
|
||||
llm_endpoint="https://example.databricks.com/serving-endpoints/opus",
|
||||
llm_api_key="attributed-key",
|
||||
)
|
||||
|
||||
assert credential.nostr_auth_tag.startswith('["auth"')
|
||||
|
||||
|
||||
class Provisioner:
|
||||
def __init__(self):
|
||||
self.healthchecked = False
|
||||
self.created = None
|
||||
self.torn_down = None
|
||||
|
||||
def healthcheck(self):
|
||||
self.healthchecked = True
|
||||
|
||||
def create_trial(self, run_id, trial_id, manifest, channel_label=None):
|
||||
self.created = (run_id, trial_id, manifest, channel_label)
|
||||
return TrialHandle(
|
||||
run_id,
|
||||
trial_id,
|
||||
manifest.sha256,
|
||||
"ws://relay",
|
||||
"channel-1",
|
||||
(),
|
||||
user=AgentCredential(
|
||||
agent_id="user",
|
||||
role="user",
|
||||
nostr_secret_key="s",
|
||||
nostr_pubkey="p",
|
||||
nostr_auth_tag="[]",
|
||||
llm_endpoint="",
|
||||
llm_api_key="",
|
||||
),
|
||||
)
|
||||
|
||||
def teardown(self, handle):
|
||||
self.torn_down = handle
|
||||
|
||||
|
||||
class Runtime:
|
||||
def __init__(self, error=None):
|
||||
self.called = None
|
||||
self.error = error
|
||||
|
||||
async def run(self, **kwargs):
|
||||
self.called = kwargs
|
||||
if self.error:
|
||||
raise self.error
|
||||
return RuntimeResult(10, 2, 3, 0.25, {"receipt_status": "pending"})
|
||||
|
||||
|
||||
async def test_agent_lifecycle_and_context(tmp_path, manifest_data):
|
||||
provisioner, runtime, context_id = Provisioner(), Runtime(), uuid4()
|
||||
environment = SimpleNamespace(context_id=context_id, environment_name="hello-world")
|
||||
agent = BuzzOrchestraAgent(
|
||||
logs_dir=tmp_path,
|
||||
manifest=manifest_data,
|
||||
provisioner=provisioner,
|
||||
runtime=runtime,
|
||||
run_id="run-1",
|
||||
)
|
||||
agent.context_id = context_id
|
||||
context = AgentContext()
|
||||
await agent.setup(environment)
|
||||
await agent.run("solve it", environment, context)
|
||||
assert provisioner.healthchecked
|
||||
assert provisioner.created[:2] == ("run-1", str(context_id))
|
||||
# The task short name labels the trial channel for spectator GUIs.
|
||||
assert provisioner.created[3] == "hello-world"
|
||||
assert provisioner.torn_down.channel_id == "channel-1"
|
||||
assert runtime.called["instruction"] == "solve it"
|
||||
assert (
|
||||
context.n_input_tokens,
|
||||
context.n_cache_tokens,
|
||||
context.n_output_tokens,
|
||||
context.cost_usd,
|
||||
) == (10, 2, 3, 0.25)
|
||||
assert context.metadata["manifest_sha256"] == agent.manifest.sha256
|
||||
assert context.metadata["trial_id"] == str(context_id)
|
||||
|
||||
|
||||
async def test_teardown_runs_when_runtime_fails(tmp_path, manifest_data):
|
||||
provisioner, runtime, context_id = (
|
||||
Provisioner(),
|
||||
Runtime(RuntimeError("runtime failed")),
|
||||
uuid4(),
|
||||
)
|
||||
environment = SimpleNamespace(context_id=context_id)
|
||||
agent = BuzzOrchestraAgent(
|
||||
logs_dir=tmp_path,
|
||||
manifest=manifest_data,
|
||||
provisioner=provisioner,
|
||||
runtime=runtime,
|
||||
)
|
||||
agent.context_id = context_id
|
||||
with pytest.raises(RuntimeError, match="runtime failed"):
|
||||
await agent.run("solve it", environment, AgentContext())
|
||||
assert provisioner.torn_down.channel_id == "channel-1"
|
||||
|
||||
|
||||
async def test_missing_integrations_fail_explicitly(tmp_path, manifest_data):
|
||||
agent = BuzzOrchestraAgent(logs_dir=tmp_path, manifest=manifest_data)
|
||||
with pytest.raises(RuntimeError, match="M1 wiring is incomplete"):
|
||||
await agent.run("solve it", SimpleNamespace(context_id=uuid4()), AgentContext())
|
||||
|
||||
|
||||
async def test_cli_runtime_construction_from_json(tmp_path, manifest_data):
|
||||
endpoint_path = tmp_path / "endpoints.json"
|
||||
endpoint_path.write_text(
|
||||
'{"frontier/rev":{"provider":"anthropic",'
|
||||
'"api_key_env":"ANTHROPIC_API_KEY"},'
|
||||
'"worker/rev":{"provider":"openai",'
|
||||
'"api_key_env":"OPENAI_API_KEY"}}'
|
||||
)
|
||||
agent = BuzzOrchestraAgent(
|
||||
logs_dir=tmp_path / "logs",
|
||||
manifest=manifest_data,
|
||||
artifact_root=tmp_path,
|
||||
endpoint_config=endpoint_path,
|
||||
buzz_acp_binary="/pinned/buzz-acp",
|
||||
buzz_agent_binary="/pinned/buzz-agent",
|
||||
buzz_dev_mcp_binary="/pinned/buzz-dev-mcp",
|
||||
buzz_cli_binary="/pinned/buzz",
|
||||
)
|
||||
assert agent.runtime.artifact_root == tmp_path
|
||||
assert agent.runtime.endpoints["frontier/rev"].provider == "anthropic"
|
||||
assert agent.runtime.buzz_acp_binary == "/pinned/buzz-acp"
|
||||
assert agent.runtime.buzz_agent_binary == "/pinned/buzz-agent"
|
||||
assert agent.runtime.buzz_dev_mcp_binary == "/pinned/buzz-dev-mcp"
|
||||
assert agent.runtime.buzz_cli_binary == "/pinned/buzz"
|
||||
|
||||
|
||||
async def test_cli_construction_requires_complete_pairs(tmp_path, manifest_data):
|
||||
with pytest.raises(ValueError, match="artifact_root"):
|
||||
BuzzOrchestraAgent(
|
||||
logs_dir=tmp_path,
|
||||
manifest=manifest_data,
|
||||
endpoint_config={},
|
||||
)
|
||||
with pytest.raises(ValueError, match="provisioner_factory"):
|
||||
BuzzOrchestraAgent(
|
||||
logs_dir=tmp_path,
|
||||
manifest=manifest_data,
|
||||
provisioner_config={},
|
||||
)
|
||||
@@ -0,0 +1,468 @@
|
||||
"""The container runtime must launch the production stack, unmodified."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from harbor.environments.base import ExecResult
|
||||
|
||||
from harbor_buzz_orchestra.container_runtime import (
|
||||
REMOTE_BIN,
|
||||
REMOTE_LOGS,
|
||||
BuzzContainerRuntime,
|
||||
EndpointLaunchConfig,
|
||||
RuntimeLaunchError,
|
||||
)
|
||||
from harbor_buzz_orchestra.manifest import ExperimentManifest
|
||||
from harbor_buzz_orchestra.provisioning import AgentCredential, TrialHandle
|
||||
|
||||
|
||||
def write_manifest(tmp_path: Path) -> ExperimentManifest:
|
||||
prompt = tmp_path / "prompt.md"
|
||||
prompt.write_text("prompt", encoding="utf-8")
|
||||
digest = hashlib.sha256(prompt.read_bytes()).hexdigest()
|
||||
roster_entry = {
|
||||
"count": 1,
|
||||
"model_revision": "r1",
|
||||
"prompt": {"path": "prompt.md", "sha256": digest},
|
||||
"generation": {"max_output_tokens": 100, "context_window_tokens": 1000},
|
||||
}
|
||||
return ExperimentManifest.load(
|
||||
{
|
||||
"condition": "test",
|
||||
"roster": [
|
||||
{
|
||||
"id": "orch",
|
||||
"kind": "orchestrator",
|
||||
"role": "lead",
|
||||
"endpoint": "orch-model",
|
||||
**roster_entry,
|
||||
},
|
||||
{
|
||||
"id": "worker",
|
||||
"kind": "worker",
|
||||
"role": "implementer",
|
||||
"endpoint": "worker-model",
|
||||
**roster_entry,
|
||||
},
|
||||
],
|
||||
"prices": {
|
||||
name: {
|
||||
"input_per_million_usd": 0,
|
||||
"cached_input_per_million_usd": 0,
|
||||
"output_per_million_usd": 0,
|
||||
}
|
||||
for name in ("orch-model", "worker-model")
|
||||
},
|
||||
"trial_budget": {"timeout_seconds": 30},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def credential(agent_id, role, endpoint):
|
||||
return AgentCredential(
|
||||
agent_id=agent_id,
|
||||
role=role,
|
||||
nostr_secret_key=f"secret-{agent_id}",
|
||||
nostr_pubkey=f"pubkey-{agent_id}",
|
||||
nostr_auth_tag="[]",
|
||||
llm_endpoint=endpoint,
|
||||
llm_api_key=f"key-{agent_id}",
|
||||
)
|
||||
|
||||
|
||||
def user_credential():
|
||||
return AgentCredential(
|
||||
agent_id="user",
|
||||
role="user",
|
||||
nostr_secret_key="secret-user",
|
||||
nostr_pubkey="pubkey-user",
|
||||
nostr_auth_tag="[]",
|
||||
llm_endpoint="",
|
||||
llm_api_key="",
|
||||
)
|
||||
|
||||
|
||||
def trial_handle(credentials, user_relay_url=""):
|
||||
return TrialHandle(
|
||||
run_id="run",
|
||||
trial_id="trial",
|
||||
manifest_hash="hash",
|
||||
relay_ws_url="ws://host.docker.internal:3600",
|
||||
channel_id="channel",
|
||||
credentials=credentials,
|
||||
user=user_credential(),
|
||||
user_relay_url=user_relay_url,
|
||||
)
|
||||
|
||||
|
||||
def runtime(tmp_path, **kwargs):
|
||||
return BuzzContainerRuntime(
|
||||
logs_dir=tmp_path / "logs",
|
||||
artifact_root=tmp_path,
|
||||
endpoints={
|
||||
"orch-model": EndpointLaunchConfig("anthropic", "ANTHROPIC_API_KEY"),
|
||||
"worker-model": EndpointLaunchConfig("anthropic", "ANTHROPIC_API_KEY"),
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class Environment:
|
||||
"""Records execs/uploads; scripted stdout per command substring."""
|
||||
|
||||
def __init__(self, responses=None):
|
||||
self.commands = []
|
||||
self.uploads = []
|
||||
self.responses = responses or {}
|
||||
|
||||
async def exec(self, command, env=None, **kwargs):
|
||||
self.commands.append((command, env))
|
||||
for needle, result in self.responses.items():
|
||||
if needle in command:
|
||||
return result
|
||||
return ExecResult(stdout="", stderr="", return_code=0)
|
||||
|
||||
async def upload_file(self, source, target):
|
||||
self.uploads.append((str(source), target))
|
||||
|
||||
async def download_dir(self, source, target):
|
||||
pass
|
||||
|
||||
|
||||
def test_maps_credentials_exactly_and_rejects_role_mismatch(tmp_path):
|
||||
manifest = write_manifest(tmp_path)
|
||||
credentials = (
|
||||
credential("orch-1", "orchestrator", "orch-model"),
|
||||
credential("worker-1", "worker", "worker-model"),
|
||||
)
|
||||
assert set(runtime(tmp_path)._classes_by_agent_id(manifest, credentials)) == {
|
||||
"orch-1",
|
||||
"worker-1",
|
||||
}
|
||||
bad = (credential("worker-1", "orchestrator", "worker-model"),)
|
||||
with pytest.raises(RuntimeLaunchError, match="role"):
|
||||
runtime(tmp_path)._classes_by_agent_id(manifest, bad)
|
||||
|
||||
|
||||
def test_prompt_hash_and_identity_override_are_fail_closed(tmp_path):
|
||||
manifest = write_manifest(tmp_path)
|
||||
prompt_ref = manifest.roster[0].prompt
|
||||
runtime(tmp_path)._verify_artifact(tmp_path / prompt_ref.path, prompt_ref.sha256)
|
||||
(tmp_path / prompt_ref.path).write_text("changed", encoding="utf-8")
|
||||
with pytest.raises(RuntimeLaunchError, match="hash mismatch"):
|
||||
runtime(tmp_path)._verify_artifact(
|
||||
tmp_path / prompt_ref.path, prompt_ref.sha256
|
||||
)
|
||||
|
||||
endpoint = EndpointLaunchConfig(
|
||||
"anthropic", "ANTHROPIC_API_KEY", {"BUZZ_ACP_MCP_COMMAND": "evil"}
|
||||
)
|
||||
with pytest.raises(RuntimeLaunchError, match="identity"):
|
||||
runtime(tmp_path)._reject_identity_overrides(endpoint)
|
||||
|
||||
|
||||
def test_user_relay_url_prefers_host_view(tmp_path):
|
||||
rt = runtime(tmp_path)
|
||||
# v1.2 handles carry the host view for the trial user explicitly.
|
||||
assert (
|
||||
rt._user_relay_url(trial_handle((), user_relay_url="http://localhost:3600"))
|
||||
== "http://localhost:3600"
|
||||
)
|
||||
# pre-v1.2 handles fall back to deriving http from the agents' ws view.
|
||||
assert rt._user_relay_url(trial_handle(())) == "http://host.docker.internal:3600"
|
||||
with pytest.raises(RuntimeLaunchError, match="ws://"):
|
||||
rt._cli_relay_url("http://relay")
|
||||
|
||||
|
||||
async def test_install_stack_uploads_the_pinned_stack(tmp_path):
|
||||
binaries = {}
|
||||
for name in ("buzz-acp", "buzz-agent", "buzz-dev-mcp"):
|
||||
path = tmp_path / name
|
||||
path.write_text("#!binary")
|
||||
binaries[name] = str(path)
|
||||
rt = runtime(
|
||||
tmp_path,
|
||||
buzz_acp_binary=binaries["buzz-acp"],
|
||||
buzz_agent_binary=binaries["buzz-agent"],
|
||||
buzz_dev_mcp_binary=binaries["buzz-dev-mcp"],
|
||||
)
|
||||
environment = Environment()
|
||||
await rt._install_stack(environment)
|
||||
assert {target for _, target in environment.uploads} == {
|
||||
f"{REMOTE_BIN}/buzz-acp",
|
||||
f"{REMOTE_BIN}/buzz-agent",
|
||||
f"{REMOTE_BIN}/buzz-dev-mcp",
|
||||
}
|
||||
assert any("chmod 0755" in cmd for cmd, _ in environment.commands)
|
||||
|
||||
|
||||
async def test_install_stack_requires_binaries_on_disk(tmp_path):
|
||||
rt = runtime(tmp_path, buzz_acp_binary=str(tmp_path / "missing"))
|
||||
with pytest.raises(RuntimeLaunchError, match="binary not found"):
|
||||
await rt._install_stack(Environment())
|
||||
|
||||
|
||||
async def test_forwarder_bridges_the_canonical_relay_address(tmp_path):
|
||||
from harbor_buzz_orchestra.container_runtime import FORWARDER
|
||||
|
||||
forwarder = tmp_path / "relay-forwarder"
|
||||
forwarder.write_text("ELF")
|
||||
rt = runtime(
|
||||
tmp_path,
|
||||
relay_gateway="host.docker.internal:3600",
|
||||
forwarder_binary=str(forwarder),
|
||||
)
|
||||
trial = TrialHandle(
|
||||
run_id="run",
|
||||
trial_id="trial",
|
||||
manifest_hash="hash",
|
||||
relay_ws_url="ws://localhost:3600",
|
||||
channel_id="channel",
|
||||
credentials=(),
|
||||
user=user_credential(),
|
||||
)
|
||||
environment = Environment(
|
||||
responses={
|
||||
FORWARDER: ExecResult(stdout="99\n", stderr="", return_code=0),
|
||||
"cat ": ExecResult(
|
||||
stdout="forwarding 127.0.0.1:3600 -> host.docker.internal:3600",
|
||||
stderr="",
|
||||
return_code=0,
|
||||
),
|
||||
}
|
||||
)
|
||||
agent = await rt._start_forwarder(environment, trial)
|
||||
assert agent is not None and agent.pid == 99
|
||||
launch = next(cmd for cmd, _ in environment.commands if FORWARDER in cmd)
|
||||
# Listens on the canonical loopback (host-header bound), targets the gateway.
|
||||
assert "127.0.0.1:3600" in launch
|
||||
assert "host.docker.internal:3600" in launch
|
||||
|
||||
# No gateway configured: the relay is reachable directly, no forwarder.
|
||||
assert await runtime(tmp_path)._start_forwarder(Environment(), trial) is None
|
||||
with pytest.raises(RuntimeLaunchError, match="ws://"):
|
||||
rt._ws_authority("http://relay")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("configured", "expected"), [(None, "0"), (7, "7")])
|
||||
async def test_launch_wires_the_desktop_environment(tmp_path, configured, expected):
|
||||
manifest = write_manifest(tmp_path)
|
||||
agent_class = manifest.roster[0]
|
||||
if configured is not None:
|
||||
agent_class = agent_class.model_copy(
|
||||
update={
|
||||
"budget": agent_class.budget.model_copy(
|
||||
update={"max_calls": configured}
|
||||
)
|
||||
}
|
||||
)
|
||||
orch = credential("orch-1", "orchestrator", "orch-model")
|
||||
trial = trial_handle((orch,))
|
||||
environment = Environment(
|
||||
responses={"buzz-acp": ExecResult(stdout="4242\n", stderr="", return_code=0)}
|
||||
)
|
||||
agent = await runtime(tmp_path)._launch_agent(
|
||||
environment=environment,
|
||||
trial=trial,
|
||||
credential=orch,
|
||||
agent_class=agent_class,
|
||||
trial_dir=tmp_path,
|
||||
)
|
||||
assert agent.pid == 4242
|
||||
command, env = environment.commands[-1]
|
||||
assert f"{REMOTE_BIN}/buzz-acp" in command
|
||||
# The real product wiring: acp spawns buzz-agent, which gets buzz-dev-mcp.
|
||||
assert env["BUZZ_ACP_AGENT_COMMAND"] == f"{REMOTE_BIN}/buzz-agent"
|
||||
assert env["BUZZ_ACP_MCP_COMMAND"] == f"{REMOTE_BIN}/buzz-dev-mcp"
|
||||
assert env["BUZZ_RELAY_URL"] == trial.relay_ws_url
|
||||
assert env["BUZZ_PRIVATE_KEY"] == orch.nostr_secret_key
|
||||
assert env["NOSTR_PRIVATE_KEY"] == orch.nostr_secret_key
|
||||
assert env["BUZZ_AGENT_NO_HINTS"] == "1"
|
||||
assert env["BUZZ_AGENT_MAX_ROUNDS"] == expected
|
||||
assert env["BUZZ_ACP_SYSTEM_PROMPT_FILE"].endswith("orch-1.system-prompt.md")
|
||||
# The composed prompt was uploaded into the container.
|
||||
assert any(
|
||||
target == env["BUZZ_ACP_SYSTEM_PROMPT_FILE"]
|
||||
for _, target in environment.uploads
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_validates_construction_bounds(tmp_path):
|
||||
# 0 is legal and means unbounded (BUZZ_AGENT_MAX_ROUNDS=0); the trial
|
||||
# budget is the clock. Only negatives are rejected.
|
||||
runtime(tmp_path, max_agent_rounds=0)
|
||||
with pytest.raises(ValueError, match="unbounded"):
|
||||
runtime(tmp_path, max_agent_rounds=-1)
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
runtime(tmp_path, readiness_timeout_seconds=0)
|
||||
|
||||
|
||||
async def test_wait_for_agents_ready_requires_every_channel_subscription(tmp_path):
|
||||
rt = runtime(tmp_path, poll_seconds=0)
|
||||
logs = {"orch-1": "", "worker-1": ""}
|
||||
|
||||
class ReadyEnvironment(Environment):
|
||||
polls = 0
|
||||
|
||||
async def exec(self, command, env=None, **kwargs):
|
||||
if command.startswith("cat "):
|
||||
agent_id = re.search(r"([\w-]+)\.stdout\.log", command).group(1)
|
||||
return ExecResult(stdout=logs[agent_id], stderr="", return_code=0)
|
||||
return ExecResult(stdout="", stderr="", return_code=0)
|
||||
|
||||
from harbor_buzz_orchestra.container_runtime import _Agent
|
||||
|
||||
agents = [
|
||||
_Agent(
|
||||
credential(agent_id, "worker", "worker-model"),
|
||||
pid=1,
|
||||
stdout_log=f"{REMOTE_LOGS}/{agent_id}.stdout.log",
|
||||
stderr_log=f"{REMOTE_LOGS}/{agent_id}.stderr.log",
|
||||
)
|
||||
for agent_id in logs
|
||||
]
|
||||
logs["orch-1"] = "subscribed to channel trial-channel\n"
|
||||
logs["worker-1"] = "subscribed to channel trial-channel\n"
|
||||
await rt._wait_for_agents_ready(ReadyEnvironment(), agents, "trial-channel")
|
||||
|
||||
logs["worker-1"] = ""
|
||||
rt_timeout = runtime(tmp_path, poll_seconds=0, readiness_timeout_seconds=0.01)
|
||||
with pytest.raises(RuntimeLaunchError, match="worker-1"):
|
||||
await rt_timeout._wait_for_agents_ready(
|
||||
ReadyEnvironment(), agents, "trial-channel"
|
||||
)
|
||||
|
||||
|
||||
async def test_dead_agent_processes_fail_the_trial(tmp_path):
|
||||
from harbor_buzz_orchestra.container_runtime import _Agent
|
||||
|
||||
agents = [_Agent(credential("worker-1", "worker", "worker-model"), 7, "o", "e")]
|
||||
environment = Environment(
|
||||
responses={
|
||||
"kill -0": ExecResult(stdout="DEAD:worker-1\n", stderr="", return_code=0)
|
||||
}
|
||||
)
|
||||
with pytest.raises(RuntimeLaunchError, match="worker-1"):
|
||||
await runtime(tmp_path)._raise_for_dead_agents(environment, agents)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "return_code", "raises"),
|
||||
[
|
||||
("M1-hello-world", 0, False),
|
||||
("M1-hello-world", 1, True),
|
||||
("other", 1, False),
|
||||
],
|
||||
)
|
||||
async def test_m1_output_probe_matches_grader_and_is_condition_scoped(
|
||||
tmp_path, condition, return_code, raises
|
||||
):
|
||||
manifest = write_manifest(tmp_path).model_copy(update={"condition": condition})
|
||||
environment = Environment(
|
||||
responses={
|
||||
"hello.txt": ExecResult(stdout="", stderr="", return_code=return_code)
|
||||
}
|
||||
)
|
||||
if raises:
|
||||
with pytest.raises(RuntimeLaunchError, match="/app/hello.txt"):
|
||||
await runtime(tmp_path)._verify_m1_output(environment, manifest)
|
||||
else:
|
||||
await runtime(tmp_path)._verify_m1_output(environment, manifest)
|
||||
probed = [cmd for cmd, _ in environment.commands if "hello.txt" in cmd]
|
||||
assert bool(probed) == (condition == "M1-hello-world")
|
||||
|
||||
|
||||
async def test_send_mentions_by_pubkey_so_task_text_stays_inert(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Task text is untrusted payload: `:%normal! @a` in a task statement must
|
||||
not be fed to member-name resolution (it would fail and kill the trial).
|
||||
An explicit --mention pins delivery to the orchestrator's pubkey."""
|
||||
rt = runtime(tmp_path)
|
||||
orch = credential("orch-1", "orchestrator", "orch-model")
|
||||
trial = trial_handle((orch,))
|
||||
calls = []
|
||||
|
||||
async def buzz_json(credential, trial, *args):
|
||||
calls.append(args)
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(rt, "_buzz_json", buzz_json)
|
||||
|
||||
await rt._send(
|
||||
trial.user,
|
||||
trial,
|
||||
"@orch-1 run `:%normal! @a` on the file",
|
||||
mention=orch.nostr_pubkey,
|
||||
)
|
||||
assert calls[-1][-2:] == ("--mention", "pubkey-orch-1")
|
||||
|
||||
# Without an explicit mention the send is unchanged (name resolution).
|
||||
await rt._send(trial.user, trial, "plain content")
|
||||
assert "--mention" not in calls[-1]
|
||||
assert calls[-1][-2:] == ("--content", "plain content")
|
||||
|
||||
|
||||
async def test_wait_for_done_requires_orchestrator_authorship(tmp_path, monkeypatch):
|
||||
rt = runtime(tmp_path, poll_seconds=0)
|
||||
orch = credential("orch-1", "orchestrator", "orch-model")
|
||||
trial = trial_handle((orch,))
|
||||
rounds = iter(
|
||||
[
|
||||
[{"id": "1", "pubkey": "someone-else", "content": "DONE: fake"}],
|
||||
[{"id": "2", "pubkey": orch.nostr_pubkey, "content": "DONE: real"}],
|
||||
]
|
||||
)
|
||||
observers = []
|
||||
|
||||
async def buzz_json(credential, *args, **kwargs):
|
||||
observers.append(credential.agent_id)
|
||||
return next(rounds)
|
||||
|
||||
monkeypatch.setattr(rt, "_buzz_json", buzz_json)
|
||||
result = await rt._wait_for_done(Environment(), orch, trial, [])
|
||||
assert json.dumps(result).find("real") > 0
|
||||
# observation happens as the trial user, never as an agent identity
|
||||
assert set(observers) == {"user"}
|
||||
|
||||
|
||||
def test_composed_system_prompt_carries_persona_and_team_roster(tmp_path):
|
||||
rt = runtime(tmp_path)
|
||||
orch = credential("orch-1", "orchestrator", "orch-model")
|
||||
worker_1 = credential("worker-1", "worker", "worker-model")
|
||||
worker_2 = credential("worker-2", "worker", "worker-model")
|
||||
trial = trial_handle((orch, worker_1, worker_2))
|
||||
persona = tmp_path / "persona.md"
|
||||
persona.write_text("# Persona body\n", encoding="utf-8")
|
||||
|
||||
path = rt._compose_system_prompt(
|
||||
trial_dir=tmp_path,
|
||||
trial=trial,
|
||||
credential=orch,
|
||||
persona_path=persona,
|
||||
)
|
||||
|
||||
composed = path.read_text(encoding="utf-8")
|
||||
assert composed.startswith("# Persona body\n")
|
||||
assert "You are `orch-1` (pubkey `pubkey-orch-1`)" in composed
|
||||
assert f"channel `{trial.channel_id}`" in composed
|
||||
assert "user `user` (pubkey `pubkey-user`)" in composed
|
||||
# roster lists teammates, never the agent itself
|
||||
assert "| worker-1 | worker | `pubkey-worker-1` |" in composed
|
||||
assert "| worker-2 | worker | `pubkey-worker-2` |" in composed
|
||||
assert "| orch-1 " not in composed
|
||||
assert path.stat().st_mode & 0o777 == 0o600
|
||||
|
||||
|
||||
async def test_stop_agents_sweeps_the_uploaded_stack(tmp_path):
|
||||
from harbor_buzz_orchestra.container_runtime import _Agent
|
||||
|
||||
environment = Environment()
|
||||
agents = [_Agent(credential("orch-1", "orchestrator", "orch-model"), 1, "o", "e")]
|
||||
await BuzzContainerRuntime._stop_agents(environment, agents)
|
||||
sweeps = [cmd for cmd, _ in environment.commands if REMOTE_BIN in cmd]
|
||||
assert len(sweeps) == 2
|
||||
assert "kill -TERM" in sweeps[0] and "kill -KILL" in sweeps[1]
|
||||
@@ -0,0 +1,49 @@
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from harbor_buzz_orchestra import ExperimentManifest, ManifestError
|
||||
|
||||
|
||||
def test_hash_is_independent_of_mapping_and_yaml_key_order(tmp_path, manifest_data):
|
||||
first = ExperimentManifest.load(manifest_data)
|
||||
path = tmp_path / "manifest.yaml"
|
||||
path.write_text(
|
||||
yaml.safe_dump(
|
||||
dict(reversed(list(copy.deepcopy(manifest_data).items()))), sort_keys=False
|
||||
)
|
||||
)
|
||||
second = ExperimentManifest.load(path)
|
||||
assert first.canonical_bytes() == second.canonical_bytes()
|
||||
assert first.sha256 == second.sha256
|
||||
assert len(first.sha256) == 64
|
||||
|
||||
|
||||
def test_hash_changes_when_staffing_changes(manifest_data):
|
||||
first = ExperimentManifest.load(manifest_data)
|
||||
changed = copy.deepcopy(manifest_data)
|
||||
changed["roster"][1]["count"] = 3
|
||||
assert ExperimentManifest.load(changed).sha256 != first.sha256
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mutation", "match"),
|
||||
[
|
||||
(lambda data: data.update({"unknown": True}), "Extra inputs"),
|
||||
(lambda data: data["roster"].pop(0), "exactly one orchestrator"),
|
||||
(lambda data: data["prices"].pop("databricks/qwen"), "prices missing"),
|
||||
(lambda data: data["roster"][1].update({"concurrency": 5}), "concurrency"),
|
||||
],
|
||||
)
|
||||
def test_invalid_manifest_is_rejected(manifest_data, mutation, match):
|
||||
mutation(manifest_data)
|
||||
with pytest.raises(ManifestError, match=match):
|
||||
ExperimentManifest.load(manifest_data)
|
||||
|
||||
|
||||
def test_non_mapping_document_is_rejected(tmp_path):
|
||||
path = tmp_path / "manifest.yaml"
|
||||
path.write_text("- not\n- a\n- mapping\n")
|
||||
with pytest.raises(ManifestError, match="root must be a mapping"):
|
||||
ExperimentManifest.load(path)
|
||||
@@ -0,0 +1,133 @@
|
||||
"""The leaderboard runner must emit only leaderboard-legal harbor settings."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
_SCRIPT = Path(__file__).parent.parent / "scripts" / "run_leaderboard.py"
|
||||
_spec = importlib.util.spec_from_file_location("run_leaderboard", _SCRIPT)
|
||||
run_leaderboard = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["run_leaderboard"] = run_leaderboard
|
||||
_spec.loader.exec_module(run_leaderboard)
|
||||
|
||||
FORBIDDEN_FLAGS = (
|
||||
"--timeout-multiplier",
|
||||
"--agent-timeout-multiplier",
|
||||
"--verifier-timeout-multiplier",
|
||||
"--agent-setup-timeout-multiplier",
|
||||
"--environment-build-timeout-multiplier",
|
||||
"--override-cpus",
|
||||
"--override-memory",
|
||||
"--override-storage",
|
||||
"--override-gpus",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def binaries(tmp_path):
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
found = {}
|
||||
for name in run_leaderboard.BINARIES:
|
||||
path = bin_dir / name
|
||||
path.write_text("#!/bin/sh\n")
|
||||
found[name] = path
|
||||
return found
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_binaries(tmp_path):
|
||||
bin_dir = tmp_path / "linux-bin"
|
||||
bin_dir.mkdir()
|
||||
for name in run_leaderboard.AGENT_BINARIES:
|
||||
(bin_dir / name).write_text("ELF")
|
||||
return run_leaderboard.find_agent_binaries(bin_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def args(tmp_path, binaries, agent_binaries):
|
||||
manifest = tmp_path / "team.yaml"
|
||||
manifest.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"condition": "unit-test",
|
||||
"roster": [
|
||||
{"id": "orch", "endpoint": "frontier"},
|
||||
{"id": "worker", "endpoint": "fast", "count": 2},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
endpoints = tmp_path / "endpoints.json"
|
||||
endpoints.write_text(json.dumps({"frontier": {"provider": "anthropic"}}))
|
||||
provisioner = tmp_path / "provisioner.json"
|
||||
provisioner.write_text("{}")
|
||||
return run_leaderboard.parse_args(
|
||||
[
|
||||
"--dataset",
|
||||
"terminal-bench/terminal-bench-2-1",
|
||||
"--attempts",
|
||||
"5",
|
||||
"--manifest",
|
||||
str(manifest),
|
||||
"--endpoint-config",
|
||||
str(endpoints),
|
||||
"--provisioner-config",
|
||||
str(provisioner),
|
||||
"--agent-bin-dir",
|
||||
str(next(iter(agent_binaries.values())).parent),
|
||||
"--job-name",
|
||||
"unit-test-job",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_command_uses_standard_settings_only(args, binaries, agent_binaries):
|
||||
command = run_leaderboard.build_command(args, binaries, agent_binaries)
|
||||
assert command[:2] == ["harbor", "run"]
|
||||
assert command.count("-k") == 1
|
||||
assert command[command.index("-k") + 1] == "5"
|
||||
for flag in FORBIDDEN_FLAGS:
|
||||
assert flag not in command
|
||||
# The full production stack rides in as agent kwargs.
|
||||
kwargs = [command[i + 1] for i, p in enumerate(command) if p == "--agent-kwarg"]
|
||||
assert any(k.startswith("buzz_acp_binary=") for k in kwargs)
|
||||
assert any(k.startswith("buzz_agent_binary=") for k in kwargs)
|
||||
assert any(k.startswith("buzz_dev_mcp_binary=") for k in kwargs)
|
||||
|
||||
|
||||
def test_agent_binaries_must_exist(tmp_path):
|
||||
with pytest.raises(SystemExit, match="buzz-dev-mcp"):
|
||||
run_leaderboard.find_agent_binaries(tmp_path)
|
||||
|
||||
|
||||
def test_forbidden_flags_are_not_accepted(tmp_path):
|
||||
for flag in FORBIDDEN_FLAGS:
|
||||
with pytest.raises(SystemExit):
|
||||
run_leaderboard.parse_args(
|
||||
[
|
||||
"--dataset",
|
||||
"d",
|
||||
"--attempts",
|
||||
"5",
|
||||
"--agent-bin-dir",
|
||||
str(tmp_path),
|
||||
flag,
|
||||
"1",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_metadata_template_matches_harbor_schema(args, tmp_path):
|
||||
from harbor.leaderboard.metadata import load_metadata
|
||||
|
||||
path = run_leaderboard.write_metadata_template(args, tmp_path)
|
||||
loaded = load_metadata(path)
|
||||
assert loaded["agent_org_display_name"] == "Block"
|
||||
assert [m["model_name"] for m in loaded["models"]] == ["frontier", "fast"]
|
||||
assert loaded["models"][0]["model_org_display_name"] == "Anthropic"
|
||||
assert loaded["models"][1]["model_provider"] == "FILL_ME"
|
||||
Generated
+2496
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user