Skip to content

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

Merged
hallerite merged 19 commits into
mainfrom
feat/harbor-separate-verifier-envs
Aug 3, 2026
Merged

feat(v1): Harbor separate verifier environments#2152
hallerite merged 19 commits into
mainfrom
feat/harbor-separate-verifier-envs

Conversation

@rasdani

@rasdani rasdani commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Wires up Harbor's [verifier].environment_mode = "separate" and [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 (#2067).

Why

A grader running in the agent's box is attackable by the thing it grades. Verified on r2e-gym: an agent-planted /testbed/conftest.py forcing pytest's results scores reward 1.0 with no fix applied. It never touches the grading script or the tests, so hiding those does nothing — the fix is grading in a different box.

How

HarborEnv (tasksets/harbor/env.py), now the taskset's default env:

  • Shared-verifier tasks (Harbor's default, and every task that says nothing): unchanged — one agent trace, graded in the box it worked in.
  • Separate-verifier tasks: the solver plays as usual; its declared artifacts and the /logs/artifacts/ convention dir are collected while its box is alive. finalize then provisions a fresh box from the task's verifier declaration, restores those artifacts, stages tests/ fresh, runs the verifier there, and records its rewards onto the solver's trace (extra reward.json keys become metrics).
  • --env.verifier-runtime.* places the grading box (default: derived from the solver's runtime policy; a network-restricted verifier on Prime needs vm true). --env.verifier-retries (default 2) retries provisioning/grading without re-running the solve; exhausted retries fail the episode — a grading box that can't be reached never reads as reward 0.
  • Under any other env, a separate-verifier task fails closed instead of silently grading in the agent's box. --taskset.ignore-separate-verifier forces shared grading.

Which image the verifier boots follows Harbor:

task.toml verifier image
environment_mode = "separate", no [verifier.environment] fresh copy of the task's own image
[verifier.environment] with docker_image that image
[verifier.environment] without docker_image rejected at load — verifiers never builds tests/Dockerfile; build and push it, or ignore_dockerfile

Also in here

  • test.sh runs by absolute path in the configured workdir, like Harbor — real tasks grade the agent's work at $PWD, and the old cd /tests graded the wrong tree
  • reward-file cleanup moved into test staging, which raises on failure — a planted, unremovable reward file fails the rollout instead of being read as the score
  • runtime.read(path, max_bytes=...): bounded reads with the cap enforced inside the box, for grading inputs whose size nothing guarantees (replaces read_bounded)
  • one task_resources helper instead of two copies of the Harbor resource conversion

Earlier revisions did this with a Task.scoring_runtime hook, then an env verifier seat; review moved it to plain env code — see the threads.

Verification

No public dataset exercises this feature (104k cached task.tomls, Harbor Hub, and laude-institute/harbor-datasets declare zero separate-mode tasks), so: local fixtures driven through the real HarborEnv.finalize against live Docker. Fails closed outside the env; the declared artifact travels while an undeclared leftover does not; stale /tests is wiped; keyed rewards land on the solver's trace; a declared verifier image is actually used (python 3.12 vs the task's 3.11); shared grading unchanged. Full test suite, ruff, and ty green. Not verified on Prime.

🤖 Generated with Claude Code


Note

Medium Risk
Changes eval integrity and sandbox lifecycle (isolated grading, artifact restore, retries); misconfiguration could fail episodes or weaken isolation via ignore_separate_verifier, but agent auth/payments are untouched.

Overview
Harbor tasks with [verifier].environment_mode = "separate" can now be graded in a second sandbox the agent never used. The new default HarborEnv runs the solver as today, collects artifacts in the agent box, then in finalize provisions a verifier box (from [verifier.environment] or a copy of [environment]), restores artifacts, wipes and stages tests/, runs test.sh, and writes rewards onto the solver trace. --env.verifier-runtime.* and --env.verifier-retries control placement and infra retries; outside this env, separate-verifier tasks error unless --taskset.ignore-separate-verifier forces shared grading.

Parsing no longer rejects separate verifier environments: VerifierConfig, verifier_box_data, and graded_elsewhere() skip in-agent scoring when the env owns grading. Declared verifier envs without a pullable docker_image fail at load (or warn with ignore_dockerfile).

Runtime/helpers: shared provision_runtime context manager; Runtime.read(..., max_bytes=...) with in-sandbox cap (backends implement _read). Harbor scoring runs bash /tests/test.sh from the task workdir, clears stale reward files during staging (fail loud), and caps reward file reads at 1 MiB.

Reviewed by Cursor Bugbot for commit 01f0e76. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add separate verifier environment support for Harbor tasks

  • Harbor tasks can now grade in a separate verifier box after the agent box is confirmed deleted, preventing cross-contamination between agent and scoring environments.
  • A new Task.scoring_runtime hook and provision_runtime context manager in runtimes/init.py manage the verifier box lifecycle; harness scoring runs on the agent box first, then task scoring runs in the verifier box.
  • taskset.py parses a VerifierConfig from Harbor task config (image, resources, workdir, network allowlist, fresh_copy flag) and builds a matching Docker or Prime runtime config for the verifier box.
  • HarborConfig gains an ignore_separate_verifier flag to force grading in the agent box regardless of task config.
  • Runtime.read gains an optional max_bytes cap enforced via head -c inside the sandbox; reward file reads use this cap. stop_confirmed and teardown_confirmed are added to Runtime so the rollout can verify the agent box is gone before starting the verifier.
  • Risk: rollouts that borrow a runtime or use shared tool servers will raise TaskError if a separate scoring box is requested.

Changes since #2152 opened

  • Introduced a two-seat environment flow for Harbor tasks with separate verifier environments [1213839]
  • Removed task-managed separate scoring runtime infrastructure across the verifier framework [1213839]
  • Removed confirmed teardown methods from the Runtime API [1213839]
  • Added a no-operation harness implementation [1213839]
  • Removed NoopHarness and NoopHarnessConfig classes from the verifiers.v1.harnesses.noop module and eliminated their exports from the verifiers.v1.harnesses package [be6573f]
  • Replaced the verifier agent seat model in HarborEnv with direct grading execution during finalize [be6573f]
  • Removed the HarborVerifierTask class from verifiers.v1.tasksets.harbor.taskset module [be6573f]
  • Added verifier_box_data utility function to derive verifier box configuration from task data [be6573f]
  • Updated Harbor documentation to reflect the architectural change from verifier seat to env-based grading [be6573f]
  • Moved verifier box teardown outside the scoring timeout in HarborEnv.finalize method [59b9bfd]
  • Fixed error propagation in Runtime.read method when max_bytes is provided [59b9bfd]
  • Added validation constraint for verifier_retries and type checking for task parameter [59b9bfd]
  • Bounded the fallback reward file read operation in HarborTask.finalize method [01f0e76]

Macroscope summarized f29df85.

@macroscopeapp

macroscopeapp Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a significant new feature (separate verifier environments) with new configuration options, new grading workflows, and retry logic. Additionally, there is an unresolved bug report about teardown failures potentially discarding valid scores. The scope and unresolved issue warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
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 Outdated
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 Outdated
hallerite
hallerite previously approved these changes Jul 31, 2026
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
@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

Comment thread docs/v1/harbor.md
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated

@mikasenghaas mikasenghaas left a comment

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.

bunch of stuff in here that is not harbor related no?

Comment thread docs/v1/harbor.md Outdated
Comment thread verifiers/v1/runtimes/docker/__init__.py Outdated
Comment thread verifiers/v1/runtimes/base.py Outdated
Comment thread verifiers/v1/runtimes/base.py Outdated

@mikasenghaas mikasenghaas left a comment

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.

wait also, we dont use agentic judge env for this right but reward funcs? we should defo use our multi-agent machinery for this kinda stuff

edit: @xeophon tells me we do. can we add a section in the pr desc which shows how to wire up and eval config for a harbor taskset w/ agentic judge through agentic judge env and results on actually running this e2e

Comment thread verifiers/v1/tasksets/harbor/taskset.py Outdated
hallerite and others added 5 commits August 3, 2026 19:12
Resolves conflicts with main's independently-landed reward.json support:
- rollout.py: keep the separate-scoring branch, timeouts now via RolloutTimeouts
- harbor/taskset.py: adopt main's REWARD_JSON_ADAPTER (pydantic strict,
  no inf/nan) and keyed-reward metrics recording; keep the PR's bounded
  read, staging split, and separate-verifier machinery; drop the
  hand-rolled _finite_number parser superseded by the adapter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on the separate-verifier PR:

