Harbor and Terminal-Bench
Harbor is an open-source harness for evaluating agents: each task is a container, the agent is installed into it and solves the task by running commands there, and a verifier decides the reward. Terminal-Bench runs on it. By the end of this page you will have run this agent on a task of your own, read its reward, cost and trajectory, and know how to point it at Terminal-Bench.Every output on this page is what the commands printed when they were run,
with
openai/gpt-5.4-mini. The model’s wording, the ids, the times and the
costs will differ on your run.omnicoreagent harbor is Harbor’s own command line with this runtime as the
agent. Every Harbor command passes through (run, view, job resume,
trial start, dataset, download and the rest), plus two of this
runtime’s own: doctor and results.
Install and check
<...> is.) Each line
is one thing a trial needs: Python, Harbor, a Docker daemon that
answers docker info, a key for the model’s provider (it says where the key
is, never what it is), and what the task container will install. It exits 1
if any check fails.
To also install the agent into a real, throwaway task container, the way a
trial does, add --container. It took almost seven minutes here, most of it
building the image and installing the runtime:
Run a task
A Harbor task is a directory. This one has a bug to fix and a verifier that writes the reward:task.toml
instruction.md
environment/Dockerfile
environment/stats.py
tests/test.sh
-n 1: one trial at a time; --ak passes an option to the agent):
Read what the trial did
results takes one job directory or a directory of jobs (newest first).
For each trial it prints the outcome (passed, partial, failed,
errored, no reward or running), the reward, the run’s own status (with
why it ended when it did not succeed, such as error (max_steps)), steps and
cost; under it, the line of an error that says what went wrong, and any MCP
server the run could not use. steps counts the steps of the trajectory
Harbor reads, whose first step is the instruction the agent was given, so it is
one more than the model turns the run took (the run’s own
totals["steps"]). --json gives every field, for one job as below; for a
directory of jobs, a list of these, newest first:
Where everything is
result.json, agent_result holds the tokens, the cost
(also per model under model_usage) and this runtime’s own facts:
omnicoreagent_detail is added when the run did not succeed (its error, or
its last answer), and omnicoreagent_mcp_failed when an MCP server was
unusable.
The trajectory
agent/trajectory.json is in Harbor’s trajectory format (ATIF-v1.8), so a
trial of this agent reads like a trial of any other, and
omnicoreagent harbor view jobs shows it. Step 1 is the instruction; then one
step per model call, with what the model said, the tools it called with their
arguments, what they returned, and what the call cost. Each command’s result
also says how it ran (abridged):
extra names the run (run_id, trace_ids), whether
anything was left out of the record (evidence_status), each MCP server’s
state, and security warnings. Both trials here showed host_execution_not_contained:
commands run directly in the task container, which is the boundary (see
How it works).
The run’s own record, in agent/omnicoreagent/, is the same as any headless
run’s (Headless runs,
Read a run).
Terminal-Bench
A dataset is namedname@version; -i picks tasks by name (globs work),
-l caps how many, -n runs that many at once (Harbor’s default is 4):
--dry-run checks the configuration and the task list without running
anything:
MCP servers in a trial
A task’s MCP servers ([[environment.mcp_servers]] in task.toml) and those
you add with --mcp-config are connected for the run, over stdio, SSE or
streamable HTTP. Harbor passes on each server’s name, transport, URL,
command and arguments; other fields in the file, such as env, are dropped.
A server that cannot start does not stop the run: the agent carries on
without its tools, and may still earn the reward. That is not the result the
task meant to measure, so the trial’s record says so. With a server whose
command does not exist:
broken-mcp.json
trial.log (MCP servers the run could not use: ...),
in agent_result.metadata.omnicoreagent_mcp_failed, and in the trajectory:
environment.skills_dir) or that you
add with --skill are loaded too.
Tasks that restrict the network
A task can let the agent reach only the hosts it names ([agent] network_mode = "allowlist"). The agent still has to reach its model, so
allow the model’s host for the run:
How it works
- This runtime is the default agent.
run,exec,job start,trial start,job initandtrial initget--agent omnicoreagent.harbor.agent:OmniCoreAgentHarborunless you pass-aor a config file (-c). - Model names. A bare name starting
gpt-,chatgpt-,o1,o3oro4becomesopenai/...;claude-becomesanthropic/...,gemini-gemini/...,deepseek-deepseek/.... Anything else must be namedprovider/model(for exampleopenrouter/meta/llama-4): a wrong guess fails a trial late. - One key.
LLM_API_KEY, from the environment or read from./.env, is put in Harbor’s environment under the name the provider reads, whichdoctorprints. It never goes in a command’s arguments, which other users of the machine can see and Harbor writes into the job’sconfig.json. A key you set under the provider’s own name is left alone. - The same runtime in the container. A release installs
omnicoreagent==<version>; a development build uploads a wheel built from its checkout. It goes in a virtual environment of its own under/installed-agent, so the task’s Python stays as the task set it up. The runtime’s own files (traces, offloaded results) are there too, outside the task’s directory the verifier checks. - The agent’s files are the task’s. The file tools (
ls,read_file,write_file,edit_file, …) work in the directory the task’s commands run in, whatever the task set:ls(".")lists it. (Before, they pointed at the agent’s own workspace, and a model spent steps on “Path not found” before falling back to commands.) - The container is the boundary. Commands run in the task’s directory
through the
localsandbox provider. Policy still decides each one (a ruleallow_host_commandsallows commands on the host; approvals nobody can give are answered byapproval_mode), and the trajectory records the rule that allowed each command. - One headless run. The agent runs
omnicoreagent runonce with the task’s instruction (Headless runs), then converts its trajectory to ATIF for Harbor. - What the model never sees. The runtime’s credentials (the model’s key,
and environment variables that hold credentials) are replaced with
[REDACTED:credential]in everything a tool returns and everything recorded. Commands see only named variables (PATH,HOME,LANG, … and those you add with--ae NAME=value, except credentials). This cannot stop a command from sending a key it can read, so run trials with a key meant for them, with a spending limit.
Options
Agent options, with--ak name=value (Harbor’s --agent-kwarg):
Harbor flags used on this page:
-p (a task or dataset directory), -d
(a registry dataset), -i/-x/-l (pick tasks), -m, -n, -o (jobs
directory, default jobs), --mcp-config, --skill, --ae,
--allow-agent-host, --dry-run. omnicoreagent harbor run --help lists
all of them; doctor and results are in the
CLI reference.
When things go wrong
Error: Cannot tell which provider serves ...
Error: Cannot tell which provider serves ...
A bare model name outside the families above:Name it
provider/model. doctor -m reports the same as a FAIL model line.FAIL model ...: no key
FAIL model ...: no key
LLM_API_KEY in the
environment, or in ./.env in the directory you run from.Error: Docker is required to run a Harbor trial
Error: Docker is required to run a Harbor trial
run, exec, job start,
trial start, job resume and others) need Docker. If docker is
installed but its daemon is down, doctor says so:Error: Harbor is not installed
Error: Harbor is not installed
Every trial errored, and Harbor exited 0
Every trial errored, and Harbor exited 0
results gives the reason. A wheel that does not exist:RuntimeError: Could not build a wheel of ...
RuntimeError: Could not build a wheel of ...
A development build builds a wheel of its checkout with Put
uv, or with
pip when uv is not on PATH. In a virtual environment made by uv,
which has no pip, the trial errors:uv on PATH, or pass --ak install_spec=... or --ak wheel=....
doctor --container finds this before a real trial does.This runtime is a development build ... with no source to build a wheel from
This runtime is a development build ... with no source to build a wheel from
A development version installed without its checkout:The container never silently falls back to whatever PyPI has. Give the
doctor the same
--ak you give run, and it checks what the container
will install:Dry run failed: No tasks matched the filter(s)
Dry run failed: No tasks matched the filter(s)
-i matches task names in the dataset; use --dry-run to check a filter.The agent wrote no result
The agent wrote no result
results prints this under a trial whose run reported no status: it
was stopped from outside (Harbor’s agent timeout) before it wrote
result.json, so there is no trajectory either. Set run_timeout a little
under the task’s [agent] timeout_sec; a run that stops itself ends as
timeout and leaves its evidence.status error (max_steps)
status error (max_steps)
The run used every step (
max_steps, 60 by default) before answering.
Raise it with --ak max_steps=..., and read the trajectory to see where
the steps went.Error: There is no Harbor job in ...
Error: There is no Harbor job in ...
results was given a directory with no job in it. Pass the jobs
directory (jobs, or what you gave -o) or one job inside it.Next
Read a run
Everything a run recorded, step by step.
Observability
Traces, stores and exporters.
Outcomes and training
Score runs and turn trajectories into training data.
Headless runs
The
omnicoreagent run each trial is built on.