Skip to content

2/3 Add the redesigned VeRO system (vero/)#45

Open
varunursekar wants to merge 53 commits into
pr1-legacy-relocatefrom
pr2-add-vero
Open

2/3 Add the redesigned VeRO system (vero/)#45
varunursekar wants to merge 53 commits into
pr1-legacy-relocatefrom
pr2-add-vero

Conversation

@varunursekar

@varunursekar varunursekar commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Second of three stacked PRs splitting #41. Base: pr1-legacy-relocate.

Adds the v0.5 system at the repo root: vero/ (core + harbor eval sidecar, build compiler, runtime/observability), vero-tasks/, runs/. Root README points to legacy/. Pure adds (197 files) + README/.gitignore.

Review after 1/3. 3/3 adds harness-engineering-bench/ on top.

🤖 Generated with Claude Code

Greptile Summary

This PR introduces the VeRO v0.5 system as a pure-add set of 197 files under vero/, vero-tasks/, and runs/. It ships the complete evaluation engine, budget ledger, Harbor eval sidecar, inference gateway, build compiler, optimizer, W&B observability sinks, and a vero-tasks library.

  • Evaluation engine & budget ledger: All three cleanup paths (EvaluationCancelledError, EvaluationExecutionError, infrastructure failure) now correctly wrap both _record and refund in asyncio.shield; a new _drain_write helper centralises the cancellation-safe write-drain loop that was previously hand-rolled inconsistently across reserve and refund.
  • Harbor sidecar security: Admin-token gating uses secrets.compare_digest with atomic file writes (fchmod+fsync+os.replace); _safe_extract_tar adopts filter="data" (matching extract_harbor_session_archive) to strip device files and setuid bits; secret sanitisation is applied longest-first to prevent partial-match leakage.
  • One dead-code artifact: validate_extra_args in HarborBackendConfig has an unreachable duplicate return value following the real return, likely a merge/edit artifact.

Confidence Score: 5/5

Safe to merge; the only finding is a dead unreachable return value that has no effect on runtime behaviour.

This is a large pure-add PR. The most security-sensitive paths (admin-token auth, tar extraction, budget atomicity, cancellation shielding) are all handled correctly. The sole finding is an unreachable duplicate return statement in a Pydantic validator — a cosmetic artifact with no runtime impact.

Files Needing Attention: vero/src/vero/harbor/backend.py — contains the dead-code duplicate return in validate_extra_args.

Important Files Changed

Filename Overview
vero/src/vero/harbor/backend.py Large Harbor evaluation backend; contains an unreachable duplicate return value in validate_extra_args (dead code, no runtime impact). All other validation, secret-sanitization, and isolation logic looks correct.
vero/src/vero/evaluation/engine.py Core evaluation orchestration: all three cleanup paths now correctly shield both _record and refund from mid-flight cancellation, addressing the prior review's concerns.
vero/src/vero/evaluation/store/budget.py Budget ledger with centralised _drain_write helper that correctly pairs forward write errors with saved CancelledErrors; the asymmetry between reserve rollback and refund forward-write is now resolved.
vero/src/vero/harbor/build/compiler.py _safe_extract_tar now uses filter="data" to strip device files and setuid bits, aligning with extract_harbor_session_archive; path-traversal and unsafe-link validation also present.
vero/src/vero/sidecar/auth.py Atomic admin-token write with fchmod + fsync + os.replace, constant-time comparison via secrets.compare_digest, and restricted directory mode to block unprivileged agent traversal.
vero/src/vero/sidecar/verifier.py Candidate selection (submit → auto_best → last-resort) and per-target scoring with idempotent finalization locked behind asyncio.Lock; baseline-floor logic correctly refuses to ship the seed unverified when infrastructure fails.
vero/src/vero/optimization/optimizer.py Multi-round, concurrent proposal scheduling; self.max_proposals < 0 guard now correctly precedes the proposal_limit check, fixing the previously unreachable negative-limit message.
vero/src/vero/runtime/wandb.py Two W&B sinks (optimizer and sidecar) with resumable state; SidecarWandbSink correctly persists run_id via UUID for restart-safe resumption, while WandbEventSink derives a stable ID from session_id.
vero/src/vero/gateway/inference.py Per-scope token budget with semaphore concurrency limiting, SHA-256 token digests in config (raw tokens never at rest), and atomic durable usage ledger.
vero/src/vero/sidecar/app.py FastAPI app with admin-token gating on finalize/evaluations/export endpoints; session export uses a temp directory cleaned up via BackgroundTask after the response stream completes.

Sequence Diagram

