Skip to content

[Harbor 2/4] eval sidecar: verifier, token auth, HTTP API (Mode A)#4

Open
varunursekar wants to merge 2 commits into
harbor-1-corefrom
harbor-2-sidecar
Open

[Harbor 2/4] eval sidecar: verifier, token auth, HTTP API (Mode A)#4
varunursekar wants to merge 2 commits into
harbor-1-corefrom
harbor-2-sidecar

Conversation

@varunursekar

@varunursekar varunursekar commented Jun 24, 2026

Copy link
Copy Markdown

Draft · Stack 2 of 4 — targets harbor-1-core (review [1/4] first; its diff is the base here).

The evaluation engine run in a sidecar container, plus the trust boundary that makes a run leaderboard-gradeable. This is the security-critical PR — a focused/security review is worthwhile here.

  • EvaluationSidecar (server.py): commit transfer from the untrusted agent repo (git fetch, hooks off, object copy) + tier-gated result write-routing across the agent-readable and admin volumes.
  • Verifier (verifier.py): commit selection (submit | auto_best) + hidden-split scoring.
  • Admin token (auth.py): per-trial, written root:600 — unreadable by the de-privileged optimizer, readable by the verifier (root, shared mode).
  • FastAPI (app.py): /eval /submit /status (agent; metered, redacted) and /finalize (verifier; token-gated). vero harbor serve assembles engine+sidecar+verifier from a ServeConfig.
  • CLI clients (cli.py) + HarborConfig/partition helpers so the package imports cleanly (build/run light up in [3/4]).

Stack: [1/4] core → this → [3/4] compiler → [4/4] docs.

🤖 Generated with Claude Code

Greptile Summary

