Skip to content

feat(v1): Harbor separate verifier environments - #2152

Open
rasdani wants to merge 10 commits into
mainfrom
feat/harbor-separate-verifier-envs
Open

feat(v1): Harbor separate verifier environments#2152
rasdani wants to merge 10 commits into
mainfrom
feat/harbor-separate-verifier-envs

Conversation

@rasdani

@rasdani rasdani commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

#2144 is merged. This PR is rebased onto current main and 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.py with a pytest_runtest_makereport hook forcing passed scores 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, or None for today's behaviour. RolloutRun resolves it in open(), 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 primitivesprovision_runtime (the start/stop context manager Agent.provision already had inline), stop_confirmed/teardown_confirmed, and read_bounded. Confirmed teardown is the point of the second one: teardown is 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 own resolve_task_verifier_mode / resolve_effective_verifier_env_config, a VerifierConfig on HarborData, and verifier_runtime_config deriving the grading box from the agent's.

reward.json — Harbor's separate verifiers write it, not reward.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]:

task.toml Harbor's verifier image here
environment_mode = "separate", no [verifier.environment] the task's own image supported
[verifier.environment] with docker_image that image supported
[verifier.environment] without docker_image built from tests/Dockerfile rejected at load

The 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_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 and the /logs/artifacts/ convention directory cross over. That's Harbor's own model — Trial._run_separate_verifier uploads artifacts and nothing else, with no full-tree copy anywhere — so #2144's collect/restore and 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 in finalize, in the agent's box, producing exactly the files that then travel.
  • The verifier's network mode is enforced, not warned about, using the allow/block policy the runtimes already have. allow_internet = false means 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.
  • 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.
  • A failed tests/ staging now raises instead of leaving test.sh missing and scoring 0.
  • A reward-less reward.json object 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_verifier forces shared grading when a sandbox per task is too expensive. Not a Harbor knob; same family as ignore_timeouts / ignore_dockerfile.

Two semantics reviewers should check

  • Provisioning now runs inside _scoring_timeout, which Harbor sets from verifier.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.
  • This reverses feat(v1): grade in an isolated box, with Harbor-native artifacts #2144's stated position that the env owns sandbox topology and that this hook shouldn't exist. That holds for the agentic judge, which composes seats it already owns. It doesn't extend to a taskset's own programmatic grader: task.score runs 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.toml files declare zero environment_mode or [verifier.environment]; neither Harbor Hub (80 datasets) nor laude-institute/harbor-datasets (103) ships one. So: local fixtures for each case, driven through the real finalizescoring_runtimescore path against live Docker containers.

PASS  shared grades in the agent's box
PASS  shared still sees the agent's leftovers
PASS  fresh_copy gets its own box
PASS  declared_image gets its own box
PASS  agent box gone before grading (fresh_copy)
PASS  agent box gone before grading (declared)
PASS  declared artifact travelled (fresh_copy)
PASS  declared artifact travelled (declared)
PASS  undeclared agent file did NOT travel (fresh_copy)
PASS  undeclared agent file did NOT travel (declared)
PASS  stale /tests wiped in the verifier box
PASS  declared verifier image actually used          # python 3.12 vs the task's 3.11
PASS  fresh copy keeps the task image
PASS  allow_internet=false enforced                  # NET=DOWN in the verifier box
PASS  public verifier keeps network
PASS  reward.json read (shared / fresh_copy / declared)
PASS  wrong answer scores 0

Case C rejected at load with the build-and-push message; loads under --taskset.ignore-dockerfile with a warning; --taskset.ignore-separate-verifier returns verifier=None. A bad verifier image ref raises SandboxError rather than scoring 0. _reward_json checked 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 /tmp and ~/.cache/harbor, not committed (AGENTS.md).

Not verified: Prime. Everything above is Docker. PrimeRuntime.teardown_confirmed is 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 Artifact classes, TaskData.artifacts vs HarborData.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, no docker_image, Harbor building tests/Dockerfile — it looks like case C, the one this PR rejects, meaning DeepSWE would need its verifier image built and pushed first. One real task.toml would 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.toml sets [verifier].environment_mode = "separate": the agent box is torn down with confirmed removal, declared artifacts and tests/ 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; RolloutRun resolves it at open() (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. Shared provision_runtime, stop_confirmed / teardown_confirmed (Docker + Prime), and read_bounded support the handoff.

Harbor: Parses separate verifier mode via Harbor’s resolvers into VerifierConfig on HarborData; ignore_separate_verifier opts out. Rejects verifier environments that would require building tests/Dockerfile (same pull-only image rule as agent env). Reads reward.json (bounded) with fallback to reward.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

  • Harbor tasks can now declare a separate verifier environment; grading runs in a clean, freshly provisioned runtime after the agent box is torn down, with collected artifacts and test files staged into the new box.
  • Introduces VerifierConfig and parse_verifier_environment in taskset.py to parse verifier image, resources, network, and timeout from task config; the ignore_separate_verifier flag on HarborConfig forces grading back into the agent runtime.
  • Adds provision_runtime async context manager in runtimes/init.py to guarantee start/stop lifecycle, and stop_confirmed/teardown_confirmed to verify container or sandbox removal before grading begins.
  • Task.scoring_runtime in 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.
  • Risk: separate scoring serializes harness scoring and task scoring (previously concurrent), and confirmed teardown raises SandboxError if the agent container is still present, which will fail the rollout.

Changes since #2152 opened

  • Refactored network policy derivation in verifier_runtime_config function to use with_task_network_policy method [0dd56a8]
  • Changed network policy resolution in parse_verifier_environment function to use explicit phase policy or environment baseline [0dd56a8]
  • Reworked PrimeRuntime.teardown_confirmed method to handle already-deleted sandboxes, poll for termination confirmation, and use exponential backoff [5fe32f4]
  • Added _is_not_found utility function to detect HTTP 404 status codes in exception chains [5fe32f4]

Macroscope summarized 7377697.

@macroscopeapp

macroscopeapp Bot commented Jul 28, 2026

Copy link
Copy Markdown

Approvability

Verdict: 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.

Comment thread verifiers/v1/tasksets/harbor/taskset.py
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
Comment thread verifiers/v1/tasksets/harbor/taskset.py
Comment thread docs/v1/harbor.md Outdated
Comment thread verifiers/v1/tasksets/harbor/taskset.py
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
Base automatically changed from feat/isolated-grading-artifacts to main July 31, 2026 00:34
Comment thread verifiers/v1/artifacts.py
Comment thread verifiers/v1/envs/agentic_judge/env.py
Comment thread docs/v1/harbor.md Outdated
Comment thread verifiers/v1/tasksets/harbor/taskset.py
Comment thread verifiers/v1/envs/agentic_judge/env.py
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
@hallerite
hallerite force-pushed the feat/harbor-separate-verifier-envs branch from 56a8704 to d3ee132 Compare July 31, 2026 00:40
Comment thread verifiers/v1/utils/git.py
Comment thread verifiers/v1/runtimes/docker/__init__.py
Comment thread verifiers/v1/utils/git.py
Comment thread verifiers/v1/envs/agentic_judge/env.py
Comment thread verifiers/v1/artifacts.py
rasdani and others added 7 commits July 31, 2026 02:44
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>
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
Comment thread verifiers/v1/runtimes/prime.py
hallerite
hallerite previously approved these changes Jul 31, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5fe32f4. Configure here.

@hallerite
hallerite self-requested a review July 31, 2026 01:31
@hallerite
hallerite dismissed their stale review July 31, 2026 01:33

clanker approved without my consent

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.

3 participants