feat(env-client): sync bootstrap constructors + public base_url (#935)#959
Conversation
|
Thanks for the PR @OfficialAbhinavSingh. I'm not a huge fan of this API design. Do you have any other suggestions? For example, chaining |
|
Thanks for the feedback @burtenshaw — happy to rework it, and chaining does feel cleaner than the The shape I have in mind: make env = await MyEnv.from_docker_image("coding-env:latest") # unchanged
env = MyEnv.from_docker_image("coding-env:latest").sync() # SyncEnvClient, no awaitA nice side effect: Two consequences worth flagging before I build it:
I'd keep the public read-only Alternatively, if you'd rather avoid new factory API entirely: constructor-owned providers (e.g. Does either shape match what you had in mind? I'll rework the PR to whichever you prefer. |
769eed0 to
f51d8b0
Compare
|
Reworked to the chained
env = await MyEnv.from_docker_image("coding-env:latest") # async, unchanged
env = MyEnv.from_docker_image("coding-env:latest").sync() # SyncEnvClient, no await
Non-breaking notes:
Full core suite is green (1435 passed) including the reworked |
f51d8b0 to
3ebd09a
Compare
|
Addressed both Bugbot findings in High — Medium — draft files. Full core suite green (1436 passed) and |
3ebd09a to
feaa418
Compare
|
Fixed the UV startup cleanup gap in Both branches of Added two regression tests: start succeeds, then construction fails on an invalid mode, and the provider's Full core suite green (1438 passed), |
feaa418 to
2a44b17
Compare
|
Good catch on the asymmetry — fixed in I also audited every provider start site in Full core suite green (1439 passed), lint clean. |
2a44b17 to
5e70d40
Compare
|
Two parts here, one of which I think is a false positive: Shared-kwargs mutation — I believe this is a false positive. def bootstrap_env(repo_id, *, use_docker=True, provider=None, **provider_kwargs):
start_args = {}
for k in ("port", "env_vars", "workers"):
if k in provider_kwargs:
start_args[k] = provider_kwargs.pop(k)
tag = provider_kwargs.pop("tag", "latest")
return start_args, tag
outer = {"env_vars": {"A": "1"}, "tag": "v2"}
lam = lambda: bootstrap_env("repo", **outer)
print(lam()) # ({'env_vars': {'A': '1'}}, 'v2')
print(lam()) # ({'env_vars': {'A': '1'}}, 'v2') -> identical, outer unchangedSo a retry keeps the original Single-use guard — good point, added in Full core suite green (1440 passed), lint clean. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5e70d40. Configure here.
|
Fixed in The async path was already covered ( Added a regression test that forces the sync setup to fail after the container starts and asserts |
5e70d40 to
fc0e50c
Compare

Summary
Closes #935. Adds synchronous bootstrap entry points to
EnvClient—from_docker_image_sync()/from_env_sync()— plus a public read-onlybase_urlproperty, so synchronous consumers (e.g. the TRL GRPO example rollout loops) can spin up a container without anawaitand without hand-rolling thestart_container()/wait_for_ready()sequence the async factories already implement.The async
from_docker_image/from_envare unchanged in behavior — both entry points now share small_bootstrap_container/_bootstrap_envhelpers (start + readiness wait, no connect), so the only difference between sync and async is how the WebSocket is connected. Non-breaking: no existing signatures change,AutoEnvuntouched.This is option A from the issue discussion (👍'd by @burtenshaw). Option B (making the existing constructors dual-mode) was prototyped but breaks
AutoEnv.from_env, which wraps the constructors inrun_async_safely()→asyncio.run(coro)and needs a real coroutine.Type of Change
Alignment Checklist
Before submitting, verify:
.claude/docs/PRINCIPLES.mdand this PR aligns with our principles (serves "minimize lifecycle deltas" — sync training loops use the same client; no principle traded off).claude/docs/INVARIANTS.mdand no invariants are violated (Gymreset/step/statesignatures, generics, and the WebSocket/MCP boundary are all unchanged; additive to the bootstrap path only)bash .claude/hooks/lint.shand tests and addressed all issuesRFC Status
Test Plan
PYTHONPATH=src:envs uv run pytest tests/ -m "not integration"— full core suite green (872 passed), including newTestSyncBootstrapConstructors(sync ctor returns a concrete connected client; kwarg forwarding; UV start-failure releases the provider) andTestBaseUrlProperty(normalization,Nonebefore start, read-only).uv run usort check+uv run ruff format --check+uv run ruff checkonsrc/ tests/— clean.Claude Code Review
Self-check against the two-tier model: no bugs, uninitialized state, or debug code; aligns with
PRINCIPLES.md(same-interface training) and violates noINVARIANTS.md(Gymreset/step/statesignatures, generics, and the agent/infra boundary all preserved). No alignment flags. Cursor Bugbot independently flagged this Low Risk — its summary is below.Note
Low Risk
Additive, non-breaking API on the client bootstrap path; async factory behavior is preserved via shared helpers with existing UV startup cleanup.
Overview
Adds synchronous bootstrap entry points on
EnvClientso callers withoutawaitcan spin up Docker or Hugging Face Spaces the same way the async factories do:from_docker_image_sync()andfrom_env_sync()return an already-connected client by reusing shared_bootstrap_container/_bootstrap_envhelpers (start + readiness only) and connecting via_run_sync(_connect_async)on the existing sync background loop.Async
from_docker_imageandfrom_envbehavior is unchanged; they now delegate bootstrap to those helpers thenawait connect(). UV-mode startup failures still callprovider.stop()before re-raise when no client exists yet.Restores a read-only
base_urlproperty (normalized URL, orNonebefore lazy provider start) for sync consumers after the core refactor removed the public attribute.Tests cover connected sync clients, kwarg forwarding, UV start-failure cleanup, and
base_urlnormalization / read-only semantics.Reviewed by Cursor Bugbot for commit 769eed0. Bugbot is set up for automated code reviews on this repo. Configure here.