This PR introduces the eval sidecar layer for Harbor: EvaluationSidecar (commit transfer + tier-gated result routing), Verifier (commit selection + hidden-split scoring), per-trial admin token auth, a FastAPI HTTP surface, and the vero harbor serve entrypoint that wires everything together from a ServeConfig. The three previously-flagged issues (stale result dirs, auto_best dataset scoping, reward_mode validation) are addressed in this revision.

  • server.py: git fetch from the untrusted agent repo uses hooks-off + file:// object copy; result write-routing is tier-gated (viewable / non_viewable / no_access) with the stale-directory fix (shutil.rmtree before re-writing).
  • verifier.py: auto_best shortlists by agent-recorded score then re-ranks with a trusted admin re-score, filtering by selection_dataset_id when the column is present; reward_mode is now Literal[\"submit\", \"auto_best\"] in ServeConfig.
  • auth.py / app.py: admin token is generated with secrets.token_urlsafe, bearer-token check uses secrets.compare_digest, but the token file is written then chmod'd (TOCTOU window) and concurrent transfers share FETCH_HEAD (race on concurrent /eval calls).

Confidence Score: 3/5

Three defects in the security-critical paths need fixes before merging: a FETCH_HEAD race in concurrent commit transfers, a write-then-chmod window on the admin token file, and a missing fallback for dataset_id in the verifier re-score loop.

The FETCH_HEAD file is overwritten by whichever git fetch completes last; with uvicorn serving concurrent /eval requests, two in-flight _transfer_commit calls can race and one resolves the other's SHA — breaking the commit-attribution invariant the trust boundary rests on. The token write-then-chmod window leaves the admin token briefly readable with default umask permissions. The verifier dataset_id fallback passes None to evaluate_admin when the column is absent, which can load the wrong dataset or raise at runtime.

vero/src/vero/harbor/server.py (_transfer_commit FETCH_HEAD race), vero/src/vero/harbor/auth.py (write_admin_token TOCTOU), vero/src/vero/harbor/verifier.py (_best_from_db dataset_id fallback)

Security Review

  • FETCH_HEAD race (server.py:118–145): Concurrent _transfer_commit calls share the single FETCH_HEAD file in the sidecar's git repo. With max_concurrency=20 and uvicorn serving concurrent /eval requests, two in-flight fetches can race: one call reads the other's SHA, causing an evaluation to run on the wrong commit while attributing the result to the original request. This breaks the commit-attribution trust boundary.
  • Token write TOCTOU (auth.py:24–28): write_admin_token creates the file with the default umask (typically 0o644) then calls chmod(0o600). During the window between the two syscalls the admin token — which gates the reward-writing /finalize endpoint — is world-readable on a filesystem accessible to the agent process.
  • No hardcoded secrets, no SQL injection surfaces, no deserialization of untrusted data beyond the agent's commit ref (passed as a list argument to git, not through a shell). Bearer-token comparison in check_admin correctly uses secrets.compare_digest.

Important Files Changed

Filename Overview
vero/src/vero/harbor/server.py Core sidecar handler: commit transfer via git fetch + tier-gated result routing. FETCH_HEAD is shared across concurrent transfers — a race condition that can assign the wrong commit SHA to an evaluation.
vero/src/vero/harbor/auth.py Per-trial admin token generation and validation. write_admin_token creates the file with default umask permissions then chmod's, leaving a brief window where the token is readable before permissions are tightened.
vero/src/vero/harbor/verifier.py Commit selection (submit
vero/src/vero/harbor/app.py FastAPI routes: unauthenticated agent endpoints (/eval, /submit, /status) and token-gated /finalize. Bearer-token check uses constant-time comparison; exception-to-status-code mapping is correct.
vero/src/vero/harbor/serve.py ServeConfig + build_components: assembles engine, sidecar, verifier; enforces Mode-A task_project guard; durable budget ledger with restart-safe reload. reward_mode is now a Literal type, closing the silent-fallback gap.
vero/src/vero/harbor/protocol.py Wire types and projection logic: EvalSummary (aggregate-safe), StatusSummary, tier_for_split (fails closed to no_access for unlisted splits). Redaction invariants look correct.
vero/src/vero/harbor/cli.py CLI clients for agent (eval/submit/status) and verifier (finalize). Token is read from file and forwarded via Authorization header.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent as Agent (agent.user)
    participant App as FastAPI App
    participant Sidecar as EvaluationSidecar
    participant AgentRepo as Agent Repo (mounted)
    participant SidecarRepo as Sidecar Repo
    participant Engine as EvaluationEngine
    participant AgentVol as Agent Volume
    participant AdminVol as Admin Volume

    Note over App,AdminVol: Startup
    App->>AdminVol: write_admin_token (root:600)

    Note over Agent,AgentVol: Agent eval loop
    Agent->>App: "POST /eval {commit, split}"
    App->>Sidecar: "evaluate(req, admin=False)"
    Sidecar->>AgentRepo: git fetch file:// (hooks off)
    AgentRepo-->>SidecarRepo: "object copy -> FETCH_HEAD"
    SidecarRepo-->>Sidecar: "rev-parse FETCH_HEAD -> sha"
    Sidecar->>Engine: "evaluate(commit=sha)"
    Engine-->>Sidecar: Experiment result
    Sidecar->>Sidecar: _route_results (tier_for_split)
    alt "tier = viewable"
        Sidecar->>AgentVol: summary.json + per-sample files
    else "tier = non_viewable"
        Sidecar->>AgentVol: summary.json only
    else "tier = no_access"
        Note over AgentVol: nothing written
    end
    App-->>Agent: EvalSummary (aggregate only)

    Note over Agent,AdminVol: Optional submit
    Agent->>App: "POST /submit {commit}"
    App->>Sidecar: submit(commit)
    Sidecar->>AdminVol: submission.json (sha)

    Note over App,AdminVol: Verifier finalizes (admin only)
    App->>App: POST /finalize (Bearer token)
    App->>App: check_admin (constant-time)
    App->>Verifier: finalize()
    alt "reward_mode = submit"
        Verifier->>AdminVol: "read submission.json -> sha"
    else "reward_mode = auto_best"
        Verifier->>Engine: get_experiments_df
        Verifier->>Engine: evaluate_admin (top-K re-score)
        Engine-->>Verifier: "admin scores -> winner sha"
    end
    loop each VerificationTarget
        Verifier->>Engine: evaluate_admin(hidden split)
        Engine-->>Verifier: score
    end
    Verifier-->>App: "{reward_key: score}"
    App-->>Verifier CLI: reward dict -> reward.json
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 Agent as Agent (agent.user)
    participant App as FastAPI App
    participant Sidecar as EvaluationSidecar
    participant AgentRepo as Agent Repo (mounted)
    participant SidecarRepo as Sidecar Repo
    participant Engine as EvaluationEngine
    participant AgentVol as Agent Volume
    participant AdminVol as Admin Volume

    Note over App,AdminVol: Startup
    App->>AdminVol: write_admin_token (root:600)

    Note over Agent,AgentVol: Agent eval loop
    Agent->>App: "POST /eval {commit, split}"
    App->>Sidecar: "evaluate(req, admin=False)"
    Sidecar->>AgentRepo: git fetch file:// (hooks off)
    AgentRepo-->>SidecarRepo: "object copy -> FETCH_HEAD"
    SidecarRepo-->>Sidecar: "rev-parse FETCH_HEAD -> sha"
    Sidecar->>Engine: "evaluate(commit=sha)"
    Engine-->>Sidecar: Experiment result
    Sidecar->>Sidecar: _route_results (tier_for_split)
    alt "tier = viewable"
        Sidecar->>AgentVol: summary.json + per-sample files
    else "tier = non_viewable"
        Sidecar->>AgentVol: summary.json only
    else "tier = no_access"
        Note over AgentVol: nothing written
    end
    App-->>Agent: EvalSummary (aggregate only)

    Note over Agent,AdminVol: Optional submit
    Agent->>App: "POST /submit {commit}"
    App->>Sidecar: submit(commit)
    Sidecar->>AdminVol: submission.json (sha)

    Note over App,AdminVol: Verifier finalizes (admin only)
    App->>App: POST /finalize (Bearer token)
    App->>App: check_admin (constant-time)
    App->>Verifier: finalize()
    alt "reward_mode = submit"
        Verifier->>AdminVol: "read submission.json -> sha"
    else "reward_mode = auto_best"
        Verifier->>Engine: get_experiments_df
        Verifier->>Engine: evaluate_admin (top-K re-score)
        Engine-->>Verifier: "admin scores -> winner sha"
    end
    loop each VerificationTarget
        Verifier->>Engine: evaluate_admin(hidden split)
        Engine-->>Verifier: score
    end
    Verifier-->>App: "{reward_key: score}"
    App-->>Verifier CLI: reward dict -> reward.json
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
vero/src/vero/harbor/server.py:118-145
**FETCH_HEAD shared across concurrent transfers**

`FETCH_HEAD` is a single file inside the sidecar's repo; with `max_concurrency=20` and uvicorn handling concurrent `/eval` requests, two in-flight `_transfer_commit` calls can race: fetch B overwrites `FETCH_HEAD` after fetch A finishes but before A's `rev-parse FETCH_HEAD` runs. A then resolves B's SHA and evaluates B's commit while attributing the result to A's request — a trust-boundary violation where one agent can inadvertently (or deliberately via rapid concurrent calls) evaluate a different commit.

The fix is to write each transfer to a unique local ref (`<target>:<unique-local-ref>`) and resolve that ref, then delete it, so concurrent transfers never share state.

### Issue 2 of 2
vero/src/vero/harbor/auth.py:24-28
**Token readable during write-then-chmod window**

`write_text` creates the file with the process umask (commonly 0o644), then `chmod` restricts it. In the window between the two syscalls the token is world-readable — any process sharing the same filesystem can race to read it. Since this token gates `/finalize` (the reward-writing endpoint), a leak lets `agent.user` call `/finalize` directly.

The fix is to open the file with the target mode in a single atomic step so it is never created with wider permissions.

```suggestion
    import os

    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
    try:
        os.write(fd, token.encode())
    finally:
        os.close(fd)
    return p
```

Reviews (3): Last reviewed commit: "fix(harbor): Mode A scorer provenance, a..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@varunursekar varunursekar requested a review from a team June 24, 2026 18:17
@varunursekar varunursekar marked this pull request as ready for review June 24, 2026 18:21
Comment thread vero/src/vero/harbor/server.py
Comment thread vero/src/vero/harbor/verifier.py Outdated
Comment thread vero/src/vero/harbor/serve.py Outdated

# Mode A
task: str | None = None
task_project: str | None = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security, the headline one. In Mode A this defaults to None, so the Evaluator runs uv in the agent's own project and loads the @task.evaluation() scorer from the agent's committed code. Verifier.finalizeevaluate_admin then scores the hidden split with that agent-controlled scorer, so a committed scorer that returns 1.0 wins the leaderboard. The token/volume/object-copy machinery secures transport, not scorer provenance. For a gradeable boundary the verifier must score with a sidecar-baked task project: require task_project/inner_task for Mode A and reject configs where the scorer would resolve from the agent repo. This is the default posture and the single most important gap in the stack.

tok = generate_token()
p = write_admin_token(tmp_path / "t", tok)
assert read_admin_token(p) == tok
assert (p.stat().st_mode & 0o777) == 0o600

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the security-critical PR, the OS-level claims are asserted only as a 0o600 mode bit plus happy-path mocks. Please add adversarial coverage: (1) agent /eval on a no_access split → rejected end-to-end; (2) an integration test that finalize does not execute an agent-supplied scorer (the Mode A provenance gap); (3) a uid-drop test that agent.user cannot read the token file.

Comment thread vero/src/vero/harbor/serve.py Outdated
workspace = await GitWorkspace.create(config.repo_path)

budget = BudgetLedger(
[SplitBudget(**b) for b in config.budgets],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_components rebuilds the BudgetLedger from config.budgets on every start, and the ledger writes persist_path on each mutation but never reads it back. So a sidecar restart mid-trial silently resets spent budget — metering is not crash-safe despite the durable-ledger framing. Add a load-if-exists path that seeds from the persisted ledger when the file exists, and from config.budgets only when it does not.

"""Best candidate by recorded score on the selection split (excludes baseline)."""
if self.engine.db is None:
raise NoCandidateError("auto_best mode but no experiment database.")
df = self.engine.db.get_experiments_df(fill_score=default_minimum_score)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto_best ranks candidates solely by the recorded mean_score from engine.db.get_experiments_df, but that DB is written by the agent's metered /eval calls and the verifier shares the same in-process engine/DB. The only admin-independent scoring at finalize is evaluate_admin on the test target — after selection, and only on the chosen winner. So a candidate that records an inflated selection score selects itself. (Mode B in #5 sharpens this: the recorded selection score is the reward a nested run of the candidate emitted, which the optimizer controls.) Before trusting the ranking, re-score under admin: re-run evaluate_admin on the selection split for the top-K and re-rank, or at minimum re-score the winner and fail closed on deviation.

Comment thread vero/src/vero/harbor/protocol.py Outdated
for sa in split_accesses:
if sa.split == split:
return sa.access
return SplitAccessLevel.viewable

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails open, the wrong default for a trust boundary. tier_for_split is the gate _route_results uses to decide whether to write full per-sample SampleResults (labels included) to the agent volume; an author who budgets a split but forgets to add it to split_accesses lands here and gets viewable → ground truth on the agent-readable volume. Default to the most restrictive tier (no_access) and update the docstring, which currently advertises the fail-open as intended. If an unlisted split should ever be visible, make that explicit and logged.

Comment on lines +70 to +73
async def evaluate(self, req: EvalRequest, *, admin: bool = False) -> EvalSummary:
sha = await self._transfer_commit(req.commit)
exp = await self.engine.evaluate(replace(req, commit=sha), admin=admin)
result_path = self._route_results(exp, admin=admin)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Gate hidden evals

tier_for_split() now treats unlisted splits as no_access, and /status reports those budgeted-but-unlisted splits as not agent-evaluable. But this path still transfers and scores the commit before _route_results() suppresses only the result files. If a config includes a budget for a hidden or unlisted split, the agent can call /eval and receive mean_score, n_samples, and budget data in the returned EvalSummary. Please reject non-admin evals whose tier is no_access before calling engine.evaluate(), or ensure those splits cannot be present in the agent budget allowlist.

Artifacts

Repro: focused pytest exercising non-admin hidden split EvaluationSidecar.evaluate behavior

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: verbose pytest output showing engine.evaluate call and leaked EvalSummary fields

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/server.py
Line: 70-73

Comment:
**Gate hidden evals**

`tier_for_split()` now treats unlisted splits as `no_access`, and `/status` reports those budgeted-but-unlisted splits as not agent-evaluable. But this path still transfers and scores the commit before `_route_results()` suppresses only the result files. If a config includes a budget for a hidden or unlisted split, the agent can call `/eval` and receive `mean_score`, `n_samples`, and budget data in the returned `EvalSummary`. Please reject non-admin evals whose tier is `no_access` before calling `engine.evaluate()`, or ensure those splits cannot be present in the agent budget allowlist.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

@shehabyasser-scale

Copy link
Copy Markdown
Collaborator

Empirical finding from a live Mode B smoke run (GAIA example, nested Modal evals, combined stack #3-#6 + fix PRs #7-#9):

With reward_mode: auto_best and total_run_budget: 1, the optimizer (claude-code / haiku) spent its only eval on the seeded baseline commit ("measure the baseline first", a natural strategy), then committed its improvements with no budget left. finalize returned:

409 {"error":"auto_best mode but no candidate experiments on split 'train'."}

because the selector excludes base_commit from the candidate pool, and the outer Harbor trial died with RewardFileNotFoundError, i.e. an exception, not a score.

Two thoughts:

  1. The base_commit exclusion itself is right (it stops "do nothing" from winning selection). But "the optimizer produced no measurable candidate" is a legitimate outcome of an optimization run, and its honest value is reward 0.0, not a missing-reward exception. As-is, harness-level aggregation counts these trials as errors rather than zeros, and with small budgets a reasonable agent strategy triggers it. Suggestion: when the pool is empty after the exclusion, write reward.json with 0.0 (plus e.g. "note": "no_candidates") instead of 409.

  2. The generated instruction should warn the optimizer that evaluating the unmodified baseline consumes budget without creating a candidate. It currently doesn't, so the agent walks into this blind.

(Fix PR #8 keeps the exclusion unchanged; this is a separate follow-up decision. Happy to implement either piece.)

varunursekar and others added 2 commits July 13, 2026 15:36
The evaluation engine, run in a sidecar container, with the trust boundary that
makes an optimization run leaderboard-gradeable:

- `EvaluationSidecar` (server.py): agent-facing handlers — commit transfer from the
  untrusted agent repo (git fetch, hooks off, object copy) and tier-gated write-routing
  of results across the agent-readable and admin volumes.
- `Verifier` (verifier.py): commit selection (submit | auto_best) + hidden-split scoring.
- Per-trial admin token (auth.py), written root:600 so the optimizer (de-privileged)
  cannot read it; only the verifier (root, shared mode) can.
- FastAPI surface (app.py): /eval, /submit, /status for the agent (metered, redacted);
  /finalize for the verifier (token-gated). `vero harbor serve` (serve.py) assembles the
  engine + sidecar + verifier from a ServeConfig and runs it under uvicorn.
- `vero harbor` CLI clients (cli.py): serve | eval | submit | status | finalize (build/run
  land with the compiler). HarborConfig + the Mode-B dataset partition helpers (config.py,
  dataset.py) are included so the harbor package imports cleanly.

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

Trust-boundary hardening for the eval sidecar (review findings on PR #4):

- protocol: tier_for_split fails CLOSED (unlisted split -> no_access, with a
  warning), instead of fail-open to viewable.
- serve: Mode A now requires task_project. build_components refuses to start a
  Mode A config without it, so the hidden-split/admin scorer is always loaded
  from the sidecar-baked task project, never the agent's committed repo (a
  committed scorer returning 1.0 can no longer win the reward).
- verifier: auto_best no longer trusts the agent-influenced recorded score. It
  shortlists by recorded score, then re-runs evaluate_admin (the trusted scorer)
  on the selection split for the top-K and ranks by the admin score; fails closed.
- serve: reload the persisted budget ledger on startup so a sidecar restart does
  not reset spent budget to full.
- tests: adversarial coverage (no_access -> 400, finalize does not run an
  agent-supplied scorer via a cheating-agent repo, admin token 0o600/unreadable).

Also folds in the valid Greptile findings on this PR: clear stale per-sample
result files on re-eval, type reward_mode as Literal[submit, auto_best], and
filter auto_best candidates by selection dataset id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +118 to +145
target = ref or "HEAD"
fetch = await workspace.sandbox.run(
[
"git",
"-c",
"core.hooksPath=/dev/null",
"-c",
"protocol.file.allow=always",
"-C",
root,
"fetch",
"--no-tags",
"--no-recurse-submodules",
f"file://{self.agent_repo_path}",
target,
],
timeout=120,
)
if fetch.returncode != 0:
raise CommitTransferError(
f"git fetch of {target!r} from agent repo failed: {fetch.stderr}"
)
rev = await workspace.sandbox.run(
["git", "-C", root, "rev-parse", "FETCH_HEAD"], timeout=30
)
if rev.returncode != 0:
raise CommitTransferError(f"rev-parse FETCH_HEAD failed: {rev.stderr}")
return rev.stdout.strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security FETCH_HEAD shared across concurrent transfers

FETCH_HEAD is a single file inside the sidecar's repo; with max_concurrency=20 and uvicorn handling concurrent /eval requests, two in-flight _transfer_commit calls can race: fetch B overwrites FETCH_HEAD after fetch A finishes but before A's rev-parse FETCH_HEAD runs. A then resolves B's SHA and evaluates B's commit while attributing the result to A's request — a trust-boundary violation where one agent can inadvertently (or deliberately via rapid concurrent calls) evaluate a different commit.

The fix is to write each transfer to a unique local ref (<target>:<unique-local-ref>) and resolve that ref, then delete it, so concurrent transfers never share state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/server.py
Line: 118-145

Comment:
**FETCH_HEAD shared across concurrent transfers**

`FETCH_HEAD` is a single file inside the sidecar's repo; with `max_concurrency=20` and uvicorn handling concurrent `/eval` requests, two in-flight `_transfer_commit` calls can race: fetch B overwrites `FETCH_HEAD` after fetch A finishes but before A's `rev-parse FETCH_HEAD` runs. A then resolves B's SHA and evaluates B's commit while attributing the result to A's request — a trust-boundary violation where one agent can inadvertently (or deliberately via rapid concurrent calls) evaluate a different commit.

The fix is to write each transfer to a unique local ref (`<target>:<unique-local-ref>`) and resolve that ref, then delete it, so concurrent transfers never share state.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment on lines +24 to +28
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(token)
p.chmod(mode)
return p

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Token readable during write-then-chmod window

write_text creates the file with the process umask (commonly 0o644), then chmod restricts it. In the window between the two syscalls the token is world-readable — any process sharing the same filesystem can race to read it. Since this token gates /finalize (the reward-writing endpoint), a leak lets agent.user call /finalize directly.

The fix is to open the file with the target mode in a single atomic step so it is never created with wider permissions.

Suggested change
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(token)
p.chmod(mode)
return p
import os
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
try:
os.write(fd, token.encode())
finally:
os.close(fd)
return p
Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/auth.py
Line: 24-28

Comment:
**Token readable during write-then-chmod window**

`write_text` creates the file with the process umask (commonly 0o644), then `chmod` restricts it. In the window between the two syscalls the token is world-readable — any process sharing the same filesystem can race to read it. Since this token gates `/finalize` (the reward-writing endpoint), a leak lets `agent.user` call `/finalize` directly.

The fix is to open the file with the target mode in a single atomic step so it is never created with wider permissions.

```suggestion
    import os

    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
    try:
        os.write(fd, token.encode())
    finally:
        os.close(fd)
    return p
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

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