Skip to content

[DRAFT] feat(python): extend ate_env SDK with sandbox fleet management and Substrate backend - #75

Open
Sunny (sunnylovestiramisu) wants to merge 2 commits into
agent-substrate:mainfrom
sunnylovestiramisu:sandbox
Open

Sunny (sunnylovestiramisu) wants to merge 2 commits into
agent-substrate:mainfrom
sunnylovestiramisu:sandbox

Conversation

@sunnylovestiramisu

@sunnylovestiramisu Sunny (sunnylovestiramisu) commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

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.Client provides 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 introduces SandboxFleet and AsyncSandboxFleet to handle automated pool pre-warming, double-buffered acquisition, network routing strategies, failure domain isolation, and clean cluster teardown.


What is Included?

  1. Fleet & Pool Orchestrators (SandboxFleet, AsyncSandboxFleet, SandboxHandle):

    • Synchronous (SandboxFleet) and pure-asyncio (AsyncSandboxFleet) pool managers for single and batch sandbox acquisition (acquire, acquire_batch).
    • Sized warm-pool provisioning (warm_pool, warm_images, unwarm_image) to absorb rollout bursts.
    • Non-blocking async batch acquisition (acquire_batch) that overlaps environment provisioning and warm-pool resumption behind LLM token generation (e.g., vLLM samplers).
    • Standardized SandboxHandle.exec() and exec_async() returning ExecResult, with context managers (with handle: / async with handle:) defaulting to release() for safe pool recycling.
  2. Pluggable Backend Drivers (ate_env.backend):

    • SubstrateBackendDriver: Built directly on top of ate_env.Client (EnvironmentService over 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.
  3. Pluggable Data Plane Runtimes (ate_env.runtime):

    • SubstrateEnvClientRuntime: Built directly on top of ate_env.Env to communicate with in-guest services over the ate-env gRPC data plane (ProcessService streaming Process / ProcessOutput and chunked FileSystemService), translating transport errors into InfrastructureError and deadline expirations into ExecResult(timed_out=True).
    • MockRuntimeHook: In-memory simulation runtime for hermetic unit and integration tests.
  4. 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.toml unchanged).
    • Pluggable windowing and warm-pool sizing strategies (none, naive, sliding, pipelined) in ate_env.strategies to bound active cluster memory/worker footprint while overlapping prefetching behind rollout execution.
  5. VeRL + Ray SWE-bench Rollout Example (examples/verl_swebench):

    • SWE-bench Adapter (examples/verl_swebench/swebench.py): SweBenchAdapter providing standardized task conversion (to_task) and both synchronous (evaluate) and native asynchronous (evaluate_async) patch application and pytest verification.
    • VeRL + Ray SWE-bench Pipeline (examples/verl_swebench/async_verl_swebench_pipeline.py & ray-job.async-verl.yaml): End-to-end KubeRay RayJob manifest and GRPO training pipeline demonstrating asynchronous rollout evaluation on Agent Substrate.

File Structure & Design Rationale

