Build, check and submit a harness
AGENTS.md in your harness repository, then give your coding agent
this prompt: "Read AGENTS.md, then build, check and commit my harness with its dev-set artifacts."
You are helping a participant enter the harness competition.
Each entry is an agent harness that runs as a WebSocket server (serve.py) and solves held-out engineering tasks over the competition protocol. Participants pair their harness with a Google-sponsored model, a commercial model on their own API key (OpenAI, Anthropic, or Gemini), or a self-hosted model at an OpenAI-compatible endpoint. The leaderboard ranks each harness + model pair together.
Your goal is to build an entry the evaluation server can run, verify it locally with the kit, and run it on the dev set to generate artifacts/. The participant submits the harness and its dev-set artifacts on the portal.
Follow these steps in order and do not skip any checks:
- Start from one of the sample submissions (Hermes Agent or Pi on Claude Opus 5.5, the Antigravity CLI on Gemini 3.8 Flash, or a custom harness on an OpenAI key or vLLM server) under
sample_submissions/in the competition repository that the organizers shared with the participant, or from the participant's existing repository. - Write the harness and
submission.yaml(Section 2) so they satisfy Sections 3 through 7. - Verify the entry locally with
python -m kit check(Section 8), then run the dev set witheval/run.pyto writeartifacts/(Section 8). Fix any issues until every check passes. - Put any required API keys in
.env(Section 6). Stage and commit everything (git add -A) so the harness andartifacts/share one commit, then push to GitHub. - Tell the participant the entry is ready to submit (Section 9). Each participant has 3 scored submissions in total, one in the queue or under evaluation at a time.
1. The entry
An entry is a single commit in a GitHub repository with this layout at its root:
submission.yaml how to install and start the harness, and which model it calls
serve.py the harness: a WebSocket server (any language works; the samples use Python)
.env API keys, if the model or harness needs any (private repository only)
artifacts/ what this harness produced on the dev set, written by eval/run.py (Section 8)
... anything else it needs: requirements.txt, prompts, or a vendored agent
When you submit, the evaluation server clones your commit, runs install inside a build container, and packages /opt/harness as the runtime bundle. For each task, it starts your serve command from that bundle in an isolated container alongside the task container and connects over WebSocket. Your harness calls its model and interacts with the task environment exclusively through the competition protocol (Section 7) to run commands, read and write files, and call MCP tools inside the task container.
Requirements:
- Use a public repository, or a private repository owned by the participant with the competition's GitHub App installed (granting read-only access to selected repositories). Any repository containing a
.envfile must be private. Private git submodules are not supported, and submodules are fetched over https only. - The submit form reads
submission.yamlat the submitted commit, falling back torecipe.yamlifsubmission.yamlis absent. Arecipe.yamlwritten in thesubmission.yamlformat is read and reviewed likesubmission.yaml; an earlier recipe format is refused for participant submissions because it does not include dev-set artifacts. - The submit form inspects
artifacts/at the submitted commit and refuses commits without usable artifacts (Section 8). The build step excludesartifacts/from/opt/harness, so your running harness never sees it. - The serve container can reach only its model endpoint. No other host is reachable, so web search and outside APIs are unavailable. The task container has no network access at all.
- Every task's prompt and files (including hidden test tasks) are sent to the model endpoint your harness calls. Make sure the participant is comfortable with their endpoint's logging policy, and never retain copies of task content.
2. submission.yaml
name: my-harness # leaderboard entry name: 2-39 chars of a-z, 0-9, and -
model_api: google-cloud/claude-opus-5-5 # the model your harness calls (Section 3)
install: >- # optional; runs once at build time as root with internet access
uv venv --python 3.12 /opt/harness/venv
&& uv pip install --python /opt/harness/venv/bin/python -r {src}/requirements.txt
serve: /opt/harness/venv/bin/python {src}/serve.py # required; starts your server on $PORT
env: # optional; non-secret settings as quoted strings
LOG_LEVEL: "info"
| Field | Rules |
|---|---|
name |
2 to 39 characters (a-z, 0-9, -), starting with a letter or digit |
model_api |
google-cloud/<id>, openai/<model>, anthropic/<model>, gemini/<model>, or openai-compatible/<model> paired with model_base_url (Section 3) |
model_base_url |
Required with openai-compatible/ (and forbidden otherwise): an https:// base URL on a public DNS hostname, typically ending in /v1 |
install |
Shell command; {src} expands to the repository checkout at /opt/harness/src |
serve |
Shell command; {src} and {port} are expanded (the port is also in $PORT); must have balanced shell quotes |
env |
NAME: "value" pairs, quoted as strings (unquoted true, 1.10, or 0755 get parsed as booleans/numbers by YAML). Never put secrets here; put keys in .env (Section 6) |
Environment variables managed by the evaluator cannot be set in env (or in .env, other than your model API key): PORT, HOME, USER, proxy variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY, NODE_USE_ENV_PROXY), and the model variables listed in Section 3.
Any {...} placeholders other than {src} and {port} are passed through untouched to the shell. Unknown top-level keys are rejected, and submission.yaml must be at most 16 KiB. Do not include repository or commit fields; the portal records those automatically.
3. The model
model_api declares which model your harness uses and how it is billed:
model_api |
Served by | Required in .env |
Host the serve container reaches |
|---|---|---|---|
google-cloud/<id> |
Google-sponsored models | Nothing (evaluator injects a per-submission key) | Sponsored model endpoint |
openai/<model> |
OpenAI (participant's account) | OPENAI_API_KEY=... |
api.openai.com |
anthropic/<model> |
Anthropic (participant's account) | ANTHROPIC_API_KEY=... |
api.anthropic.com |
gemini/<model> |
Google AI Studio (participant's account) | GEMINI_API_KEY=... |
generativelanguage.googleapis.com |
openai-compatible/<model> + model_base_url |
Self-hosted server (vLLM, SGLang, etc.) | MODEL_API_KEY=... |
Host of model_base_url |
Available Google-sponsored models (all support tool calling):
model_api |
Model |
|---|---|
google-cloud/gemini-4-argon |
Gemini 4 Argon |
google-cloud/gemini-3.8-flash |
Gemini 3.8 Flash |
google-cloud/gemini-3.1-pro-preview |
Gemini 3.1 Pro (preview) |
google-cloud/claude-opus-5-5 |
Claude Opus 5.5 |
google-cloud/gpt-oss-120b |
gpt-oss 120B |
google-cloud/qwen3-coder-480b |
Qwen3 Coder 480B A35B |
google-cloud/qwen3-next-80b |
Qwen3-Next 80B A3B Instruct |
google-cloud/deepseek-v3.2 |
DeepSeek-V3.2 |
google-cloud/kimi-k2-thinking |
Kimi K2 Thinking |
google-cloud/minimax-m2 |
MiniMax M2 |
google-cloud/glm-5.2 |
GLM 5.2 |
google-cloud/gemma-4-26b |
Gemma 4 26B A4B |
google-cloud/llama-4-maverick |
Llama 4 Maverick 17B 128E |
At runtime, the serve container receives the same set of environment variables regardless of provider:
| Variable | Value |
|---|---|
PORT |
Port your WebSocket server must listen on |
MODEL_API |
Full model_api string from submission.yaml |
MODEL_NAME |
Bare model identifier to send in API requests (e.g. claude-opus-5-5) |
MODEL_BASE_URL, MODEL_API_KEY |
OpenAI-compatible base URL (usually ending in /v1) and API key, populated for every model |
OPENAI_BASE_URL, OPENAI_API_KEY |
Identical to MODEL_BASE_URL and MODEL_API_KEY, so the standard OpenAI SDK works with zero configuration |
ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY |
Populated for Claude models: Anthropic Messages API base URL (no /v1) and key |
GOOGLE_GEMINI_BASE_URL, GEMINI_API_KEY |
Populated for Gemini models: Gemini API base URL (no /v1) and key |
env and .env entries |
Passed through as written |
If you use openai-compatible/, your endpoint must accept Authorization: Bearer <MODEL_API_KEY>, support tools and tool_calls on POST /v1/chat/completions (for vLLM, pass --enable-auto-tool-choice and the appropriate --tool-call-parser), handle both streaming and non-streaming requests, and serve concurrent tasks. The egress proxy blocks hostnames that resolve to private IP ranges.
Model call tips:
- Stream long responses or set generous timeouts, since extended reasoning turns can stay quiet for several minutes.
- Retry 429 rate limits, network timeouts, and 5xx errors with exponential backoff. Do not retry 400 errors.
- Google-sponsored models default to their highest reasoning level (
maxfor Claude and GLM;highfor Gemini, gpt-oss, Kimi, and MiniMax) on both the evaluation server andkit/model_proxy. Passreasoning_effortexplicitly to use a lower level. - Claude Opus 5.5 rejects
temperature,top_p,top_k, andthinkingblocks withbudget_tokens, so omit them. Set a generousmax_tokens(32,000 is a good default). - Claude caches a prompt prefix only where the request marks it: put
"cache_control": {"type": "ephemeral"}on the system prompt and on the newest message (Claude allows 4 such marks). Without the marks every turn pays the full input price for the whole conversation, which is many times the cost; theminimal,plan-verifyandnode-protocolsamples show how. Gemini, OpenAI and vLLM cache on their own. - Gemini 4 Argon accepts a thinking level (
low,medium, orhighviareasoning_effort) and returns HTTP 400 if given athinkingBudget. Keepmax_tokensat or below 65,536. - Track elapsed time against
limits.agent_timeout_secand wrap up model calls before the deadline.
4. The build container (install)
- Runs once in a clean
debian:bookwormcontainer asroot, with internet access and no task data, subject to a 30-minute timeout. - Your repository is checked out at
/opt/harness/src(the initial working directory).$HOMEis a temporary scratch directory. - Preinstalled tools include
uv(/opt/harness/bin/uv), Node 22 (/opt/harness/node, wherenpm -ginstalls packages),git,curl, standard build tools, andpython3. - Only
/opt/harnessis archived into the runtime bundle (up to 2 GiB compressed / 8 GiB unpacked), and all symlinks must resolve inside/opt/harness. System packages installed outside/opt/harness(e.g., viaapt-get) are discarded after the build. Your git repository (including full history) must be at most 1 GiB: every committed version ofartifacts/stays in git history, so keep artifacts compact or squash history if it grows. - Pin dependency versions for reproducible builds. The serve container uses the same base image, so binaries built during
installrun identically at evaluation time.
5. The serve container (serve)
- Each task starts a fresh container from the base image with your
/opt/harnessbundle mounted read-only. Yourservecommand runs in/opt/harness/srcas uid 1000 with writable/tmpand$HOMEtmpfs mounts (1 GB each), 2 CPUs, 4 GB RAM, and a 512-process limit. - Listen on
0.0.0.0:$PORTwithin 120 seconds. If your server fails to accept a WebSocket connection in time, the task fails and counts against your score. Each container serves one WebSocket connection for one task. - Route all outbound traffic through
HTTPS_PROXY/HTTP_PROXY/ALL_PROXY(NODE_USE_ENV_PROXY=1is pre-set for Node.js). Standard Python (requests,httpx), OpenAI, Anthropic, and Google SDKs use these environment variables automatically. Direct DNS resolution is disabled; the proxy resolves hostnames for you. Runtime package downloads are blocked. - Standard output and standard error are captured as the serve log.
6. Keys: .env
Place all API keys in a .env file at the root of your repository, with one NAME=value per line:
OPENAI_API_KEY=sk-...
- Include the model API key Section 3 requires (none for
google-cloud/). Variables are injected into the serve container's environment at runtime; the.envfile itself is never copied into the bundle. - Commit
.envto a private repository with the competition's GitHub App installed. Public repositories containing a.envfile are rejected (if you ever push a key to a public repo, revoke it immediately). - If
.envis gitignored, force-add it withgit add -f .env. - Keep each value on a single line without variable interpolation (
${...}). Evaluator-reserved variable names (Section 2) are rejected except for the required model key. Limits: at most 32 variables and 16 KiB.
7. The protocol
Each task communicates over a single WebSocket connection using UTF-8 JSON text frames (up to 16 MiB per frame, one JSON object per frame). The evaluator sends the opening message:
{"type": "task", "protocol": 1, "session": "3f2c9a1e0b7d4c55", "instruction": "<the task, verbatim>",
"workdir": "/app",
"limits": {"agent_timeout_sec": 21600, "exec_timeout_sec": 600, "max_output_bytes": 100000,
"max_write_bytes": 10000000},
"mcp_servers": [{"name": "apps", "url": "http://127.0.0.1:8000/mcp/", "transport": "streamable-http"}]}
Your server then sends action requests, each with a unique id of your choice (up to 8 run concurrently and the rest queue), and receives a matching {"type": "result", "id": ...} response:
| Request | Result |
|---|---|
{"type": "exec", "id": "1", "command": "ls -la", "timeout_sec": 120} |
exit_code, output (combined stdout/stderr; head and tail preserved past 100 KB), truncated, timed_out |
{"type": "read_file", "id": "2", "path": "/app/data.csv"} |
ok, content_b64, size, truncated (capped at 10 MB) |
{"type": "write_file", "id": "3", "path": "/app/out/report.md", "content_b64": "...", "append": false} |
ok, size; creates parent directories automatically; up to 10 MB per call |
{"type": "http", "id": "4", "method": "POST", "url": "<an mcp_servers URL>", "headers": {}, "body_b64": "..."} |
ok, status, headers, body_b64, truncated; bodies up to 10 MB |
Protocol details:
execrunsbash -cinside the task container starting inworkdiras the task user. Only filesystem state persists across calls (working directory changes viacdand shell variables do not). Commands time out atmin(timeout_sec, exec_timeout_sec, remaining_task_time)(defaulttimeout_sec: 120), returningtimed_out: trueandexit_code: 124.- File paths may be absolute or relative to
workdir, and file contents are base64-encoded in both directions. To write files larger than 10 MB, split them across multiplewrite_filecalls withappend: true. httpreaches only the task's declaredmcp_servers(if any) inside the task container. These servers use Streamable HTTP MCP;tools/listresponses on some tasks include hundreds of tools and exceed 500 KB.- Failed operations return
ok: falsewith anerrormessage. Malformed frames also return an error without aborting the task, but more than 20 malformed frames end the session.
One-way messages from your harness (no reply):
{"type": "log", "text": "..."}: appends a line to the trial log.{"type": "usage", "model": "...", "input_tokens": N, "output_tokens": N, "cache_read_tokens": N}: reports token usage for a model call.{"type": "done", "final_message": "..."}: finishes the task cleanly.{"type": "error", "message": "..."}: aborts the task early (any files already written in the container are still graded).
When the task time limit expires, the evaluator sends {"type": "cancel", "reason": "timeout"} and closes the WebSocket 10 seconds later. On receiving cancel, stop in-flight model calls immediately and send done with your best summary; both final_message and any files on disk still count. If the socket closes before done or error (or if your server never listens), the task fails and counts against your harness.
Failures on the evaluator's side are retried, up to three attempts per task; a task still without a result counts 0 in that test run, and a test run in which no task could be graded runs again, keeping the runs already scored. For Google-sponsored models, endpoint outages, timeouts, and rate limits are evaluator faults; for self-hosted or BYO-key models, endpoint errors and outages count against your entry. For any model, an invalid request from your harness (such as a 400 error) counts against your entry. A harness that exits with an error before its model produces any output is treated as not started and run again, up to three attempts in all; if it never starts, the task counts 0.
See sample_submissions/taskbridge.py in the competition repository for a ready-to-use implementation (WebSocket server, per-task session handling, cancellation, and a local subprocess relay), or docs/protocol.md for the full specification.
8. Local checks
How tasks are graded:
- Graders inspect the exact deliverable file paths, names, and formats requested in the task instruction. Deliverables account for the vast majority of every score.
- Rubric judges also read
done.final_messagealongside the deliverable files. Always finish with a concise summary of what you produced and where each file is saved.
High-scoring harnesses read the full instruction carefully before acting, inspect the starting files in workdir, work in small verified steps, write an initial deliverable early and refine it, and budget time so they always send done before agent_timeout_sec.
The local kit in the competition repository runs the same checks as the evaluation server. You need uv and a local Docker daemon (remote DOCKER_HOST endpoints are rejected because the kit bind-mounts local directories). Containers run as linux/amd64 (emulated on Apple Silicon). Running the dev set also requires Harbor (uv tool install harbor==0.20.0 --with websockets). Keep your entry in its own git repository beside the competition repository; inside the clone, kit check warns and never reports it ready to submit. From the competition repository root:
cp -r sample_submissions/pi-opus ../my-harness && git -C ../my-harness init # a starting point of its own
uv run python -m kit check ../my-harness [--cancel-test] [--rebuild]
uv run python eval/run.py ../my-harness # run and score the dev set, writing ../my-harness/artifacts/
If your harness files and install command have not changed since the last build, the cached bundle is reused automatically (pass --rebuild to force a fresh build). If the LLM judge fails during a dev-set run, eval/run.py reports the error and exits non-zero; after fixing your judge setup, re-run grading without repeating the trials via uv run python eval/run.py --regrade runs/<name>-<time>.
kit check stage |
What it verifies |
|---|---|
submission.yaml |
Validates all schema and field rules from Section 2 |
files |
Confirms files referenced by serve are committed, scans for leaked API keys outside .env, and warns on uncommitted changes |
.env |
Validates .env syntax (Section 6) and checks that the required model API key is present |
build |
Runs install in the builder container and packs /opt/harness into a valid bundle |
model |
Sends a one-line test prompt from inside the serve container through the egress proxy to verify model connectivity |
listen |
Confirms serve binds and accepts a WebSocket connection on $PORT within 120 seconds |
smoke |
Runs an end-to-end smoke task where the harness writes /app/hello.txt and sends done |
cancel |
(With --cancel-test) Cancels a running task after 45 seconds and verifies the harness cleanly sends done |
artifacts |
Checks that artifacts/ passes all server validation rules and is committed (warns if not generated yet) |
Detailed logs are written to ~/.cache/plebench/<name>/: build.log, serve.log, egress.log (allowed and blocked outbound connections), relay.log (protocol requests and responses), and check.json. To test a google-cloud/ model locally, start the local Vertex AI proxy in your own Google Cloud project with kit/model_proxy/start.sh (VERTEXAI_PROJECT, MODEL_PROXY_URL, MODEL_PROXY_KEY). Additional kit subcommands: python -m kit validate, build, up, and models.
Dev-set artifacts
Every submission must include an artifacts/ directory in the same commit as the harness, holding the outputs your harness produced on the dev set. Running eval/run.py <entry> generates <entry>/artifacts/ automatically (one subfolder per dev task with files/<container path>, final_message.md, and a top-level manifest.json recording the run and harness state). Never create or edit artifacts/ by hand.
uv run python eval/run.py ../my-harness # runs the dev set and writes ../my-harness/artifacts/
uv run python eval/run.py ../my-harness --tasks <id> # reruns a single task, keeping the other task folders
uv run python -m kit artifacts ../my-harness # validates artifacts/ against the evaluation server's rules
uv run python eval/run.py --artifacts ../my-harness # grades committed artifacts as the server does; only the judge is called
git -C ../my-harness add -A && git -C ../my-harness commit -m "dev artifacts" # commit everything together
When you submit, the evaluation server runs four steps in order: it builds the harness and runs the smoke task; grades the committed artifacts with the dev-set graders to compute the dev score (any task without usable artifacts counts 0); runs the harness live on a few randomly sampled dev tasks to verify that its scores reproduce (entries scoring well below their committed artifacts are flagged for organizer review); and runs the harness on the hidden test set, whose score ranks the leaderboard. Your harness runs three times on the hidden test set, all at once, and your test score is the mean of the three runs. A submission that fails the build or smoke step stops there and is not graded.
At submit time, the submit form refuses any commit that:
- Lacks an
artifacts/folder, - Substitutes an archive (
artifacts.zip,.tar.gz, etc.) instead of an unpacked directory, - Tracks artifact files with Git LFS,
- Exceeds 512 MiB or 10,000 files in
artifacts/, or - Submits a folder in which no dev task has usable artifacts.
Individual dev tasks with a missing folder, a symlink, a file over 95 MiB, or no gradable deliverables score 0. Whenever you modify your harness, rerun the dev set and commit everything together (git add -A): manifest.json hashes all harness files, and the server checks that your submitted code matches the harness that generated artifacts/ (a change to README, LICENSE or CHANGELOG files alone gives a note instead of a warning). See docs/artifacts.md in the competition repository for the full rules.
Before submitting, confirm that every kit check stage passes, eval/run.py completed with deliverables in the expected paths, and kit artifacts accepts your committed artifacts/ directory.
9. Submit and track
- Commit and push all changes to GitHub. Record the commit SHA via
git rev-parse HEAD. - Before their first submission, the participant signs in to the portal with GitHub and registers there. The participant opens the portal's Submit page (
/competition/submit), selects the repository and branch, reviews the checks, and submits. - The submission page tracks progress through
queued,validating(commit and artifact structure checks),building,smoke,running(the dev artifacts are graded, then the reproduction, then the test set),grading, and finallyscoredorfailed. It also shows the dev score and reproduction verdict (pending,consistent,flagged,unmeasured, ornot_run). If a submission fails, inspect the failure reason and build/smoke logs, fix the issue, push a new commit, and resubmit. After a failure on the evaluator's side (failed, reasonerror) with test runs already in (the submission page says they are kept), do not resubmit: an organizer re-queues it, and only the missing runs run.
A submission reserves a slot as soon as it is queued. If it fails before reaching running (invalid, build_failed, or smoke_failed) or fails due to an evaluator error, the slot is returned. A participant can have one submission in the queue or under evaluation at a time, and can submit the next one once it is scored or failed. An evaluation takes about a day once it starts, and up to about three days when a harness uses most of the 6-hour limit on many tasks; time in the queue comes on top.
If the entry uses the participant's own API key or model server, the participant pays for every model call the evaluation server makes: the smoke task, the reproduction check on a few dev tasks, and the hidden test set three times, including any task or run repeated after a fault on the evaluator's side. The key's rate limits or the server must sustain about 36 tasks at once; 429s and timeouts from that key or server count against your entry.
10. Troubleshooting
| Symptom | Fix |
|---|---|
| refused: has no submission.yaml at its root | Commit submission.yaml at the root of the repository, push, and verify the branch/SHA |
| refused: .env ... | Fix the .env issue reported (Section 6); remember that any repo with .env must be private |
| refused: no artifacts/ folder | Run eval/run.py on your entry, then stage and commit everything (git add -A) |
| refused: an archive or Git LFS tracks | Commit artifacts/ as a normal unpacked directory without Git LFS (git lfs untrack) |
| refused: no dev task in artifacts/ has usable artifacts | Run python -m kit artifacts to see what failed in each task folder; fix or rerun the dev set |
| A dev task counts 0: missing or refused | Rerun that task with eval/run.py --tasks <id> and verify with python -m kit artifacts |
| the harness changed after these artifacts were made | Rerun the dev set with your updated harness, then commit everything together (git add -A) |
| Build failed: could not clone | Verify the repo URL and pushed commit SHA; for private repos, ensure the GitHub App is installed on the participant's account |
| Build failed otherwise | Inspect the build log; run python -m kit check locally to reproduce |
| Smoke failed: the server never listened | Bind to 0.0.0.0:$PORT (not 127.0.0.1) within 120 s; install dependencies under /opt/harness and invoke binaries by full path |
| Smoke failed: connection refused or 403 from a host | The serve container reaches only your model's endpoint: remove calls to other hosts, and make sure your HTTP client uses HTTPS_PROXY |
| Smoke failed: 401 from the model | Check that your API key in .env is valid and uses the exact variable name from Section 3 |
socket closed, harness:AgentTimeoutError |
Send done before agent_timeout_sec expires; stream long model calls and enforce a step time budget |
| Low score despite completing the run | Double-check exact deliverable paths and filenames in the task prompt, and include a clear summary in done.final_message |
11. Fair play
Do not hard-code task solutions or write logic that fingerprints specific tasks. Committed artifacts/ must be the unaltered output of your submitted harness, without precomputed answers to dev or test tasks. Do not bypass network restrictions, inspect grading scripts, or tamper with the evaluation container. Your declared model_api must match the model your harness calls, and you must not retain or share task contents. Organizers audit top-ranking code and artifacts before publishing final results.