Skip to content

fix: [no-ticket] harbor - access-tier integrity (free-baseline privilege, k-anonymity floor, trusted nested CLI)#34

Merged
varunursekar merged 15 commits into
harbor-6-selection-integrityfrom
harbor-7-access-tiers
Jul 17, 2026
Merged

fix: [no-ticket] harbor - access-tier integrity (free-baseline privilege, k-anonymity floor, trusted nested CLI)#34
varunursekar merged 15 commits into
harbor-6-selection-integrityfrom
harbor-7-access-tiers

Conversation

@shehabyasser-scale

@shehabyasser-scale shehabyasser-scale commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #33 (harbor-6-selection-integrity). Part of the pre-paper hardening series; closes the access-tier group.

What leaks this closes

1. Free baseline eval was a privilege escalation (server.py). The agent's one budget-free baseline eval called engine.evaluate(admin=True). Admin does not just skip the ledger; it also bypasses the no_access tier gate, so the free eval could target the held-out test split and read its aggregate score straight off the EvalSummary response. Fix: engine.evaluate gains a distinct free parameter that waives only the budget debit and nothing else; admin and free are separate authorities. The freebie is also consumed only after the eval succeeds, so an infra failure no longer burns it.

2. The engine's no_access gate was dormant in the deployed sidecar (serve.py). build_components never passed split_accesses into the EvaluationEngine, so the engine-side hard rejection (added in an earlier PR) never armed. The de-facto gate was the budget ledger (no_access splits are unbudgeted), which is precisely what every unmetered path (admin, the free baseline) skips. Fix: wire split_accesses through; the gate fails closed on unlisted splits.

3. Per-sample label reconstruction on non_viewable splits (server.py, protocol.py). EvalRequest.sample_ids / num_samples let the agent evaluate arbitrary subsets, and the aggregate mean_score in the response over a singleton subset is that sample's score verbatim; n singleton evals reconstruct the hidden split's labels wholesale. Fix: k-anonymity floor (default 5, k_anonymity_floor in build.yaml/serve.json) on agent-chosen proper subsets of non_viewable splits, rejected up front (no commit transfer, no eval, no budget debit) and advertised in status() as min_subset_samples. Full-split evals always pass, so a split smaller than the floor degrades to full-split-only rather than becoming unevaluable. Scope note: the floor multiplies reconstruction cost by ~k (overlapping k-subsets still admit elimination); the binding constraint remains the budget. It is a cost lever, not a proof.

4. The nested scorer CLI resolved from the candidate's own lockfile (runner.py, config.py). Mode B shells out to uv run --project <candidate> harbor run ..., so the orchestrator that writes the trial result.json files came from whatever the agent's pyproject.toml/uv.lock pinned; one edited line could point it at a fork that fabricates perfect rewards without running anything. Fix: HarborConfig.harbor_requirement; when set, the runner layers --with <spec> over the candidate env, and uv's ephemeral overlay takes precedence over a conflicting project pin for both the console script and sys.path (verified empirically: project pinning six==1.16.0, --with six==1.17.0 resolves 1.17.0). Honest residual: agent code still imports into the nested harbor process via --agent-import-path, so in-process tampering remains possible; a full boundary needs out-of-process verification and is tracked for a later PR.

Tests

  • test_engine.py: free runs without debiting budget; free does NOT bypass the no_access gate.
  • test_harbor_server.py: free baseline passes free=True, admin=False; failed free eval keeps the freebie; floor rejects sub-k subsets before any work, allows at-floor subsets, full splits, viewable splits, admin, and floor<=1; status advertises the floor.
  • test_harbor_protocol.py: min_subset_samples advertised for non_viewable only.
  • test_harbor_runner.py: --with placement before the harbor executable; absent when unset.
  • 158 passed, 2 skipped (pre-existing) across the affected files.

🤖 Generated with Claude Code

Greptile Summary

This PR hardens the eval sidecar's access-tier boundary against four privilege escalation / information-leak vulnerabilities, and adds several supporting mechanics. All four security fixes are correctly implemented and the two previously-flagged issues (free baseline race, build_status floor default) have been addressed.

  • Security fixes (core): engine.evaluate now takes a free parameter that waives only budget—not tier access—so the free baseline eval can no longer touch no_access splits; split_accesses is wired into the engine inside build_components so the no_access gate is actually armed; a k-anonymity floor blocks agent-chosen sub-k subsets of non_viewable splits before commit transfer or budget debit; harbor_requirement overlays a trusted CLI spec over the candidate env via uv run --with.
  • Supporting mechanics: Fail-closed ledger restore on parse error (corrupt file → zero remaining, not configured full); per-eval result-dir versioning with _eval_seq ordinals; mean_score_se and n_scored/n_errored added to summary.json; infra-dead attempt classification + bounded retry (off by default, with documented adversarial risk); model override for transfer-target evals; exhaust_budget instruction lever.

