Skip to content

feat(harbor_agent): NeMo Gym sandbox execution backend + Terminal-Bench 2.1 config - #2031

Draft
hemildesai wants to merge 23 commits into
mainfrom
hemil/harbor-nemo-gym-sandbox-tb21
Draft

feat(harbor_agent): NeMo Gym sandbox execution backend + Terminal-Bench 2.1 config#2031
hemildesai wants to merge 23 commits into
mainfrom
hemil/harbor-nemo-gym-sandbox-tb21

Conversation

@hemildesai

@hemildesai hemildesai commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What

Integrates the nemo_gym.sandbox API into the harbor agent non-intrusively: a new NemoGymSandboxEnvironment (a Harbor BaseEnvironment) executes Harbor trials inside sandboxes managed by any NeMo Gym sandbox provider (opensandbox, docker, daytona, apptainer, ecs_fargate). Selected purely via config — no changes to the harbor_agent execution path.

  • custom_envs/nemo_gym_sandbox/environment.py — AsyncSandbox-backed backend: task.toml resources → SandboxSpec, tar-based upload_dir/download_dir with per-file fallback, bash -ic exec wrapping (parity with Harbor's docker/daytona backends), always-terminate on stop(), opt-in allow_unenforced_internet_isolation, image_rewrites.
  • configs/harbor_agent_opensandbox.yaml — Terminal-Bench 2.1 (89 tasks) with stock Terminus-2 through the opensandbox provider in server-proxy mode. Provider referenced via sandbox_provider: ${sandbox} (mini_swe_agent_2 style; include the shipped provider config with a second --config). Sampling/harness match the Qwen3.6-27B model card (temp 1.0 / top_p 0.95 / top_k 20, 3h agent timeout, 256K ctx / 80K max output). 30 GiB sandbox ephemeral storage to avoid kubelet evictions on disk-heavy tasks.
  • prepare_terminal_bench_2_1.py + checked-in 89-row benchmark input JSONL (pinned harbor-framework/terminal-bench-2-1).
  • harbor_ray_task_num_cpus config knob — Harbor jobs are I/O-bound Ray tasks; default 1 CPU/task caps concurrency at the driver CPU count, so a fractional value (0.25) enables true full-89 concurrency on a 24-CPU driver.
  • fix: extract_usage sets input_tokens_details.cache_write_tokens (required by openai ≥2.40; every live-model /run 500'd without it).
  • 18 unit tests (fake registered provider; tar round-trip verified at byte level).

Result — Terminal-Bench 2.1, Qwen3.6-27B

Ran the full 89-task benchmark end-to-end: vLLM-served Qwen3.6-27B on 2 GPU replicas, gym/Ray driver, and sandboxes on a live OpenSandbox deployment reached over an istio ambient multi-cluster mesh (server-proxy mode, no public endpoint). Traces captured for all 89 tasks (rollouts + per-trial trajectory, asciinema recordings, verifier artifacts).

Value
This run (TB 2.1, Harbor/Terminus-2, single run) pass@1 = 59.55% (53/89)
Model card (TB 2.0, Harbor/Terminus-2, avg of 5 runs) 59.3

Reproduces the model-card Terminal-Bench number essentially exactly (within ~0.25 points). Caveats: TB 2.1 vs 2.0 (26 tasks fixed; same count/harness), single run vs 5-run average, and sandbox resources set to 4 vCPU / 16 GiB / 30 GiB.

Note: commits carry DCO sign-off; GPG signature to be added on amend (non-interactive session).

🤖 Generated with Claude Code

Update: cross-cluster hardening + 5-run measurement (in progress)

Follow-up commit hardens the opensandbox provider + harbor agent for sandboxes reached across a load-balanced/multi-cluster path, based on failure modes hit while running the benchmark end-to-end:

  • connection.disable_connection_pooling (provider, on by default in the shipped config): a load balancer between client and OpenSandbox server can silently reap idle pooled connections; the SDK then reuses a dead socket and exec hangs (observed as dozens of CLOSE_WAIT sockets wedging tasks, while fresh-connection requests completed in ~1s). With pooling disabled every request opens a fresh connection — no reaped socket is ever reused. Verified A/B on a live mesh: identical run with pooling on collapsed (8/89 tasks in 40 min, CLOSE_WAIT≈80); with the option on, 57/89 in 27 min with CLOSE_WAIT=0.
  • Hard exec wall-clock cap (provider): backstop around submit+poll+retries so a pathologically wedged exec raises TimeoutError (which Terminus-2 records as a command timeout) instead of hanging the task.
  • default_exec_timeout_s 14400 → 300superseded: that reasoning accounted only for Terminus-2's tmux plumbing and missed Harbor's verifier, which also calls exec() without timeout_sec. The value is now 1800 in both the env default and the TB config — see the gold-patch section below for the measurement.
  • nemo_model_server_timeout_sec 7200 → 2400: lets a silently-dropped model call self-heal within 40 min instead of blocking a task for 2h.
  • cpu_pin_enabled (opt-in, default false): pins every Harbor exec (tmux session + verifier) to a random contiguous core block sized by the task's cpu count, so nproc inside reports the real budget and co-resident sandboxes spread across core blocks. Fail-open (no taskset/small host → unpinned). Measured on a full TB 2.1 run under card conditions: no e2e-time or score delta vs baseline (58.4% / 3h13m / 12.4% timeouts, all within the 5-run baseline spread) — task wall time on this benchmark is dominated by model turns, not sandbox CPU contention. Kept for CPU-bound workloads.

Serving guidance for Qwen3.6 (hybrid GDN attention) on vLLM, learned the hard way:

  • Use stock vllm/vllm-openai (v0.25.1 used here; multi-arch incl. arm64).
  • Disable prefix caching (--no-enable-prefix-caching)superseded: the engine-core freezes initially attributed to prefix caching turned out to be the default (FlashInfer) GDN prefill kernel; with --gdn-prefill-backend triton, prefix caching ON is stable (validated over a further 5-run cohort, zero freezes) and scores better — see the final card-parity section below.
  • Warm each replica before an eval burst (a short + a long generation crossing the ~8k boundary, plus a small concurrent batch) to pre-trigger kernel JIT.

5-run pass@1 (final)

Two cohorts, differing only in the per-task agent timeout (the 4h budget compensates the per-turn re-prefill slowdown from running without prefix caching — see serving notes):

Run Agent timeout pass@1 agent-timeout rate
1 3h 57.30% (51/89) 16.9%
2 3h 53.93% (48/89) 20.2%
3 4h 62.92% (56/89) 10.1%
4 4h 53.93% (48/89) 11.2%
5 4h 59.55% (53/89) 10.1%
  • 3h cohort mean: 55.6 · 4h cohort mean: 58.8 ± 4.5 · all-5 mean: 57.5 ± 3.8
  • References: earlier single clean run 59.55%; model card 59.3 (TB 2.0, 5-run average)
  • The 4h cohort — whose timeout rate matches the card-harness conditions (~10%) — lands on the model-card number within noise; run-to-run spread of ±4-5 points on an 89-task benchmark is expected (1 task ≈ 1.1 points).

Serving notes for Qwen3.6 (hybrid GDN) on vLLM

Hard-won operational guidance from ~30h of serving under full-benchmark agentic load:

  • --gdn-prefill-backend triton — with the default (FlashInfer) GDN prefill kernel, the engine core froze roughly hourly under concurrent long-context load: requests parked, prompt/generation counters static, no errors, health endpoint green. Reproduced on two different vLLM builds, with prefix caching on and off, on fresh and long-lived engines. On the Triton/FLA kernel: zero freezes across ~16h of identical load. (An upstream issue with the full signature is worth filing.)
  • Prefix caching: keep it ON once the Triton GDN kernel is in place. It was temporarily disabled while isolating the freeze (it's flagged experimental for this hybrid-GDN family), but the final card-parity cohort ran 5 freeze-free runs with caching enabled — and it's worth ~3–4 points on this workload: per-turn re-prefill dominates deep agentic trajectories, so caching cuts turn latency, which cuts agent-timeout rate (~11% vs ~13–17% cacheless) even on a shorter 3h budget.
  • --compilation-config '{"cudagraph_mode": "PIECEWISE"}' — attention ops run eagerly, outside graph replay.
  • Warm each replica before an eval burst (one short + one long generation crossing the ~8k boundary + a small concurrent batch) to pre-trigger kernel JIT; and restart engines between long evals.
  • Pin eval driver pods against node consolidation (karpenter.sh/do-not-disrupt: "true" or equivalent) — multi-hour drivers are casualties of routine autoscaler bin-packing otherwise. The same applies to sandbox pods on autoscaled pools: one mid-eval node roll (triggered by an AMI-alias drift) killed ~12 in-flight sandboxes whose pods carried no disruption protection, surfacing as opaque proxy 500s.
  • Sandbox resources 8 CPU / 16 GiB / 30 GiB for TB 2.1's heavier tasks.

Follow-up: 4-replica scaling + SGLang A/B (5 runs each, 4h timeout, 8C/16G sandboxes)

Serving stack pass@1 (5 runs) Mean ± std Avg timeout rate Engine freezes
vLLM v0.25.1, --gdn-prefill-backend triton 61.80, 55.06, 56.18, 56.18, 57.30 57.3 ± 2.6 ~13% 0
SGLang v0.5.15.post1 (defaults + qwen3 reasoning parser) 56.18, 56.18, 60.67, 51.69, 56.18 56.2 ± 3.2 ~17% 0
  • Scaling 2→4 replicas doubled fast-phase throughput (~50% more tokens/episode) but did not move pass@1 or the timeout rate: the benchmark's deep tail is bounded by the per-task wall-clock budget, not aggregate serving capacity.
  • Both stacks ran ~20h each under full-benchmark load with zero engine freezes — SGLang out of the box (after a readiness-probe adjustment: its /health generates a token and can exceed a 1s probe timeout, flapping healthy pods out of the Service; set probe timeoutSeconds ≥ 15), vLLM with the Triton GDN kernel noted above. Scores are statistically indistinguishable; vLLM sustained ~40% more tokens/episode at lower timeout rates on this workload.

Harness validation: gold-patch (oracle) run — 89/89

Ran all 89 TB 2.1 tasks with Harbor's OracleAgent, which uploads each task's reference solution/ and runs it instead of generating with a model. This exercises the sandbox backend, upload_dir, exec and the verifier end-to-end with the model removed from the equation, so the expected score is a clean sweep.

Result: 89/89 (reward 1.0 on every task), 22 min wall clock, zero agent timeouts.

Getting there surfaced one real bug in this integration, now fixed in this PR:

default_exec_timeout_s was silently truncating verification. Harbor's verifier calls environment.exec() without timeout_sec, so this default — not the task's declared [verifier] timeout_sec — is what bounds verification. The old values (300 in the environment, 600 in the TB config) were justified by reasoning only about Terminus-2's short tmux plumbing, which missed the verifier entirely. 87 of the 89 TB 2.1 tasks declare a verifier budget above 600s (median 900, max 12000), so verification was being killed mid-run and the task scored 0 instead of failing loudly.

exec cap gold-patch score what changed
600s 83/89 compile-compcert killed at 602s; query-optimize verifier killed at 604s → RewardFileNotFoundError
1200s 83/89 query-optimize fixed; compile-compcert still killed
1800s 85/89 both fixed
12000s 85/89 no further gain

Longest single command observed was 1384s, so 1800s is the smallest value that costs nothing — now the default in both the environment and this config. Because this bug suppressed slow-verifying tasks, previously reported pass@1 numbers are conservative.

The remaining 4 gold-patch failures were not harness issues and are deliberately not fixed here — three were upstream drift (a dataset host that refuses this network's egress; RcppParallel 6.0.0 requiring cmake, published 4 days before the run; planarity 1.0.0 renaming attributes pyknotid reads, published 4 weeks before) and one was macOS ._* sidecar files in our local dataset copy. Patching those reference solutions would only change an oracle run — solution/ is never uploaded for a model agent — so it cannot affect a scored eval, and fixing them for model runs would mean altering the task environment rather than the solution. Worth noting for anyone reproducing: sandbox resources were not implicated — the config's uniform 8 CPU / 16 GiB / 30 GiB is a strict superset of what all 89 task.toml files declare (max 4 CPU / 8 GiB / 10 GiB).

Final: card-parity cohort (prefix cache ON + Triton GDN kernel, 3h agent timeout — model-card conditions)

Run pass@1 agent-timeout rate
1 60.67% (54/89) 10.1%
2 61.80% (55/89) 9.0%
3 57.30% (51/89) 13.5%
4 66.29% (59/89) 11.2%
5 59.55% (53/89) 13.5%
Mean 61.1 ± 3.3 (model card: 59.3) 11.5%

🤖 Generated with Claude Code

hemildesai and others added 2 commits July 14, 2026 19:07
…ch 2.1 opensandbox config

Adds NemoGymSandboxEnvironment, a harbor BaseEnvironment implementation backed
by the nemo_gym.sandbox API (AsyncSandbox), selected purely via
harbor_environment_import_path — no changes to the harbor_agent server code.
Any Gym sandbox provider (opensandbox, docker, daytona, apptainer, ecs_fargate)
becomes a Harbor execution backend through config.

- task.toml resources map to SandboxSpec.resources; per-task Docker images are
  required (no Harbor-side builds); tar-based upload_dir/download_dir with
  per-file fallback; sandboxes are always terminated on stop() to avoid
  leaking remote resources; opt-in allow_unenforced_internet_isolation.
- harbor_agent_opensandbox.yaml runs Terminal-Bench 2.1 (89 tasks, pinned
  harbor-framework/terminal-bench-2-1 checkout via
  TERMINAL_BENCH_2_1_TASKS_DIR) with the stock Terminus-2 harness through the
  opensandbox provider in server-proxy mode, mirroring the Qwen3.6 model-card
  harness settings (3h agent timeout, 32 CPU / 48 GB per task, 256K ctx /
  80K max output).
- prepare_terminal_bench_2_1.py regenerates the checked-in 89-row benchmark
  input JSONL from the pinned tasks repo.

Note: signed off (DCO) but not GPG-signed — no interactive pinentry available
in this session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…40 compatibility

openai 2.40+ made InputTokensDetails.cache_write_tokens required; extract_usage
built the details dict without it, so every /run 500'd at HarborVerifyResponse
validation when running against a live model server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

hemildesai and others added 13 commits July 14, 2026 20:14
…r TB2.1 config

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
… episodes

The 120s default httpx timeout caused Terminus-2 to abandon and re-send
long thinking-mode generations, piling duplicate in-flight requests onto the
model server under concurrency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…ral storage to 30GiB

- Adopt the mini_swe_agent_2 config style: the opensandbox provider is now
  referenced via `sandbox_provider: ${sandbox}` and its definition comes from
  the shipped provider config, included with `--config
  nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml`. Swapping
  providers is now swapping that --config path, with no edits to this file.
- Add `override_storage_mb: 30720` (30 GiB). Root cause of mid-run sandbox 500s:
  disk-heavy TB tasks (image builds, dataset downloads) overran the 10 GiB
  task.toml ephemeral-storage default and the kubelet evicted the pod, so the
  server's proxy calls to that pod's execd 500'd. Raising disk_gib via the
  sandbox API (SandboxSpec.resources.disk_gib -> ephemeral-storage limit)
  fixes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Raise the agent request-semaphore concurrency to 89 so all Terminal-Bench 2.1
tasks run at once (paired with gym eval --concurrency 89 and vLLM
--max-num-seqs 96).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…e full concurrency

Each Harbor job is dispatched as a Ray task; Ray's default 1 CPU/task capped
in-flight jobs at the driver's physical CPU count (e.g. 24), so --concurrency 89
only ran ~24 tasks at once. Add harbor_ray_task_num_cpus (default None = current
behavior); jobs are I/O-bound (remote sandbox + model server), so a fractional
value (0.25 in the TB2.1 config) packs all 89 onto a 24-CPU driver. Verified:
89 concurrent sandboxes, Ray available CPU ~1.75/24.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…del-card 59.3)

Full Terminal-Bench 2.1 run complete: 53/89 passed (mean reward 0.5955),
essentially matching the Qwen3.6-27B model card's Terminal-Bench 2.0 = 59.3.
Traces for all 89 tasks captured on Lustre (rollouts + per-trial trajectory,
asciinema recordings, verifier artifacts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Confirmed all per-trial trajectories are ATIF-v1.5. Running TB2.1 5x
sequentially on the 2-GPU deployment for the model-card 5-run average
(after a 4-GPU num-repeats attempt hit GB300 scheduling limits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Root causes of the wedges (user insight): reusing one driver + abrupt kills
accumulate stuck state. Two mechanisms: (1) killing a run leaves its in-flight
LLM requests queued on vLLM; (2) short-timeout health probes abandon connections
that vLLM keeps processing, piling up until the engine stalls. Fix: each run is
its own k8s Job (fresh Ray/servers) with a shared Lustre venv; runs complete
without killing; no POST-probing of vLLM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…am drops

OpenSandbox exec streams a command's output over one long-lived SSE connection
through the server proxy. When that connection crosses a load balancer with a
TCP idle/stream-duration cap (e.g. an AWS NLB fronting an istio east-west
gateway, default 350s), a command that runs a while gets its stream silently
reaped and the client hangs (~4h) since the SSE read timeout is disabled.

Add OpenSandboxOperationConfig.background_exec (default False): submit the
command in the background, then issue short get_command_status / get_background_
command_logs polls instead of holding one stream open. No single request is
long enough to be reaped, so it is robust across LB idle/duration limits. Also
default command_retries 0->3 in the shipped config (recovers RST-family drops)
and enable background_exec there since OpenSandbox exec always traverses the
server proxy. Root cause confirmed via OpenSandbox issues #1277/#507/#96 and a
mesh trace (sbx east-west NLB, 350s idle, no override).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Background exec polled at a fixed interval added that full interval of latency to
every short command. Agents issue many sub-second commands (tmux keystrokes,
pane captures), so a multi-second poll crawled the turn loop and left the GPU
idle. Poll fast initially (background_poll_initial_s=0.25) and back off toward
the cap, so short commands are detected almost immediately while long ones don't
spam requests; frequent early polls also keep the connection warm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
… for cross-cluster LB paths

- provider: disable_connection_pooling option — pass a no-keepalive httpx
  transport to the SDK so every request opens a fresh connection; an LB
  between client and server that silently reaps idle pooled connections
  (observed as CLOSE_WAIT zombies wedging exec) can never cause a reaped
  socket to be reused. Enabled in the shipped opensandbox.yaml.
- provider: hard wall-clock cap around the whole exec dispatch (submit +
  poll + retries) as a backstop against pathological wedges.
- config: foreground SSE exec is the default again (background_exec:
  false); background mode multiplies short cross-cluster requests and
  churns connections without fresh-connection transport.
- harbor env: default_exec_timeout_s 14400 -> 300 (per-command bound, not
  a trial bound; a trial-length value lets one stuck exec hang the task
  until the trial deadline). TB2.1 config uses 600.
- harbor config: nemo_model_server_timeout_sec 7200 -> 2400 so a serving
  stack that silently drops an in-flight request self-heals in <=40min.
- harbor app: await the Ray ObjectRef directly instead of
  asyncio.to_thread(ray.get, ...) — the default executor's thread cap
  queued finished results behind long-blocking gets, delaying /run
  responses in waves.
- tests: disable_connection_pooling transport, exec hard-cap timeout,
  uncapped-exec path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Exec-path 500s with no server-side error are consistent with sandbox
containers dying under load; roomier limits keep execd alive on
memory/CPU-hungry tasks (also matches the resources used for the final
benchmark runs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Sandbox containers see the host's full core count (cgroup cpu limits
don't shrink nproc), so build tools inside tasks fan out to e.g. 96
workers and get CFS-throttled. With cpu_pin_enabled, every Harbor exec
is prefixed with a fail-open taskset pin to a random contiguous
core block sized by the task's cpu count: nproc inside reports the
real budget (tools right-size their parallelism) and co-resident
sandboxes spread across different core blocks instead of stacking on
cores 0..N-1. The tmux server Terminus-2 starts is launched via exec,
so the whole agent session inherits the affinity; the verifier exec
gets its own block. POSIX-sh compatible; unpinned when taskset is
missing or the host has <= width cores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
@hemildesai
hemildesai marked this pull request as ready for review July 27, 2026 14:40
hemildesai and others added 8 commits July 27, 2026 07:45
…ers)

Resolves the .gitignore conflict by keeping both sides' entries.

SECURITY: EXPERIMENT_LOG.md was committed and pushed to this public repo in
6ac1c22, 369071e and 70cd889. It contains internal infrastructure
identifiers (AWS account IDs, EKS cluster ARNs/names, internal mesh
endpoints). This commit removes it from tracking and adds it to .gitignore
alongside OPENSANDBOX_SSE_DROP_REPORT.md so it cannot be re-added. The local
working copy is preserved.

NOTE: removing the file at the branch tip does NOT purge it from git history;
the blobs remain reachable from the three commits above and require a history
rewrite to fully remove.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
- Lint check: apply ruff format to the files this PR touches.
- secrets-detector: PINNED_COMMIT is the git commit SHA the Terminal-Bench
  2.1 dataset checkout is pinned to, not a credential. Marked with the
  detector's documented inline mitigation (pragma: allowlist secret).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
run() now awaits the Ray ObjectRef directly instead of going through
asyncio.to_thread(ray.get, ...), but _harbor_run_mocks still patched
asyncio.to_thread, so the mocked trial dir was never returned: four
TestApp::test_run_* cases saw an empty trajectory (reward 0.0 / empty
output), and test_run_failed_execution passed only incidentally because
awaiting a MagicMock raises.

Mock .remote(...) to return a fresh awaitable per call instead, covering
both the bare and .options(num_cpus=...) dispatch paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Reverts the direct `await future` on the Ray ObjectRef, restoring the
original result-consumption path. app.py's remaining delta vs main is just
the harbor_ray_task_num_cpus knob and its runner.options(...) dispatch.

Also reverts the test mock that had been realigned to the awaitable path;
_harbor_run_mocks patches asyncio.to_thread again, matching the code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
…cate verification

Harbor's verifier calls environment.exec() WITHOUT timeout_sec, so
default_exec_timeout_s -- not the task's declared [verifier] timeout_sec --
is what actually bounds verification. The previous defaults (300 in the
environment, 600 in the TB 2.1 config) were justified by reasoning only about
Terminus-2's short tmux plumbing, which missed the verifier (and the oracle
agent) entirely.

Impact: 87 of the 89 TB 2.1 tasks declare a [verifier] timeout_sec above 600
(median 900, max 12000), so verification was being killed mid-run and the task
scored 0 rather than erroring -- a silent correctness bug that understates
pass@1.

Measured with a gold-patch (oracle) run of all 89 tasks, which executes each
task's reference solution and should score 89/89:
  600s   -> 83/89  (compile-compcert killed at 602s; query-optimize's verifier
                    killed at 604s -> RewardFileNotFoundError)
  1200s  -> 83/89  (query-optimize fixed; compile-compcert still killed)
  1800s  -> 85/89  (both fixed)
  12000s -> 85/89  (no further gain)
Longest single command observed: 1384s. 1800s is therefore the smallest value
that costs nothing, and is what both the env default and the TB config now use.

Adds a regression test asserting the default stays generous enough for
verifier budgets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
… runs

A gold-patch (oracle) run executes each task's reference solve.sh and should
score 89/89, which makes it the cleanest end-to-end check of the harness. Three
TB 2.1 tasks fail that check for reasons outside the harness -- the task images
are pinned in time while their unpinned dependencies and a dataset host are not:

  build-cython-ext   planarity 1.0.0 renamed the drawplanar attrs pyknotid 0.5.3
                     reads verbatim -> KeyError: 'pos'
  mcmc-sampling-stan RcppParallel 6.0.0 switched to a CMake build; the image has
                     no cmake -> rstan never installs
  caffe-cifar-10     the CIFAR-10 URL redirects to a host that TCP-RSTs some
                     networks; get_cifar10.sh has no error handling, so caffe
                     aborts on a missing file

Adds task_patches/ plus an opt-in --apply-task-patches flag. Off by default, so
the benchmark is prepared exactly as published.

Integrity: patches may touch '*/solution/solve.sh' and nothing else -- enforced
before git apply runs, since patching tests/ or task.toml would change what is
graded. solution/ is uploaded only by the oracle agent and never by a model
agent, so scored model runs are unaffected either way. A patch that does not
apply raises rather than being skipped: the checkout is pinned, so a failure
means the pin moved.

The CIFAR-10 mirror is md5-verified against the official tarball. README.md
records the evidence for each patch and when to drop it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
test_environment.py sat next to the module it covers; move it alongside the
other harbor_agent suites in tests/. Collection is unchanged (114 tests) since
the runner collects the whole server directory, and the test's imports are
absolute, so this is purely a layout change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Reverts 0a55fed. The patches only modified solution/solve.sh, which is
uploaded by Harbor's OracleAgent and never by a model agent, so they could
only ever affect gold-patch validation runs -- they cannot change a scored
eval. That makes them dead weight in this PR.

The underlying breakages are unaffected by this revert and still apply to
model runs (a blocked dataset host on caffe-cifar-10; missing cmake on
mcmc-sampling-stan; planarity drift on build-cython-ext); fixing those for
scored runs would mean changing the task environment, which would change the
benchmark rather than the reference solution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
@github-actions github-actions Bot added the sla:triage-overdue Review assignment is over the one-business-day SLA label Jul 28, 2026
@hemildesai
hemildesai marked this pull request as draft July 28, 2026 19:36
@github-actions github-actions Bot removed the sla:triage-overdue Review assignment is over the one-business-day SLA label Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant