Skip to main content

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

(The real line names OpenAI’s own key variable where <...> 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
Run it (-n 1: one trial at a time; --ak passes an option to the agent):
Harbor’s own summary, at the end:
Of the five minutes, the agent’s run took 18 seconds; the rest was building the image and installing the runtime into it.
Installing the runtime into the container counts against Harbor’s agent-setup timeout (360 seconds). On a slow machine or network it can run out, and the trial errors with AgentSetupTimeoutError: Agent setup timed out after … seconds. Give setup longer with Harbor’s own flag, for example --agent-setup-timeout-multiplier 3.

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:
Harbor exits 0 even when every trial errored. Read the results, not the exit code.

Where everything is

In the trial’s 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):
The document’s 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 named name@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:
Each task builds its own image, and each trial installs the runtime into it, so start with one task and read its result before running all 89.

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
The same appears in trial.log (MCP servers the run could not use: ...), in agent_result.metadata.omnicoreagent_mcp_failed, and in the trajectory:
The trial’s MCP servers are allowed by policy without asking, since nobody is there to answer. Skills a task provides (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:
Everything else the agent needs is fetched when it is installed, while the network is open. If the model’s host is not allowed, the run ends saying it could not connect to it.

How it works

  • This runtime is the default agent. run, exec, job start, trial start, job init and trial init get --agent omnicoreagent.harbor.agent:OmniCoreAgentHarbor unless you pass -a or a config file (-c).
  • Model names. A bare name starting gpt-, chatgpt-, o1, o3 or o4 becomes openai/...; claude- becomes anthropic/..., gemini- gemini/..., deepseek- deepseek/.... Anything else must be named provider/model (for example openrouter/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, which doctor prints. It never goes in a command’s arguments, which other users of the machine can see and Harbor writes into the job’s config.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 local sandbox provider. Policy still decides each one (a rule allow_host_commands allows commands on the host; approvals nobody can give are answered by approval_mode), and the trajectory records the rule that allowed each command.
  • One headless run. The agent runs omnicoreagent run once 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

A bare model name outside the families above:
Name it provider/model. doctor -m reports the same as a FAIL model line.
The real line names OpenAI’s own variable. Set LLM_API_KEY in the environment, or in ./.env in the directory you run from.
Commands that start containers (run, exec, job start, trial start, job resume and others) need Docker. If docker is installed but its daemon is down, doctor says so:
results gives the reason. A wheel that does not exist:
A development build builds a wheel of its checkout with uv, or with pip when uv is not on PATH. In a virtual environment made by uv, which has no pip, the trial errors:
Put uv on PATH, or pass --ak install_spec=... or --ak wheel=.... doctor --container finds this before a real trial does.
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:
-i matches task names in the dataset; use --dry-run to check a filter.
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.
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.
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.