sequenceDiagram
    participant Agent
    participant Sidecar as EvaluationSidecar (app.py)
    participant Engine as EvaluationEngine
    participant Budget as BudgetLedger
    participant Backend as HarborBackend
    participant Verifier as CanonicalVerifier

    Agent->>Sidecar: POST /eval
    Sidecar->>Engine: evaluate(request, AGENT)
    Engine->>Budget: reserve(backend_id, set, cost)
    Budget-->>Engine: budget snapshot
    Engine->>Backend: evaluate(candidate)
    Backend-->>Engine: EvaluationRecord
    Engine->>Engine: asyncio.shield(_record(record))
    Engine-->>Sidecar: projected disclosure
    Sidecar-->>Agent: EvaluationReceipt

    Note over Sidecar,Verifier: Finalization (admin-token gated)
    Verifier->>Engine: quiesce_agent_evaluations()
    Engine->>Engine: cancel remaining agent tasks
    Verifier->>Verifier: _select_candidate()
    Verifier->>Engine: evaluate_record(candidate, ADMIN)
    Engine->>Backend: evaluate(selected candidate)
    Backend-->>Engine: VerificationResult
    Verifier->>Verifier: _atomic_write_json(finalize.json)
Loading

Reviews (24): Last reviewed commit: "Stop advertising the retired insights su..." | Re-trigger Greptile

Introduce the v0.5 codebase: vero/ (core + harbor eval sidecar, build
compiler, runtime/observability), vero-tasks/, and runs/. Root README points
to legacy/ for the original-paper code.
@socket-security

socket-security Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​orjson@​3.11.910010010010070
Addedpypi/​wandb@​0.28.074100100100100
Addedpypi/​litellm@​1.92.074100100100100
Addedpypi/​openai@​2.46.090100100100100
Addedpypi/​pydantic@​2.13.4100100100100100
Addedpypi/​fastapi@​0.139.0100100100100100

View full report

Comment thread vero/src/vero/optimization/optimizer.py Outdated
Comment thread vero/src/vero/harbor/build/compiler.py Outdated
varunursekar and others added 12 commits July 23, 2026 11:51
…back #1)

Addresses the 'baseline floor fails open' regression:
- baseline_floor now defaults to false. The floor gates a ship on a
  validation comparison while the reward is on the (possibly
  differently-distributed) target, so it is opt-in.
- Pin the fixed seed's score to avoid re-scoring it every run
  (reproducibility + one fewer eval): VerificationTarget.baseline_reward
  (per-target, post scale/offset) and VerificationSelection
  .baseline_selection_score (for the floor). Plumbed through the build
  config + compiler.
