Repository navigation
[DRAFT] feat(python): extend ate_env SDK with sandbox fleet management and Substrate backend - #75
Sunny (sunnylovestiramisu) wants to merge 2 commits into
Conversation
5c64ac4 to
7ad2595
Compare
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
| proc = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, start_new_session=True) |
There was a problem hiding this comment.
Command Injection in Subprocess Calls (CWE-78)
Analyzed by AI as
More Details
Detected user input entering a subprocess call unsafely. This could result in a command injection vulnerability. An attacker could use this vulnerability to execute arbitrary commands on the host, which allows them to download malware, scan sensitive data, or run any command they wish on the server. Do not let users choose the command to run. In general, prefer to use Python API versions of system commands. If you must use subprocess, use a dictionary to allowlist a set of commands.
| Attribute | Value |
|---|---|
| Impact | |
| Likelihood |
AI Analysis
The snippet shows subprocess.Popen(cmd, ...) being called, but the source of cmd is not visible in the provided code. Without seeing how cmd is constructed or where it originates from, it's unclear whether user input directly flows into it unsafely. The snippet alone is insufficient to determine if this is exploitable—additional context about cmd's origin is needed to assess the actual injection risk.
Remediation
Command injection vulnerabilities occur when user-supplied input is passed unsanitized to a system command execution function like subprocess.run(). This allows an attacker to execute arbitrary commands on the host system, leading to data theft, malware installation, or full system compromise.
To fix this issue, user input should never be directly passed to command execution functions. Instead, use Python APIs for system operations whenever possible. If you must use subprocess, validate and sanitize all input, and use a whitelist to strictly control allowed commands.
Code examples
# VULNERABLE CODE - User input is passed directly to subprocess.run()
import subprocess
def run_command(user_input):
result = subprocess.run(user_input, shell=True)
return result.stdout# SECURE CODE - Use Python APIs instead of subprocess when possible
import os
def list_files(directory):
return os.listdir(directory)
If you must use `subprocess`, sanitize input and use a whitelist:# SECURE CODE - Whitelist allowed commands and sanitize input
import subprocess
ALLOWED_COMMANDS = ["ls", "grep", "find"]
def run_command(user_input):
if user_input in ALLOWED_COMMANDS:
sanitized_input = [user_input] # Run command directly, no shell
result = subprocess.run(sanitized_input, stdout=subprocess.PIPE)
return result.stdout.decode()
else:
return "Command not allowed"Additional recommendations
- Follow the principle of least privilege and run commands with minimal permissions
- Implement secure coding practices like input validation per OWASP guidelines
- Consider using parameterized APIs or ORMs instead of constructing OS commands
- Perform thorough testing, code review, and penetration testing to identify injection flaws
Rule ID: WS-I013-PYTHON-00058
To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason
If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).
|
We already have a sandbox SDK in the clients directory. What's missing in them? |
ff81cc4 to
9d95204
Compare
9d95204 to
c8f9fc0
Compare
c8f9fc0 to
4d94d0f
Compare
Tomer Glottmann (tomergee)
left a comment
There was a problem hiding this comment.
Thanks for putting this together. The control-plane / data-plane split and the rule that transport failures raise while agent failures come back as an ExecResult are both the right calls, and I want to keep them. My main concern is that the new layer sits beside the client we already have in clients/python instead of on top of it, and in a few places that leaves the Substrate path non-functional. Details below, roughly by importance.
1. Build the fleet on Client / Env, not next to them
We already ship an asyncio gRPC client: Client does environment lifecycle against ate-env-api (create / get / suspend / delete) and Env does processes and files with the env-id metadata the server expects. This PR uses Env for one of its three data planes and never uses Client for the control plane. Concretely:
-
SubstrateBackendDriverdoesn't call anything.ensure_templatehashes the image into a name and stores it in a dict (substrate.py#L155).warm_poolinvents actor ids (#L175).acquirepops one or mintsact-<uuid>and returns it as RUNNING (#L197).releaseandreaponly touch dicts. The one network call is a GET probe on the router inpreflight. Against a real cluster every handle points at an actor that was never created, so the description's "manages ActorTemplates and paused actor pools" and "verified on GKE" can't be describing this code.test_substrate_driver.pyonly checks dict lengths.Most of the replacement already exists:
acquireisClient.create(id, template_name=...),releaseisClient.delete, pre-warm is create +Client.suspend.Task.imagecurrently never leaves the process (fleet.pyL169 uses it as a dict key), so per-task images, which is the SWE-bench case, can't work until image → ActorTemplate is either created by the driver or required to be pre-baked, the wayintegrations/nemo-gymdoes it today. -
Env.shell()is reimplemented twice, inexec(substrate_env_client.py#L148) andexec_async(#L204), and both copies drop the part that matters: neither passestimeouttostart_process, so the guest never kills the command. The sync path gives up client-side attimeout_s + 5(#L169) but doesn't cancel the coroutine, so the process keeps running in the sandbox. The async path has no timeout at all (#L249):timeout_sis ignored and theexcept TimeoutErrorunder it is unreachable. On 3.10, whichrequires-pythonstill allows,concurrent.futures.TimeoutErrorisn't the builtin either, so even the sync path lands inexcept Exceptioninstead of returningtimed_out=True.Env.shell(..., timeout=timeout_s)does the right thing in one line. -
The async client gets wrapped sync, then async again.
ClientandEnvare asyncio-native.SubstrateEnvClientRuntimeruns them on a private event-loop thread (_SharedAteClientManager), exposes a syncexec, andAsyncSandboxFleetthen pushes the sync fleet onto a thread pool of up to 1024 workers (async_fleet.py#L65). The asyncexecoverride hops back to the private loop. That's two loops, cross-thread futures, and a module-global client cache thatclose()never closes (L129). An async-first fleet overClient, with a thin sync facade if someone needs one, is less code and matches the rest of the package. -
Second error hierarchy.
errors.pyalready maps gRPC status to typed exceptions.exceptions.pyadds a parallel tree, with aTimeoutErrorthat shadows the builtin, and the bridge between them loses information:NotFoundError→CommandStartError(non-retryable, #L188) collapses "environment is gone" with "cwd doesn't exist". TheExecResultvsInfrastructureErrorcontract is good; it should live inerrors.pyand be expressed with the classes we already have.
2. The default data plane isn't our guest
data_plane defaults to "router", which POSTs JSON to atenet-router /process and parses exitCode. ate-env-guest serves gRPC ProcessService / FileSystemService plus GET /readyz (main.go#L61); there is no HTTP /process. ate-env-api itself reaches guests over gRPC through the router with ate-target-actor metadata (server.go#L392-L412). So the router runtime, and the base64-through-argv file helpers with their 96 KiB cap, exist to drive a guest this repo doesn't ship. Unless there's a target I'm missing, drop it; Env already does files and streaming properly.
Related: the gRPC runtime is built by splitting the ate-target-actor header into atespace / actor and feeding that to Client.env() (fleet.py#L211). Env id == actor id is the API server's mapping, not a client contract. grpc_endpoint also accepts an {actor_id} placeholder (the pipeline defaults to {actor_id}.ate-system.svc:7777), which implies per-actor addresses, while Client only works against ate-env-api; the RayJob manifest uses a third hostname on :50051. And FleetConfig.endpoint means the Substrate API in the driver but is set to :7777, the ate-env-api port, in the sync example and the pipeline default. One named ate-env-api endpoint and nothing else would remove all of that.
3. Duplicates of things already in the repo
providers/nemo_gym.pyvs theintegrations/nemo-gymprovider that merged in #69. The new one is sync, dict-based, hard-codesstatus()to RUNNING, and its own docstring says it doesn't implement the NeMo Gym async interface. Both register undernemo_gym.sandbox_providers, and the integration depends on this package, so installing it registers both. Remove the new one.adapters/swebench.pyand the/testbeddefaultcwd: it's the default on the abstractRuntimeGuestHook.exec(base.py#L105), the handle, both runtimes, the mock and the NeMo provider. SWE-bench belongs inexamples/, andcwdshould default to whatever the image says.connector.py/strategies.py: onlyresolve_endpointis used (by the backend), the tunnel strategy spawnskubectl port-forwardand nothing closes it,strategiesis only reachable from the sync fleet, andnum_generationsis never read.config.pyimports frombackend.substrate(L20).- The
sandbox_sdk.*logger names, the "Kubernetes Authors" header and the "upstream agent-sandbox" docstrings say this is a port of the agent-sandbox fleet SDK. If that's the plan, the cleaner shape is a Substrate backend for that SDK, implemented onate_env.Client, rather than a second SDK insideclients/python.
4. Smaller things
SandboxHandle.execreturnsstdoutas astr(handle.py#L97), so theExecResultthe design is built around is unreachable through the handle; the example scores by substring match.__exit__recycles on success and releases on exception (#L162). Recycling a sandbox that just ran agent code back into a shared pool without a reset leaks state between rollouts; release should be the default._build_runtimespecial-cases the mock withisinstance(fleet.py#L198), so an injected driver that doesn't populatedata_planesfails."grpc"and"ate_env"are the same endpoint (substrate.py#L133) and only"grpc"is validated forgrpc_endpoint; the pipeline defaults to"ate_env".- Backend dicts are mutated from up to 1024 threads with no lock.
pydanticbecomes a hard dependency of the client for one config class.- Tests: 157 pass, 1 fails on macOS (
test_exec_semantics.pyL136 assumes GNUbase64errors on a directory). Nothing exercises the Substrate driver against a server, which is how (1) got through. The description links aDESIGN.mdthat isn't in the diff.
Suggested shape
Keep BackendDriver / RuntimeGuestHook as the two seams, make them async, and implement the Substrate pair directly on Client / Env; the data plane can literally be Env. The fleet is then warm-pool sizing, batch acquire and reap-by-run-id, a few hundred lines. Everything workload-specific (SWE-bench, VeRL, NeMo Gym) stays in examples/ and integrations/. Happy to pair on the Client-backed driver if useful.
Sequencing
I would break this into 3 to 5 smaller PRs, each taking care of one thing, rather than trying to cover the whole of agent-sandbox's functionality in one PR. Roughly: get correct rollout deployments first (a Client-backed Substrate driver that actually creates, suspends and deletes environments, with the timeout plumbed through), then add the fleet / batching layer on top of that, then the integrations (NeMo Gym, the VeRL example, the SWE-bench adapter) as their own PRs.
4d94d0f to
410485a
Compare
Tomer Glottmann (tomergee)
left a comment
There was a problem hiding this comment.
Scope: this review is for PR 1 only, commit c131276 ("build Substrate driver and runtime directly on Client and Env"). PR 2 (b576989) and PR 3 (410485a) get their own reviews.
This review covers only commit 1: the driver, the runtime, the errors, the configuration, the types, the
synchronous fleet, the handle, the strategies, the USER_GUIDE, and the tests. Line numbers refer to head
410485a. Commits 2 and 3 do not change these files, except where this review says so. I tested items 1, 2,
4, 5, 6, and 11 against the fake ate-env-api server. The smoke run on glottman-sandbox-test-1 on 2026-10-12
also shows item 1.
Thank you for the rework. The structure is correct now. The driver creates, suspends, and deletes
environments through Client. The runtime uses Env.shell with the guest timeout. There is one error
hierarchy. The router data plane and pydantic are gone. The problems that remain are in the synchronous
facade and in the lifecycle bookkeeping. The items are in order of severity.
Blocking
-
The synchronous facade operates only one time.
_sync(backend/substrate.py L111-119 and
runtime/substrate_env_client.py L103-111) runs each call in a newasyncio.runloop. The code creates
theClienton the first call (L105-109). Its grpc.aio channel is attached to that first loop. The
secondacquireon aSandboxFleetfails withSandboxStartError: ... Event loop is closed. I got the
same failure against the fake server and on the live cluster. In this commit,SandboxFleetis the only
fleet. Thus the fleet layer,strategies.py, andSandboxFleet.rundo not operate on Substrate. Make
the Substrate path async only. Alternative: give the facade one permanent loop thread that owns the
Client, and useasyncio.run_coroutine_threadsafe. A newasyncio.runfor each call cannot operate
with a shared channel. -
teardown()does not remove the warm pool.warm_pool_asyncrecords each pre-warmed actor with
run_id="warm-pool"(L260-262).reap_asyncremoves only the actors that have the run id of the fleet
(L312). Aftersetup()andteardown(), each unclaimed warm actor stays SUSPENDED on the cluster. Test
result: two actors warmed, two actors on the server after teardown.run_naive(strategies.py L73-81)
causes this on each run. Makereapremove the warm pool, or record the warm actors with the run id of
the owner. -
You cannot set a real ActorTemplate name.
ensure_template(L149-169) usesimage_templates, or
it usesEnvironmentSpec.template_key()(types.py L234-240). The result is a hash name, for example
tmpl-sweb-eval-pytest-522-<12 hex>.FleetConfighas no template field.SandboxFleet.__init__
(fleet.py L66-75) does not giveimage_templatesordefault_templateto the driver. Thus each fleet
that you build from a configuration sendsCreateEnvironmentwith a template name that does not exist.
Only a driver that you build by hand operates. Add atemplateorimage_templatesfield to
FleetConfig, or addTask.template. Give a clear error when the template does not exist. -
The USER_GUIDE examples do not run. Section 1:
with SandboxFleet(config) as fleetcauses a
TypeError, becauseSandboxFleethas no context manager.fleet.acquire(tasks)and
fleet.release(sandboxes)accept oneTask, not a list. Section 2:await fleet.setup(tasks)accepts
no arguments. Run each example. Recommendation: make each example a test against the mock backend.
Before merge
-
Preflight cannot fail.
preflight_async(L143-144) writes a log message and continues for all
exceptions, exceptNotFoundError. A closed port passes preflight. I tested this. No code raises
PreflightError. RaisePreflightErrorfor all errors that are not NOT_FOUND. -
Failed deletes are not shown.
release_async(L303-304) andreap_async(L319-320) write failed
deletes at the DEBUG level. With the current decode error on env main, each released warm actor stays on
the cluster, and there is no warning. Write a WARNING at minimum.reapmust return or raise the list of
actors that it could not delete. -
Parameters that the code accepts but does not use.
acquire(timeout_s)(L197-234) and
warm_pool(wait=)(L236) are not used. Thusacquire_timeout_shas no effect, and there is no
readiness check after a claim of a suspended actor. No code readsready_timeout_s,step_timeout_s,
num_generations, orlabels. The driver keepsauth_token(L85) but does not attach it to the
channel. Connect these parameters, or remove them. -
Round-1 items that only commit 2 corrects. In this commit,
SandboxHandle.execreturns astr
(handle.py L71, L97).__exit__recycles the sandbox on success (L162-166). Commit 2 corrects both. If
this commit becomes its own PR, put those changes in it. Also,recycle()suspends an actor that ran
agent code and puts it back in the pool without a reset. Write this in the docstring. -
Unused router code and per-actor endpoint code. Remove
router_url,grpc_endpoint, and
validate_grpc_target. Remove"router"and"grpc"fromALLOWED_DATA_PLANES(config.py L32,
L75-76). The"grpc"plane is a copy of"ate_env"(substrate.py L171-183). Remove it. Remove
DataPlaneEndpoint.protocol="http"and theatenet-routerandate-target-actordocstrings
(types.py L287-297). Keep one ate-env-api endpoint and one plane. -
Error taxonomy.
SandboxStartError,PreflightError,CapacityError, and
OwnedByAnotherRunErrorare subclasses ofEnvError, not ofInfrastructureError. Thus a failed
acquire has noretryableattribute. A loop that masksInfrastructureErrordoes not see a failed
acquire. No code raisesCapacityErrororOwnedByAnotherRunError. Make acquire failures subclasses
ofInfrastructureError. Remove the unused classes. -
The tests pass because the mocks are not attached to a loop.
test_data_planes.pyuses a
MagicMockclient for the synchronousacquire(L30-35).test_substrate_env_client_sync_execcalls
exec_async(L109-121). No test runs the synchronous fleet,setup()andteardown(), or preflight
against the fake server. This is the reason the tests did not find items 1, 2, and 5. Add these three
tests. They are small. -
List commands run in
sh -c.exec_asyncjoins the argv list withshlex.join
(substrate_env_client.py L123).Env.shellthen runs the string withsh -c. This is not the contract
incommand_to_argv(runtime/base.py L30-43). That contract says: the code runs a list directly,
without a shell, and runs a string with bash. UseEnv.start_process(argv)for lists, or change the
contract and the docstring. -
SandboxHandle.host,port, andip_address(handle.py L47-67) readendpoint. For Substrate,
endpointis the ate-env-api address. These properties return the API server, not the sandbox. Remove
them.
Nits
fleet.pyL27,handle.pyL21, andbackend/mock.pyL22 import from the deprecated.exceptions
shim. Import from.errors.exceptions.pyalso exportsbuiltins.TimeoutErrorwith the same name.- The logger names are
sandbox_sdk.fleetandsandbox_sdk.strategies. The package isate_env. config.pyimports frombackend.substrate(L21).ValidationErroris a subclass of the gRPC
InvalidArgumentError. The 100-line__init__withobject.__setattr__can be a@dataclasswith
__post_init__validation.model_dumpandmodel_validateare pydantic names.__init__.pyadds the aliasesFleetandEnvHandle, and a__version__that is a copy of the
pyproject value. The commit removes theClientquickstart from the module docstring. Keep the
quickstart.- The
ExecResultdocstring (types.py L262-265) says thatexit_code == -1shows a signal. This does not
occur. The guest reports 128 plus the signal number. Write in the docstring that the 137-and-duration
test (substrate_env_client.py L153-154) can give an incorrect result, for example after an OOM kill. - The code divides
NotFoundErrorintoSandboxUnavailableErrorandCommandStartErrorwith a substring
match on the message (L164-168). _InteractiveSession(L271-286) isexecwith a different signature.open_sessionon the handle makes
a result withexit_code=0. Remove the session API until a real streaming session exists.write_file_via_execandread_file_via_exec(runtime/base.py L46-81) are the base64 helpers from the
router version. They have the 96 KiB limit. Only the testLocalRuntimeuses them.- The synchronous
stream_process(L217-232) collects the full stream before it yields. - There are three environment variables for one endpoint:
SUBSTRATE_API_ENDPOINT,ATE_ENV_API_TARGET,
andATE_API_URL(L69-75). The e2e test in this repository usesATE_ENV_API_TARGET. ensure_template(env: EnvironmentSpec | str)useshasattr(env, "template_key")to find the type.SubstrateEnvClientRuntime(endpoint="localhost:7777", env_id=""): makeenv_ida required parameter.
The code ignoresendpointwhen you giveclient.warm_pool_asynccreates and suspends one actor at a time. Useasyncio.gatherwith a limit.fleet._build_runtimeusesisinstanceto findMockBackendDriver. Commit 2 replaces this.- The Substrate path does not read
ResourceLimits,runtime_bundle,snapshot_storage_uri, or the
placement fields. - README: replace "high-throughput Sandbox Fleet SDK" and "massive RL rollouts" with a description of the
functions.
Data point for the design section
On glottman-sandbox-test-1, a claim from the warm (suspended) pool ran its first command after 2.6 s to
3.7 s. A new create from the golden snapshot took 0.4 s to create plus 2.9 s to the first command. The
warm pool does not decrease the time to the first command. Write this in the design section. Items 2 and 6
are the cost of the warm pool.
|
Rebase onto cccfb08 removes the The two |
cccfb08 to
103e058
Compare
103e058 to
02c63b8
Compare
13e632a to
e317859
Compare
…R 2) Introduce AsyncSandboxFleet as an asyncio-native pool manager and refine handle lifecycle semantics: - Introduce AsyncSandboxFleet using pure asyncio queues, locks, and tasks for non-blocking concurrent rollout acquisition. - Support async setup(tasks=None) to pre-warm the pool from a task list. - Standardize SandboxHandle.exec() and exec_async() to return ExecResult. - Add SandboxHandle.release_async() and recycle_async(), documenting that recycling suspends the actor without resetting in-guest filesystem or memory state. - Change SandboxHandle context manager (__exit__ / __aexit__) to default to release() rather than teardown/destroy. - Export AsyncSandboxFleet in ate_env top-level module. - Add test_async_fleet.py covering concurrency, timeouts, and batch acquisition. - Resolve task images to (template, rootfs_image) for Substrate backends and pass rootfs_image to Client.create.
Add self-contained examples for distributed RL rollouts with VeRL, Ray, and SWE-bench using ate_env: - Add examples/verl_swebench/swebench.py providing task conversion and atomic patch evaluation for SWE-bench (pytest-5221). - Add async_verl_swebench_pipeline.py demonstrating overlapping vLLM token generation with async batch sandbox pre-warming using the ate_env data plane. - Add KubeRay deployment manifest (ray-job.async-verl.yaml) configured to run distributed rollouts against Agent Substrate. - Add clients/python/USER_GUIDE.md detailing the two-layer architecture, with concrete warm pool vs fresh creation latency benchmarks and VeRL integration. - Add README.md with execution instructions.
e317859 to
bba16ef
Compare
Summary
This PR extends the unified Python client (
clients/python/src/ate_env) with high-level Sandbox Fleet Management and Substrate Backend Orchestration.While the existing
ate_env.Clientprovides single-environment lifecycle and guest operations over gRPC, large-scale RL post-training workloads (such as VeRL, NeMo-Gym, and Ray actor groups) require managing hundreds of parallel sandboxes across heterogeneous hardware. This PR introducesSandboxFleetandAsyncSandboxFleetto handle automated pool pre-warming, double-buffered acquisition, network routing strategies, failure domain isolation, and clean cluster teardown.What is Included?
Fleet & Pool Orchestrators (
SandboxFleet,AsyncSandboxFleet,SandboxHandle):SandboxFleet) and pure-asyncio(AsyncSandboxFleet) pool managers for single and batch sandbox acquisition (acquire,acquire_batch).warm_pool,warm_images,unwarm_image) to absorb rollout bursts.acquire_batch) that overlaps environment provisioning and warm-pool resumption behind LLM token generation (e.g., vLLM samplers).SandboxHandle.exec()andexec_async()returningExecResult, with context managers (with handle:/async with handle:) defaulting torelease()for safe pool recycling.Pluggable Backend Drivers (
ate_env.backend):SubstrateBackendDriver: Built directly on top ofate_env.Client(EnvironmentServiceover gRPC) to manage ActorTemplates, pre-warmed suspended actor pools (create+suspend), run-scoped reaping, and per-sandbox data-plane routing coordinates (x-env-id,x-env-atespace).MockBackendDriver: Hermetic in-memory driver for unit testing without cluster dependencies.Pluggable Data Plane Runtimes (
ate_env.runtime):SubstrateEnvClientRuntime: Built directly on top ofate_env.Envto communicate with in-guest services over theate-envgRPC data plane (ProcessServicestreamingProcess/ProcessOutputand chunkedFileSystemService), translating transport errors intoInfrastructureErrorand deadline expirations intoExecResult(timed_out=True).MockRuntimeHook: In-memory simulation runtime for hermetic unit and integration tests.Configuration & Pooling Strategies (
ate_env.config,ate_env.strategies):FleetConfig&SecretStr: Pure standard-library@dataclass(frozen=True)configuration with strict unknown-field rejection and zero extra third-party dependencies (pyproject.tomlunchanged).none,naive,sliding,pipelined) inate_env.strategiesto bound active cluster memory/worker footprint while overlapping prefetching behind rollout execution.VeRL + Ray SWE-bench Rollout Example (
examples/verl_swebench):examples/verl_swebench/swebench.py):SweBenchAdapterproviding standardized task conversion (to_task) and both synchronous (evaluate) and native asynchronous (evaluate_async) patch application and pytest verification.examples/verl_swebench/async_verl_swebench_pipeline.py&ray-job.async-verl.yaml): End-to-end KubeRayRayJobmanifest and GRPO training pipeline demonstrating asynchronous rollout evaluation on Agent Substrate.File Structure & Design Rationale
Why this structure?
How to Use It
Testing & Verification
The changes have been verified with automated unit tests, end-to-end integration tests, and live Kubernetes RayJob deployments: