[Harbor 3/4] Mode B (nested harbor run) + the vero harbor build compiler#5
[Harbor 3/4] Mode B (nested harbor run) + the vero harbor build compiler#5varunursekar wants to merge 2 commits into
Conversation
| # Execute | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def _build_command( |
There was a problem hiding this comment.
HarborConfig.n_attempts / max_retries are typed fields, which signals they are honored, but _build_command never emits flags for them — so every nested run uses harbor defaults. On a budget-graded run that is a real cost/correctness bug. Either map them to the real harbor run flags here, or drop the fields. (Confirms Greptile's P1.)
| continue | ||
| task_name = data.get("task_name") | ||
| if task_name: | ||
| trials[task_name] = data |
There was a problem hiding this comment.
trials[task_name] = data is last-write-wins over an unordered rglob; with retries or a reused jobs_dir, a failing attempt can clobber a passing one, feeding a wrong score into auto_best. Collect all matches and pick deterministically (latest mtime / highest attempt index), and/or namespace jobs_dir per attempt. (Confirms Greptile's P1.)
| values = [float(v) for v in rewards.values()] | ||
| return sum(values) / len(values) if values else 0.0 | ||
|
|
||
| def _existing(self, params: EvaluationParameters, sample_id: int) -> SampleResult | None: |
There was a problem hiding this comment.
_existing treats a persisted error sample as 'done', so a sample whose nested run failed once is permanently skipped on resume and silently degrades the candidate's score. Either don't persist error samples, or make the pending/skip predicate treat error results as not-done.
| ["tar", "xf", "-", "--strip-components", str(strip)], | ||
| cwd=dest, stdin=archive.stdout, check=True, | ||
| ) | ||
| archive.wait() |
There was a problem hiding this comment.
archive.wait()'s return code is discarded; if git archive fails, tar can still exit 0 on a truncated stream and you commit a near-empty baseline whose SHA gets baked into both images. Assert archive.wait() == 0 (and reap the Popen on the tar-failure path). (Confirms Greptile.)
| environment: | ||
| VERO_HOME_DIR: "/opt/vero_home" | ||
| {% for secret in secrets %} | ||
| {{ secret }}: "${{ '{' }}{{ secret }}{{ '}' }}" |
There was a problem hiding this comment.
This renders SECRET: "${SECRET}", resolved from the run host's env, not anything the compiler validates. An unset var interpolates to empty and the sidecar comes up credential-less, failing every eval opaquely. Consider failing the build / emitting a required-secrets manifest when a declared secret is missing. Note test_rendered_files_parse only asserts the literal ${...} string, which hides this.
| # ...except locked paths (e.g. the scorer): root-owned + unwritable. | ||
| if [ -e "/work/agent/{{ p }}" ]; then | ||
| chown -R root:root "/work/agent/{{ p }}" | ||
| chmod -R a-w "/work/agent/{{ p }}" |
There was a problem hiding this comment.
This chown root:root + chmod -R a-w applies to the working tree under /work/agent, but the verifier never executes the working tree: _transfer_commit fetches git blobs and the evaluator checks the commit out into a fresh temp copy. The agent can git add -f a modified scorer and commit it regardless of working-tree perms. So read_only_paths is not a tamper control — please document it as advisory-only and move scorer-provenance enforcement into the sidecar (see the Mode A scorer-provenance comment on #4).
- Mode B (runner.py): `HarborRunner`, an `EvalStrategy` that — for each candidate — runs a *nested* `harbor run` of the agent over the selected Harbor tasks (e.g. on Modal) and collates the verifier rewards into vero `SampleResult`s. One Harbor task = one sample; inference is fully delegated, scoring comes from Harbor's verifier. - The compiler (build/): `vero harbor build` renders a `BuildConfig` into a runnable Harbor task directory — a Docker Compose environment (optimizer workbench `main` + the eval sidecar + three volumes), two Dockerfiles, instruction.md, tests/test.sh, and the seed/solve scripts — baking the dataset/scorer/baseline repo and the sidecar's ServeConfig. Supports Mode A (local dataset/scorer) and Mode B (a registry or local Harbor benchmark, passed through to the HarborConfig). - `.gitignore`: un-ignore src/vero/harbor/build/ (the repo's `build/` rule was hiding the compiler package). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mkdtemp cleanup Mode B runner + build-compiler robustness (review findings on PR #5): - runner: emit --n-attempts / --max-retries (the typed HarborConfig fields were silently dropped); pick the best trial per task deterministically instead of last-write-wins over an unordered rglob; treat a persisted error sample as not-done so a transient failure is re-run on resume. - compiler: assert git archive exited 0 (and reap it) so a truncated stream cannot bake a near-empty baseline; validate declared secrets at build time and render compose secrets with a fail-fast guard so an unset host var fails loudly instead of producing a credential-less sidecar. - seed.sh: document that read_only_paths is advisory only and scorer provenance is enforced sidecar-side. - compiler: stage the dataset in a cleaned-up TemporaryDirectory (Greptile: the mkdtemp scratch dir was leaking, datasets can be gigabytes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
767570e to
38709b1
Compare
05cdeea to
44285bf
Compare
| finally: | ||
| # Let git see SIGPIPE if tar died, then reap it (no zombie). | ||
| if archive.stdout is not None: | ||
| archive.stdout.close() | ||
| archive_err = (archive.communicate()[1] or b"").decode(errors="replace") |
There was a problem hiding this comment.
communicate() after stdout.close() raises ValueError — breaks git-repo builds
archive.stdout.close() (line 103) closes the underlying OS file descriptor and marks the Python file object as closed. The very next line calls archive.communicate(), which internally calls selector.register(self.stdout, EVENT_READ) → self.stdout.fileno() → ValueError: I/O operation on closed file (CPython's _fileobj_lookup re-raises ValueError for non-int file objects). This means _prepare_baseline_repo always raises ValueError for any git-tracked agent repo, so compile_task is entirely broken for the normal case and all of the built-fixture tests would fail.
The intended fix (get stderr for the error message + reap the child) should instead read archive.stderr directly and call archive.wait():
finally:
if archive.stdout is not None:
archive.stdout.close()
archive_err = (archive.stderr.read() if archive.stderr else b"").decode(errors="replace")
archive.wait()Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/build/compiler.py
Line: 100-104
Comment:
**`communicate()` after `stdout.close()` raises `ValueError` — breaks git-repo builds**
`archive.stdout.close()` (line 103) closes the underlying OS file descriptor and marks the Python file object as closed. The very next line calls `archive.communicate()`, which internally calls `selector.register(self.stdout, EVENT_READ)` → `self.stdout.fileno()` → `ValueError: I/O operation on closed file` (CPython's `_fileobj_lookup` re-raises `ValueError` for non-int file objects). This means `_prepare_baseline_repo` always raises `ValueError` for any git-tracked agent repo, so `compile_task` is entirely broken for the normal case and all of the `built`-fixture tests would fail.
The intended fix (get stderr for the error message + reap the child) should instead read `archive.stderr` directly and call `archive.wait()`:
```python
finally:
if archive.stdout is not None:
archive.stdout.close()
archive_err = (archive.stderr.read() if archive.stderr else b"").decode(errors="replace")
archive.wait()
```
How can I resolve this? If you propose a fix, please make it concise.
Draft · Stack 3 of 4 — targets
harbor-2-sidecar.HarborRunner, anEvalStrategythat — per candidate — runs a nestedharbor runof the agent over the selected Harbor tasks (e.g. on Modal) and collates the verifier rewards into veroSampleResults. One Harbor task = one sample; inference is delegated, scoring comes from Harbor.vero harbor buildrenders aBuildConfiginto a runnable Harbor task — a Docker Compose env (optimizer workbench + eval sidecar + 3 volumes), two Dockerfiles,instruction.md,tests/test.sh, seed/solve scripts — baking the dataset/scorer/baseline repo and the sidecar'sServeConfig. Mode A and Mode B.Review focus: the nested-run + collation, and the trust-boundary plumbing baked into the generated compose (volumes,
root:600token, secrets → sidecar only). Also un-ignoressrc/vero/harbor/build/(the repo'sbuild/rule was hiding the package).Stack: [1/4] core → [2/4] sidecar → this → [4/4] docs.
🤖 Generated with Claude Code
Greptile Summary
This PR adds Mode B (
HarborRunnernestedharbor runstrategy) and thevero harbor buildcompiler that renders aBuildConfiginto a Docker-Compose-based Harbor task directory. The compiler bakes the dataset, baseline repo, sidecarServeConfig, and all secrets-plumbing into the generated environment.HarborRunner(runner.py): correctly builds theharbor runcommand (including--n-attempts/--max-retries), uses_trial_rankto deterministically pick the best trial per task, and gates resume onis_error()so transient failures are re-run.vero harbor buildcompiler (build/): renders Dockerfiles, compose, seed/test/solve scripts from Jinja2 templates; trust-boundary plumbing (secrets → sidecar only,root:600admin token, advisoryread_only_paths) looks correct. However,_prepare_baseline_repohas a broken stderr-capture pattern:communicate()is called immediately afterarchive.stdout.close(), which raisesValueError: I/O operation on closed fileon CPython for every git-backed agent repo.test_harbor_runner.pyhas thorough coverage including aTestReviewFixesclass that directly verifies the prior-round fixes;test_harbor_build.pycovers the compiler but itsbuilt-fixture tests will fail due to thecommunicate()bug.Confidence Score: 4/5
Hold until the
communicate()/stdout.close()ordering in_prepare_baseline_repois corrected — as written,compile_taskalways raisesValueErrorfor any git-backed agent repo, which is the typical case.The runner logic and trust-boundary wiring are sound, and the prior-round fixes (returncode check, TemporaryDirectory, retry ranking,
--n-attempts/--max-retries) are all present. The single blocking issue is in_prepare_baseline_repo: callingarchive.communicate()afterarchive.stdout.close()causes CPython to raiseValueErrorwhen it tries to register the closed file descriptor with a selector insidecommunicate(). This makescompile_taskbroken for all git repos, which would be immediately visible if the test suite is run.vero/src/vero/harbor/build/compiler.py — the
_prepare_baseline_repofinally block needsarchive.stderr.read()+archive.wait()instead ofarchive.communicate().Important Files Changed
_prepare_baseline_repohas a P1 bug —communicate()afterstdout.close()raisesValueErroron CPython, breakingcompile_taskfor all git repos. Temp-dir andgit archivereturncode fixes from prior review are correctly applied, but the stderr-capture approach is wrong._build_commandnow correctly includes--n-attempts/--max-retries;_load_trialsuses_trial_rankto avoid last-write-wins on retries. No new issues found.from_filehandles monorepo layout correctly. No issues.${VAR:?msg}fail-fast interpolation; admin token volume mounted read-only to main; trust boundary wiring looks correct.test_structureand otherbuilt-fixture tests will fail due to the P1communicate()bug in_prepare_baseline_repo. The archive-failure and missing-secret tests are correct.TestReviewFixesdirectly validates the fixes from the previous review round.read_only_pathschmod is advisory only and that authoritative enforcement is sidecar-side. No issues.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Caller participant HarborRunner participant harbor_CLI as harbor run (nested) participant jobs_dir as jobs/timestamp/trial participant Sidecar as eval-sidecar (vero harbor serve) Caller->>HarborRunner: produce_sample_results(workspace, params, result_dir) HarborRunner->>HarborRunner: _task_names_for(params) → [(sample_id, task_name)] HarborRunner->>HarborRunner: filter pending (skip _is_done samples) HarborRunner->>harbor_CLI: uv run harbor run -i task0 -i task1 --jobs-dir jobs/ harbor_CLI->>Sidecar: eval requests per task Sidecar-->>harbor_CLI: verifier rewards harbor_CLI->>jobs_dir: result.json per trial harbor_CLI-->>HarborRunner: exit (non-zero tolerated) HarborRunner->>HarborRunner: _load_trials(jobs_dir) → best trial per task HarborRunner->>HarborRunner: _collate → _sample_result per pair HarborRunner->>Caller: save_sample_result (SampleResult) for each sample%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Caller participant HarborRunner participant harbor_CLI as harbor run (nested) participant jobs_dir as jobs/timestamp/trial participant Sidecar as eval-sidecar (vero harbor serve) Caller->>HarborRunner: produce_sample_results(workspace, params, result_dir) HarborRunner->>HarborRunner: _task_names_for(params) → [(sample_id, task_name)] HarborRunner->>HarborRunner: filter pending (skip _is_done samples) HarborRunner->>harbor_CLI: uv run harbor run -i task0 -i task1 --jobs-dir jobs/ harbor_CLI->>Sidecar: eval requests per task Sidecar-->>harbor_CLI: verifier rewards harbor_CLI->>jobs_dir: result.json per trial harbor_CLI-->>HarborRunner: exit (non-zero tolerated) HarborRunner->>HarborRunner: _load_trials(jobs_dir) → best trial per task HarborRunner->>HarborRunner: _collate → _sample_result per pair HarborRunner->>Caller: save_sample_result (SampleResult) for each sampleComments Outside Diff (2)
vero/src/vero/harbor/runner.py, line 739-752 (link)n_attemptsandmax_retriessilently dropped from harbor runHarborConfigexposesn_attemptsandmax_retriesas typed fields (notextra_argspass-throughs), signalling that callers expect to configure retry behavior. Neither field is translated to a CLI flag in_build_command, so everyharbor runinvocation uses whatever the harbor defaults are regardless of what the caller configured. A user who setsmax_retries=0to disable retries on a budget-sensitive run will get the default retry behavior and may blow their budget.Prompt To Fix With AI
vero/src/vero/harbor/runner.py, line 799-813 (link)task_namewhen harbor retries a taskrglob("result.json")visits all result files across all timestamp directories underjobs_dir. If harbor'sn_attemptsormax_retriescauses the same task to produce multiple trial entries with the sametask_name,trials[task_name] = datasimply overwrites with whichever filerglobhappens to yield last — filesystem iteration order is not defined. This can cause a passing retry to be silently replaced by a failing one (or vice versa), yielding an incorrect score for that sample.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "fix(harbor): nested-run flags/dedup/resu..." | Re-trigger Greptile