Skip to content

Commit 29f2388

Browse files
jdchawla29nancyjlau
andcommitted
feat(integrations): hud.integrations, with Harbor as its first frontend
integrations/ becomes hud.integrations because an adapted image imports it: the image's CMD serves hud.integrations.harbor:environment, so the module has to ship in the wheel that image installs. Nothing in the SDK or CLI imports it — a Harbor taskset is loaded and placed explicitly. Harbor implements the contract: - load() carries what a task declares, not just its identity: [metadata] as Task.columns (difficulty/category/tags — the platform's facets), [environment] cpu/memory/gpu as RuntimeConfig.resources, real descriptions on templates, verifier.env for the verifier. Time budgets stay off the row — a budget bounds the rollout, not the substrate — and agent_timeout() exposes them for rollout_timeout. - adapt() packages the environment constructor into one HUD-speaking image per build context, whose CMD serves that constructor; rows then run on any container placement. Images are content-addressed and kept, built once per group under a lock. - environment() is what those images serve: the workspace applies the task's declared network isolation, env, workdir and user, healthchecks gate serving, URL MCP servers are published as capabilities, and tests/test.sh grades in place under the task's verifier timeout. Behaviour that cannot be reproduced faithfully is refused, not dropped: allowlist egress, a verifier with its own environment, stdio MCP servers, non-linux os, TPUs, and multi-step tasks (which load, but do not adapt). Environments are grouped by build context *and* declared policy, so one environment always serves one policy. Container, workspace and grading mechanics were ported from #478. Co-authored-by: Nancy <[email protected]>
1 parent 40b4c9f commit 29f2388

12 files changed

Lines changed: 1346 additions & 224 deletions

File tree

docs/v6/advanced/harbor-convert.mdx

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
---
22
title: "Harbor interop"
3-
description: "Load Harbor tasks into the HUD runtime or export HUD tasks to Harbor folders, with the converter CLI and a field-by-field mapping between formats."
3+
description: "Load Harbor tasks into the HUD runtime or export HUD tasks to Harbor folders, with a field-by-field mapping between formats."
44
icon: "ship"
55
---
66

77
Everything that authors tasks - HUD's own `env.py`, platform rows, **Harbor**
88
task dirs - is a *frontend* that loads into the same primitives (`Environment`,
99
`Task`, `Taskset`). Integrations are **loaders, not converters**: no codegen
10-
roundtrip to run foreign tasks. The Harbor integration lives in the SDK repo at
11-
[`integrations/harbor.py`](https://github.com/hud-evals/hud-python/blob/main/integrations/harbor.py)
12-
- a recipe built only on the public SDK surface; copy it into your project or
13-
run it from a checkout.
10+
roundtrip to run foreign tasks. Each one implements
11+
[`hud.environment.Integration`](https://github.com/hud-evals/hud-python/blob/main/hud/environment/integration.py)
12+
- `load(ref) -> Taskset` and `environment(ref) -> Environment` - and ships as
13+
`hud.integrations.<format>`.
14+
15+
Harbor's agent works *inside* its container, so its environment constructor is
16+
only meaningful in there. `adapt()` packages it: one HUD-speaking image per
17+
distinct build context, whose CMD serves `hud.integrations.harbor:environment`.
18+
The same rows then run on any container placement.
1419

1520
## Prerequisites
1621

@@ -24,34 +29,57 @@ directly - one row per task dir (`id` = the dir name), sharing one declarative
2429
`Environment` per distinct `environment/` build context:
2530

2631
```python
27-
from integrations.harbor import detect, load
32+
from hud.integrations import harbor
2833

29-
assert detect("./terminal-bench")
30-
taskset = load("./terminal-bench")
34+
assert harbor.detect("./terminal-bench")
35+
taskset = harbor.load("./terminal-bench")
3136

3237
for task in taskset:
33-
print(task.env, task.id)
38+
print(task.env, task.id, task.columns["difficulty"])
3439
```
3540

36-
Like every task row, the result carries no placement. Run it by supplying one -
37-
today that means a substrate already serving the control channel
38-
(`runtime=Runtime(url)`); a docker provider that builds and runs each task's
39-
`environment/` image is the planned follow-up:
41+
Each row carries what the task declared: `[metadata]` as `columns` (difficulty,
42+
category, tags - the platform's filter/leaderboard facets) and
43+
`[environment]` cpu/memory/gpu as `runtime_config`. Time budgets stay off the
44+
row, since they bound the *rollout* rather than the substrate - read one with
45+
`harbor.agent_timeout(task_dir)` and pass it as `rollout_timeout`.
46+
47+
## Run Harbor tasks
48+
49+
Build the images once, then place the rows anywhere:
4050

4151
```python
42-
from hud import Runtime
52+
from hud.eval import DockerRuntime
4353

44-
job = await taskset.run(agent, runtime=Runtime("tcp://127.0.0.1:8765"))
54+
await harbor.adapt("./terminal-bench") # local images
55+
job = await harbor.load("./terminal-bench").run(agent, runtime=DockerRuntime())
4556
```
4657

58+
`adapt(path, push="registry.io/acme")` pushes instead, and writes
59+
`.hud-images.json` next to the tasks so `load()` stamps each row with its image
60+
- from there `hud deploy` of the generated contexts under `.hud-adapt/` works
61+
unchanged (both run the image's own CMD). `ModalRuntime` / `DaytonaRuntime`
62+
replace the CMD with their `command=` - pass
63+
`harbor.serve_command(env_name)` to serve the group's constructor there.
64+
65+
Tasks whose declared behaviour cannot be reproduced faithfully are refused
66+
rather than silently downgraded. Adaptation replaces the container's own boot
67+
process with the serving command, so anything depending on that boot process is
68+
refused: a Dockerfile `ENTRYPOINT`, `healthcheck` (nothing would start the
69+
services it awaits), and `mcp_servers` (nothing would start the servers they
70+
point at) — alongside `network_mode = "allowlist"`, a verifier with its own
71+
environment, non-linux `os`, TPUs, and multi-step `[[steps]]` tasks. Supported
72+
and applied: `no-network` isolation, `environment.env`, `workdir`, and
73+
`agent`/`verifier` `user`.
74+
4775
## Export HUD tasks to Harbor
4876

4977
`export(source, out_dir)` goes the other way: it turns a HUD task source (a
5078
`.py` file/dir exposing `Task`s, or a `.json`/`.jsonl` taskset next to its
5179
`env.py`) into self-contained Harbor task folders:
5280

5381
```python
54-
from integrations.harbor import export
82+
from hud.integrations.harbor import export
5583

5684
created = await export("tasks.py", "harbor_tasks")
5785
```

hud/integrations/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Interop frontends: foreign benchmark formats as HUD primitives.
2+
3+
Each format implements :class:`hud.environment.Integration` — see its module
4+
docstring for the contract (``load`` / ``environment``). Format
5+
modules also keep an ergonomic function surface (``harbor.load(...)``) and
6+
format extras: ``harbor.detect`` recognizes the layout, ``harbor.adapt``
7+
packages the constructor into container images, ``harbor.export`` is the
8+
reverse direction.
9+
"""
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Harbor (terminal-bench layout) interop: load, adapt, export.
2+
3+
Harbor task structure::
4+
5+
task_name/
6+
├── instruction.md # agent prompt
7+
├── task.toml # config: timeouts, metadata
8+
├── environment/Dockerfile # container the agent works in
9+
├── tests/test.sh # verification -> writes reward.txt / .json
10+
└── solution/ # optional (ignored)
11+
12+
Harbor's agent works *inside* its container, so :func:`environment` (the
13+
:class:`~hud.environment.Integration` constructor) is meaningful only in
14+
there — :func:`adapt` packages it: one HUD-speaking image per env group
15+
whose CMD serves ``harbor.environment``, and the same rows then run on any
16+
container placement::
17+
18+
await harbor.adapt("./tasks") # local images
19+
job = await harbor.load("./tasks").run(agent, runtime=DockerRuntime())
20+
21+
await harbor.adapt("./tasks", push="registry.io/x") # or hud deploy the
22+
job = await harbor.load("./tasks").run(agent, runtime=HUDRuntime()) # contexts
23+
24+
Plus :func:`export`, the reverse direction (HUD tasks -> Harbor folders).
25+
Compose-based and prebuilt-``docker_image`` tasks are not supported yet.
26+
"""
27+
28+
from __future__ import annotations
29+
30+
from typing import TYPE_CHECKING
31+
32+
from hud.environment import Integration
33+
34+
if TYPE_CHECKING:
35+
from pathlib import Path
36+
37+
from hud.environment import Environment
38+
from hud.eval import Taskset
39+
40+
from ._adapt import adapt, environment, serve_command
41+
from ._export import ALLOWED_PROTOCOLS, CONTROL_PORT, DEFAULT_ANSWER_FILE, export
42+
from ._load import IMAGES_MANIFEST, agent_timeout, detect, grouped, load
43+
44+
45+
class Harbor(Integration):
46+
"""The :class:`~hud.environment.Integration` contract for Harbor."""
47+
48+
name = "harbor"
49+
50+
def load(self, ref: str | Path) -> Taskset:
51+
return load(ref)
52+
53+
def environment(self, ref: str | Path, *, name: str | None = None) -> Environment:
54+
return environment(ref, name=name)
55+
56+
57+
integration = Harbor()
58+
59+
__all__ = [
60+
"ALLOWED_PROTOCOLS",
61+
"CONTROL_PORT",
62+
"DEFAULT_ANSWER_FILE",
63+
"IMAGES_MANIFEST",
64+
"Harbor",
65+
"adapt",
66+
"agent_timeout",
67+
"detect",
68+
"environment",
69+
"export",
70+
"grouped",
71+
"integration",
72+
"load",
73+
"serve_command",
74+
]

0 commit comments

Comments
 (0)