Repository navigation
feat(harbor): per-step workdir, setup.sh, healthcheck, artifacts and separate verification for [[steps]] tasks (M34 follow-up) - #4951
Conversation
|
/ok to test 78a6161 |
|
🌿 Preview your docs: https://nvidia-preview-pr-4951-33234040328a.docs.buildwithfern.com/nemo/gym |
78a6161 to
6424698
Compare
6424698 to
2f5bab5
Compare
|
/ok to test 2f5bab5 |
|
/ok to test 55b208c |
|
/ok to test 1b41124 |
55b208c to
1b41124
Compare
|
/ok to test 3657d15 |
ffrujeri
left a comment
There was a problem hiding this comment.
If a non-last turn's verifier stop fails, _start_with_pull_retries overwrites self._sandboxes[_verifier_key(...)] on the next separate turn and the old sandbox leaks until ttl_s. Could you retry the stop, or mask the turn as provider_unavailable, before reusing the key?
|
/ok to test 2e24547 |
|
Fixed in 2e24547 (folded into the per-step separate verification commit): before a separate turn's verifier is created under the episode's verifier key, a previous turn's verifier still owned under that key (its stop raised in the |
|
/ok to test 924ccb0 |
|
/ok to test 7b2dcb6 |
ffrujeri
left a comment
There was a problem hiding this comment.
Thanks for the fix, LGTM.
|
/ok to test 11757aa |
|
/ok to test f7e0d51 |
yuhezhang-ai
left a comment
There was a problem hiding this comment.
Thanks for extending Harbor's multi-step support. I reviewed f7e0d51a1054bd219028a7ea043957aa555a1dbc against its immediate stack parent #4872 at 816e65bcb2cfc0312cc79dee2e4c1fa0e78c7051.
Please address the five grading/isolation findings before merge: retained test reports can produce a false passing score, a step's environment network restriction is dropped, separate steps miss agent-entrypoint failures, shared opt-out steps can use the agent's user, and artifact-forced separate steps can fail image resolution. The four remaining inline comments are marked non-blocking.
Please also align the PR description with the current behavior: later step preparation failures count as zero, the 600-second step verifier default changes existing grading budgets, and the partial log cleanup affects single-step shared tasks.
Validation used existing tests and focused local reproductions; no live sandbox provider or model rollout was run for this review. Codex independently checked the findings. Claude delivered a second opinion, but its execution audit remained incomplete after a malformed tool call and its filesystem checks encountered sandbox restrictions.
|
/ok to test 3e51c0b |
|
/ok to test 3f2c52c |
ffrujeri
left a comment
There was a problem hiding this comment.
LGTM. The five blocking findings from @yuhezhang-ai look fixed at this head, each with a test; leaving it to them to clear their request.
yuhezhang-ai
left a comment
There was a problem hiding this comment.
Thanks for addressing the review. Rechecked 3f2c52ccaf7df9356f63de0c065ae53677bbbff8 against its immediate parent #4872: the five blocking findings are fixed, with regression coverage, and the four non-blocking comments have also been addressed.
The required checks and focused verification pass. Approving this change; end-to-end RL testing on main and any resulting patches can follow in separate PRs, as discussed.
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40551394222 |
|
/ok to test faf15e2 |
yuhezhang-ai
left a comment
There was a problem hiding this comment.
Rechecked faf15e267acaad86efa26475b4c257412bb6487d against the current stack parent #4872 (8034d4568ce33b53ebab58b9f49a2d9d9694d837). This is a rebase of the previously approved version: all six patches and all 35 affected files are unchanged. The five blocking fixes remain intact, and all review threads are resolved.
Required checks and the current unit/server suites pass. Renewing approval after the rebase; end-to-end RL validation on main and any resulting patches can follow in separate PRs, as discussed.
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40612812940 |
|
/ok to test 49e0dc5 |
…llow-up) Harbor 0.22.0's MultiStepTrial._prepare_step uploads steps/<name>/workdir/ into the environment's working directory and runs steps/<name>/workdir/setup.sh as the step's agent user before the step's agent; the loader refused both. - New HarborStepsMixin (environment_servers/harbor/steps.py): the folder is merged into the working directory (an upload over what previous turns left, the digest's ignored names left out), then `bash <workdir>/setup.sh` runs from the working directory as the turn's agent user, within the new step_setup_timeout_s (unset by default: the step's own agent budget, [steps.agent].timeout_sec else [agent].timeout_sec; Harbor bounds the script by nothing but the trial timeout). The bound is counted in the episode budget for every step that ships the script. - A non-zero exit (or a run past that bound) is the step's failure, as Harbor's step exception_info: the turn is recorded with failure_kind harbor:step_setup_failed, no response and no verifier result, no agent session or verifier runs for it, and the episode stops there. Under mean the turns before it are averaged, as Harbor averages the steps with a result; under final the episode is masked with that kind, since Harbor's trial then has no reward at all (roll_up_turns masks a stopping turn whose agent never ran instead of labelling it 0). A first turn that fails there is a terminal agent-stage failure with that kind. A sandbox that cannot run the script is a retryable outage. - HarborTurnResult.response is optional for such a turn; the episode's response is the last turn's whose agent ran. - Loader: the workdir/ and setup.sh refusals are gone; HarborTurn.workdir_dir and setup_script name the step's files; a setup.sh beside instruction.md (not a Harbor file) only warns. - README, configs/harbor.yaml and the multi-step docs page describe the order, the bound and the failure semantics; tests in test_multi_turn.py (the fake records the user each exec ran as) and test_harbor_tasks.py. - An entry of workdir/ the sandbox refuses to unpack over what the previous turns left (a non-empty directory at the name of a shipped file, which the previous turn's agent can plant in the working directory it owns) is the step's failure as well, failure_kind harbor:step_workdir_failed, as Harbor's docker cp refuses the mismatch and its trial errors: the SandboxTransferError becomes a StepPrepareError instead of a retryable outage, so an agent cannot turn every later attempt into an unscored retry. - Nothing stops what an earlier turn left running before setup.sh runs, so a step whose agent is root after a step whose agent was not runs, in the window between the upload and the exec, whatever that earlier agent put at the script's path (Harbor shares the window). Documented in the mixin, the _prepare_turn docstring, README and the docs page; the loader warns about such a task, naming both steps. - A symbolic link the previous turn's agent planted at the name of an entry the step's workdir/ ships is removed, as root, in the same shell line right before the extraction (upload_dir(replace_links=True): tar_extract_command lists the archive's members and unlinks a link at each path). GNU tar and Harbor's docker cp replace such a link with the shipped entry; BusyBox tar (Alpine images) keeps it and writes the shipped files through it, as root, wherever it points. A real-BusyBox test in test_sandbox_transfer.py shows the diversion without the option and the merge at its own names with it, on both tars; the multi-turn test asserts the extraction exec carries the loop. README and the docs page no longer present the plain extraction as landing the way docker cp lands it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Adil Asif <adasif@nvidia.com>
…ow-up) Harbor 0.22.0's MultiStepTrial._prepare_step polls a step's own healthcheck after its setup.sh and before its agent, through the same run_healthcheck loop as the task-level check; the loader refused the key. - The seed's healthcheck loop is extracted into HarborEntrypointMixin._poll_healthcheck (Docker's start-period and retries semantics, the entrypoint probe kept for the seed) and reused for the step: HarborStepsMixin._run_step_healthcheck runs the command from the working directory as the turn's agent user, after the step's setup and not after a failed one. - A check that never passes is the step's failure, as Harbor's HealthcheckError in the step's exception_info: failure_kind harbor:step_healthcheck_failed, the same failed turn and early stop as a failed setup.sh. - The task's [environment.healthcheck] ran at seed and is not run again per step, as Harbor runs it once at environment start (its StepConfig says the step check supplements it). - The episode budget counts the check's worst case per step that declares one (the start period, one straddling attempt, then the counted attempts, each timeout_sec plus the exec grace and an interval_sec sleep), so a slow check cannot eat the final turns' budget under enforce_task_timeouts. - Loader: the per-step healthcheck refusal is gone; README and the multi-step docs page describe the timing, the budget and the failure; tests in test_multi_turn.py and test_harbor_tasks.py. - Only a check that ran and never passed is the step's failure: _poll_healthcheck raises HealthcheckFailed (a RuntimeError subclass) and the step catches that type alone, so a sandbox stopped under the poll (the facade's RuntimeError) or a transfer/probe error propagates as the retryable outage it is; an attempt whose result carries an error_type other than timeout raises SandboxProbeError instead of counting as a failure. - The budget's worst case charges the start_interval_sec sleep that follows the attempt straddling the start period (the loop sleeps by when the attempt began), then the counted attempts with an interval_sec sleep between them; the test drives _poll_healthcheck against a scripted clock and checks the budget covers the elapsed time, to within 0.01 s at the worst alignment. - An attempt that overruns its timeout_sec counts as one failed attempt here, as Docker's HEALTHCHECK counts a timed-out probe and as the seed's task-level loop does; Harbor 0.22.0 differs (its Docker exec raises on the timeout, no caller of run_healthcheck catches it, so the first such attempt errors the whole trial). The lenient count is kept on purpose and the difference is stated in the mixin, the test docstring, README and the docs page instead of being presented as Harbor's behaviour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Adil Asif <adasif@nvidia.com>
… mode too (M34 follow-up) Harbor 0.22.0's MultiStepTrial runs a collection pass per step after the step's agent and before its verifier, whatever the verifier mode (_collect_step_artifacts over _collect_artifacts_phased): the collect hooks, /logs/artifacts, the task's artifacts and the step's own [[steps]] artifacts. Gym collected artifacts on the separate-verifier path only and the loader refused every task with per-step artifacts (all 36 of the hub's gabeorlanski/slopcodebench). - HarborVerificationMixin._collect_turn_artifacts: per turn, the task's [[verifier.collect]] hooks and then the step's own [[steps.verifier.collect]] hooks (Harbor's _collect_hooks_for) for main and the main entries, then the sidecar hooks and entries, staged under numbered roots in that turn's verifier logs folder (artifacts/NN/<destination>, the separate path's layout); nothing is stopped, the sandbox keeps running for the next turn; the hooks run once per turn, in the pass, not again in the shared verifier (_run_shared_verifier takes the reserved logs_dir). The step's hooks load (HarborStepVerifier.collect), ride in the turn's verifier settings and are counted in the episode budget for their turn. - _collected_artifacts appends the step's entries to the task's after the convention entry, as Harbor's with_convention_entry does; each entry carries where it was declared. - artifacts/manifest.json records every pass (the separate path's too): index, source, service, origin, destination, type and status (ok, absent, link, failed, skipped), as Harbor's artifact handler writes its manifest. - A collection the sandbox cannot serve masks the turn as an outage. - Loader: the per-step artifacts refusal is gone; a step entry follows the task entry's rules (a source under /tests or /logs/verifier is refused). - Fixture: the real task.toml and Dockerfile of gabeorlanski/code_search from the pulled slopcodebench dataset (stub steps); loader and server tests run its five checkpoints with their /app artifacts and excludes. - README and the multi-step docs page describe the pass and the manifest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Adil Asif <adasif@nvidia.com>
…4 follow-up) Harbor 0.22.0 resolves the verifier mode per step (resolve_step_verifier_mode: the step's explicit mode, else separate with a [steps.verifier.environment], else the task's) and runs a separate step's verifier in an environment of its own around that step; the loader refused per-step separate mode and any multi-step task with a separate task-level verifier, and the seed refused separate_verifier_for_artifacts on such a task. - Loader: HarborTurn carries verifier_mode, verifier_image and verifier_image_config, resolved by the single-step rules on the step's effective environment ([steps.verifier.environment], else [verifier.environment], else a copy of [environment]). A step without an image starts from steps/<name>/tests/ when that folder exists, which prepare does not build: refused naming the image to declare; one starting from tests/ reads the recorded build, and verifier_build_context plans it whatever the task's own mode (verifier_build_environment, used by the build planner). A [verifier] network restriction needs every step separate. - Server: _run_separate_verifier is split into the collection-and-stop phase and _verify_separately (create the verifier sandbox, restore the archives, upload tests/ then steps/<name>/tests/, run test.sh, stop it). A separate turn runs the latter on a per-turn view of the task (_turn_verifier_view: the step's verifier over the task's, hooks included, in separate mode, with the turn's image), fed the turn's collection pass. The agent's sandbox keeps running across turns, since the next turn opens on it, except on the last turn of a task whose last step verifies separately: there, as Harbor's _run_step does on the last step of a separate-mode task, main is stopped before the sidecar pass and the whole agent side before the verifier grades. One verifier sandbox per turn, under the episode's verifier key. A shared turn's collected archives are removed once its pass ends (it restores nothing); a separate turn's go with the restore. - separate_verifier_for_artifacts applies per turn (task or step artifacts); the seed checks every separate turn's image, env templates and user group; the budget counts a verifier sandbox start (and overlay) per separate turn. - Each turn reports verifier_mode; the episode's is the shared one or "mixed". - README and the multi-step docs page describe the resolution, what the verifier sandbox receives, the last-turn stop and what stays refused; tests in test_multi_turn.py and test_harbor_tasks.py. - The per-step separate-verification tests patch _start_with_pull_retries, one level below _create_verifier_sandbox, so the real creator runs on the turn's view and the spec it hands the provider (image, resources, working directory, network policy) is what the tests assert. - A step without its own [steps.verifier].timeout_sec is graded within Harbor's step default of 600 s, never the task's [verifier].timeout_sec: Harbor's StepConfig.verifier is a full VerifierConfig (0.22.0 and main alike), so its fallback to the task's figure never runs. step_verifier_timeout_sec, _turn_verifier_view, _turn_verifier_settings and the episode budget follow; the loader warns once per task naming the steps the task's figure does not reach; README and the docs page say so (loader and budget tests updated). - HarborStepVerifier refuses environment_mode = "shared" together with a [steps.verifier.environment], as Harbor's _validate_mode_env_consistency does on the VerifierConfig a step's verifier is; the loader surfaces it as HarborTaskError naming the step (test added). - The pre-turn reset of /tests, /logs/verifier and the server's trajectory.json runs only when the previous turn verified in the agent's sandbox: after a separate turn nothing was graded there and the image's /tests stays, as under Harbor 0.22.0's separate mode and Harbor main's gate on a preceding shared verifier (the separate-mode test asserts no reset, the mixed-mode test the reset after the shared turn only). The docs page says the last turn's agent side is already stopped when its verifier sandbox is attempted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Adil Asif <adasif@nvidia.com>
… the oracle alone (M34 follow-up) Harbor 0.22.0 loads a task's trajectory.json (steps/<first step>/trajectory.json for a multi-step task, where a top-level one is ignored) into the agent's session before its first turn, as its --load-trajectory option does, for every agent but the oracle and nop (Trial._resolve_load_trajectory: the oracle runs the task's own solution, and task authors validate with it). Gym hands no prior context to a harness, so such a task ran without the context it was written for. - The loader records the file (HarborTask.seed_trajectory: the first step's for a multi-step task, else the top-level one, as Harbor's _task_trajectory_path picks one path or the other) with a warning naming it; a multi-step task's top-level file and a later step's are ignored, as Harbor ignores them, so a hub task Harbor runs is not dropped at discovery. - The environment server runs the task for the oracle (gym dataset validate works as Harbor's oracle does) and refuses it at seed, terminally, for any other agent, naming the file and that loading a trajectory into a harness is not supported yet (remove the file to run without it), before any sandbox is paid for. - The file stays in the task's folder digest and package hash (content_hash and archive_hash already cover it), so a fetched task still verifies. - README (feature table) and the multi-step docs page describe it; loader tests cover the single-step and multi-step shapes and the digest, server tests the oracle and a model agent. - The refusal applies to a known non-oracle implementation only: an agent block the server cannot read (several implementations under the instance, a client the merged config does not show) runs the task with a warning, as _seed_solution treats the same shape, rather than refusing the oracle wired that way (_check_seed_trajectory). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Adil Asif <adasif@nvidia.com>
…t with HarborTurn HarborStep read as either of Harbor's two kinds of step: a [[steps]] entry of task.toml (Harbor's StepConfig) or a step of an ATIF trajectory, which Gym mirrors as AtifStep. HarborTaskStep names the first; its [steps.agent] and [steps.verifier] tables follow as HarborTaskStepAgent and HarborTaskStepVerifier. Like the mirror's other task.toml tables (HarborAgent, HarborVerifier), only the top-level schema keeps a Config suffix. HarborTurn is that entry loaded with its steps/<name>/ folder, which the harbor server runs as one agent turn; the pair mirrors HarborTaskConfig and HarborTask, and the docstrings now say so. HarborTurn no longer stores its name twice; it reads config.name. The docstring also says that one turn per task step is this server's choice, as in Harbor's multi-step trial, not something a task step implies: Harbor's simulated-user trials run many turns of the agent under test inside one step. Ported from #4728 (c0a4fcb, 5568103) onto the per-step features of this branch, where HarborTurn also carries the step's verifier mode and image and HarborTaskStepVerifier validates its environment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Adil Asif <adasif@nvidia.com>
|
/ok to test 3323404 |
yuhezhang-ai
left a comment
There was a problem hiding this comment.
Rechecked 33234040328a3366b3e6f55f2bd1685058cfd091 against the current stack parent #4872 (fb511ae6d9211418beec4711c68becd115fb16be). All six PR changes remain equivalent to the previously approved version, and the earlier blocking fixes are intact. No remaining blocking findings.
Required checks and the current unit/server suites pass. Renewing approval after the rebase; end-to-end RL testing on main and any resulting patches can follow in separate PRs, as discussed.
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40727571028 |
Stacked on #4872 (M38 second slice). Follow-up to M34 #4819: the per-step Harbor features the loader refused, from Felipe's gap analysis (gaps 3 and 6).
feat(harbor): per-step workdir/setup.sh, healthcheck, artifacts and separate verification for
[[steps]]tasks (M34 follow-up)Summary
M34 runs a Harbor
[[steps]]task as turns on one sandbox, but its loader refused every per-step feature of Harbor 0.22.0'sMultiStepTrialby name:steps/<name>/workdir/andsetup.sh,[steps.healthcheck], per-stepartifacts, and per-step (or task-level) separate verification. The hub'sgabeorlanski/slopcodebench(36 tasks) could not load at all because every checkpoint declaresartifacts. This stack implements the five follow-ups, one commit each, with Harbor 0.22.0's semantics (trial/multi_step.py,trial/trial.py,models/task/verifier_mode.py) cited in code and docs. The one Harbor file Gym still cannot honour, the seedtrajectory.json, is handled as Harbor's own oracle handles it: the oracle runs the task, every other agent is refused at seed naming the file.What changes
steps/<name>/workdir/andworkdir/setup.sh(environment_servers/harbor/steps.py, newHarborStepsMixin): before each turn's agent session, after the step's user switch, the folder is merged into the working directory (an upload over what previous turns left, as root with the folder's modes, the digest's ignored names left out) andbash <workdir>/setup.shruns from the working directory as the turn's agent user, within the newstep_setup_timeout_s(unset by default: the step's own agent budget,[steps.agent].timeout_secelse[agent].timeout_sec; Harbor bounds the script only by the trial timeout). The bound is counted in the episode budget for every step that ships the script. A non-zero exit (or a run past the bound) is the step's failure, as Harbor's stepexception_info: the turn is recorded withfailure_kind: harbor:step_setup_failed, noresponseand no verifier result, no agent session or verifier runs for it, and the episode stops there; undermeanthe turns before it are averaged (Harbor's_aggregate_step_rewards), underfinalthe episode is masked with that kind, since Harbor's trial then has no reward at all. A first turn that fails there is a terminal agent-stage failure with that kind. An entry of the folder the sandbox refuses to unpack over what the previous turns left (a non-empty directory at a shipped file's name, which the previous turn's agent can plant in the working directory it owns) is the step's failure as well,harbor:step_workdir_failed, as Harbor'sdocker cprefuses the same mismatch and its trial errors: never a retryable outage, so an agent cannot turn every later attempt into an unscored retry. A symbolic link the previous turn's agent planted at the name of an entry the folder ships is removed, as root, in the same shell line right before the extraction (upload_dir(replace_links=True)): GNU tar anddocker cpreplace such a link with the shipped entry, BusyBox tar (Alpine images) keeps it and writes the shipped files through it, as root, wherever it points; a real-BusyBox test shows the diversion without the option and the merge at its own names with it, on both tars. Nothing stops what an earlier turn left running before the script runs, so a step that is root after a non-root step can have itssetup.shreplaced in that window (Harbor shares it): documented, and the loader warns about such a task.HarborTurnResult.responseis optional for such a turn. Asetup.shbesideinstruction.md(not a Harbor file) only warns.[steps.healthcheck]: the seed's healthcheck loop is extracted intoHarborEntrypointMixin._poll_healthcheck(Docker's start-period and retries semantics) and reused per turn after the setup (not after a failed one), from the working directory as the turn's agent user. Only a check that ran and never passed (HealthcheckFailed) is the step's failure (harbor:step_healthcheck_failed); a sandbox stopped under the poll, or a provider that could not run the command, is a retryable outage, as forsetup.shand the collect hooks. An attempt that overruns itstimeout_seccounts as one failed attempt (Docker'sHEALTHCHECKsemantics, and what the seed's task-level loop does); Harbor 0.22.0 differs there (its Docker exec raises on the timeout and no caller ofrun_healthcheckcatches it, so the first such attempt errors the whole trial), which the code, the tests and the docs state instead of claiming parity. The task's[environment.healthcheck]ran at seed and is not run again per step, as Harbor runs it once at environment start. The episode budget counts the check's worst case per step (start period, the attempt that may start just inside it and itsstart_interval_secsleep, then the counted attempts with aninterval_secsleep between them, eachtimeout_secplus the exec grace), checked against a simulated poll at its worst alignment.artifacts, collected in shared mode too: Harbor's per-step collection pass after the step's agent and before its verifier (_collect_step_artifacts): the task's[[verifier.collect]]hooks and then the step's own[[steps.verifier.collect]]hooks (Harbor's_collect_hooks_for; the step's table now loads,HarborStepVerifier.collect, and is budgeted),/logs/artifacts, the task'sartifactsand the step's own, staged under numbered roots in that turn's verifier logs folder (artifacts/NN/<destination>, the separate path's layout); the hooks run once per turn. Every pass, the separate path's included, writesartifacts/manifest.json(index, source, service, origin, destination, type, status) as Harbor's artifact handler writes its manifest. A collection the sandbox cannot serve masks the turn as an outage. Fixture: the realtask.tomland Dockerfile ofgabeorlanski/code_searchfrom the pulled slopcodebench dataset.HarborTurn.verifier_moderesolves as Harbor'sresolve_step_verifier_mode(the step's explicit mode, elseseparatewith a[steps.verifier.environment], else the task's); the turn's verifier image follows the single-step rules on the step's effective environment ([steps.verifier.environment], else[verifier.environment], else a copy of[environment])._run_separate_verifieris split into the collection-and-stop phase and_verify_separately; a separate turn runs the latter on a per-turn view of the task (_turn_verifier_view), fed the turn's collection pass, in a verifier sandbox created and stopped around that turn under the episode's verifier key. The agent's sandbox keeps running for the next turn, except on the last turn of a task whose last step verifies separately: there, as Harbor's_run_stepdoes on the last step of a separate-mode task,mainis stopped before the sidecar pass and the whole agent side before the verifier grades. A shared turn's collected archives are removed once its pass ends (it restores nothing).separate_verifier_for_artifactsapplies per turn; the seed checks every separate turn's image, env templates and user group; the budget counts a verifier sandbox start (and overlay) per separate turn; each turn reportsverifier_mode, the episode's is the shared one ormixed. A step's verifier runs within its own[steps.verifier].timeout_sec, else Harbor's step default of 600 s, never the task's[verifier].timeout_sec(Harbor'sStepConfig.verifieris a fullVerifierConfigwith that default, on 0.22.0 and main alike), in both modes and in the budget; the loader warns once per task, naming the steps the task's figure does not reach. A step that saysenvironment_mode = "shared"and declares a[steps.verifier.environment]is refused at load, as Harbor's validator refuses it. The pre-turn reset of/tests,/logs/verifierand the server's transcript runs only after a turn that verified in the agent's sandbox; after a separate turn the image's/testsstays, as under Harbor 0.22.0's separate mode and Harbor main's gate on a preceding shared verifier. A step whose verifier Harbor would build from its ownsteps/<name>/tests/Dockerfileis refused naming the image to declare (prepare buildstests/Dockerfileonly; that build is now planned whatever the task's own mode,verifier_build_environment). A[verifier]network restriction needs every step separate. The tests patch_start_with_pull_retries, so the real_create_verifier_sandboxruns on the turn's view and the spec it hands the provider is what they assert.trajectory.json, run for the oracle alone: the loader recordssteps/<first step>/trajectory.json(a multi-step task's top-level file and a later step's are ignored, as Harbor's_task_trajectory_pathignores them) or a single-step task'strajectory.jsonasHarborTask.seed_trajectory, with a warning. Harbor 0.22.0'sTrial._resolve_load_trajectoryloads that file into the agent's session for every agent but the oracle and nop; no harness here can be seeded with it yet, so the environment server runs the task for the oracle (gym dataset validateworks as Harbor's does) and refuses it at seed, terminally, for any agent whose implementation is known and is not the oracle, naming the file, before any sandbox is paid for; an agent block the server cannot read (several implementations under the instance) warns and runs, as the solution placement treats that shape. The file stays in the folder digest and package hash.Left untouched, as decided: the dataset multiplier, resume across steps. (The unset step verifier timeout no longer falls back to the task's: Harbor never does, see 4.)
Docs:
environment_servers/harbor/README.md(Multi-step tasks section, feature table),environment_servers/harbor/configs/harbor.yaml(step_setup_timeout_s) andfern/versions/latest/pages/data/multi-step-tasks.mdxdescribe every behaviour, with what Harbor 0.22.0 does beside it.Test plan
environment_servers/harbor/tests responses_api_agents/harbor_oracle_agent/tests -k "not partial_eval and not buildah and not validate_ and not real_image": 535 passed on the base, 564 passed at the head (+29: workdir/setup.sh and its bound, the refused workdir entry, the failed-setup roll-up undermeanandfinal, healthcheck, its outage cases and its budget against a simulated poll, artifacts and manifest, a step's own collect hooks, archive removal, slopcodebench checkpoints, separate turns with the last-turn stop and the real sandbox spec, mixed modes, forced separate per turn, budget per separate turn, the seed trajectory for the oracle, a model agent and an unreadable agent block). Every commit's suites green from a git-archive copy (see the report).tests/unit_tests/test_harbor_tasks.py(+test_harbor_build_plan.py,test_harbor_image_build.py,test_harbor_run_summary.py,test_harbor_validation.py,test_sandbox_transfer.py): 472 passed at the head (the transfer file's BusyBox tests run wherebusyboxis installed, as on the test node).ruff check,ruff format,pre-commit run --files <changed>at every commit.python:3.12-slim):gym dataset validateof four tasks:stepfeat(four steps:workdir/+setup.sh,[steps.healthcheck], per-step artifacts in both modes, a step-level[[steps.verifier.collect]]hook whose output the separate verifier reads, and a separate last step graded after the agent side is stopped),setupfail(a second step'ssetup.shpaststep_setup_timeout_s = 5underfinal: the episode is masked withharbor:step_setup_failed),seedtraj(a first step'strajectory.jsonand an ignored top-level one, run by the oracle) andplantdir(the first step's oracle solution leaves/app/setup.sh/x/where the second step shipsworkdir/setup.sh: the second turn isharbor:step_workdir_failed, unmasked, undermeanthe episode scores the first turn, nothing retried). Result at the head:stepfeat,seedtrajandplantdiroracle solved 1.0,setupfailmasked as intended. See the report's runC section, including the provider observation (OpenSandbox reports a killed command as exit -1signal: killedand a failed tar asexit status 2without the command's stderr).gym dataset validateof the same four tasks plusplantlinkonbash:5.2(Alpine plus bash, BusyBox tar 1.37.0,ENTRYPOINT []), whose first step's oracle plantsln -s /etc /app/fixturesandln -s /etc /app/sub/deepwhere the second step shipsworkdir/fixtures/input.txtandworkdir/sub/deep/x.txt: the second turn'stest.shasserts both paths are directories holding the shipped files, nothing reached/etcandsetup.shran; resultplantlink oracle solved 1.0(the other four as before). See the report's runD section.python:3.12-slim, no Compose): a two-step task whose task-leveltests/ships a hidden marker, whose second step's solution records what/tests,/logs/verifierand/logs/agenthold when its agent starts, and whose second step'stest.shrewards 1 only when none of the first turn's grading files were there (multi_step_reward_strategy = "final"). Harbor 0.22.0's oracle: the second step saw/tests/hidden_marker.txt,/tests/test.shand three files under/logs/verifier(its reset runs only right before each step's shared verifier), reward 0. Harbor main (b4732c06, 0.24.0, a throwaway venv):/testsand/logs/verifierempty, reward 1 (its_prepare_stepresets them when the previous step ran a shared verifier). Gym at this head throughgym dataset validate:/testsand/logs/verifierempty, notrajectory.json, reward 1 (the pre-turn reset of M34's3c1887440, gated as in 4 above). All three sides agree on/logs/agent: no transcript reaches the second agent. Harbor's OpenSandbox environment reached this deployment only with its self-signed certificate trusted throughSSL_CERT_FILEand the proxy API-key header added by a launcher shim (both things Gym's provider does by default; nothing in Harbor's step logic changed). See the round-4 report.gym dataset pull harbor:gabeorlanski/slopcodebench(36 tasks): the base loader refused all 36 (per-step healthcheck and artifacts are not supported yet); the realcode_searchtask.toml loads and runs its five checkpoints in the unit tests (its Dockerfile setsSHELL, so it is a prepare-built image: not run live here).Checklist
Test fixture provenance
The test fixture
tests/unit_tests/fixtures/harbor_slopcodebench/code_search/(task.toml, environment/Dockerfile,step instructions and stub test.sh scripts) is synthetic: it reproduces only the shape of the Harbor hub dataset
gabeorlanski/slopcodebench(five steps with per-step/appartifacts,min_rewardon two reward keys, a build-modeDockerfile with a
SHELLline). Its step names (scaffold,lexer,index,query,report), budgets,resources, exclude list and reward gates are the tests' own; the only lines it shares with the dataset's task.toml are
TOML table headers and schema keys (
schema_version,artifacts = [],workdir = "/app",difficulty,category).No text of the dataset's task.toml, Dockerfile, instructions or tests is copied, so no external source URL, revision
or third-party license applies to any file of this PR.
Migration note for datasets whose sidecars write verifier inputs
The shared verifier's reset now deletes every undeclared file under
/logs/verifierbeforetest.shruns, so analready-pulled dataset whose sidecar writes an input there (kumo: the
verifierservice appends/logs/verifier/actions.jsonl, whichmain'stests/score.pyreads) scores 0, unmasked, until itsdataset.tomldeclares the file:
Declared names are literal top-level file names (no path separators, no
*,?or[: the server keeps them withfind -path, which matches a glob) and never a grader output name. The declaration is part of the task digest. Nodataset.toml in this repository declares
actions.jsonl, so the kumo dataset's owners must add it out of band.Behaviour notes
[steps.verifier].timeout_secis graded within Harbor's 600 s step default, not the task's[verifier].timeout_sec, which changes existing grading budgets./logs/verifier(except the task's declared[verifier].inputs) beforetest.sh, in single-step shared tasks too.
🤖 Generated with Claude Code