Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/v6/experimental/compose.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ supervised startup order - are the container boundaries and `depends_on` conditi

A Compose row selects its document with `RuntimeConfig.compose`; `compose_project` names the root
that travels when the project is serialized and uploaded. Everything the build needs - contexts,
bind mounts, `env_file`, configs, secrets - must live under that root. Host environment
interpolation, `include`, and `extends` are not part of the serialized contract: the document that
uploads is the document that runs.
bind mounts, `env_file`, configs, secrets - must live under that root. Interpolation resolves from
the `.env` beside the Compose file and from defaults in the document; HUD does not read values from
the process environment, so an unbound variable is rejected. `include` and `extends` are not part
of the serialized contract: the document that uploads is the document that runs.

Every runnable service has an `image`, a `build`, or both; paths are relative to the project. An
optional `build.sh` beside the document is a preparation hook - it resolves or builds prerequisite
Expand Down
35 changes: 23 additions & 12 deletions docs/v6/experimental/harbor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@ images. `export()` materializes HUD tasks as self-contained Harbor folders.
A Harbor source is either one task directory or a dataset directory containing
task directories. Each task contains `task.toml` and `instruction.md`, plus an
environment image or build recipe and a `tests/` verifier.
Multi-container sources use Harbor's `environment/docker-compose.yaml` recipe
name; other Compose-like filenames remain ordinary environment files.

```python
from hud.integrations import harbor
from hud.eval import DockerRuntime

taskset = harbor.adapt("./terminal-bench")
job = await taskset.run(agent, runtime=DockerRuntime())
result = harbor.adapt("./terminal-bench")
job = await result.taskset.run(agent, runtime=DockerRuntime())
```

Each returned row carries its instruction and grading configuration in `args`,
Harbor metadata in `columns`, resource requirements in `runtime_config`, and,
when declared, a [`Task.verifier`](/v6/experimental/verifier-environments).
The row's Compose path points into a generated project under `.hud-adapt/`.
Tasks that cannot be adapted appear in `result.failures`, with every detectable
finding classified by a stable code. Other tasks in the dataset are still
packaged.

```text
.hud-adapt/<environment>/
Expand Down Expand Up @@ -97,18 +102,24 @@ runtime isolation are described in
The adapter handles:

- an `environment/Dockerfile`, an `environment.docker_image`, or a Compose
project with a `main` service;
- Compose sidecars with one declared TCP endpoint each;
project; when authored Compose omits `main`, the environment recipe supplies
that agent service;
- Compose sidecars with any number of declared TCP endpoints;
- HTTP MCP servers (`sse` and `streamable-http`), healthchecks, network modes,
allowlists, phase users, environment variables, and CPU/memory/GPU requests;
allowlists, phase users, environment variables, and placement requirements;
- inline verifier scripts and separate verifier Dockerfiles;
- verifier collect hooks and declared artifact paths from `main` or a sidecar.
- verifier collect hooks and declared artifact paths from `main` or a sidecar,
including directory exclusions and host-side destinations.

Unsupported declarations fail during adaptation. These include non-Linux and
TPU environments, stdio MCP servers, skills directories, multi-step tasks,
Compose interpolation/include/extends, and sidecars without exactly one usable
TCP endpoint. A project also fails if its main service has neither an image nor
a valid build recipe.
Placement requirements are copied to the task that declares them: the main
environment configures the actor task, and a separate verifier environment can
configure its verifier task independently. The selected runtime decides whether
it can provision those requirements.

Unsupported declarations fail during adaptation. These include stdio MCP
servers, skills directories, multi-step tasks, Compose variables not bound by
the project `.env` or document defaults, `include`, and `extends`. A project
also fails if its main service has neither an image nor a valid build recipe.

## Export HUD tasks to Harbor

Expand Down Expand Up @@ -156,7 +167,7 @@ covers that review.

| Function | Contract |
| --- | --- |
| `harbor.adapt(path, *, hud_requirement="hud") -> Taskset` | Package one Harbor task or a dataset as generated Compose environments and native task rows. |
| `harbor.adapt(path, *, hud_requirement="hud") -> AdaptResult` | Package valid tasks as generated Compose environments and native task rows, and return structured failures for the rest. |
| `await harbor.export(source, out_dir, *, answer_file=..., timeout_sec=600) -> list[Path]` | Write HUD task rows as Harbor task directories. |

<CardGroup cols={2}>
Expand Down
31 changes: 16 additions & 15 deletions docs/v6/experimental/verifier-environments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ verifier = Environment("judge")
@actor.template(id="solve")
async def solve():
answer = yield "Write the secret to the target system."
yield 0.0 # the verifier task supplies the authoritative reward
yield {"score": 0.0, "answer": answer}

@verifier.template(id="verify")
async def verify(expected: str):
answer = yield ""
yield 1.0 if answer == expected else 0.0
actor_result = yield ""
yield 1.0 if actor_result["answer"] == expected else 0.0

task = solve()
task.verifier = verify(expected="secret")
Expand All @@ -75,24 +75,26 @@ sequenceDiagram
Engine->>Actor: close connection + clean up
Engine->>Judge: provision + connect
Engine->>Judge: tasks.start(verifier)
Engine->>Judge: tasks.grade(answer)
Engine->>Judge: tasks.grade(actor result)
Judge-->>Engine: authoritative evaluation
Engine->>Judge: close connection + clean up
```