- test.sh now runs by absolute path in the runtime's configured workdir
  instead of behind a hardcoded cd /tests. Harbor's exec cwd is the
  environment's workdir, and real tasks (e.g. terminal-bench-2) grade the
  agent's work at $PWD — grading them in /tests scores the wrong tree.
- The reward-file cleanup moves into _stage_tests, which raises on failure:
  chained before test.sh with &&, a planted reward file the agent made
  unremovable would skip the tests but still be read as the score.
- parse_task and parse_verifier_environment share one task_resources
  helper instead of two copies of the Harbor-resource conversion.
- policy_base is now network_base; in this repo 'policy' means the model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback: one read API instead of two. read grows an optional
max_bytes that enforces the cap inside the box before transfer; runtimes
now implement _read (the whole-file primitive) and inherit the capped
path. Docker and Prime teardown_confirmed gain docstrings tying them to
the base contract a separate grading box relies on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reward.json's keyed form records non-reward keys as metrics (main's
behavior since #2224), not as separate rewards. The tests/Dockerfile
shortcoming now says what is actually missing: only Harbor's local image
build — a pre-built, pullable separate verifier image is supported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review consensus: map Harbor's separate-verifier pattern onto the env layer
(the isolated-judge shape) instead of a Task hook the rollout special-cases.

