feat(v1): Harbor separate verifier environments - #2152
Conversation
ApprovabilityVerdict: Needs human review This PR introduces a substantial new feature (separate verifier environments) with new runtime lifecycle methods, changes to the core scoring flow, and significant new configuration parsing logic. Additionally, there is an unresolved high-severity bug regarding Prime verifier config validation that needs attention. You can customize Macroscope's approvability policy. Learn more. |
56a8704 to
d3ee132
Compare
Three runtime primitives an isolated grading box needs, none of which have a caller yet. `provision_runtime` is the start/stop context manager `Agent.provision` already had inline, lifted so a taskset can provision a box without an agent seat. `Agent.provision` now delegates to it, so "start() inside the try" lives in one place. `stop_confirmed` / `teardown_confirmed` are the strict counterpart to `stop` / `teardown`. Teardown is best-effort on purpose: a leaked container is a billing problem, and failing a rollout over one would be worse. But a caller that provisions a second box needs the first one *gone* first — an agent can leave a background process running past its final turn, and a grading box that comes up alongside it is not isolated from it. Docker asks the daemon whether the container is still listed rather than trusting a suppressed `rm --force`; Prime raises on a failed delete instead of logging it. Runtimes that can't prove removal inherit a `NotImplementedError`. `read_bounded` truncates in the box via `head -c` rather than after the transfer, for reading a scoring input whose size we can't assume. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`RolloutRun` has always handed `task.score` the agent's own runtime, so a taskset's grader runs where the agent had root. That is reachable: an agent-planted `/testbed/conftest.py` with a `pytest_runtest_makereport` hook forcing `passed` scores 1.0 with no fix applied (verified on r2e-gym). Hiding the grading script only moves the target — pytest's plugin surface alone offers conftest, sitecustomize, ini files, and the venv's own entry points. `Task.scoring_runtime` returns a context manager to score inside, or None to keep today's behaviour. The task owns whatever has to survive the move, via collect/restore while its own box is still up. Three deliberate choices in the rollout: - Resolved in `open()`, before the box boots, so a task that scores elsewhere but borrowed its runtime or has shared tool servers fails before anything is provisioned rather than after a full agent run. - Harness and task scoring serialize instead of gathering. The harness's metrics describe the agent's session, so they are read off the agent's box before the task's context manager can tear it down. - Provisioning happens inside the scoring deadline. A grading box that can't be reached in time raises a scoring timeout; it must never become reward 0, which would look like a failed attempt. Harness cleanup is skipped when the runtime is already stopped, since reaching a second box means the first one is gone. should not exist. That holds for the agentic judge, which composes seats it already owns. It does not extend to a taskset's own programmatic grader: `task.score` runs inside the rollout, where no env can reach it. No caller yet — Harbor is the first, in the next commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Harbor tasks can declare that their verifier runs in its own container (`[verifier].environment_mode`, `[verifier.environment]`). We ignored the first and rejected the second, so a task that asked for isolation was graded in the box the agent had just had root in. Harbor resolves the verifier's environment as `[verifier.environment]` if declared, else a deep copy of `[environment]`. That gives three cases, and only the third is a problem: - mode-only separate → the task's own image, so a second box is all it takes; - a declared environment with a `docker_image` → pull that instead; - a declared environment *without* one → Harbor builds the verifier image from `tests/Dockerfile`. Verifiers pulls and never builds, so this is rejected with a message saying to build and push the image and name the ref. `ignore_dockerfile` remains the one escape hatch, and now warns, because falling back to the agent's image runs the verifier somewhere the task never declared. Only declared artifacts cross over, which is Harbor's own model (`Trial._run_separate_verifier` uploads artifacts and nothing else) — so #2144's `collect`/`restore` and its 32 MB budget carry this unchanged. Artifacts are restored before `tests/` is staged, and `/tests` is wiped first: an artifact entry pointing into `/tests` would otherwise hand the agent the grader's own scripts, and a fresh container of the task image can ship a stale `/tests` of its own. `[[verifier.collect]]` keeps working here. The hooks run in `finalize`, in the agent's box, producing exactly the files that then travel — the two compose, so unlike #2067 there is no need to reject them. Two smaller changes: - The verifier's declared network mode is enforced rather than warned about, via the `allow`/`block` policy the runtimes already have. `allow_internet = false` means the grader really has no network, which is the point of declaring it. - A failed `tests/` staging now raises instead of leaving `test.sh` missing and scoring 0. A false zero reads as a failed attempt. `--taskset.ignore-separate-verifier` forces shared grading when a sandbox per task is too expensive. Design and prior art: xeophon in #2067, rewritten against #2144. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Harbor's separate verifiers write `/logs/verifier/reward.json`, not `reward.txt`. Without reading it every separate-mode task would score 0 — a false failure, indistinguishable from a real one. Two shapes, both of Harbor's: a bare number, or an object of numbers where a `reward` key is the scalar and any others are extra metrics that v1 records per key. Anything else — a string value, a bool, a list, malformed JSON, no file — falls through to `reward.txt`, so shared mode behaves exactly as before. Read through `read_bounded`: a grading input has no size we can assume. Checked against 14 payload shapes with a stubbed runtime (scratch script, not committed, per AGENTS.md). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d3ee132 to
7377697
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5fe32f4. Configure here.
| updates |= verifier.resources.model_dump(exclude_none=True) | ||
| if verifier.workdir is not None: | ||
| updates["workdir"] = verifier.workdir | ||
| return type(config).model_validate({**config.model_dump(), **updates}) |
There was a problem hiding this comment.
Prime verifier omits required VM
High Severity
verifier_runtime_config can build a Prime verifier with a restricted policy (allow_internet = false / allowlist) while leaving vm=False from an unrestricted agent config. PrimeConfig then rejects that combination during model_validate, so the rollout fails in open() before the agent runs—even though Docker handles the same task fine.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5fe32f4. Configure here.


#2144 is merged. This PR is rebased onto current
mainand now contains only the Harbor separate-verifier stack.Wires up Harbor's
[verifier].environment_mode/[verifier.environment], which we currently ignore and hard-reject respectively — so a task that asks to be graded away from the agent is graded inside the box the agent just had root in.Design and prior art are @xeophon's, from #2067. This is that work rewritten against #2144 rather than merged with it; see Why not merge #2067 at the end.
Why this is a correctness gap, not a nicety
Verified on r2e-gym while testing something else: an agent-planted
/testbed/conftest.pywith apytest_runtest_makereporthook forcingpassedscores reward 1.0 with no fix applied. It never touches the grading script or the test files, so hiding those does nothing. Pytest's plugin surface alone offers conftest,sitecustomize, ini files and the venv's own entry points — any grader running in the agent's box is reachable, and patching individual paths is whack-a-mole.What lands
Task.scoring_runtime(task.py,rollout.py) — returns a context manager to score inside, orNonefor today's behaviour.RolloutRunresolves it inopen(), before the box boots, so a task that scores elsewhere but borrowed its runtime or has shared tool servers fails immediately rather than after a full agent run. Harness and task scoring stop being gathered: the harness's metrics describe the agent's session, so they're read off the agent's box before the task's context manager can tear it down.Runtime primitives —
provision_runtime(the start/stop context managerAgent.provisionalready had inline),stop_confirmed/teardown_confirmed, andread_bounded. Confirmed teardown is the point of the second one:teardownis best-effort because a leaked container is a billing problem, but a caller standing up a grading box needs the first box gone — an agent can leave a background process running past its final turn.Harbor wiring (
tasksets/harbor/taskset.py) — mode resolution via Harbor's ownresolve_task_verifier_mode/resolve_effective_verifier_env_config, aVerifierConfigonHarborData, andverifier_runtime_configderiving the grading box from the agent's.reward.json— Harbor's separate verifiers write it, notreward.txt. Without this every separate-mode task would score 0, which reads as a failed attempt.Three cases, and why one is rejected
Harbor resolves the verifier environment as
[verifier.environment]if declared, else a deep copy of[environment]:environment_mode = "separate", no[verifier.environment][verifier.environment]withdocker_image[verifier.environment]withoutdocker_imagetests/DockerfileThe third is rejected because verifiers pulls images and never builds them — the same rule
[environment]already follows. The error says to build and push the image and name the ref.ignore_dockerfileremains the one escape hatch and now warns, because falling back to the agent's image runs the verifier somewhere the task never declared.Only declared artifacts and the
/logs/artifacts/convention directory cross over. That's Harbor's own model —Trial._run_separate_verifieruploads artifacts and nothing else, with no full-tree copy anywhere — so #2144'scollect/restoreand its 32 MB budget carry this unchanged, and #2067's parallel artifact layer and 256 MB budget are not ported.Smaller decisions worth a look
[[verifier.collect]]keeps working in separate mode. feat(v1): support Harbor's separate verifier environments #2067 rejects it. It composes fine: the hooks already run infinalize, in the agent's box, producing exactly the files that then travel.allow/blockpolicy the runtimes already have.allow_internet = falsemeans the grader really has no network — verified below. feat(v1): support Harbor's separate verifier environments #2067 warns instead; I think a declaration that silently doesn't hold is worse than one that occasionally bites.tests/is staged, and/testsis wiped first. An artifact entry pointing into/testswould otherwise hand the agent the grader's own scripts, and a fresh container of the task image can ship a stale/tests.tests/staging now raises instead of leavingtest.shmissing and scoring 0.reward-lessreward.jsonobject sums.{"correctness": 1.0, "style": 0.25}records two rewards and totals 1.25 — v1's existing keyed-reward convention, and feat(v1): support Harbor's separate verifier environments #2067's behaviour, but it can exceed 1.0. Flagging rather than changing.ignore_separate_verifierforces shared grading when a sandbox per task is too expensive. Not a Harbor knob; same family asignore_timeouts/ignore_dockerfile.Two semantics reviewers should check
_scoring_timeout, which Harbor sets fromverifier.timeout_sec(default 600s). A grading box that can't be reached in time raises a scoring timeout; it must never become reward 0. Verified below with a bad image ref.task.scoreruns inside the rollout, where no env can reach it. The conftest result above is the argument.Verification
No public dataset exercises this feature. 104,131 cached
task.tomlfiles declare zeroenvironment_modeor[verifier.environment]; neither Harbor Hub (80 datasets) norlaude-institute/harbor-datasets(103) ships one. So: local fixtures for each case, driven through the realfinalize→scoring_runtime→scorepath against live Docker containers.Case C rejected at load with the build-and-push message; loads under
--taskset.ignore-dockerfilewith a warning;--taskset.ignore-separate-verifierreturnsverifier=None. A bad verifier image ref raisesSandboxErrorrather than scoring 0._reward_jsonchecked against 14 payload shapes (scalar, map, map-with-reward, empty, string values, bools, list, malformed, missing).910 local tests pass (63 credential-gated tests skipped);
ruff check,ruff format --check,ty check verifiers, and pre-commit are all green. Scratch scripts and fixtures live in/tmpand~/.cache/harbor, not committed (AGENTS.md).Not verified: Prime. Everything above is Docker.
PrimeRuntime.teardown_confirmedis written but unexercised, and Prime provisioning has been timing out independently — which is exactly the path where the scoring-timeout interaction matters most.Why not merge #2067
A real test merge of the two branches conflicts in 3 files / 9 hunks, with seven semantic collisions underneath: two
Artifactclasses,TaskData.artifactsvsHarborData.artifacts, two artifact transports, opposite strictness on the convention dir, and contradictory handling of[[verifier.collect]]and[verifier.environment]. #2067 is also 7 commits behind main. Resolving it would have meant deleting most of its diff, so the surviving design was rebuilt on #2144's transport instead.Open question for @xeophon
DeepSWE v1.1 isn't on Harbor Hub, in the public dataset repo, or in the local cache, so I couldn't read one of its
task.tomls. From your two review comments on #2067 —[verifier.environment]present,allow_internet = false, nodocker_image, Harbor buildingtests/Dockerfile— it looks like case C, the one this PR rejects, meaning DeepSWE would need its verifier image built and pushed first. One realtask.tomlwould settle it.🤖 Generated with Claude Code
Note
High Risk
Changes rollout scoring order and sandbox lifecycle (confirmed teardown can fail the rollout; verifier provisioning counts against scoring timeout) on security-critical grading paths; Prime confirmed teardown is implemented but not exercised in the PR’s verification.
Overview
Adds isolated Harbor grading when
task.tomlsets[verifier].environment_mode = "separate": the agent box is torn down with confirmed removal, declared artifacts andtests/are staged into a fresh verifier runtime, and scoring runs there instead of in the compromised agent environment.Framework:
Task.scoring_runtime()lets tasks supply an async context manager for the grading box;RolloutRunresolves it atopen()(fails fast if the rollout borrows the runtime or uses shared MCP tools), runs harness scoring on the agent box then task scoring in the separate box serially under the scoring timeout, and skips harness cleanup when the agent runtime is already stopped. Sharedprovision_runtime,stop_confirmed/teardown_confirmed(Docker + Prime), andread_boundedsupport the handoff.Harbor: Parses separate verifier mode via Harbor’s resolvers into
VerifierConfigonHarborData;ignore_separate_verifieropts out. Rejects verifier environments that would require buildingtests/Dockerfile(same pull-only image rule as agent env). Readsreward.json(bounded) with fallback toreward.txt. Docs cover separate verifiers, stricter collect-hook failures, and remaining gaps.Reviewed by Cursor Bugbot for commit 5fe32f4. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add separate verifier environments for Harbor task grading
VerifierConfigandparse_verifier_environmentin taskset.py to parse verifier image, resources, network, and timeout from task config; theignore_separate_verifierflag onHarborConfigforces grading back into the agent runtime.provision_runtimeasync context manager in runtimes/init.py to guarantee start/stop lifecycle, andstop_confirmed/teardown_confirmedto verify container or sandbox removal before grading begins.Task.scoring_runtimein task.py provides an extension point for tasks to supply a separate scoring runtime; rollout.py enforces that separate scoring requires an owned runtime with no shared tool servers.SandboxErrorif the agent container is still present, which will fail the rollout.Changes since #2152 opened
verifier_runtime_configfunction to usewith_task_network_policymethod [0dd56a8]parse_verifier_environmentfunction to use explicit phase policy or environment baseline [0dd56a8]PrimeRuntime.teardown_confirmedmethod to handle already-deleted sandboxes, poll for termination confirmation, and use exponential backoff [5fe32f4]_is_not_foundutility function to detect HTTP 404 status codes in exception chains [5fe32f4]Macroscope summarized 7377697.