- Fail safe when the floor is on, unpinned, and the seed re-score can't be
  measured: ship nothing (NoCandidateError -> shipped=false + infra error)
  instead of shipping the best candidate unverified (the fail-open bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
#1)

Add an admin-only way to generate the number to pin: CanonicalVerifier
.measure_baseline(replicates) admin-scores the fixed seed N times on the
selection partition and each target and returns per-key mean/stddev. Exposed
as POST /score/baseline and 'vero harbor score-baseline --replicates N'.
Reuses the existing admin scoring machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…atten

The benchmark-config tests read harness-engineering-bench/ (a separate branch
in the stacked split), so skip them when it isn't checked out. Update the
paths for the candidates/ -> top-level flatten (all benchmarks are now
top-level siblings of gaia/).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
min_aggregate_cases defaulted to 1 — a vacuous floor that let an agent read
held-out validation labels one case at a time via single-case aggregate
evals. Default to 5 in both the sidecar policy and the build AgentAccessSpec
so a build that omits it is safe rather than unfloored (shipped builds
already set 5 explicitly).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Re-enabling harness isolation broke the eval: 'uv run' as the unprivileged
harness got a fresh empty uv cache and couldn't resolve the candidate package
from it (No module named 'gaia_agent'), with no network to download at eval
time. Pre-warm /home/harness/.cache/uv from the build (root) cache in the
sidecar image, matching the UV_CACHE_DIR the backend sets for the harness user.

Also fixes a test regression from feedback #2: with the k-anonymity floor
defaulting to 5, the instruction now reads 'must include at least 5 cases'
rather than 'arbitrary subsets'.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ack #8)

The mean-of-k recorded only score/n_attempts/n_scored, so an outage-diluted
cell (dead attempts zero-filled) was indistinguishable from a genuinely clean
low cell. Add n_dead_infra (dead-to-infrastructure attempts, classified via
the existing error taxonomy) and n_clean (the rest), so the split is visible
in the live metrics rather than only reconstructable from raw trial records.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- optimizer.run: validate self.max_proposals >= 0 before the proposal_limit
  guard, so the specific 'must be non-negative' error is reachable (was a
  dead branch that always tripped the generic message first).
- compiler._safe_extract_tar: extractall(filter='data') to strip device
  files / setuid bits and neutralize unsafe links, matching
  extract_harbor_session_archive (also silences the py3.14 deprecation).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A cold-start walkthrough of the harbor path: the three-container trust
boundary, a run end-to-end, the evaluation core, disclosure/selection, the
five security mechanisms, and a suggested outside-in review order keyed to
the source files.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Update harbor_requirement pin from harbor[modal]==0.18.0 to ==0.20.0
across the README and harbor tests. Remove the PYTHONPATH injection in
the harness eval launch: it was a no-op (uv strips PYTHONPATH from the
child env) and did not fix the "No module named <agent>" failure under
the dropped uid, so it and its test assertion are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The candidate is checked out at <mktemp>/repository; `mktemp -d` makes
that parent 0700 root. The eval launch chowned the repository (and the
staging tree) to the harness user but not the mktemp parent, so once the
process dropped to harness it could not traverse into its own workspace
to resolve the editable candidate package's absolute path — every eval
(baseline included) failed with "No module named <agent>" while harbor
itself still loaded from the harness-owned cache.

Grant the harness user traversal on that parent (chmod o+x) alongside the
existing chowns. o+x suffices because run_as drops to the user's own
uid+gid, so it is "other" relative to the root-owned parent. The dir
holds only candidate code, so widening traversal leaks no trusted data.

Reproduced and fixed against the real compiled sidecar image with a
faithful user+group+extra_groups privilege drop: 0700 -> "No module
named gaia_agent"; 0701 -> import resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a reachability probe: after chowning/granting traversal, run `test -r
<project_path>` as the dropped user before launching harbor. Readability
requires traversing every ancestor, so any provisioning gap (a missing
chown, a non-traversable parent) is caught here with a clear message
instead of resurfacing as a cryptic "No module named <agent>" several
retries downstream — which is exactly how the checkout-parent bug hid.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Extract the harness provisioning commands and reachability probe into
vero.harbor.isolation as a single source of truth, and have the eval
launch use them, so the runtime and the test can't drift.

Add tests/test_v05_harbor_isolation_container.py: inside a throwaway
Linux container it loads the real sandbox.py + isolation.py standalone
(stdlib-only, no install) and asserts, against the real uid/gid drop and
the real provisioning commands, that (1) a checkout under a 0700 mktemp
parent is unreachable to the dropped user when only the leaf is chowned
(the regression sentinel for the shipped bug), (2) it becomes reachable
after harness_grant_commands and the probe agrees, and (3) trusted
root-only state stays unreadable to the dropped user. Skips when no
docker daemon is available.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Comment thread vero/src/vero/evaluation/store/budget.py Outdated
varunursekar and others added 4 commits July 23, 2026 17:08
Agents don't know what "vero" is; every name they meet should say what it
holds. The workspace context directory is now .evals/ with results/ (was
evaluations/), tasks/ (was cases/), and plan.json (was evaluations.json).
Host-side paths (~/.vero, ./.vero/session) are operator-facing and unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>
One well-named entry point over the .evals/ context — the new avatar of the
legacy VeroAgent's three structured tools. Runner subcommands (run/status/
result/submit) delegate to the sidecar client lazily; the viewers (list/show/
cases/trace/diff/plan/tasks) are unprivileged stdlib readers of the disclosure-
projected tree, with bounded, paginated, metadata-first output. The compiler
bakes skills/evals/SKILL.md into the optimizer workspace by default
(include_evals_skill), and the instruction template now teaches `evals`.

Co-Authored-By: Claude Fable 5 <[email protected]>
min_aggregate_cases moves onto the core EvaluationAccessPolicy (omitted
resolves to 5 under aggregate disclosure, 1 otherwise) and the engine refuses
agent-chosen aggregate subsets below the floor after cost resolution, so the
protection holds on every path — programmatic API and local runs included —
not just compiled harbor deployments. The sidecar keeps its earlier check as
defense-in-depth and now passes the floor through its explicit authorization.

AgentAccessSpec.to_access_policy() is the single typed translation from build
spec to runtime policy; the compiler emits it nested in serve.json and
SidecarEvaluationPolicy holds it as `access`, replacing the hand-rolled flat
keys (disclosure/agent_evaluable/min_aggregate_cases/expose_case_resources)
that had to be kept in sync across three representations.

Co-Authored-By: Claude Fable 5 <[email protected]>
The sidecar's context writer predates the .evals rename and had its own
literals: exposed task resources landed in .evals/cases/ (README, skill,
and instruction docs all say tasks/) and plan.json was never written in
the harbor topology, breaking the documented 'evals plan' first step.
Caught live in the first gaia run of the new stack.
Comment thread vero/src/vero/evaluation/budget.py Outdated
AgentContextDirectory now owns write_case_resources and
write_evaluation_plan; both WorkspaceContextManager and the sidecar
feed it ContextPlanEntry rows (policy, evaluation set, resolved
budget), so the tree layout can no longer drift between topologies.
Top-level names (results/, tasks/, candidates/, plan.json) are module
constants. Behavior note: case-resource exposure now keys on
expose_case_resources alone (which model validation already ties to
agent_visible); the sidecar previously also required agent_can_evaluate.
…ecar

The gateway now keeps a size-rotated JSONL log of every request it
proxies or denies (scope, attribution, model, status, latency, tokens,
and head+tail-truncated request/response bodies; SSE responses are
captured from the chunks already tapped for usage). On by default in
compiled tasks at /state/inference/requests with a 16KB per-body cap
(inference_gateway.log_requests / request_log_body_bytes).

The trusted sidecar - the only credential holder - mirrors the gateway
state into the existing W&B run: a poller logs per-scope usage series
(live optimizer producer-scope burn included) every 30s and ships
rotated log files as artifacts, sharing the sink step counter. At
finalize the gateway ledger and request log are copied into the
session's artifacts so /session/export preserves every
request-response, W&B or not.
The loader resolves an existing local task_source to an absolute path,
while the committed partition manifest records it relative to itself;
compare resolved locations before rejecting. Registry sources still
compare literally.
@shehabyasser-scale

Copy link
Copy Markdown
Collaborator

Really nice split. Breaking #41 into relocate-legacy / add-system / add-benchmarks makes it reviewable again (Greptile passes each now), keeps the old stack intact under legacy/, and reads as clean pure-adds. I went through the system tree closely against the legacy Harbor stack.

Two things I'd flagged on the earlier v0.5 snapshot are already handled here, thanks for that:

  • Baseline floor fails safe now (verifier.py:383, raises NoCandidateError if the seed can't be re-scored rather than shipping it unverified).
  • k-anonymity floor defaults to 5 (build/config.py:29, and resolved to 5 under AGGREGATE in evaluation/models.py:707).
  • The in-process .veroaccess ACL being gone is fine: it's replaced by the privilege-drop + root-owned session dir isolation. Might be worth a one-line note in the PR that this was intentional.

One integrity concern I'd want resolved before this is relied on for competitive selection:

Infra-classified cases are excluded from the aggregate rather than scored at failure_score. An all-dead "infrastructure" case returns CaseStatus.ERROR (backend.py:1083-1105, the comment there literally says "excluded from the aggregate") and is dropped from informative_scores, so the mean divides by the informative count only (backend.py:1324-1330). The transient-infra classification matches the candidate process's own exception type name + message against a regex allowlist (error_taxonomy.py:131-159: rate.?limit|...|time(?:d.?)?out|connection|...). Budget and auth are detected out of band (good), but transient-infra is not.

If a candidate-attributable failure can reach that path (a candidate that raises ConnectionError, or emits a "timeout"/"connection" message, on a hard case), that case drops out of its own denominator and inflates the mean over the rest. That's the one-sided-selection lever the legacy code deliberately kept off: legacy always zero-filled dead attempts and documented infra retry as trusted-candidate-only. Here there's no trusted-only gate and infrastructure_max_attempts defaults to 3 (on).

Could you confirm whether the routing prevents a candidate-raised exception from being classified transient-infra? If it doesn't, I think the fix is:

  1. Score candidate-attributable all-dead cases at failure_score (restore the zero-fill invariant); restrict exclusion to harness-produced signals (coverage gaps, gateway-ledger budget events).
  2. Default infrastructure_max_attempts to 1, and gate retry > 1 to trusted / finalization evaluations only.

One more (high), in scale-vero-tasks: a single non-finite metric poisons the whole report. runner.py:154 writes score/metrics unguarded, so a NaN/inf from one case makes vero reject the report and collapse the entire run to the failure value, discarding all per-case data. Suggest enforcing finiteness at the TaskResult boundary.

Smaller, non-blocking follow-ups:

  • Whole-run retry replaces rather than merges groups (backend.py:1222), so with aggregate_attempts: best it's a best-of-3 amplifier tripped by one missing task.
  • A candidate self-timeout is classed transient-infra and excluded (backend.py:1041-1044) instead of scored at failure_score.
  • The legacy operator ERROR alarm and the partly-zero-filled-mean WARNING are gone (raw n_dead_infra/n_clean counts survive as metrics, but there's no grep-able signal in the logs).
  • No per-case infra_retry audit marker, so a score that survived N re-rolls looks identical to a first-attempt score.
  • DGM / Evolutionary counters + seeded RNG reset on resume (strategy.py:241), silently shifting the search distribution.

Happy to pair on the aggregate/retry piece if that's useful.

varunursekar and others added 19 commits July 24, 2026 15:16
Addresses greptile P1s on #45. In BudgetLedger.reserve/refund a failing
durable write (transient OSError) escaped at the shield-drain await and
swallowed the run's CancelledError, breaking structured cancellation and
leaving a reservation charged on disk. Both drains now catch the write
failure and re-raise the originating cancellation, chaining the write
error for diagnosis. In the engine, the EvaluationExecutionError handler
and the infrastructure-failure refund now wrap record+refund in
asyncio.shield like the EvaluationCancelledError handler, so a
cancellation racing cleanup cannot interrupt the refund and leak budget.
Addresses the competitive-selection integrity concern on #45. The
transient-infra category is detected from the candidate process's own
exception type/message, and excluded cases drop out of the aggregate
denominator — so a candidate emitting a timeout/connection error on a
hard case could inflate its mean, with whole-sub-run retry (default 3)
amplifying it under aggregate_attempts: best.

For competitive (agent) evaluations a within-trial transient-infra
result is now scored at the failure value (the zero-fill invariant), and
whole-sub-run infrastructure retry is disabled. Trusted finalization
re-scores — run by the operator, not gameable — keep the exclusion and
the configured retries. Genuinely harness-produced deadness stays
excluded for both: coverage gaps (no trial produced) and gateway-ledger
budget/auth (terminating, out of band). Default infrastructure_max_
attempts lowered to 1.
Both specs govern the fair-play boundary but neither said so. Give each a
docstring naming its role and fields: AgentAccessSpec is what the optimizer
may see and spend while searching a partition, VerificationTargetSpec is how
the trusted verifier finally scores the candidate that search picked.

Note that min_aggregate_cases is only consulted when disclosure is AGGREGATE;
under FULL the agent already sees every per-case result, so the floor is inert
there whatever it is set to.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
EvaluationModel was a BaseModel with extra="forbid" and nothing else, but its
name claimed an evaluation-contract scope it never had: WorkspaceOverlaySpec
and WandbSpec have nothing to do with evaluation. Worse, the build and runtime
layers imported it from vero.evaluation purely to inherit a config flag, which
made the evaluation package a dependency of code that otherwise has no reason
to touch it.

Move the base to vero/models.py as StrictModel, a dependency leaf importing
only pydantic, and rebase all 79 subclasses onto it. Fold in the 13 models that
hand-rolled the same ConfigDict (Candidate, SessionManifest, RuntimeEvent, the
session specs, and others), and drop it from WorkspaceOverlaySpec and
HarborBuildConfig, which were setting it redundantly on an already-strict base.

Strictness is the point and is unchanged: a typo'd key in a build YAML must
fail the build rather than be dropped in silence, leaving a default that
quietly widens the optimizer's access.

EvaluationModel is removed rather than aliased; it had no references outside
src/vero. Test results are identical to before the change.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Give the five undocumented models in the build schema the same treatment as
AgentAccessSpec and VerificationTargetSpec: a summary, the context needed to
use them, and an Attributes block.

HarborBuildConfig is the payload, since it is the whole grammar of a
benchmark's build.yaml and had a one-line docstring for 59 fields. Its header
also records the two cross-field rules validate_references enforces, which
were previously only discoverable by reading the validator.

The Inference*Spec docstrings note two things that were only visible in the
gateway: a denied model returns 403 model_denied, and the token budget is
checked before a request starts, so one request can overshoot it. The
InferenceGatewaySpec docstring also states plainly that the per-scope
allow-list confines the target only against a non-adversarial optimizer, since
that spec is where someone would otherwise assume the guarantee is airtight.

Inline comments that would have duplicated an Attributes entry are folded in
rather than left alongside, so the two cannot drift apart.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A build could name a model that no gateway scope allows. Nothing caught it, so
the mismatch surfaced as a 403 model_denied at run time — and for a
verification target, only after search had already spent its whole budget,
which is the most expensive moment to learn about a typo.

Check each model that will actually be requested against the scope that will
actually serve it: the task model against evaluation, and each target's
scoring model (its own override, else the task model) against finalization.
Verification runs the target agent too, which is why finalization has to allow
the target's model and not just a judge's.

This stays narrow on purpose. It only objects when a model that will genuinely
be requested is unreachable, so a finalization scope that allows extra models
still builds. All five current benchmarks pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The compiled path assumed both ends were Harbor: the outer optimizer and the
inner evaluation. That left no config-driven route for a target that is not an
agent — a solver, an index build, a data pipeline — where there is nothing for
a nested `harbor run` to drive. ALE-Bench is that case today and handles it by
bypassing the compiler entirely, hand-writing a sidecar factory that
re-implements build_harbor_components with one backend swapped out.

Make the inner backend a choice instead. HarborDeploymentConfig.backends is now
a union discriminated on a new `type` tag, build_harbor_components dispatches on
it, and HarborBuildConfig grows evaluation_backend plus a command_backend spec.
The compiler emits the matching backend, bakes the scoring harness into the
trusted sidecar at /opt/harness, and tells it where the case files are; a
command backend's case ids name whatever its harness understands, so they are no
longer resolved as local Harbor task directories.

Everything above the backend seam is untouched. Disclosure, budgets, the
gateway, and verification are backend-agnostic already, which is the point: a
task on this route gets the metered gateway and the build-time validators that
a hand-written factory does not.

Compatibility is preserved in both directions. The tag defaults to "harbor", a
serve.json written before the union still loads, and all five benchmarks parse
unchanged. agent_import_path becomes optional and is required for a harbor
backend; Harbor-only fields set on a command build are reported rather than
silently ignored.

Nesting each backend's fields under its own sub-spec would make that structural
rather than validated, but it would rewrite every benchmark's build.yaml, so it
is left as a TODO.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A command build had to name a task_source it never used, so the only way to
satisfy the schema was to point it at an empty directory. Nothing on that path
reads it: it names Harbor task definitions, and a command backend has none.

Make it optional and require it for a harbor backend alongside
agent_import_path, reporting both together when either is missing. Setting it on
a command build is now rejected as Harbor-only, which also means a case id is
resolved as a local Harbor task directory exactly when a task_source names one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
CommandBackendSpec documented harness_source as resolved relative to the build
YAML, matching agent_repo, task_source, task_manifest, and overlay sources, but
the loader never resolved it. A relative path was therefore interpreted against
the working directory, so the existence check failed from anywhere except the
YAML's own directory.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The TODO said what to do and why to defer, but not what it would cost, so the
question would be re-litigated from scratch. Note the measurement and the
migration trick that avoids a breaking change.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Every container path in a compiled task was written down two or more times: as a
Python constant in the compiler and again as a literal in the Jinja templates.
/work/agent appeared 19 times across 10 files. Changing a constant therefore did
not propagate to the templates, and three paths (/opt/seed.sh,
/opt/inference.json, /opt/overlay) had no constant at all. I had already created
one instance of the hazard: HARNESS_DIR in the compiler and /opt/harness
hardcoded in the sidecar Dockerfile.

Add vero.harbor.layout.TaskLayout, a frozen dataclass holding every container
path, service name, and port, with child paths derived as properties so a base
and its children cannot drift. Every template now receives it, so no template
spells out a path of its own. It lives outside build/ because harbor.inference
also needs two of these as field defaults.

Path values are unchanged and deliberately so: they are a contract with the
benchmarks' read_only_paths and with compiled task directories that are checked
in. Renaming an attribute is now cheap; repointing one is still breaking.

Measured: literal path occurrences outside the definition drop from 93 across 13
files to 0, with the only remaining copies being layout.py itself and one pinning
test. That test exists because every other test now references the layout, which
would otherwise make them tautological — it is the single place the values are
written twice on purpose.

Verified by rendering four tasks covering every template branch (gateway on/off,
overlay, wandb, budget-blind, harness isolation off, harbor and command
backends) before and after: the generated output is byte-identical. Jinja runs
with StrictUndefined, so a mistyped layout attribute fails the build rather than
rendering an empty path.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The compose template named the factory as a string literal, duplicating a value
that also exists in Python — the last instance of exactly what layout.py was
created to remove, and the worst-behaved one. A stale dotted path here fails when
the sidecar container starts, so neither the type checker nor the suite would
catch a rename of the module or the function.

Derive FACTORY_PATH in deployment.py from the function object itself and render
it into the template. This does not belong in layout.py: that module is
dependency-free by design, and a hand-maintained literal there would reproduce
the same problem one level over, when the referent is an object we can introspect.

The compiler imports it inside compile_harbor_task rather than at module scope.
A top-level import does work today, but only because importing a submodule skips
the parent package's partially-run __init__, which imports this package before
deployment — an ordering accident, not a property anyone would notice breaking.
It would also pull the whole runtime stack into `vero harbor build`.

The test asserts on the rendered compose file rather than on the constant, so it
still holds if a literal is put back, and it imports the path to prove it
resolves. Verified both ways: renaming the function needs no template edit and
stays green, and a stale literal fails.

Left alone: agent_import_path in each build.yaml is the same species of string,
but it points into the target repo, whose dependencies are not installed on the
build host, so it genuinely cannot be resolved at compile time. Dotted paths
pointing inside vero get derived and resolution-tested; ones pointing outside
cannot be.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The proxy route was declared in inference.py and then independently rebuilt by
string concatenation in four places that knew nothing about it: launch.json, the
compose file's OPENAI_BASE_URL, the seed script, and the trusted backend. Five
copies of three conventions — the scope segment, the attribution segment, and
/v1. Change the route and four sites keep producing URLs the gateway 404s or
403s on, inside a container, at run time.

Put the route shape in the layout as one string and derive everything from it:
scope_route for the gateway to register, scope_url for callers using this
layout's gateway, and scope_path for the trusted backend, which reads its base
URL from the deployment config rather than assuming ours. The optimizer's
attribution segment was duplicated three times and is now a field too. The
templates receive a precomputed producer_base_url instead of concatenating.

The test asserts on the compiled artifacts — compose, launch.json, and the seed
script — rather than on the constant, so it still holds if a template goes back
to building the URL by hand. Verified by doing exactly that with a wrong shape:
it fails.

Also folded in, both hygiene rather than risk: VERO_EVAL_URL was a
producer/consumer pair split between the compose template and the CLI that reads
it, and the gateway's upstream env var names were the last constants still local
to the compiler.

The OPENAI_* scrub set is now a named constant with the invariant written down.
Worth being precise about what that invariant is, since it looks alarming: the
blanking loop is default-deny, so a new credential added to `secrets` is blanked
for the optimizer automatically. The exclusion list is the explicit-allow
exception for the two names the template sets deliberately, and forgetting one
emits a duplicate compose key rather than leaking anything.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
task.toml.j2 hardcoded build_timeout_sec = 1800 while the timeout_sec two lines
above it was already parametrized, so one timeout in the same generated file was
tunable and the other was not. The outer build is not trivial — for a source
build it copies the vero tree, installs it plus harbor into the sidecar, syncs
the baseline, and pre-warms a uv cache — and there was no way to raise the limit
without editing the template.

Typed as an int to match the neighbouring verifier timeout, so the default still
renders as 1800 and compiled output is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
harbor/ held fourteen flat modules that are not peers: they run in three
different processes on either side of a trust boundary, and nothing in the
layout said so. Most of them were not Harbor code at all. The sidecar stack --
sidecar, app, serve, verifier, transport, auth, isolation, session -- imports
only itself, never backend, build, deployment or cli, and inference.py imports
only layout. deployment.py is the single module binding the two, which is
exactly its job. The nested-backend work sharpened the point: a compiled task
can now use a command backend and no HarborBackend at all, while every module
implementing it still lived under harbor/.

Split those out to vero/sidecar/ and vero/gateway/, hoist layout.py to the top
level so gateway/ needs no backwards edge, and leave harbor/ holding what is
genuinely Harbor: build/, backend, generation, deployment, cli. Regroup
evaluation/ along its own dependency layers into backends/, scoring/ and
store/. A reviewer can now read the trust architecture off the tree.

Package docstrings carry what the tree cannot: that nothing in sidecar/ is
Harbor-specific, and that its co-location is load-bearing -- HTTP carries the
API, but candidate code, the .evals context and the admin token all move
through volumes shared with main, so it is not deployable away from the task it
serves. That is also why it keeps the name.

evaluation/ is an internal regroup: every name is still re-exported from
vero.evaluation, so only deep importers moved. harbor/ -> sidecar//gateway/ is
a real move, so those exports were relocated rather than aliased back; leaving
compatibility shims in harbor/__init__ would preserve the false impression this
commit exists to remove.

Pure moves and import rewrites. No logic changes: 399 passed / 9 skipped before
and after.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
circle_factory built SidecarEvaluationPolicy with flat disclosure and
expose_case_resources keyword arguments, but those moved under a nested
access=EvaluationAccessPolicy some time ago. The sidecar container therefore
died at startup with three validation errors before serving anything.

Predates the package restructure -- the same call fails identically on
pr2-add-vero -- and no test covers it, because nothing exercises the example
factories. Found by actually booting the container: it now starts and serves
/health and /status, reporting full disclosure on development, aggregate on
validation, and the budget ledger.

ale_factory.py in harness-engineering-bench has the same stale construction and
is broken the same way.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The example only had the hand-written path: circle_factory.py wiring a
CommandBackend directly, with the agent handed raw OPENAI_* credentials. That
is the same shape as ale-bench, and it means the one worked example of a
non-agent target is also an example of an unmetered optimizer.

The nested-backend refactor makes the compiled path available to such a target,
so add a build.yaml beside the hand-written task: same packing.py, same
evaluate.py, but `evaluation_backend: command` and an inference gateway. The
optimizer now reaches its model only through a scoped, allow-listed, metered
token; the raw upstream stays in the gateway container and is scrubbed to "" in
main. Two wirings of one problem, directly comparable.

Verified end to end with codex/gpt-5.4: reward 2.6324 against a 0.9598 baseline
and a ~2.635 known optimum, 0 exceptions in 20m46s. The gateway metered every
optimizer call -- 57 requests, 3.6M tokens, no denials -- and the evaluation
scope stayed at zero, since a command harness makes no model calls of its own.

passthrough_environment is set and commented because it is easy to get wrong:
CommandBackend deliberately runs the harness with PATH=os.defpath, so python
(in /usr/local/bin on the base image) is not found unless PATH is forwarded.
The failure surfaces only when the harness first runs, after the agent has
already spent its budget.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
test_v05_harbor_build.py carried two tests that read build YAMLs out of
harness-engineering-bench, guarded by a skipif on that directory existing. It
does not exist on this branch, so they always skipped here and only ran one
branch up -- where they had been failing for a while against stale expectations
nobody saw. A test that can silently skip on the branch that owns it is a test
that rots.

Drop them and the BENCHMARK_ROOT/_requires_benchmarks harness; they move to
pr3-harness-bench, where the benchmarks are always present and the guard can go
away entirely. Suite is unchanged at 399 passed, with skips down from 9 to 5.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
report.py was the largest module in the codebase at 43K, but 57% of that was
one raw string: a complete 25K HTML document with its own CSS and JavaScript,
pasted into _REPORT_HTML. Inside a string literal it gets no highlighting, no
formatter, no linter, and no diff worth reading -- and it made the module look
like a 43K Python problem when the Python is 19K.

Extract it verbatim to templates/report.html, loaded through
importlib.resources the way the packaged evals skill already is. Non-Python
artifacts get their own directory rather than sitting beside the modules,
matching harbor/build/templates and skills/evals. Substitution is unchanged:
still one replace of __VERO_REPORT_DATA__.

The extracted text is byte-identical to the old literal, so generated reports
are unchanged by construction rather than by inspection. The wheel ships the
file alongside the existing Jinja templates and skills/evals/SKILL.md, checked
by building one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Comment thread vero/src/vero/evaluation/store/budget.py Outdated
varunursekar and others added 4 commits July 25, 2026 22:27
The main container blanks every declared secret except the two the compose
template assigns itself -- the optimizer's scoped token and its gateway URL.
GATEWAY_ROUTED_CREDENTIALS named that exemption in Python while the template
spelled the same names out again, five copies of OPENAI_API_KEY and four of
OPENAI_BASE_URL between them.

The duplication is one-directional and quiet. Blanking is default-deny, so a
newly declared secret is scrubbed automatically and adding one is safe. The
failure is the other way: a name added to the exemption but not assigned by the
template would be neither blanked nor set, leaving whatever the host exported
in the optimizer's container -- the one outcome the scrub exists to prevent.

Move both names onto LAYOUT beside the other env-name fields, derive the
exemption from LAYOUT.routed_credential_envs, and have the compose file and
codex's provider config reference them. Nine literals become one definition.

Add a test that renders a compiled task and asserts every exempt name is
positively set with a scoped value, and that the raw upstream is blanked in the
same container. Verified it fails as intended by adding a third name to the
exemption: "STRAY_KEY is skipped by the scrub but never set".

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
config.py held three unrelated things in 953 lines: the leaf models a
build.yaml composes, HarborBuildConfig itself, and the ${NAME:-default}
substitution language plus the YAML reader. Split into specs.py (313),
config.py (635), and loader.py (155). Only two places imported deeply;
build/__init__.py re-exports the same eleven names.

validate_references was 136 lines running six unrelated checks, including
45 lines of task_manifest JSON parsing. It is now six named validators in
the original order, so error precedence is unchanged and a rejected build
points at the rule it broke. The manifest check reads and parses the file
once instead of twice.

The fields are grouped into six private base classes, each with its own
docstring and the validators that concern only itself. Grouping by
inheritance rather than nesting keeps the wire format flat, so every
checked-in build.yaml parses identically -- verified by loading all five
benchmark configs and by comparing the old and new field sets (62 each,
same annotations and defaults).

That retires a duplicate list: the Harbor-only fields were enumerated once
as declarations and once in a hand-maintained frozenset, and
_HARBOR_ONLY_FIELDS is now derived from _HarborEvaluationFields. The two
had already drifted -- reward_key is Harbor-only in fact, since a command
harness reports its own reward, but was missing from the list, so setting
it on a command build passed validation and did nothing. It is now in the
group and rejected.

Two tests. One sweeps the derived set, so a field added to the group is
covered the day it lands; the other asserts the groups stay flat on the
wire and that every field belongs to one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The package restructure moved the sidecar stack, the gateway, and the
evaluation store without updating this doc, leaving fourteen paths that
resolve to nothing: harbor/sidecar.py, harbor/verifier.py,
harbor/session.py, harbor/app.py, harbor/inference.py, and
evaluation/budget.py. Since the doc's whole purpose is a suggested reading
order, a dead path is worse here than in a code comment.

Also names the four modules the build pipeline now passes through.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Each of reserve's charge write, reserve's rollback write, and refund's
write hand-rolled the same shield-and-drain loop, and each got the same
detail wrong at least once: a write failure replacing the CancelledError
of the run being torn down. Two were patched individually in review; the
third, reserve's charge write, still called write.result() unguarded, so
an OSError there discarded the cancellation and the caller saw the wrong
exception.

_drain_write now owns the loop and returns both outcomes rather than
raising one, which is what makes the invariant checkable: no call site can
propagate a write error in place of a cancellation, because it has to
decide about both. Draining to completion is preserved -- an abandoned
write could overwrite a later one once the ledger lock is released.

Behaviour is unchanged except in the case that was broken. Reserve still
rolls its charge back on cancellation, and now skips the rollback when the
charge never landed. Refund still commits, since returning budget is safe
even for a run that is going away; that asymmetry is now stated rather
than implied.

The new test fails on the old code with OSError: disk gone.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
varunursekar added a commit that referenced this pull request Jul 26, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
_execute_record shields _record and refund in both exception handlers but
left the success-path _record bare, so a cancellation arriving after the
evaluation finished could abandon it part-way -- the budget charged for a
run the engine never finished publishing.

The mechanism is narrower than "the database write is lost": that write is
an asyncio.to_thread, and a cancelled await does not stop the thread, so
the file lands either way. What is actually skipped is everything after it
-- the listener loop, where the trusted side mirrors an evaluation to W&B
and the session archive -- plus the whole of _record when the cancellation
arrives before its lock is acquired.

Shield does not wait for the shielded coroutine, only protect it: the
caller still unwinds at once while _record completes in the background.
That is already the contract of the four shielded calls beside it, so this
makes the path consistent rather than introducing a new guarantee.

The test cancels during a listener, the one genuinely interruptible step,
and times out on the unshielded code.

Reported by greptile on #45.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
varunursekar added a commit that referenced this pull request Jul 26, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
varunursekar and others added 2 commits July 26, 2026 17:29
A held-out target can now set n_attempts/aggregate_attempts to score each
case several times and combine them (e.g. 3x mean to average a noisy final
eval), while search and selection keep the global attempts. Backends are
per-partition, so the override is injected into the target partition's
backend at compile time; a config validator rejects overriding an
agent-evaluable partition (shared with search) or a command backend.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…uction

The instruction's insights paragraph was gated on overlay_present, which is
true whenever the always-baked evals skill is added -- so every optimizer was
told to dispatch an insights-generator subagent and read skills/insights that
no benchmark ships. The insights skill is retired; drop the paragraph.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
varunursekar added a commit that referenced this pull request Jul 27, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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