Confidence Score: 5/5

Safe to merge. All four access-tier vulnerabilities are correctly closed, the previously flagged race and default-mismatch issues are resolved, and the new infra retry path is disabled by default with clear documentation of its adversarial risks.

The four security fixes are each correctly implemented and independently tested. The free baseline now uses free not admin, split_accesses is wired through so the engine gate is armed, the k-anonymity floor blocks sub-k subsets before any expensive work, and harbor_requirement overlays the trusted CLI. The one finding — premature discard_history updates that can overcount discarded rounds in the infra retry audit marker — affects only audit metadata, is in a path that is off by default, and does not affect any score or access gate.

No files require special attention. runner.py has the minor audit-trail finding in _retry_transient_infra, but that codepath is gated by infra_retry_rounds > 0 which defaults to 0.

Important Files Changed

Filename Overview
vero/src/vero/harbor/server.py Core security fixes: separates free from admin authority, adds k-anonymity floor before commit transfer, versions result dirs with _eval_seq. Free baseline race addressed (flag claimed before await, refunded in BaseException handler). Looks correct.
vero/src/vero/evaluation/engine.py Adds free parameter to evaluate; gates tier check and budget reserve correctly; evaluate_admin gains model override via task_params copy without mutating shared run_constraints. Logic is clean.
vero/src/vero/harbor/serve.py Wires split_accesses into EvaluationEngine (arming the previously-dormant no_access gate), passes k_anonymity_floor to the sidecar, and changes corrupt-ledger fallback from configured budget to fail-closed zero. All changes are correct.
vero/src/vero/harbor/runner.py Adds dead-attempt exception classification, infra retry loop, harbor_requirement trusted CLI overlay, and model override via task_params. The infra retry has a minor audit-trail inaccuracy when a retry round produces no trials (discard_history is updated prematurely). Off by default, so impact is limited.
vero/src/vero/harbor/protocol.py Adds k_anonymity_floor parameter with default 5 (matching sidecar enforcement default); emits min_subset_samples only for non_viewable splits. Previously-flagged default mismatch is resolved.
vero/src/vero/harbor/config.py Adds harbor_requirement, infra_retry_rounds, infra_retry_delay_s fields with clear guards: negative rounds rejected, zero delay with retries enabled rejected. Validation is tight.
vero/src/vero/harbor/verifier.py Threads model override through _admin_eval_score to evaluate_admin; applies same override to baseline eval for like-for-like comparison; adds _dominant_sample_errors diagnostic helper. Return type changed from `float

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant A as Agent
    participant S as EvaluationSidecar
    participant E as EvaluationEngine
    participant B as BudgetLedger

    A->>S: "evaluate(req, admin=False)"
    Note over S: 1. k-anonymity floor check
    alt sub-k subset on non_viewable
        S-->>A: KAnonymityError (400)
    end
    S->>S: _transfer_commit(req.commit)
    Note over S: 2. Free baseline claim before await
    S->>E: "evaluate(req, admin=False, free=free_baseline)"
    Note over E: 3. no_access tier gate (now armed)
    alt no_access split
        E-->>S: InvalidSplitError
        S->>S: refund freebie (except BaseException)
        S-->>A: error
    end
    alt not admin and not free
        E->>B: reserve(dataset_id, split, n)
    end
    E-->>S: Experiment
    S->>S: "_route_results (versioned __e{seq} dir)"
    S-->>A: EvalSummary
Loading
%%{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 A as Agent
    participant S as EvaluationSidecar
    participant E as EvaluationEngine
    participant B as BudgetLedger

    A->>S: "evaluate(req, admin=False)"
    Note over S: 1. k-anonymity floor check
    alt sub-k subset on non_viewable
        S-->>A: KAnonymityError (400)
    end
    S->>S: _transfer_commit(req.commit)
    Note over S: 2. Free baseline claim before await
    S->>E: "evaluate(req, admin=False, free=free_baseline)"
    Note over E: 3. no_access tier gate (now armed)
    alt no_access split
        E-->>S: InvalidSplitError
        S->>S: refund freebie (except BaseException)
        S-->>A: error
    end
    alt not admin and not free
        E->>B: reserve(dataset_id, split, n)
    end
    E-->>S: Experiment
    S->>S: "_route_results (versioned __e{seq} dir)"
    S-->>A: EvalSummary
Loading

Reviews (2): Last reviewed commit: "Merge pull request #35 from scaleapi/har..." | Re-trigger Greptile

shehabyasser-scale and others added 2 commits July 9, 2026 02:20
…ity floor, trusted nested CLI)

Four related fixes to keep hidden-split information and scoring authority
where they belong:

1. The free baseline eval no longer rides the admin flag. engine.evaluate
   gains a distinct `free` parameter that waives only the budget debit;
   admin=True also bypassed the no_access tier gate, so the agent's one
   free eval could target the held-out test split and read its aggregate
   score off the response. The freebie is also now consumed only after a
   successful eval, so an infra failure no longer burns it.

2. serve.py now passes split_accesses into the EvaluationEngine. Without
   it the engine-side no_access gate was dormant and the budget ledger
   (no_access splits are unbudgeted) was the only gate, which is exactly
   what every unmetered path skipped.

3. k-anonymity floor on subset evals of non_viewable splits (default 5,
   configurable via build.yaml / serve.json). EvalSummary.mean_score over
   an agent-chosen singleton subset is that sample's label-derived score
   verbatim, so n singleton evals reconstructed a hidden split's labels
   wholesale. Full-split evals always pass (their aggregate is the
   intended surface), so splits smaller than the floor stay evaluable.
   The floor is advertised in status() as min_subset_samples.

4. HarborConfig.harbor_requirement: when set, the nested `harbor run` is
   layered over the candidate env with `uv run --with <spec>`, so the
   orchestrator that produces trial result.json resolves from the trusted
   spec, not from the candidate's own pyproject/uv.lock (one edited line
   there could point at a fork that fabricates results). Verified that
   uv's ephemeral overlay takes precedence over a conflicting project
   pin for both the console script and sys.path. This raises the bar,
   not a full boundary: agent code still imports into the nested harbor
   process; out-of-process verification is tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alue, versioned re-evals, dead-attempt causes)

Four small fixes so recorded numbers say what they are:

1. summary.json now carries n_scored, n_errored, and score_se beside
   mean_score: a mean over 3-of-18 scored samples, or one dominated by
   errored zero-fills, is a different measurement than a clean
   full-split mean, and both the agent and any auditor should see that
   without per-sample access. All three are label-safe aggregates.

2. summary.json status now writes the enum VALUE ("success"), not
   str(enum) ("ExperimentResultStatus.SUCCESS").

3. Result dirs are versioned per eval ({split}__{commit12}__eN instead
   of wipe-and-rewrite keyed on (split, commit)): repeat measurements of
   one commit (multifidelity confirms, champion re-evals) are exactly
   the evidence worth comparing, and the second eval erased the first.

4. Mean-mode collation records dead_exception_types per sample: n_dead
   alone hides WHY attempts died, and cause matters (rate-limit deaths
   are infra noise, crashes point at the candidate; measured live,
   110/129 UnicodeDecodeError deaths sat on two never-solved tasks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread vero/src/vero/harbor/server.py
Comment thread vero/src/vero/harbor/protocol.py Outdated
…fails closed on corrupt restore)

Two operational fixes:

1. instruct_exhaust_budget (default True): the instruction's "unspent
   budget is wasted" persistence bullet becomes a build-config lever,
   like instruct_multifidelity. On preserves current behavior; off makes
   stopping-early the agent's own choice, which is the ablation arm for
   measuring what the exhortation itself contributes to optimizer
   persistence. The "scores are noisy" fact stays unconditional.

2. _load_or_build_ledger fails CLOSED on a persisted ledger that exists
   but cannot be parsed: metered budgets restore with zero remaining,
   and the unreadable file is preserved as ledger.corrupt for the
   operator. The old fallback restored the CONFIGURED budgets, which
   refunded the agent everything already spent, so any crash that
   corrupted the flush minted budget. A missing file is still a fresh
   boot; admin and finalize are unaffected either way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shehabyasser-scale and others added 12 commits July 9, 2026 02:39
Two Greptile P2s on #30, fixed at the stack tip:

- An empty transcript file no longer surfaces as "" feedback: empty
  candidates are skipped (an empty pane falls through to the
  trajectory), and if everything is empty the search moves to the next
  failed attempt rather than short-circuiting on "".

- The no-verifier-rewards error branch (agent died before scoring) now
  attaches the failure transcript like any failed sample: a candidate
  edit that crashes the agent lands exactly here, and the transcript is
  the only way the optimizer can see the crash it caused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… floor default, ordinal resume, SE naming)

- Free-baseline flag is claimed BEFORE the eval await and refunded on
  failure: setting it only after success reopened a window where two
  concurrent baseline evals both resolved free (asyncio interleaves at
  await points). Claim-then-refund keeps both properties: concurrent
  callers see the claim, and a failed eval does not burn the freebie.

- build_status defaults k_anonymity_floor to 5, matching the sidecar's
  enforcement default: a caller that forgets to pass the floor must not
  advertise a laxer one than gets enforced.

- _route_results resumes the eval ordinal past surviving __eN dirs on a
  reused volume: a restarted sidecar started back at e1 and silently
  wiped the prior session's evidence, the exact erasure the versioned
  dirs exist to prevent.

- score_se renamed to mean_score_se and documented: it is the SE of the
  zero-filled mean_score over n_samples, not of the n_scored subset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e crash vs infra outage)

Measured live in the transfer matrix: three champions scored 0/72 on an
off-model executor because their optimizers hardcoded temperature=0,
which that provider rejects. The harness handled it safely (rewards
floored, finalize shipped) but the durable record could not say WHY, and
"why" decides opposite actions: a deterministic candidate crash is a
real, reportable portability failure; an infra outage means invalidate
and re-run. Two changes:

- Collation's no-verifier-rewards error string now names the dead
  attempts' exception types ("attempts died: UnsupportedParamsError
  x6"). The error string is the one field that flows to the DB, the
  per-sample files, and the verifier.

- _admin_eval_score returns (score, failure_cause); a floored target's
  target_errors entry carries the dominant per-sample causes (frequency
  summary, top 3). Diagnostics are fail-safe: any surprise result shape
  degrades to a fixed string, never fails finalize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ask crashes cluster

Greptile follow-up on #37: _dominant_sample_errors keyed on the raw error
string, which embeds the task name, so identical exceptions across a
multi-task slice landed in 1x singletons instead of one dominant cause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… finalize

Home-model evals cannot see model-specific couplings the optimizer bakes
in. Measured live in the wave-1 transfer matrix: three of five champions
independently hardcoded temperature=0 (a variance trick on their home
model, gpt-4.1-mini) and scored 0/72 on claude-opus-4-8, which rejects
it, while looking healthy on every eval the optimization loop ever ran.
The portability failure was invisible until a separate, manual,
after-the-fact probe.

VerificationTarget gains `model`: a target with an executor override
scores the selected commit under a model it was NOT optimized on, in the
same finalize battery as its home-model reward. The baseline is scored
under the same override so the comparison stays like-for-like. Plumbing:
build.yaml TargetSpec -> compiler -> serve.json _TargetCfg ->
VerificationTarget -> engine.evaluate_admin(model=...) -> task_params
["harbor_model_override"] -> HarborRunner -m flag. The override rides
task_params, so Mode A ignores it and the runner needs no new state; the
shared run_constraints are copied, never mutated.

Test fakes of evaluate_admin widened to accept the new kwarg (a strict
signature turned the new call into a retried TypeError, flooring
rewards, which is itself a nice demonstration of the floor's fail-safe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…utage retry, key-budget alarm

Three infra failure modes measured live in the E5 matrix runs, fixed at
the measurement layer:

- Dead attempts are classified infra vs candidate (conservative
  exception-type allowlist + the litellm key-budget message signature).
  Labels flow into dead_exception_types, error strings, and a new
  n_dead_infra metric. Classification never moves a score: every dead
  attempt still zero-fills, or faking infra would excuse failures.
  Exception type names are candidate-authored, so brackets are
  neutralized before labeling (a class named 'XError[infra]' cannot
  walk in pre-suffixed).

- An OPT-IN, bounded, backoff-spaced within-eval retry re-measures
  samples whose every attempt died of a transient infra cause (the
  65-second DNS blip that killed 44/72 attempts of one eval). Off by
  default: against an adversarial optimizer the qualifying predicate is
  a re-roll lever, since a stochastic candidate that raises allowlisted
  exceptions on failing attempts converts all-bad rounds into fresh
  draws. Retry rounds run in fresh sibling jobs dirs (nesting would
  pool dead attempts into later resumed means) and recovered samples
  carry an infra_retry audit marker naming the discarded attempts.

- An ERROR-level alarm names key-budget exhaustion (a spent key fails
  every later call identically; two matrix cells of budget-exceeded
  zeros were nearly booked as a portability finding), with hedged
  wording since the signature reads candidate-process exceptions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d audit history

- HarborConfig rejects infra_retry_delay_s <= 0 when retries are enabled
  (a zero delay silently nullified the backoff) and negative
  infra_retry_rounds.
- The infra_retry audit marker now lists EVERY discarded round in order
  (discarded_rounds), not just the one immediately before recovery;
  recovered_round names when the sample finally measured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: [no-ticket] harbor - infra resilience (dead-attempt classification, opt-in outage retry, key-budget alarm)
feat: [no-ticket] harbor - transfer targets (per-target executor-model override at finalize)
fix: [no-ticket] harbor - floored rewards name their cause (candidate crash vs infra outage)
fix: [no-ticket] harbor - ops integrity (exhaust-budget instruction lever, fail-closed ledger restore)
fix: [no-ticket] harbor - honest measurement signals (summary qualifiers, versioned re-evals, dead-attempt causes)
@varunursekar
varunursekar merged commit a60b11c into harbor-6-selection-integrity Jul 17, 2026
3 checks passed
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