- HarborEnv, the taskset's own default env: solver seat plus a verifier
  seat for tasks declaring [verifier].environment_mode = "separate". The
  seat provisions a fresh box resolved from minted task data (image,
  workdir, resources, network policy — the same compilation the solver's
  box gets), restores the solver's collected artifacts, stages tests/,
  grades, and finalize records the verifier's rewards onto the solver's
  trace. Placement defaults to the solver's runtime policy;
  --env.verifier.runtime.* overrides.
- noop harness: a seat that runs no program and never calls the model,
  for rollouts whose work is entirely task hooks.
- HarborVerifierTask: minted per episode from the solver's trace; the
  verifier box is task data, not hand-derived runtime config.
- HarborTask.solved fails closed under any other env instead of silently
  grading a separate-verifier task in the agent's box.
- Dropped: Task.scoring_runtime, the rollout's serial-scoring branch, and
  stop_confirmed/teardown_confirmed — under the agreed threat model the
  guarantee is that the grader never executes in a box the agent
  controlled, which the fresh box alone provides; nothing called the
  confirmed-teardown pair anymore.

Verified against live Docker: guard fires outside the env; solver leg
records no reward and collects artifacts; verifier leg grades in a fresh
box (declared artifact travelled, undeclared leftover did not), reads
reward.json, records extra keys as metrics, runs zero model turns; the
declared-image case boots the declared image; shared-mode grading
unchanged. 910-test suite green; ruff/ty clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread verifiers/v1/tasksets/harbor/__init__.py
Comment thread verifiers/v1/tasksets/harbor/env.py Outdated
Review follow-up: no verifier seat, no noop harness — the verifier is a
deterministic script, so the env grades it directly instead of dressing
it as an agent rollout.

- HarborEnv.finalize provisions the grading box (provision_runtime),
  restores the solver's collected artifacts, stages tests/, runs the
  verifier, and records its rewards — extra reward.json keys as metrics —
  onto the solver's trace.
- verifier_runtime on HarborEnvConfig places the grading box (None
  derives it from the solver's runtime policy); verifier_retries retries
  the infrastructure around grading without re-running the solve.
- HarborVerifierTask collapses to verifier_box_data — the minted task
  data the grading runtime resolves from — plus the existing staging and
  grading methods on HarborTask.
- The noop harness is deleted; run() still resolves the verifier box
  before the solve so an impossible pairing costs nothing.

Same Docker smoke as before, now through the real HarborEnv.finalize:
guard fires outside the env, marked solver leg records nothing, artifact
travels while the undeclared leftover does not, keyed reward lands on
the solver's trace, declared verifier image boots. Suite green;
ruff/ty clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread verifiers/v1/tasksets/harbor/env.py Outdated
Comment thread verifiers/v1/runtimes/base.py
Comment thread verifiers/v1/tasksets/harbor/env.py
Comment thread verifiers/v1/tasksets/harbor/env.py Outdated
# Artifacts first, tests second: an artifact entry pointing
# into /tests must not survive staging, which wipes and
# rebuilds that directory.
await restore(box, solution.state.artifacts)

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.

btw at some point i think we could have smth like Runtime.restore(artifacts: Artifacts), prob implemented using upload primitives in the base runtime but fine for now

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.

yeah, good point

- verifier_retries rejects negative values (ge=0) instead of asserting
  after zero attempts
- the capped read goes through a temp file, not a pipe: head | base64
  exits 0 on a missing path, so the capped and uncapped reads disagreed
  on failure — both raise now
- the scoring deadline covers provisioning and grading but no longer the
  box's teardown, which could run out the clock and discard a score
  already in hand
- HarborEnv.run raises on a non-Harbor task instead of quietly running it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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 2 potential issues.

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 59b9bfd. Configure here.

scores = await grader._graded(box, solution)
return scores
except Exception as e: # noqa: BLE001 - each attempt's failure is retried
last = e

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Teardown failure discards graded score

Medium Severity

return scores still exits through AsyncExitStack, so a raising stop()/teardown is caught by the broad retry except and discards a score already in hand. The scoring timeout was moved off teardown, but teardown exceptions still retrigger a full reprovision and regrade, and can fail the episode despite a successful grade.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 59b9bfd. Configure here.

Comment thread verifiers/v1/tasksets/harbor/taskset.py
mikasenghaas
mikasenghaas previously approved these changes Aug 3, 2026
@hallerite
hallerite merged commit 704a472 into main Aug 3, 2026
13 checks passed
@hallerite
hallerite deleted the feat/harbor-separate-verifier-envs branch August 3, 2026 22:50
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.

4 participants