fix: [no-ticket] harbor - access-tier integrity (free-baseline privilege, k-anonymity floor, trusted nested CLI)#34
Conversation
…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>
| free_baseline = ( | ||
| not admin | ||
| and self.base_commit is not None | ||
| and sha == self.base_commit | ||
| and not self._free_baseline_used | ||
| ) | ||
| if free_baseline: | ||
| self._free_baseline_used = True | ||
| exp = await self.engine.evaluate( | ||
| replace(req, commit=sha), admin=admin or free_baseline | ||
| replace(req, commit=sha), admin=admin, free=free_baseline | ||
| ) | ||
| # Consume the freebie only after the eval succeeded: an eval that | ||
| # raised (invalid split, infra failure) has given the agent nothing, | ||
| # so it must not burn the one free reference measurement. | ||
| if free_baseline: | ||
| self._free_baseline_used = True |
There was a problem hiding this comment.
Race window on
_free_baseline_used flag
Moving _free_baseline_used = True to after the await engine.evaluate call reintroduces a race window. In asyncio, execution is synchronous between await points, so the old code (which set the flag before the eval await) was effectively atomic: once A resumed from _transfer_commit and set the flag, any concurrent B that resumed later would see _free_baseline_used = True. Now the flag isn't set until after the full eval completes, so two concurrent requests that both finish _transfer_commit will both read _free_baseline_used = False, both set free_baseline = True, and both call engine.evaluate(..., free=True) — giving the agent two free baseline evals.
The safer fix is to optimistically set the flag before the eval and restore it on failure:
if free_baseline:
self._free_baseline_used = True # claim before any await
try:
exp = await self.engine.evaluate(
replace(req, commit=sha), admin=admin, free=free_baseline
)
except Exception:
if free_baseline:
self._free_baseline_used = False # restore so a retry is still free
raisePrompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/server.py
Line: 120-133
Comment:
**Race window on `_free_baseline_used` flag**
Moving `_free_baseline_used = True` to after the `await engine.evaluate` call reintroduces a race window. In asyncio, execution is synchronous between `await` points, so the old code (which set the flag **before** the eval await) was effectively atomic: once A resumed from `_transfer_commit` and set the flag, any concurrent B that resumed later would see `_free_baseline_used = True`. Now the flag isn't set until after the full eval completes, so two concurrent requests that both finish `_transfer_commit` will both read `_free_baseline_used = False`, both set `free_baseline = True`, and both call `engine.evaluate(..., free=True)` — giving the agent two free baseline evals.
The safer fix is to optimistically set the flag before the eval and restore it on failure:
```python
if free_baseline:
self._free_baseline_used = True # claim before any await
try:
exp = await self.engine.evaluate(
replace(req, commit=sha), admin=admin, free=free_baseline
)
except Exception:
if free_baseline:
self._free_baseline_used = False # restore so a retry is still free
raise
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good catch, and you are right about the asyncio interleaving. Fixed in e1b1d6b (stack tip, harbor-9-ops): the flag is claimed BEFORE the eval await (so a concurrent second baseline eval sees the claim and pays) and refunded in an except block if the eval raises, which keeps the property this PR wanted (a failed eval does not burn the freebie). Regression test: test_freebie_claimed_before_eval_await asserts the flag is visible during the engine call.
| k_anonymity_floor: int = 1, | ||
| ) -> StatusSummary: |
There was a problem hiding this comment.
build_status defaults k_anonymity_floor to 1 (the "disabled" sentinel), while EvaluationSidecar defaults to 5. Any caller that forgets to pass the floor will advertise min_subset_samples = 1 for non_viewable splits, causing agents to send sub-floor requests that the sidecar then rejects with KAnonymityError. Defaulting to 5 — the same as EvaluationSidecar.__init__ — keeps the function's own default consistent with the enforcement default.
| k_anonymity_floor: int = 1, | |
| ) -> StatusSummary: | |
| k_anonymity_floor: int = 5, | |
| ) -> StatusSummary: |
Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/protocol.py
Line: 107-108
Comment:
`build_status` defaults `k_anonymity_floor` to `1` (the "disabled" sentinel), while `EvaluationSidecar` defaults to `5`. Any caller that forgets to pass the floor will advertise `min_subset_samples = 1` for `non_viewable` splits, causing agents to send sub-floor requests that the sidecar then rejects with `KAnonymityError`. Defaulting to `5` — the same as `EvaluationSidecar.__init__` — keeps the function's own default consistent with the enforcement default.
```suggestion
k_anonymity_floor: int = 5,
) -> StatusSummary:
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Agreed; fixed in e1b1d6b (stack tip): build_status now defaults k_anonymity_floor=5, matching the sidecar's enforcement default, with a comment explaining why the two defaults must not drift. Test: test_default_floor_matches_sidecar_enforcement_default.
… 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>
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 calledengine.evaluate(admin=True). Admin does not just skip the ledger; it also bypasses theno_accesstier gate, so the free eval could target the held-out test split and read its aggregate score straight off theEvalSummaryresponse. Fix:engine.evaluategains a distinctfreeparameter that waives only the budget debit and nothing else;adminandfreeare 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_componentsnever passedsplit_accessesinto theEvaluationEngine, 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: wiresplit_accessesthrough; the gate fails closed on unlisted splits.3. Per-sample label reconstruction on non_viewable splits (
server.py,protocol.py).EvalRequest.sample_ids/num_sampleslet the agent evaluate arbitrary subsets, and the aggregatemean_scorein 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_floorin 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 instatus()asmin_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 touv run --project <candidate> harbor run ..., so the orchestrator that writes the trialresult.jsonfiles came from whatever the agent'spyproject.toml/uv.lockpinned; 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 andsys.path(verified empirically: project pinning six==1.16.0,--with six==1.17.0resolves 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:freeruns without debiting budget;freedoes NOT bypass the no_access gate.test_harbor_server.py: free baseline passesfree=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_samplesadvertised for non_viewable only.test_harbor_runner.py:--withplacement before theharborexecutable; absent when unset.🤖 Generated with Claude Code
Greptile Summary
This PR closes four access-tier integrity gaps in the Harbor eval sidecar: the free baseline eval was riding
admin=True(which bypasses the no_access tier gate), the engine-side no_access gate was never wired withsplit_accessesso every unmetered path walked past it, agents could reconstruct non-viewable split labels via singleton subset evals, and the nested scorer CLI resolved from the candidate's own lockfile.admin/freeseparation (engine.py,server.py):engine.evaluategains a distinctfreeparameter; free baseline no longer ridesadmin=True, so tier gates continue to apply.serve.py):split_accessesis now passed toEvaluationEngineso the no_access rejection is fail-closed on all paths including unmetered ones.server.py,protocol.py, configs): subset evals onnon_viewablesplits below the configured floor (default 5) are rejected before commit transfer, budget debit, or eval.runner.py,config.py):harbor_requirementlayers--with <spec>over the candidate env so the nested orchestrator resolves from a trusted pinned spec.Confidence Score: 3/5
The core access-control fixes are correct and well-tested, but the free-baseline guard regresses: moving the consumed flag to after the async eval creates a window two concurrent requests can both slip through.
Three of the four fixes are clean. The fourth moves
_free_baseline_used = Trueto afterawait engine.evaluate, creating a race window that didn't exist in the old code. The old code set the flag synchronously before the eval await, which was atomic in asyncio's cooperative scheduler. Now two concurrent baseline requests can both finish_transfer_commit, both read the flag as False, and both call the eval withfree=True.vero/src/vero/harbor/server.py (free baseline flag placement) and vero/src/vero/harbor/protocol.py (build_status default floor parameter)
Important Files Changed
freeparameter toevaluate— correctly separated fromadminso a free eval still runs through the no_access tier gate._free_baseline_used = Trueto afterawait engine.evaluateopens a concurrent-request race window.split_accessesintoEvaluationEngine(dormant no_access gate now armed) and threadsk_anonymity_floorinto the sidecar.min_subset_samplesper split. Defaultk_anonymity_floor=1in the signature is inconsistent with the sidecar's enforced default of 5.--with harbor_requirementoverlay before theharborexecutable. Clean and well-tested.k_anonymity_floor: int = 5build config field.k_anonymity_floorfrom build config into serve.json.KAnonymityError→ HTTP 400 exception handler.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Agent participant Sidecar as EvaluationSidecar participant Engine as EvaluationEngine participant Budget as BudgetLedger Agent->>Sidecar: evaluate(req) Note over Sidecar: [NEW] k-anonymity floor check Sidecar->>Sidecar: _transfer_commit(req.commit) Note over Sidecar: detect free_baseline Sidecar->>Engine: "evaluate(req, admin=admin, free=free_baseline)" Note over Engine: [NEW] no_access gate armed alt not admin AND not free Engine->>Budget: reserve(dataset_id, split, n) end Engine-->>Sidecar: Experiment Note over Sidecar: [CHANGED] _free_baseline_used=True after eval Sidecar-->>Agent: EvalSummary%%{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 Agent participant Sidecar as EvaluationSidecar participant Engine as EvaluationEngine participant Budget as BudgetLedger Agent->>Sidecar: evaluate(req) Note over Sidecar: [NEW] k-anonymity floor check Sidecar->>Sidecar: _transfer_commit(req.commit) Note over Sidecar: detect free_baseline Sidecar->>Engine: "evaluate(req, admin=admin, free=free_baseline)" Note over Engine: [NEW] no_access gate armed alt not admin AND not free Engine->>Budget: reserve(dataset_id, split, n) end Engine-->>Sidecar: Experiment Note over Sidecar: [CHANGED] _free_baseline_used=True after eval Sidecar-->>Agent: EvalSummaryPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(harbor): access-tier integrity (free..." | Re-trigger Greptile