Summary
Two agent-launch helpers mutate os.environ directly during per-rollout setup. In a multi-rollout (concurrent) eval — the default usage pattern at any meaningful scale — these writes race across rollouts running in the same Python process. The last writer wins; earlier rollouts can be misconfigured silently.
Concrete file:line
These run per-rollout (each Rollout.run), but os.environ is process-global. Two rollouts launched concurrently with different agents/models can interleave their env mutations.
Concrete impact
# Rollout A (model=gemini-2.5-flash) sets GEMINI_BASE_URL=https://a/
# Rollout B (model=gemini-2.5-pro) sets GEMINI_BASE_URL=https://b/
# Rollout A's subsequent agent invocation now reads B's URL.
For --concurrency > 1 runs the bug surfaces probabilistically. Symptoms:
- Wrong model called.
- Wrong provider URL.
- Cross-test contamination in CI.
Severity
P2.
- Single-task evals are unaffected.
- Multi-task evals on the same process trigger the race.
- The default
--concurrency is documented as 4 (varies by sandbox) — the race surface is non-trivial.
Proposed fix
-
Stop mutating os.environ. Build a per-rollout env: dict[str, str] and pass it to the child process via subprocess/asyncio.create_subprocess_* env= kwarg. The per-rollout env never touches the process env.
-
If the launcher truly needs the env to be visible to nested calls (e.g. an SDK reads os.environ lazily), scope the mutation with contextvars.ContextVar or a with patch.dict(os.environ, ...) context manager that restores on exit — and serialize concurrent rollouts that hit this code path.
-
Add a regression test that runs two rollouts concurrently with conflicting env values and asserts they don't cross-contaminate.
Related
Companion to #418 (closed: "eval prunes Docker globally"). Both are "per-rollout helper mutates process-global state" antipattern — the global-Docker-prune fix should set the pattern, and these two launchers should follow it.
Found via
v0.5 e2e validation arch-audit at 5a7a326.
Summary
Two agent-launch helpers mutate
os.environdirectly during per-rollout setup. In a multi-rollout (concurrent) eval — the default usage pattern at any meaningful scale — these writes race across rollouts running in the same Python process. The last writer wins; earlier rollouts can be misconfigured silently.Concrete file:line
src/benchflow/agents/pi_acp_launcher.py:113-117— writes provider URL/key env vars toos.environ.src/benchflow/agents/openclaw_acp_shim.py:138,:358— same pattern.These run per-rollout (each
Rollout.run), butos.environis process-global. Two rollouts launched concurrently with different agents/models can interleave their env mutations.Concrete impact
For
--concurrency > 1runs the bug surfaces probabilistically. Symptoms:Severity
P2.
--concurrencyis documented as 4 (varies by sandbox) — the race surface is non-trivial.Proposed fix
Stop mutating
os.environ. Build a per-rolloutenv: dict[str, str]and pass it to the child process viasubprocess/asyncio.create_subprocess_*env=kwarg. The per-rollout env never touches the process env.If the launcher truly needs the env to be visible to nested calls (e.g. an SDK reads
os.environlazily), scope the mutation withcontextvars.ContextVaror awith patch.dict(os.environ, ...)context manager that restores on exit — and serialize concurrent rollouts that hit this code path.Add a regression test that runs two rollouts concurrently with conflicting env values and asserts they don't cross-contaminate.
Related
Companion to #418 (closed: "eval prunes Docker globally"). Both are "per-rollout helper mutates process-global state" antipattern — the global-Docker-prune fix should set the pattern, and these two launchers should follow it.
Found via
v0.5 e2e validation arch-audit at
5a7a326.