The actor task is graded to complete its generator lifecycle, but that grade is best-effort: when
the verifier phase begins, the actor grade is cleared, and the verifier evaluation replaces it as
the run's grade of record. Agent failures and actor-grading failures are recorded on the trace
while the verifier still runs when the phase boundary can be reached; a verifier provisioning or
grading failure leaves the run errored and ungraded.
The actor task is graded to complete its generator lifecycle. Its full result frame is sent to the
verifier, whose evaluation replaces it as the run's grade of record. An actor result must include
the numeric `score` required of every task result plus whatever fields its verifier consumes. If
actor grading fails, the engine supplies an error result containing the submitted `answer` so an
authoritative verifier can still run. A verifier provisioning or grading failure leaves the run
errored and ungraded.

If both rows name the same environment and the verifier has no row-level `runtime_config`, the
engine keeps the actor connection and substrate alive and starts the verifier task on that control
channel immediately after the actor task completes. A different environment name or verifier
runtime configuration forces actor cleanup followed by a fresh provider acquisition.

`HostedRuntime` does not accept verifier task rows; verifier environments run under a
client-driven provider such as `LocalRuntime`, `DockerRuntime`, or a custom provider.
`HostedRuntime` supports the same-environment form without a verifier `runtime_config`, keeping
both phases inside one hosted acquisition. Verifiers that require another runtime remain
client-driven through `LocalRuntime`, `DockerRuntime`, or a custom provider.

## What runs where

Expand All @@ -101,10 +103,9 @@ client-driven provider such as `LocalRuntime`, `DockerRuntime`, or a custom prov
| Actor | Actor environment setup, the agent loop, actor task teardown | Actor capabilities and state | Provisional; retained only when no verifier exists |
| Verifier | Verifier setup and grading; no agent loop | None through HUD's agent interface | Authoritative |

The engine forwards the final answer (`run.trace.content`) to the verifier. Files, processes,
sockets, and environment memory do not cross between distinct substrates automatically - any
graded state transfer is an explicit adapter or provider contract, such as an artifact snapshot,
object-store reference, or shared service endpoint.
The engine forwards the actor task's result to the verifier unchanged. Files, processes, sockets,
and environment memory do not cross between distinct substrates automatically - the actor result
must carry any artifact reference, object-store key, or shared service endpoint the verifier needs.

## Harbor verifier environments

Expand Down
4 changes: 2 additions & 2 deletions docs/v6/internals/walkthrough.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ The score travels back: `TaskRunner.grade` -> server reply -> `client.grade` ->
When `task.verifier` is present, the actor grade is provisional. A verifier in the same environment
with no row-level runtime configuration starts on the existing client. Otherwise the actor client
and provider exit before the same provider is called with the verifier row. `_verify` starts that
task without an agent loop and immediately grades it with `run.trace.content`; its evaluation
task without an agent loop and immediately grades it with the actor result frame; its evaluation
replaces the actor grade. The full phase contract is documented in
[verifier environments](/v6/experimental/verifier-environments#provisioning-order).

Expand All @@ -694,7 +694,7 @@ Every hop above, in order:
7. **Checkpoint** - `run` holds a live client (manifest + suspended runner) and the prompt.
8. `await agent(run)` - agent opens capabilities via `run.client`, loops, fills `run.trace` (answer on `trace.content`).
9. `Run.__aexit__` - `client.grade` -> `tasks.grade` -> `TaskRunner.grade` resumes the generator to the second yield -> provisional `score` -> `run.grade.reward`.
10. Optional verifier - reuse the live substrate or finish actor cleanup and acquire the verifier substrate; start and grade the verifier with `trace.content`; replace the actor grade.
10. Optional verifier - reuse the live substrate or finish actor cleanup and acquire the verifier substrate; start and grade the verifier with the actor result; replace the actor grade.
11. Unwind - close the active client, stop the substrate (`serve` runs `env.stop()`), `trace_exit`, return the graded `Run` to `Taskset.run`.

</div>
Expand Down
25 changes: 18 additions & 7 deletions docs/v6/reference/runtime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,21 @@ Compose project, hardware, and timeouts. Set it on the runtime (`runtime_config=
supports.

```python
from hud.eval import RuntimeConfig, RuntimeResources, RuntimeGPU, RuntimeLimits
from hud.eval import RuntimeConfig, RuntimeGPU, RuntimeLimits, RuntimeResources, RuntimeTPU

RuntimeConfig(
image="my-env",
resources=RuntimeResources(cpu=4, memory_mb=8192, gpu=RuntimeGPU(type="A100", count=1)),
resources=RuntimeResources(
cpu=4,
memory_mb=8192,
storage_mb=32768,
gpu=RuntimeGPU(type=["H100", "A100"], count=1),
),
limits=RuntimeLimits(startup_timeout_s=300, run_timeout_s=1800),
)

RuntimeConfig(compose="./compose.yaml")
RuntimeConfig(resources=RuntimeResources(tpu=RuntimeTPU(type="v5", topology="2x2")))
```

| Field | Description |
Expand All @@ -74,12 +80,15 @@ RuntimeConfig(compose="./compose.yaml")
| `compose` | Local path to a Compose file, or its serialized `ComposeConfig`. Mutually exclusive with `image`. |
| `compose_project` | Local project root for upload, or a serialized `ComposeProjectRef`. Requires `compose`. |
| `compose_service_access` | Mount the runtime's Docker socket into Compose `main` at `/media/hud/docker.sock`. Requires `compose`. |
| `resources` | `RuntimeResources(cpu, memory_mb, gpu=RuntimeGPU(type, count))`. |
| `resources` | Placement requests: CPU, memory, disk, acceptable GPU types and count, OS, or TPU slice. |
| `limits` | `RuntimeLimits(startup_timeout_s, run_timeout_s)`. |

Support differs per runtime: `DockerRuntime`, `ModalRuntime`, and `DaytonaRuntime` accept it (Docker
ignores `limits`; Daytona ignores `run_timeout_s` and resource overrides when booting from a snapshot).
`LocalRuntime` and `HUDRuntime` reject a per-task `runtime_config`.
Support differs per runtime. Providers reject unsupported requirements except `storage_mb`, which
is best effort: `DockerRuntime` admits against available disk and `DaytonaRuntime` provisions
enough whole GiB, while providers without disk sizing proceed with their default capacity.
Daytona accepts a list of GPU alternatives. Docker ignores `limits`; Daytona rejects
`run_timeout_s` and resource overrides when booting from an already-built snapshot. `LocalRuntime`
rejects a per-task `runtime_config`.

## Runtime directory

Expand Down Expand Up @@ -150,7 +159,9 @@ ModalRuntime(image_name=None, *, image=None, command=None, app_name="hud-envs",

For Compose input, `ModalRuntime` runs the project in a Docker-in-Docker
sandbox in your Modal account and merges `env_vars` into `main` at acquisition
time. See [Compose environments](/v6/experimental/compose#runtime-behavior).
time. GPU requests require a plain or platform-materialized image; Modal Compose
rejects them because its nested Docker daemon cannot receive the sandbox GPU.
See [Compose environments](/v6/experimental/compose#runtime-behavior).

Requires the `modal` extra and a configured token.

Expand Down
2 changes: 2 additions & 0 deletions hud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
RuntimeGPU,
RuntimeLimits,
RuntimeResources,
RuntimeTPU,
SubprocessRuntime,
SyncPlan,
Task,
Expand All @@ -48,6 +49,7 @@
"RuntimeGPU",
"RuntimeLimits",
"RuntimeResources",
"RuntimeTPU",
"SubprocessRuntime",
"SyncPlan",
"Task",
Expand Down
22 changes: 14 additions & 8 deletions hud/environment/egress.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,23 @@ def bind_addresses(
("127.0.0.1", VISITOR_PORT),
*(("127.0.0.1", port) for port in reserved_ports),
}
addresses: dict[str, str] = {}
ports_by_name: dict[str, list[int]] = {}
for peer in peers:
if peer.name in addresses:
raise ValueError(f"two peers are called {peer.name!r}")
ports = ports_by_name.setdefault(peer.name, [])
if peer.port in ports:
raise ValueError(f"peer {peer.name!r} declares port {peer.port} twice")
ports.append(peer.port)

addresses: dict[str, str] = {}
for name, ports in ports_by_name.items():
for index in range(1, 256):
host = f"127.0.0.{index}"
if (host, peer.port) not in taken:
if all((host, port) not in taken for port in ports):
break
else:
raise ValueError(f"too many peers on port {peer.port}")
taken.add((host, peer.port))
addresses[peer.name] = host
raise ValueError(f"no loopback address can route peer {name!r}")
taken.update((host, port) for port in ports)
addresses[name] = host
return addresses


Expand All @@ -235,7 +240,7 @@ def hosts_text(
lines = "".join(
[
*(f"127.0.0.1\t{name}\n" for name in local_aliases),
*(f"{addresses[peer.name]}\t{peer.name}\n" for peer in peers),
*(f"{host}\t{name}\n" for name, host in addresses.items()),
]
)
return f"{base.rstrip(chr(10))}\n{lines}" if base.strip() else lines
Expand Down Expand Up @@ -523,6 +528,7 @@ def handle(self) -> None:

class _UnixServer(socketserver.ThreadingUnixStreamServer):
daemon_threads = True
request_queue_size = socket.SOMAXCONN

def get_request(self) -> tuple[socket.socket, tuple[str, int]]:
# A unix peer has no address; the handler wants one to log.
Expand Down
Loading
Loading