Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ while this project adds the environment-shaped API on top.
- **`cmd/ate-env-api`** — The API service that manages environments and proxies remote guest requests.
- **`cmd/ate-env-guest`** — The daemon server running inside each actor serving command executions, file read/write, and built-in MCP tools.
- **`clients/go`** — The Go client library to manage environments, run commands, and perform file operations.
- **`clients/python`** — The async Python client library ([README](clients/python/README.md)).
- **`clients/python`** — Python client for environment lifecycle management and batch sandbox orchestration ([README](clients/python/README.md)).
- **`integrations/nemo-gym`** — A [NeMo Gym](https://github.com/NVIDIA-NeMo/Gym) sandbox provider that runs rollout sandboxes as environments, built on the Python client ([README](integrations/nemo-gym/README.md)).

## Installation
Expand Down
9 changes: 5 additions & 4 deletions clients/python/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# ate-env-client — Async Python Client
# ate-env-client — Python Client & Sandbox Fleet SDK

> [!WARNING]
> This is an alpha API and is likely to change until v1.0 is released.

Async Python client for the [Agent Substrate Environment](../../README.md)
API (`ate-env-api`): environment lifecycle, remote command execution, and
streaming file I/O over gRPC.
Python client library for the [Agent Substrate Environment](../../README.md)
API (`ate-env-api`):
1. **Single Environment Lifecycle & Guest Operations**: `Client` and `Env` for fine-grained gRPC control, process execution, and file streaming.
2. **Fleet & Sandbox Orchestration**: `SandboxFleet` for managing batches of sandboxes with pool sizing, pre-warming, and execution strategies.

Requires Python >= 3.10. Everything is `asyncio`-native: methods are
coroutines, log/file streams are async iterators, and cancellation works
Expand Down
65 changes: 60 additions & 5 deletions clients/python/src/ate_env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,48 +32,103 @@ async def main():
asyncio.run(main())
"""

# Layer 1: Core Client & Env
from .client import DEFAULT_ATESPACE, Client
from .env import Env, Process

# Layer 2: High-Level Fleet Management & Abstractions
from .config import FleetConfig
from .fleet import SandboxFleet
from .handle import SandboxHandle

# Exceptions
from .errors import (
CommandExecutionError,
CommandStartError,
CommandTimeoutError,
EnvError,
FailedPreconditionError,
InfrastructureError,
InvalidArgumentError,
NotFoundError,
PermissionDeniedError,
PreflightError,
ProcessExitedError,
RpcError,
SandboxProtocolError,
SandboxStartError,
SandboxUnavailableError,
map_rpc_error,
)
from .exceptions import SandboxError

# Types
from .types import (
DataPlaneEndpoint,
EnvironmentInfo,
EnvironmentSpec,
EnvironmentStatus,
ExecResult,
FleetPlan,
PlacementSpec,
PlanEntry,
ProcessInfo,
ProcessOutput,
ProcessState,
RawSandboxInstance,
ResourceLimits,
ShellResult,
Signal,
Task,
Template,
)

__all__ = [
# Low-level API
"Client",
"DEFAULT_ATESPACE",
"Env",
"Process",
# High-level Fleet API
"FleetConfig",
"SandboxFleet",
"SandboxHandle",
# Low-level Errors
"EnvError",
"EnvironmentInfo",
"EnvironmentStatus",
"FailedPreconditionError",
"InvalidArgumentError",
"NotFoundError",
"PermissionDeniedError",
"Process",
"ProcessExitedError",
"RpcError",
"map_rpc_error",
# Fleet / Execution Exceptions
"SandboxError",
"CommandExecutionError",
"CommandStartError",
"CommandTimeoutError",
"InfrastructureError",
"PreflightError",
"SandboxProtocolError",
"SandboxStartError",
"SandboxUnavailableError",
# Types
"EnvironmentInfo",
"EnvironmentStatus",
"ProcessInfo",
"ProcessOutput",
"ProcessState",
"RpcError",
"ShellResult",
"Signal",
"Template",
"map_rpc_error",
"ResourceLimits",
"PlacementSpec",
"EnvironmentSpec",
"Task",
"ExecResult",
"DataPlaneEndpoint",
"RawSandboxInstance",
"PlanEntry",
"FleetPlan",
]

20 changes: 20 additions & 0 deletions clients/python/src/ate_env/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .base import BackendDriver
from .mock import MockBackendDriver
from .substrate import SubstrateBackendDriver

__all__ = ["BackendDriver", "MockBackendDriver", "SubstrateBackendDriver"]

57 changes: 57 additions & 0 deletions clients/python/src/ate_env/backend/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Optional

from ..types import EnvironmentSpec, RawSandboxInstance


class BackendDriver(ABC):
"""
Control Plane (Backend Protocol) Interface.

Governs fleet provisioning, template creation, warm-pool sizing,
instance claims, snapshotting, and run isolation / reaping.
"""

@abstractmethod
def preflight(self) -> None:
"""Validate cluster connectivity, permissions, storage, and worker capacity."""

@abstractmethod
def ensure_template(self, env: EnvironmentSpec) -> str:
"""Ensure template/golden snapshot exists. Returns unique template ID."""

@abstractmethod
def warm_pool(self, template_id: str, replicas: int, wait: bool = True) -> None:
"""Provision warm replicas (Ready pods in K8s or Paused Actors in Substrate)."""

@abstractmethod
def unwarm_pool(self, template_id: str) -> None:
"""Scale down warm pool for the template to 0."""

@abstractmethod
def acquire(self, template_id: str, run_id: str, timeout_s: float = 180.0) -> RawSandboxInstance:
"""Instantly claim a sandbox instance from warm pool or golden."""

@abstractmethod
def release(self, instance_id: str, recycle: bool = False) -> None:
"""Release or recycle instance."""

@abstractmethod
def reap(self, run_id: str) -> int:
"""Force delete all resources associated with a run_id."""
84 changes: 84 additions & 0 deletions clients/python/src/ate_env/backend/mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import collections
import time
import uuid
from typing import Dict, List, Optional, Set

from ..errors import SandboxStartError
from ..types import EnvironmentSpec, RawSandboxInstance
from .base import BackendDriver


class MockBackendDriver(BackendDriver):
"""In-memory mock backend driver for hermetic unit testing and PoC simulation."""

def __init__(self, fail_preflight: bool = False, fail_acquire: bool = False):
self.fail_preflight = fail_preflight
self.fail_acquire = fail_acquire
self.templates: Dict[str, EnvironmentSpec] = {}
self.warm_pools: Dict[str, int] = collections.defaultdict(int)
self.instances: Dict[str, RawSandboxInstance] = {}
self.reaped_runs: Set[str] = set()

def preflight(self) -> None:
if self.fail_preflight:
raise RuntimeError("Mock preflight failed: cluster unreachable")

def ensure_template(self, env: EnvironmentSpec) -> str:
tid = env.template_key()
self.templates[tid] = env
return tid

def warm_pool(self, template_id: str, replicas: int, wait: bool = True) -> None:
self.warm_pools[template_id] = replicas

def unwarm_pool(self, template_id: str) -> None:
self.warm_pools[template_id] = 0

def acquire(self, template_id: str, run_id: str, timeout_s: float = 180.0) -> RawSandboxInstance:
if self.fail_acquire:
raise SandboxStartError("Mock acquire failed: capacity exhausted")

inst_id = f"mock-sb-{uuid.uuid4().hex[:8]}"
inst = RawSandboxInstance(
instance_id=inst_id,
endpoint=f"http://127.0.0.1:8000/{inst_id}",
template_id=template_id,
run_id=run_id,
metadata={"created_at": time.time(), "template": template_id}
)
self.instances[inst_id] = inst
return inst

def release(self, instance_id: str, recycle: bool = False) -> None:
if instance_id in self.instances:
if not recycle:
del self.instances[instance_id]

def reap(self, run_id: str) -> int:
self.reaped_runs.add(run_id)
to_del = [iid for iid, inst in self.instances.items() if inst.run_id == run_id]
for iid in to_del:
del self.instances[iid]
return len(to_del)

def build_runtime(
self, instance: RawSandboxInstance, data_plane: str = "mock"
) -> tuple[Any, Optional[Any]]:
from ..runtime.mock import MockRuntimeHook
return MockRuntimeHook(), None
Loading
Loading