clients/python/
├── README.md                                 # Updated overview & quickstart links
├── USER_GUIDE.md                             # Production deployment & usage guide
├── pyproject.toml                            # Unchanged from upstream/main (zero extra dependencies)
├── src/ate_env/
│   ├── __init__.py                           # Unified entrypoint exporting Client, Env, SandboxFleet, AsyncSandboxFleet, and Types
│   ├── types.py                              # Core primitives: EnvironmentSpec, Task, ExecResult, RawSandboxInstance (without redundant status field)
│   ├── errors.py                             # Unified error hierarchy (EnvError, InfrastructureError, SandboxStartError, etc.)
│   ├── exceptions.py                         # Compatibility re-exports of the error taxonomy
│   ├── config.py                             # Pure-stdlib @dataclass(frozen=True) FleetConfig & SecretStr (no Pydantic dependency)
│   ├── fleet.py                              # Synchronous SandboxFleet orchestration
│   ├── async_fleet.py                        # Pure-asyncio AsyncSandboxFleet with non-blocking acquire / acquire_batch (PR 2)
│   ├── handle.py                             # SandboxHandle wrapper (exec/exec_async -> ExecResult, context manager defaults to release())
│   ├── strategies.py                         # Windowing & pooling strategies (none, naive, sliding, pipelined)
│   │
│   ├── backend/                              # Control plane management (sandbox lifecycle & warm pools)
│   │   ├── __init__.py                       # Exports BackendDriver, SubstrateBackendDriver, MockBackendDriver
│   │   ├── base.py                           # BackendDriver protocol (including build_runtime hook)
│   │   ├── substrate.py                      # Agent Substrate driver built directly on ate_env.Client (create, suspend, delete)
│   │   └── mock.py                           # Hermetic in-memory mock backend driver
│   │
│   └── runtime/                              # Data plane execution (in-guest command execution & file I/O)
│       ├── __init__.py                       # Exports RuntimeGuestHook, SubstrateEnvClientRuntime, MockRuntimeHook
│       ├── base.py                           # RuntimeGuestHook protocol and ExecResult / ShellStream normalization
│       ├── substrate_env_client.py           # In-guest ate-env gRPC data plane built directly on ate_env.Env (Process/FileSystem)
│       └── mock.py                           # In-memory mock guest runtime
│
└── tests/                                    # Test suite (116 unit tests passing, 26 e2e skipped)
    ├── test_async_fleet.py                   # AsyncSandboxFleet concurrency, timeouts, & batch acquisition (PR 2)
    ├── test_config.py                        # FleetConfig validation, SecretStr, & unknown-field rejection
    ├── test_data_planes.py                   # Per-sandbox data-plane routing & runtime construction
    ├── test_exec_semantics.py                # ExecResult invariants, exit codes, timeouts, & handle release semantics
    ├── test_fleet_strategies.py              # Fleet sizing strategies (none, naive, sliding, pipelined)
    ├── test_fleet_types.py                   # EnvironmentSpec template key hashing & Task metadata
    ├── test_substrate_driver.py              # SubstrateBackendDriver lifecycle against fake ate-env-api gRPC server
    └── test_substrate_env_client.py          # SubstrateEnvClientRuntime exec, streaming, & file I/O tests

examples/verl_swebench/                       # End-to-end RL Rollout Example (PR 3)
├── README.md                                 # Architecture & GKE execution instructions
├── async_verl_swebench_pipeline.py           # End-to-end Ray + VeRL + Substrate GRPO training loop using evaluate_async
├── ray-job.async-verl.yaml                   # KubeRay RayJob manifest for GKE (--task-type swebench, SUBSTRATE_API_ENDPOINT)
└── swebench.py                               # SweBenchAdapter with sync evaluate() and native async evaluate_async()

Why this structure?

  • Separation of Concerns: Decouples the Control Plane (backend/ - creating, scheduling, snapshotting actors) from the Data Plane (runtime/ - running commands and transferring files inside actors).
  • Single Import Root: Users can access low-level gRPC clients (Client, Env, Process) or high-level distributed pooling (AsyncSandboxFleet, FleetConfig) from import ate_env.
  • Backend Portability: Evaluators write code against SandboxHandle without needing to know whether the sandbox is running on local mocks, GKE pods, or Substrate actors.
  • Fail-Safe Error Taxonomy: Transport failures (InfrastructureError, SandboxUnavailableError) are strictly separated from user command failures (ExecResult.ok == False), preventing network retries from being misinterpreted as agent failures.

How to Use It

  1. Synchronous Sandbox Fleet
from ate_env import FleetConfig, SandboxFleet, Task

config = FleetConfig(
    backend="substrate",
    endpoint="localhost:7777",
    data_plane="ate_env",  # or "router"
)

tasks = [
    Task(id="task-1", image="docker.io/library/python:3.11"),
    Task(id="task-2", image="docker.io/library/python:3.11"),
]

with SandboxFleet(config) as fleet:
    # Acquire sandboxes from pre-warmed pool
    sandboxes = fleet.acquire(tasks)
    try:
        for task, sb in zip(tasks, sandboxes):
            res = sb.exec("python3 -c 'print(\"hello world\")'")
            print(f"Task {task.id} output: {res.stdout.strip()} (exit code {res.exit_code})")
    finally:
        fleet.release(sandboxes)
  1. Async Non-Blocking Rollouts (VeRL / vLLM Pipeline)
import asyncio
from ate_env import AsyncSandboxFleet, FleetConfig, Task

async def run_rollouts():
    # Configure Fleet using the official ate_env gRPC data plane
    config = FleetConfig(
        backend="substrate",
        data_plane="ate_env",  # Native in-guest gRPC via ate-env (ProcessService & FileSystemService)
        endpoint="http://ateapi.ate-system.svc.cluster.local:8080",
        grpc_endpoint="substrate-env.ate-system.svc.cluster.local:50051",
    )
    
    tasks = [Task(id=f"t-{i}", image="docker.io/library/python:3.11") for i in range(4)]
    
    fleet = AsyncSandboxFleet(config)
    await fleet.setup(tasks)

    # Overlap sandbox acquisition & warm pool claiming while vLLM decodes tokens
    warm_task = asyncio.create_task(fleet.acquire_batch(tasks))
    await asyncio.sleep(1.5)  # Simulate token generation
    sandboxes = await warm_task

    # Evaluate rollouts natively over async gRPC
    for sb in sandboxes:
        async with sb:
            await sb.write_file_async("/testbed/calc.py", "def add(a, b): return a + b\n")
            result = await sb.exec_async(["python3", "-c", "import calc; assert calc.add(2, 3) == 5"])
            print(f"Sandbox {sb.sandbox_id} exit_code: {result.exit_code} (ok={result.ok})")

    await fleet.teardown()

asyncio.run(run_rollouts())

Testing & Verification

The changes have been verified with automated unit tests, end-to-end integration tests, and live Kubernetes RayJob deployments:

  1. Local End-to-End Pipeline with Mock Backend
python examples/verl_swebench/async_verl_swebench_pipeline.py \
  --backend mock \
  --task-type swebench \
  --num-iters 2 \
  --group-size 2
  1. Live GKE Cluster Verification
kubectl apply -f examples/verl_swebench/ray-job.async-verl.yaml
kubectl get rayjob async-verl-swebench-sandbox-sdk -n default

@wiz-alphabet

wiz-alphabet Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total -

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

Comment on lines +72 to +73
proc = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, start_new_session=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High SAST Finding

Command Injection in Subprocess Calls (CWE-78)
Analyzed by AI as Inconclusive

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 High
Likelihood Medium

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

@rakyll

Copy link
Copy Markdown
Collaborator

We already have a sandbox SDK in the clients directory. What's missing in them?

@sunnylovestiramisu
Sunny (sunnylovestiramisu) force-pushed the sandbox branch 3 times, most recently from ff81cc4 to 9d95204 Compare October 7, 2026 18:04
@sunnylovestiramisu Sunny (sunnylovestiramisu) changed the title [DRAFT] feat: add top-level sandbox SDK with Substrate integration feat(python): extend ate_env SDK with sandbox fleet management and Substrate backend Oct 7, 2026
@sunnylovestiramisu Sunny (sunnylovestiramisu) changed the title feat(python): extend ate_env SDK with sandbox fleet management and Substrate backend [DRAFT] feat(python): extend ate_env SDK with sandbox fleet management and Substrate backend Oct 7, 2026

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.

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:

  • SubstrateBackendDriver doesn't call anything. ensure_template hashes the image into a name and stores it in a dict (substrate.py#L155). warm_pool invents actor ids (#L175). acquire pops one or mints act-<uuid> and returns it as RUNNING (#L197). release and reap only touch dicts. The one network call is a GET probe on the router in preflight. 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.py only checks dict lengths.

    Most of the replacement already exists: acquire is Client.create(id, template_name=...), release is Client.delete, pre-warm is create + Client.suspend. Task.image currently never leaves the process (fleet.py L169 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 way integrations/nemo-gym does it today.

  • Env.shell() is reimplemented twice, in exec (substrate_env_client.py#L148) and exec_async (#L204), and both copies drop the part that matters: neither passes timeout to start_process, so the guest never kills the command. The sync path gives up client-side at timeout_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_s is ignored and the except TimeoutError under it is unreachable. On 3.10, which requires-python still allows, concurrent.futures.TimeoutError isn't the builtin either, so even the sync path lands in except Exception instead of returning timed_out=True. Env.shell(..., timeout=timeout_s) does the right thing in one line.

  • The async client gets wrapped sync, then async again. Client and Env are asyncio-native. SubstrateEnvClientRuntime runs them on a private event-loop thread (_SharedAteClientManager), exposes a sync exec, and AsyncSandboxFleet then pushes the sync fleet onto a thread pool of up to 1024 workers (async_fleet.py#L65). The async exec override hops back to the private loop. That's two loops, cross-thread futures, and a module-global client cache that close() never closes (L129). An async-first fleet over Client, with a thin sync facade if someone needs one, is less code and matches the rest of the package.

  • Second error hierarchy. errors.py already maps gRPC status to typed exceptions. exceptions.py adds a parallel tree, with a TimeoutError that shadows the builtin, and the bridge between them loses information: NotFoundError → CommandStartError (non-retryable, #L188) collapses "environment is gone" with "cwd doesn't exist". The ExecResult vs InfrastructureError contract is good; it should live in errors.py and 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.py vs the integrations/nemo-gym provider that merged in #69. The new one is sync, dict-based, hard-codes status() to RUNNING, and its own docstring says it doesn't implement the NeMo Gym async interface. Both register under nemo_gym.sandbox_providers, and the integration depends on this package, so installing it registers both. Remove the new one.
  • adapters/swebench.py and the /testbed default cwd: it's the default on the abstract RuntimeGuestHook.exec (base.py#L105), the handle, both runtimes, the mock and the NeMo provider. SWE-bench belongs in examples/, and cwd should default to whatever the image says.
  • connector.py / strategies.py: only resolve_endpoint is used (by the backend), the tunnel strategy spawns kubectl port-forward and nothing closes it, strategies is only reachable from the sync fleet, and num_generations is never read. config.py imports from backend.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 on ate_env.Client, rather than a second SDK inside clients/python.

4. Smaller things

  • SandboxHandle.exec returns stdout as a str (handle.py#L97), so the ExecResult the 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_runtime special-cases the mock with isinstance (fleet.py#L198), so an injected driver that doesn't populate data_planes fails. "grpc" and "ate_env" are the same endpoint (substrate.py#L133) and only "grpc" is validated for grpc_endpoint; the pipeline defaults to "ate_env".
  • Backend dicts are mutated from up to 1024 threads with no lock.
  • pydantic becomes a hard dependency of the client for one config class.
  • Tests: 157 pass, 1 fails on macOS (test_exec_semantics.py L136 assumes GNU base64 errors on a directory). Nothing exercises the Substrate driver against a server, which is how (1) got through. The description links a DESIGN.md that 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.

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.

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

  1. 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 new asyncio.run loop. The code creates
    the Client on the first call (L105-109). Its grpc.aio channel is attached to that first loop. The
    second acquire on a SandboxFleet fails with SandboxStartError: ... Event loop is closed. I got the
    same failure against the fake server and on the live cluster. In this commit, SandboxFleet is the only
    fleet. Thus the fleet layer, strategies.py, and SandboxFleet.run do not operate on Substrate. Make
    the Substrate path async only. Alternative: give the facade one permanent loop thread that owns the
    Client, and use asyncio.run_coroutine_threadsafe. A new asyncio.run for each call cannot operate
    with a shared channel.

  2. teardown() does not remove the warm pool. warm_pool_async records each pre-warmed actor with
    run_id="warm-pool" (L260-262). reap_async removes only the actors that have the run id of the fleet
    (L312). After setup() and teardown(), 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. Make reap remove the warm pool, or record the warm actors with the run id of
    the owner.

  3. You cannot set a real ActorTemplate name. ensure_template (L149-169) uses image_templates, or
    it uses EnvironmentSpec.template_key() (types.py L234-240). The result is a hash name, for example
    tmpl-sweb-eval-pytest-522-<12 hex>. FleetConfig has no template field. SandboxFleet.__init__
    (fleet.py L66-75) does not give image_templates or default_template to the driver. Thus each fleet
    that you build from a configuration sends CreateEnvironment with a template name that does not exist.
    Only a driver that you build by hand operates. Add a template or image_templates field to
    FleetConfig, or add Task.template. Give a clear error when the template does not exist.

  4. The USER_GUIDE examples do not run. Section 1: with SandboxFleet(config) as fleet causes a
    TypeError, because SandboxFleet has no context manager. fleet.acquire(tasks) and
    fleet.release(sandboxes) accept one Task, 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

  1. Preflight cannot fail. preflight_async (L143-144) writes a log message and continues for all
    exceptions, except NotFoundError. A closed port passes preflight. I tested this. No code raises
    PreflightError. Raise PreflightError for all errors that are not NOT_FOUND.

  2. Failed deletes are not shown. release_async (L303-304) and reap_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. reap must return or raise the list of
    actors that it could not delete.

  3. Parameters that the code accepts but does not use. acquire(timeout_s) (L197-234) and
    warm_pool(wait=) (L236) are not used. Thus acquire_timeout_s has no effect, and there is no
    readiness check after a claim of a suspended actor. No code reads ready_timeout_s, step_timeout_s,
    num_generations, or labels. The driver keeps auth_token (L85) but does not attach it to the
    channel. Connect these parameters, or remove them.

  4. Round-1 items that only commit 2 corrects. In this commit, SandboxHandle.exec returns a str
    (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.

  5. Unused router code and per-actor endpoint code. Remove router_url, grpc_endpoint, and
    validate_grpc_target. Remove "router" and "grpc" from ALLOWED_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 the atenet-router and ate-target-actor docstrings
    (types.py L287-297). Keep one ate-env-api endpoint and one plane.

  6. Error taxonomy. SandboxStartError, PreflightError, CapacityError, and
    OwnedByAnotherRunError are subclasses of EnvError, not of InfrastructureError. Thus a failed
    acquire has no retryable attribute. A loop that masks InfrastructureError does not see a failed
    acquire. No code raises CapacityError or OwnedByAnotherRunError. Make acquire failures subclasses
    of InfrastructureError. Remove the unused classes.

  7. The tests pass because the mocks are not attached to a loop. test_data_planes.py uses a
    MagicMock client for the synchronous acquire (L30-35). test_substrate_env_client_sync_exec calls
    exec_async (L109-121). No test runs the synchronous fleet, setup() and teardown(), 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.

  8. List commands run in sh -c. exec_async joins the argv list with shlex.join
    (substrate_env_client.py L123). Env.shell then runs the string with sh -c. This is not the contract
    in command_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. Use Env.start_process(argv) for lists, or change the
    contract and the docstring.

  9. SandboxHandle.host, port, and ip_address (handle.py L47-67) read endpoint. For Substrate,
    endpoint is the ate-env-api address. These properties return the API server, not the sandbox. Remove
    them.

Nits

  • fleet.py L27, handle.py L21, and backend/mock.py L22 import from the deprecated .exceptions
    shim. Import from .errors. exceptions.py also exports builtins.TimeoutError with the same name.
  • The logger names are sandbox_sdk.fleet and sandbox_sdk.strategies. The package is ate_env.
  • config.py imports from backend.substrate (L21). ValidationError is a subclass of the gRPC
    InvalidArgumentError. The 100-line __init__ with object.__setattr__ can be a @dataclass with
    __post_init__ validation. model_dump and model_validate are pydantic names.
  • __init__.py adds the aliases Fleet and EnvHandle, and a __version__ that is a copy of the
    pyproject value. The commit removes the Client quickstart from the module docstring. Keep the
    quickstart.
  • The ExecResult docstring (types.py L262-265) says that exit_code == -1 shows 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 NotFoundError into SandboxUnavailableError and CommandStartError with a substring
    match on the message (L164-168).
  • _InteractiveSession (L271-286) is exec with a different signature. open_session on the handle makes
    a result with exit_code=0. Remove the session API until a real streaming session exists.
  • write_file_via_exec and read_file_via_exec (runtime/base.py L46-81) are the base64 helpers from the
    router version. They have the 96 KiB limit. Only the test LocalRuntime uses 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,
    and ATE_API_URL (L69-75). The e2e test in this repository uses ATE_ENV_API_TARGET.
  • ensure_template(env: EnvironmentSpec | str) uses hasattr(env, "template_key") to find the type.
  • SubstrateEnvClientRuntime(endpoint="localhost:7777", env_id=""): make env_id a required parameter.
    The code ignores endpoint when you give client.
  • warm_pool_async creates and suspends one actor at a time. Use asyncio.gather with a limit.
  • fleet._build_runtime uses isinstance to find MockBackendDriver. 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.

@tomergee

Copy link
Copy Markdown
Collaborator

Rebase onto main first. This branch is still based on 0d359ea, before #77 (Substrate bump to f03327bd) merged, and the rest of this comment follows from that.

cccfb08 removes the guestConn interceptors and keeps forwardOutgoingContext as the writer of ate-target-actor. On this branch that is a valid fix for the duplicate header. main took the opposite choice in #77: forwardOutgoingContext is gone and the interceptors are the only writer. Git merges the two without a conflict, and the result has no writer at all. I merged this head with main locally: TestProxyGuestServices and TestProxyGuestServicesClientTargetActorHeader fail, and on a cluster each guest call gets 404 "invalid actor reference". After the rebase, drop the server.go hunk from this commit; the fix is already on main.

The two _pb2_grpc.py edits change generated files by hand: the grpcio version guard now warns instead of raising. Regenerate with clients/python/scripts/gen-protos.sh, or pin grpcio in pyproject.toml.

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

3 participants