Skip to content

Concurrent rollouts race on os.environ: pi-acp launcher and openclaw shim mutate process env per rollout #539

Description

@bingran-you

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

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Anti-pattern / type safety / docs precision / minor schema drift / non-deterministic but contained.area:sandboxIssue / PR lives primarily in the "sandbox" subsystem.bugSomething isn't workingquestionFurther information is requestedstatus:blockedWaiting on external dependency. Add a comment explaining why.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions