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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ or one of them only where it is used.
- Defaults are local and carry no external coordinates: SQLite document store, filesystem object
store, an OCI registry at `localhost:5000`, env-var secret store, `LocalRunner`, `local` Docker
sandboxes for envs and agents. MongoDB, S3, Cloud Storage, ECR, AWS Secrets Manager, Google Cloud
Secret Manager, Modal and E2B exist as implementations and are selected by config.
Secret Manager, Modal, E2B and Tensorlake exist as implementations and are selected by config
(Tensorlake through the `tensorlake` extra).
- The model endpoint is unset until `[model] base_url` / `api_key` (or `LITELLM_BASE_URL` /
`LITELLM_API_KEY`) is configured. A loopback URL handed into a `local` container (the model
endpoint, an agent, peer or gateway on this machine) becomes `host.docker.internal`. On Linux that
Expand Down Expand Up @@ -95,7 +96,7 @@ and deserialized through a registry.
| `task/`, `task_step/` | A `Task` holds its `TaskStep`s inline; `Task.run()` executes them as a DAG. `depends_on` (None means all prior steps) gates a step, independent steps run concurrently, `fail_task_on_error` makes a failure fatal or tolerated, `retry_config` rolls a failed span back through the step journal and re-dispatches it. Built-in steps live in `task_step/task_steps/` (`deploy_env`, `deploy_agent`, `prompt_agent`, the verifiers under `verifiers/`, and more). |
| `store/` | Four store ABCs with local and cloud implementations: `DocumentStore` (SQLite, MongoDB), `ObjectStore` (filesystem, S3, Cloud Storage), `ImageStore` (local OCI registry, ECR), `SecretStore` (env vars or file, AWS Secrets Manager, Google Cloud Secret Manager). `VersionedEntityStore` implements the shared versioned get/put logic, `QueryBuilder` is the immutable chained query API, `store/base.py` holds the error types. A new backend must pass the conformance kits in `tst/store/`. |
| `config/` | The `Config` singleton (`get_config`, `configure`, `reset_config`) in `config/runtime.py`, file discovery in `config/loader.py`, and `load_impl`, which resolves `module:Class` pointers. `agent_env.store` re-exports the config names for compatibility. |
| `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store) and `EnvironmentGatewayProvider`, which renders a docker-compose for the gateway and its MCP servers inside the sandbox; `providers/env_state/` holds env-state providers (`local_postgres` built in). |
| `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra), `tensorlake` (the `tensorlake` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store) and `EnvironmentGatewayProvider`, which renders a docker-compose for the gateway and its MCP servers inside the sandbox; `providers/env_state/` holds env-state providers (`local_postgres` built in). |
| `a2a_agent/` | The `A2AAgent` entity (`a2a_agent`), its stores and the validator steps. The protocol package provides the agent-side framework. |
| `runner/` | The `[runner]` seam: `Runner.submit()` returns `(run_id, instance_id)`; `LocalRunner` is built in. |
| `explorer/` | Optional local web UI: `agent-env up`, needs the `explorer` extra, binds loopback `:8234`. |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Or install it with pip, into a virtualenv:
pip install agentenv-framework
```

To use the SDK in your own project, add it as a dependency with `uv add agentenv-framework`. The explorer, `agent-env up`, needs the `explorer` extra (`uv tool install 'agentenv-framework[explorer]'`, or the same extra with uvx or pip) and an `.agentenv/config.toml` in the current folder or above it; an empty one keeps every local default.
To use the SDK in your own project, add it as a dependency with `uv add agentenv-framework`. The explorer, `agent-env up`, needs the `explorer` extra (`uv tool install 'agentenv-framework[explorer]'`, or the same extra with uvx or pip) and an `.agentenv/config.toml` in the current folder or above it; an empty one keeps every local default. The `tensorlake` sandbox backend needs the `tensorlake` extra (`pip install 'agentenv-framework[tensorlake]'`) and an `api_key` in `[sandbox.providers.tensorlake.config]`. Its sandboxes boot from the public image `agentenv-dind-host-v1`; set `image` there to use another registered Docker-capable image.

A plain install runs on local stores and needs no cloud SDK. The cloud store backends are extras: `aws` for S3, Secrets Manager, DynamoDB and ECR (`pip install 'agentenv-framework[aws]'`), and `gcp` for Cloud Storage, Secret Manager, Firestore and Artifact Registry (`pip install 'agentenv-framework[gcp]'`). A config that names a backend without its extra fails, naming the extra to install.

Expand Down
53 changes: 52 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ marks, or contributor names to endorse or promote Scale AI, or related products.

## Python dependencies

The runtime dependencies of `agentenv-framework` and its `aws`, `explorer`, `gcp` and `sail` extras, at the versions resolved in `uv.lock`. Development-only dependencies are not listed.
The runtime dependencies of `agentenv-framework` and its `aws`, `explorer`, `gcp`, `sail` and `tensorlake` extras, at the versions resolved in `uv.lock`. Development-only dependencies are not listed.

### a2a-sdk 0.3.26

Expand Down Expand Up @@ -716,6 +716,14 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `explorer`, `gcp
- Source: <https://pypi.org/project/synchronicity/0.12.6/>
- [License text 1](#license-text-1)

### tensorlake 0.5.148

- License: Apache-2.0
- Copyright: Copyright [2025] [Tensorlake]
- Author: Tensorlake Inc. <support@tensorlake.ai>
- Source: <https://github.com/tensorlakeai/tensorlake>
- [License text 1](#license-text-1)

### tiktoken 0.12.0

- License: MIT
Expand Down Expand Up @@ -806,6 +814,22 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `explorer`, `gcp
- Source: <https://github.com/facelessuser/wcmatch>
- [License text 4](#license-text-4)

### websocket-client 1.9.2

- License: Apache-2.0
- Copyright: Copyright 2026 engn33r
- Author: liris
- Source: <https://github.com/websocket-client/websocket-client>
- [License text 1](#license-text-1)

### websockets 17.2

- License: BSD-3-Clause
- Copyright: Copyright (c) Aymeric Augustin and contributors
- Author: Aymeric Augustin <aymeric.augustin@m4x.org>
- Source: <https://github.com/python-websockets/websockets>
- [License text 71](#license-text-71)

### yarl 1.24.2

- License: Apache-2.0
Expand Down Expand Up @@ -7714,3 +7738,30 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

### License text 71

```text
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,12 @@ gcp = [
sail = [
"sail~=0.12.8", # Sailbox exec/fs/listener/egress APIs validated against the 0.12 line
]
# The Tensorlake sandbox provider, selected as the built-in `tensorlake` backend.
tensorlake = [
"tensorlake~=0.5.140", # AsyncSandbox APIs validated against 0.5.140 to 0.5.149
]
dev = [
"agentenv-framework[aws,explorer,gcp,sail]", # so the AWS, explorer, GCP backend and Sail provider tests run in CI
"agentenv-framework[aws,explorer,gcp,sail,tensorlake]", # so the AWS, explorer, GCP backend, Sail and Tensorlake provider tests run in CI
"fastmcp>=3.4,<4", # the standalone-fastmcp mount path of the protocol; <4 keeps mcp 1.x
"griffelib==2.3.0", # the plugin API check; exact, since what it reports as a break changes between releases
"moto>=5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/cli/a2a_agent/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@click.option("--ttl-seconds", type=click.IntRange(min=MIN_TTL_SECONDS, max=MAX_TTL_SECONDS), default=DEFAULT_TTL_SECONDS,
help=f"VM lifetime in seconds (default {DEFAULT_TTL_SECONDS})")
@click.option("--sandbox", default=None,
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, local) "
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, tensorlake, local) "
"or a name from [sandbox.providers] in .agentenv/config.toml; comma-separated for a "
"fallback chain. Defaults to [sandbox].agent_default (else local) when omitted.")
def deploy(agent_id: str, agent_version: int | None, env_var_pairs: tuple[str, ...], ttl_seconds: int, sandbox: str):
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/cli/env/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
@click.option("--gateway-mode", type=click.Choice([m.value for m in GatewayMode], case_sensitive=False),
default=GatewayMode.PERFORMANCE.value, help="Gateway mode (performance or consistent)")
@click.option("--sandbox", default=None,
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, local) "
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, tensorlake, local) "
"or a name from [sandbox.providers] in .agentenv/config.toml; comma-separated for a "
"fallback chain. Defaults to [sandbox].default (else local) when omitted.")
@click.option("--service-db", "service_db_env_id", default=None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def effective_network_policy(cls, policy: Optional[NetworkPolicy]) -> NetworkPol
"modal_vm": "agent_env.providers.sandbox_providers.modal_vm_sandbox:ModalVmSandboxProvider",
"e2b": "agent_env.providers.sandbox_providers.e2b:E2BSandboxProvider",
"sail_vm": "agent_env.providers.sandbox_providers.sail_vm.provider:SailVmSandboxProvider",
"tensorlake": "agent_env.providers.sandbox_providers.tensorlake.provider:TensorlakeSandboxProvider",
"local": "agent_env.providers.sandbox_providers.local_sandbox:LocalSandboxProvider",
}

Expand Down
147 changes: 147 additions & 0 deletions src/agent_env/providers/sandbox_providers/tensorlake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# `tensorlake` sandbox provider

Runs agent-env sandboxes on [Tensorlake](https://tensorlake.ai) sandboxes: MicroVMs booted from a
Docker-capable host image. Like `sail_vm`, `modal_vm` and `e2b`, it is a VM provider: agent-env's
docker-in-VM flows run on it unchanged. That covers the gateway's docker-compose, an agent's `docker run`,
image loading and artifact collection.

## Set up

### 1. Install the extra

The Tensorlake SDK is an optional extra:

```bash
pip install 'agentenv-framework[tensorlake]' # or: uv add 'agentenv-framework[tensorlake]'
```

Without it, selecting `tensorlake` fails with a `ConfigError` that names this extra. Nothing else needs it.

### 2. Get a Tensorlake API key

Create a key in the Tensorlake dashboard. For local work, export it:

```bash
export TENSORLAKE_API_KEY=...
```

For a shared deployment, put it in your secret store instead, for example as `tensorlake_api_key`, and
reference it with `secret:tensorlake_api_key`. Never put the key itself in `config.toml`. The provider
reads the key only from its config, never from the process environment.

### 3. Configure

A complete `.agentenv/config.toml` that runs every environment and agent on Tensorlake, with the local stores:

```toml
[sandbox]
default = "tensorlake" # environments and sandboxes
agent_default = "tensorlake" # agents

[sandbox.providers.tensorlake.config]
api_key = "env:TENSORLAKE_API_KEY" # or "secret:tensorlake_api_key"

[model]
base_url = "https://litellm.example.com"
api_key = "env:LITELLM_API_KEY"
```

To keep the local default and use Tensorlake per run instead, add only the
`[sandbox.providers.tensorlake.config]` table and pass `--sandbox tensorlake`. `agent-env config show` prints
the file in effect and masks the key.

### 4. Run something

The bundled `hello` task deploys a sandbox, loads a file into it and checks it, with no model needed:

```bash
agent-env run hello --sandbox tensorlake
```

`agent-env -v run …` also logs each sandbox as it starts
(`Tensorlake sandbox started: sandbox_id=… image=agentenv-dind-host-v1 attribution=…`).

## Configuration reference

All keys go under `[sandbox.providers.tensorlake.config]`. Unknown keys are refused.

| Key | Default | Meaning |
|---|---|---|
| `api_key` | required | The Tensorlake API key, as an `env:` or `secret:` reference. |
| `image` | `"agentenv-dind-host-v1"` | A registered Docker-capable Tensorlake image to boot from. |
| `api_url` | `"https://api.tensorlake.ai"` | The Tensorlake API. Every SDK call uses it, whatever `TENSORLAKE_API_URL` says. |

## The host image

Sandboxes boot from `agentenv-dind-host-v1` by default, a public image built from `host_image.Dockerfile`
next to this file: `tensorlake/ubuntu-systemd` with Docker Engine and the Compose v2 plugin. Tensorlake
runs `dockerd` only as the systemd `docker.service`, so the provider starts it with `systemctl` and does
not start `dockerd` by hand.

To use your own image, publish it once and set `image`:

```bash
tl sbx image create host_image.Dockerfile -n my-docker-host \
--disk_mb 30720 --builder_disk_mb 24576 --docker_compat
```

An image that is not registered fails the create with a `ValueError` that contains this command. The
provider never boots a different image. A changed Dockerfile is
published under a new version, never over the old one.

## Resources and lifetime

- **CPU and memory:** Tensorlake hosts run without KVM, so cores are slower than the request suggests.
The provider raises a request to at least 2 CPUs, 4 GiB of memory and 1 GiB of memory per CPU. More
than 8 GiB of memory per CPU is refused.
- **Disk:** at least 30 GiB, the disk the host image is published with, and at most 100 GiB.
- **Changed or ignored requests:** a raised CPU, memory or disk request logs a warning. `boot_mode` has no
Tensorlake equivalent, so a `boot_mode` that is set logs a warning and has no effect.
- **Refused requests:** a request Tensorlake can't meet is refused before anything is created.
- **Lifetime:** the sandbox's `timeout` is Tensorlake's idle timeout. After it, the sandbox terminates.

## Networking

- **Ports:** each exposed port gets a public URL, `https://<port>-<sandbox id>.sandbox.tensorlake.ai`,
which fills `tunnel_urls`. It has no platform authentication, like Modal's tunnels. A Bearer header
would hand the Tensorlake API key to every workload that calls the URL.
- **Egress:** allow-all, or an allowlist of hostnames, IPv4 addresses and IPv4 CIDRs. A policy with IPv6
entries is refused with `NetworkPolicyUnsupportedError` before anything is created, and a fallback chain
skips to its next provider. Tensorlake applies the policy outside the VM, so one policy covers `dockerd` and every
container. Tensorlake refuses a policy update with a host that does not resolve.
- **Image downloads:** before it loads images under an allowlist, the provider adds the hosts of the
signed download URLs, then waits up to 60 s for the change to reach the host firewall.
- **Reconnect:** `get_sandbox` restores the ports and the applied egress policy. A policy it can't
represent leaves `network_policy` unknown, and image loading then fails closed.

## Attribution and cost

Tensorlake sandboxes have no labels. The provider logs one `agent_env.tensorlake_sandbox_started` event per
sandbox, with `tensorlake_sandbox_id`, the full attribution (including `[sandbox.attribution]` defaults),
`cpus`, `memory_mb` and `disk_mb`. To attribute cost, join Tensorlake's per-sandbox usage on the sandbox id.

## Cleanup

- A sandbox that fails to start is deleted.
- A sandbox whose setup fails after it starts (ports, Docker) is terminated.
- If the caller is cancelled while the create request is in flight, the provider deletes the sandbox
that request makes, once it answers.
- `terminate` is idempotent: a sandbox that is already gone is not an error.

## Things to know

- **Commands run as `tl-user`:** agent-env's `sudo` prefixes stay, and file uploads land in `/tmp` first,
then a root command joins them at the target path.
- **Lost connections:** when the command stream ends without an exit status, `exec` returns exit code -1.
`exec_script` retries idempotent scripts on -1 only.
- **Uploads:** the ingress proxy refuses request bodies above about 4 MB, so uploads go through the file
API in 512 KiB parts, 8 at a time, and the joined file is checked against its sha256. An image tarball
from an object store that can't sign URLs, such as the local default, is streamed the same way.
- **The model key enters the sandbox:** an agent's `LITELLM_API_KEY` is passed in, as on other providers.

## Testing

`tst/integration/providers/sandbox_providers/tensorlake_sandbox_smoke_test.py` runs against a real
Tensorlake account when `[sandbox.providers.tensorlake.config]` resolves. Otherwise it skips with
`agentenv-capability-missing: remote_sandbox`. The unit tests are
`tst/unit/providers/sandbox_providers/tensorlake_sandbox_provider_test.py` and `tensorlake_sandbox_test.py`.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tensorlake sandbox provider; needs the ``tensorlake`` extra."""
Loading
Loading