feat(v1): Harbor separate verifier environments - #2152
Conversation
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. #2144 argued the env should own sandbox topology and that this hook 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>
ApprovabilityVerdict: Needs human review Unable to check for correctness in ef5503b. This PR introduces a substantial new feature for running verifiers in isolated sandbox environments, with new runtime lifecycle methods (stop_confirmed, teardown_confirmed), changes to the scoring flow, and integration with Harbor's separate verifier mode. The scope and runtime implications warrant human review. You can customize Macroscope's approvability policy. Learn more. |
| runtime_policy | ||
| ) is not type(config): | ||
| raise TaskError( | ||
| "a separate verifier needs a container runtime matching the run's policy; " |
There was a problem hiding this comment.
why do we name config policy here?
| if environment.os != TaskOS.LINUX or unsupported: | ||
| raise ValueError( | ||
| f"{task_dir.name}: verifier environment declares " | ||
| f"{unsupported or environment.os}, which a single-container runtime " |
There was a problem hiding this comment.
doesn't have to do anything with single-container, no?
| # A declared environment states its own resources; what it leaves out is the | ||
| # run's default, not the agent task's. A fresh copy is the task's environment, | ||
| # so it keeps whatever the agent box resolved to. | ||
| resources=( |
There was a problem hiding this comment.
what about time? verification also has a time config, which we now ignore by default (+ supports multiplier) but we need to wire it up here
Stacked on #2144 (
feat/isolated-grading-artifacts). Review that first; this PR's base is that branch, notmain.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 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).909 non-e2e tests,
ruff check,ruff format --check,ty check verifiersall 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 core rollout scoring topology and sandbox lifecycle (confirmed teardown before grading); incorrect isolation or timeout handling could mis-score rollouts or leak agent influence on graders.
Overview
Adds isolated grading for Harbor tasks that set
[verifier].environment_mode = "separate": the agent runs in one sandbox, then only declared artifacts move into a second box fortest.shand scoring, after the agent box is confirmed torn down.Task.scoring_runtimeis the extension point (defaultNone= score in the agent box).RolloutRunresolves it before boot, rejects borrowed runtimes or shared MCP when a separate box is required, and runs harness scoring on the agent box before entering the task’s scoring context manager so metrics still reflect the agent session. Verifier provisioning counts against_scoring_timeout.Runtime helpers: shared
provision_runtime(used byAgent.provision),stop_confirmed/teardown_confirmedon Docker and Prime, andread_boundedfor capped reads inside the box.Harbor: stops rejecting
[verifier.environment]; parses separate mode via Harbor’s resolvers,VerifierConfig,ignore_separate_verifier, verifier network policy on the grading box, artifact collect/restore + wiped/testsstaging,reward.jsonparsing withreward.txtfallback, and load-time rejection when Harbor would buildtests/Dockerfilewithout a pullable image.Docs updated for separate verifiers and remaining gaps (shared-verifier network policy, Dockerfile builds).
Reviewed by Cursor Bugbot for commit ef5503b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add separate verifier environments for Harbor task scoring
VerifierConfigdataclass andHarborTask.scoring_runtimeoverride so Harbor tasks can grade in a freshly provisioned, isolated runtime (Docker or Prime) instead of the shared agent box.Task.scoring_runtimehook to task.py and scoring path in rollout.py: harness scoring runs on the agent box first, then task scoring runs inside the separate verifier box within the scoring timeout.stop_confirmed/teardown_confirmedto base.py with implementations for Docker and Prime runtimes, ensuring the agent box is fully torn down before the verifier box is provisioned.read_boundedtoRuntimefor size-capped file reads, and_reward_jsontoHarborTaskto parse structuredreward.jsonrewards before falling back toreward.txt.provision_runtimeasync context manager in runtimes/init.py for automatic start/stop lifecycle, used in both the refactored agent provisioning and verifier box setup.Macroscope summarized ef5503b.