Skip to content

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

Open
rasdani wants to merge 5 commits into
feat/isolated-grading-artifactsfrom
feat/harbor-separate-verifier-envs
Open

feat(v1): Harbor separate verifier environments#2152
rasdani wants to merge 5 commits into
feat/isolated-grading-artifactsfrom
feat/harbor-separate-verifier-envs

Conversation

@rasdani

@rasdani rasdani commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2144 (feat/isolated-grading-artifacts). Review that first; this PR's base is that branch, not main.

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 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).

909 non-e2e tests, ruff check, ruff format --check, ty check verifiers 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 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 for test.sh and scoring, after the agent box is confirmed torn down.

Task.scoring_runtime is the extension point (default None = score in the agent box). RolloutRun resolves 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 by Agent.provision), stop_confirmed / teardown_confirmed on Docker and Prime, and read_bounded for 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 /tests staging, reward.json parsing with reward.txt fallback, and load-time rejection when Harbor would build tests/Dockerfile without 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

  • Introduces a VerifierConfig dataclass and HarborTask.scoring_runtime override so Harbor tasks can grade in a freshly provisioned, isolated runtime (Docker or Prime) instead of the shared agent box.
  • Adds Task.scoring_runtime hook 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.
  • Adds stop_confirmed / teardown_confirmed to base.py with implementations for Docker and Prime runtimes, ensuring the agent box is fully torn down before the verifier box is provisioned.
  • Adds read_bounded to Runtime for size-capped file reads, and _reward_json to HarborTask to parse structured reward.json rewards before falling back to reward.txt.
  • Adds provision_runtime async context manager in runtimes/init.py for automatic start/stop lifecycle, used in both the refactored agent provisioning and verifier box setup.
  • Risk: rollouts that request a separate scorer must own the agent runtime and have no shared tool servers — mismatched configurations raise at init time.

Macroscope summarized ef5503b.

rasdani and others added 5 commits July 28, 2026 07:13
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>
@macroscopeapp

macroscopeapp Bot commented Jul 28, 2026

Copy link
Copy Markdown

Approvability

Verdict: 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; "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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=(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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.

2 participants