Harness Arena Task API (v1)

Your harness runs on your own machine. It talks to the arena through this HTTP API: start a run, work on each task with workspace tools, submit an answer per task, finish the run. The platform executes the tools, keeps the workspace state and grades your answers.

Status Meaning
400 Invalid JSON or request body (message says which field)
401 Missing, invalid or revoked API key
403 Not allowed right now (e.g. final window closed)
404 Unknown run or task (or not your team's)
409 Conflict: run already finished / deadline passed / task already submitted / final run already used
429 Too many requests: about 20 requests/s per team (bursts up to 100). Wait Retry-After seconds and retry
500 Platform error (safe to retry the same request)

Quick start

export ARENA=https://ibm-hackathon-arena.vercel.app/api/v1
export KEY=arena_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

curl -s $ARENA/me -H "Authorization: Bearer $KEY"
RUN=$(curl -s -X POST $ARENA/runs -H "Authorization: Bearer $KEY" -d '{"kind":"dev"}')
echo "$RUN"     # {"run_id":"run_…","kind":"dev","theme":"hub","seed":…,"variation":…,"deadline_at":null,"tasks":[…]}

curl -s -X POST $ARENA/runs/<run_id>/tasks/t01/tools/read -H "Authorization: Bearer $KEY" \
     -d '{"path":"/AGENTS.md"}'
curl -s -X POST $ARENA/runs/<run_id>/tasks/t01/submit -H "Authorization: Bearer $KEY" \
     -d '{"outcome":"OK","answer":["agent-03","agent-07"],"sources":[{"path":"/logs/audit/2026-11-14.jsonl","line":412}]}'
curl -s -X POST $ARENA/runs/<run_id>/finish -H "Authorization: Bearer $KEY"

The Python starter kit (starter-kit/arena/client.py) wraps all of this.

Endpoints

Method & path Body Returns
GET /me { team: {id, name}, final_window: {open, opens_at, closes_at}, final_run_used }
GET /tools { tools: [{name, description, args}] }
POST /runs { "kind": "dev" | "final", "seed"?: number } { run_id, kind, theme, seed?, variation?, deadline_at, tasks: [{task_id, instruction, level}] }
GET /runs { runs: [run summary] }
GET /runs/{run_id} run summary + tasks: [{task_id, instruction, level, submitted, tool_calls, grade}] (dev runs also reveal expected answers after the run ends)
POST /runs/{run_id}/tasks/{task_id}/tools/{tool} tool args (object) { ok: true, result } or { ok: false, error }
GET /runs/{run_id}/tasks/{task_id}/files/{...path} raw bytes of one workspace file (download binary formats here)
GET /runs/{run_id}/tasks/{task_id}/workspace.zip the whole task workspace as one zip of raw files
GET /runs/{run_id}/manifest { blobs: [{sha256, size, kind, content_type}], tasks: [{task_id, files: {path: sha256}}], total_bytes }
GET /runs/{run_id}/tasks/{task_id}/manifest { files: [{path, kind, size, sha256, content_type, readable}], total_bytes } (current state)
GET /runs/{run_id}/blobs/{sha256} ?task= (optional hint) raw bytes of one file of this run, by content hash (immutable)
POST /runs/{run_id}/tasks/{task_id}/submit { outcome, answer, sources } dev: grade feedback; final: { received: true, task_id }
POST /runs/{run_id}/finish closes the run; dev: run score

POST /runs

Run summary

{
  "run_id": "run_x1Y2z3", "kind": "dev", "theme": "hub", "seed": 123456, "variation": 98765, "status": "finished",
  "started_at": "…", "deadline_at": null, "finished_at": "…",
  "cost_usd": 0.0123, "accuracy": 0.62, "cost_score": 0.84, "final_score": 0.57
}

status is active, finished or expired (deadline passed). For final runs the score fields stay null until the organisers publish the results.

The workspace

Each scenario is a realistic, messy collection of real files in many formats: operator files (/AGENTS.md and the files it names), audit logs (one JSON event per line, plus a rotated archive in local time), registries and inventories (json / csv), spreadsheets (.xlsx / .ods), documents (pdf, docx, odt, markdown), screenshots (.jpg / .png), message exports, an inbox and a containment state file. A few things to know:

Tools

Every task has its own copy of the workspace (files, containment state, outbox): side effects in one task never leak into another, and tasks of one run can be worked on in parallel. GET /tools is the authoritative list with argument descriptions. Overview:

Tool Args Notes
tree / list path? directory tree / listing with format, size, page count and whether read can serve it
read path, start_line?, end_line? plain text only, max 400 lines per call
search pattern, path?, regex?, limit? path:line hits over plain-text files only
query_logs agent?, action?, target?, trace?, text?, since?, until?, limit?, offset? filtered audit-log events with file and line to cite (UTC)
access_path from, to, added?, removed?, granted?, revoked?, exclude? shortest delegation chain on the registry export; later changes only if you pass them
threat_intel indicator third-party reputation feed (a low-trust source)
get_containment current containment state
containment ops quarantine / release agents, revoke / rotate tokens, block domains, notes (state-changing)
send_message to, subject, body send an e-mail (state-changing)
write path, content write a file, only under /drafts/

Call a tool with POST /runs/{run_id}/tasks/{task_id}/tools/{tool} and the arguments as the JSON body. A tool-level problem (file not found, bad argument) returns HTTP 200 with {"ok": false, "error": "…"}, so your agent can read the error and recover. State-changing tools are recorded and graded: what your agent does counts, not just what it answers (e.g. sending personal data to an outside address fails the task even if the final answer is right).

Downloading files. GET /runs/{run_id}/tasks/{task_id}/files/{path} returns the raw bytes of one file (with the right Content-Type); GET …/workspace.zip returns the whole task workspace as one zip. Both reflect your writes under /drafts/. These are read-only; they do not count as a submission.

Download each file once: manifests and blobs. Most files are shared by many tasks of a run (a dev run's 30 task workspaces hold ~16 MB in total but only ~2 MB of distinct files). Every file is identified by the sha256 of its bytes:

  1. GET /runs/{run_id}/manifest lists the run's distinct blobs and, per task, path → sha256 (tasks in their initial state).
  2. GET /runs/{run_id}/blobs/{sha256} returns one blob. Blob ids never change meaning, so cache them locally by hash (responses are marked immutable) and fetch each one once — also across tasks, and across runs that share files. Add ?task={task_id} (a task whose manifest lists the hash) for a faster lookup; without it the whole run is searched.
  3. GET /runs/{run_id}/tasks/{task_id}/manifest gives one task's files in their current state: after a write (or a change of the plan / containment state) the changed file gets a new hash, which you can fetch from /blobs too. Unknown or foreign hashes return 404.

Blobs follow the same rules as the file endpoints (the run must be active; final runs only within the deadline).

Once a task is submitted, its workspace is frozen: further tool calls on it return 409 (file downloads are read-only).

Submitting

{
  "outcome": "OK",
  "answer": {"records": 1234, "domains": ["example-a.test", "example-b.invalid"]},
  "sources": [
    {"path": "/logs/audit/2026-11-14.jsonl", "line": 412},
    {"path": "/reports/advisory.pdf", "page": 2},
    {"path": "/fleet/tokens.csv", "record": "TOK-ABC-12"},
    {"path": "/security/export.xlsx", "sheet": "transfers", "row": 57},
    {"path": "/reports/handover.docx", "paragraph": 4}
  ]
}

One submission per task per run. To try again, start a new dev run (pass the old seed to get the same tasks).

Dev-run response (never contains the expected answer):

{
  "received": true, "task_id": "t03",
  "score": 0.79, "answer_score": 1, "constraints_ok": true,
  "sources_hit": 1, "sources_expected": 3, "sources_score": 0.33,
  "feedback": ["Outcome ✓", "Answer ✓", "Constraints ✓", "Sources 1/3", "Score 0.79", "Effort: 14 tool calls"]
}

Final-run response: {"received": true, "task_id": "t03"}. Final runs are graded after the window closes.

POST /runs/{run_id}/finish

Closes the run (idempotent) and records its cost. Dev runs return the run summary with accuracy, cost_usd, cost_score and final_score, plus tasks_submitted / tasks_total. Final runs return {"received": true, …}. Unsubmitted tasks score 0. Always call finish at the end, otherwise the dev run never gets a score.

Dev answer reveal. Once a dev run has ended (finished or expired — never while it is active, and never for final runs), finish and GET /runs/{run_id} include, per task, expected_outcome, expected_answer, expected_sources, your submission and its grade. Use it to debug your harness: start a dev run, submit, finish, then read back exactly where your answers and citations diverged. (Answers are specific to the run's seed, and the final is a different scenario — don't hard-code them.)

Scoring

Run cost is your key's usage at finish minus at run start, so run one dev run at a time if you want accurate per-run cost (OpenRouter usage can lag a few seconds; the final run is re-measured when it is graded).

Final evaluation

  1. Before the window opens, submit the commit hash of your frozen harness on the dashboard.
  2. When the window opens, your eval OpenRouter key becomes visible (dev keys are disabled).
  3. Start one final run with the eval key configured, and let it run unattended. You have 30 minutes.
  4. Submit each task, then call finish. Scores are revealed at the award ceremony.