Skip to content

feat(providers): add the tensorlake sandbox provider - #155

Open
shanshantensorlake-spec wants to merge 2 commits into
scaleapi:mainfrom
tensorlakeai:feat/tensorlake_sandbox
Open

shanshantensorlake-spec wants to merge 2 commits into
scaleapi:mainfrom
tensorlakeai:feat/tensorlake_sandbox

Conversation

@shanshantensorlake-spec

@shanshantensorlake-spec shanshantensorlake-spec commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary

Feature request issue at #154

Adds tensorlake, a built-in VM sandbox provider that runs on Tensorlake MicroVMs. You install it with the tensorlake extra. Docker-in-VM flows (gateway, agents, image loading) run on it unchanged.

  • Boots agentenv-dind-host-v1 (Docker and Compose; we published it in Tensorlake's public image registry). The Dockerfile is in the provider folder.
  • Raises CPU, memory and disk requests to Tensorlake's minimums. Refuses requests it can't meet before anything is created.
  • Egress: allow-all or an IPv4 allowlist. Before a download under an allowlist, adds the signed download host. A lock keeps concurrent downloads from dropping each other's hosts.
  • Reconnect restores ports and the egress policy. A sandbox with no public URL still reconnects, so teardown can stop it.
  • Uploads go in 512 KiB parts and are checked with sha256.

Setup and config: src/agent_env/providers/sandbox_providers/tensorlake/README.md.

Testing

  • Unit: tensorlake_sandbox_provider_test.py, tensorlake_sandbox_test.py.
  • Integration: tensorlake_sandbox_smoke_test.py runs with a Tensorlake key. Without one, it skips with remote_sandbox.
  • agent-env run hello --sandbox tensorlake

RetriggerView in GreptileConfidence Score: 4/5

The behavioral fixes appear sound, but the remaining requirement about where operating limits are stored must be satisfied before merging.

Fix All in CursorFindings

  1. P2 Operating limits lack names ▶
Fix with agent prompt
### Issue 1
src/agent_env/providers/sandbox_providers/tensorlake/sandbox.py:undefined-329
The new provider embeds operating limits directly in command text, including the five-second `curl` timeout. The repository requires magic numbers to be stored as class or instance variables with descriptive names.

Extract this timeout, the upload retry count, and the builder disk size into named settings so their purpose and units are clear. This repository requirement must be satisfied before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds the optional tensorlake VM sandbox provider for the existing Docker-based deployment flows.

  • Tensorlake VMs run existing agent and gateway Docker workflows.
  • Tensorlake sandboxes keep restricted downloads inside their allowlist.
  • Tensorlake uploads files in checked, concurrent parts.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Configured Tensorlake provider] --> B[Create MicroVM]
  B --> C[Expose requested ports]
  C --> D[Start Docker and check Compose]
  D --> E[Existing gateway and agent flows]
  E --> F[Reconnect or terminate]
Loading

Reviews (2) · Last reviewed commit: "fix(tensorlake): address review comments..." · Reviewed by Greptile

@shanshantensorlake-spec
shanshantensorlake-spec requested a review from a team as a code owner October 10, 2026 01:54
@socket-security

socket-security Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​tensorlake@​0.5.14898100100100100

View full report

Comment thread src/agent_env/providers/sandbox_providers/tensorlake/sandbox.py
Comment thread src/agent_env/providers/sandbox_providers/tensorlake/provider.py
Comment thread src/agent_env/providers/sandbox_providers/tensorlake/sandbox.py Outdated
Comment thread tst/unit/test_optional_extras_stay_optional.py
Comment thread THIRD_PARTY_NOTICES.md Outdated
Comment thread src/agent_env/providers/sandbox_providers/tensorlake/sandbox.py Outdated
- Read the applied egress policy inside the per-sandbox lock, so two
  wrappers of one sandbox no longer drop each other's download host.
- On cancellation, wait for a pending create and delete its sandbox
  before the cancellation goes on, so asyncio.run cannot leak it.
- Bound the egress wait by elapsed time, and each probe by the time left.
- Check that core imports with every optional-only package blocked,
  including packages that two extras share.
- Name the probe timeout, join retries and builder disk size.
- List tensorlake 0.5.148 in THIRD_PARTY_NOTICES.md, as uv.lock resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator

🤖 Automated Claude routine — Env Pod PR Context Review. This is a bot, not Edgar Arakelyan; not a human review.

Context from outside this diff, not a defect in it: #148 (Vercel sandbox provider, open) and the draft #139 (Freestyle VM provider) each add a provider too, and all three edit the same shared files. None of the three references the others, so the overlap isn't visible from any one branch.

Checked at this PR's head (9a4cf9c) against #148's (53090f5):

  • tst/util/capabilities.py:86 and :88 — both PRs rewrite these exact two lines. Here they become ("modal", "modal_vm", "e2b", "sail_vm", "tensorlake") and ("e2b", "sail_vm", "tensorlake"); on feat(sandbox): add Vercel Sandbox provider #148, the same two lines with "vercel". feat(sandbox): add Freestyle VM provider #139 also touches this file.
  • src/agent_env/providers/sandbox_providers/sandbox_provider.py — both insert a _BUILTIN_SANDBOX_PROVIDERS entry immediately after the sail_vm line (:255 here).
  • tst/unit/test_capabilities.py — the one worth coordinating. This PR removes _fake_e2b_builder and test_sail_availability_follows_the_provider_build, replacing them with _fake_builder plus _CONFIGURED_PROVIDERS = pytest.mark.parametrize("provider", ["e2b", "sail_vm", "tensorlake"]). feat(sandbox): add Vercel Sandbox provider #148 appends its three test_vercel_* cases directly after the body of the test_sail_availability_... this PR deletes (its :174 → :192).

The reason that last one is more than a textual conflict: the obvious "keep both sides" resolution either resurrects the helpers this PR deleted, or merges cleanly while leaving the later provider out of _CONFIGURED_PROVIDERS, so its three availability cases silently stop running. And the merge isn't mechanical either way — #148 doesn't just add "vercel" to the tuples, it inserts a vercel-only branch inside remote_sandbox_is_available (an early if provider != "vercel": return True, then a config/VERCEL_*-env credential check), because vercel's availability doesn't follow the plain build-outcome shape the parametrized tests here assert. So vercel can't simply join the parametrize list.

Nothing to change in this PR. Just flagging it so whichever lands second doesn't lose a provider's coverage in the merge.


Generated by Claude Code

@earakely-scale

Copy link
Copy Markdown
Collaborator

Thanks for asking, and sorry for the wait. GitHub now shows the PR as conflicting with main, which moved under it: #159 and #164 changed how a sandbox provider declares what it runs, and #169 changed how a VM loads images. I merged your branch onto current main (d24ed8f) locally; with the steps below the unit suite is 7573 passed / 15 skipped, uv lock --check is clean and the plugin-API check reports no break. Two things to do, then one bug.

1. Merge main. Four conflicts:

  • AGENTS.md: take main's providers row (it gained a clause) and add `tensorlake` (the `tensorlake` extra) after `sail_vm` (the `sail` extra).
  • pyproject.toml: in the dev extra keep both your agentenv-framework[aws,explorer,gcp,sail,tensorlake] line and main's new fastmcp line.
  • tst/unit/providers/sandbox_providers/provider_capabilities_test.py: main rewrote it. Take main's file and re-add your rows, which now also pin the sandbox's platform:
    +from agent_env.providers.sandbox_providers.tensorlake.provider import TensorlakeSandboxProvider
    +from agent_env.providers.sandbox_providers.tensorlake.sandbox import TensorlakeSandbox
    -                    SailVmSandboxProvider, ChainedSandboxProvider]
    +                    SailVmSandboxProvider, TensorlakeSandboxProvider, ChainedSandboxProvider]
    -], ids=["local", "modal", "modal_vm", "e2b", "sail_vm"])
    +    (TensorlakeSandboxProvider, False, True, LOADABLE, NAME, False),
    +], ids=["local", "modal", "modal_vm", "e2b", "sail_vm", "tensorlake"])
    -@pytest.mark.parametrize("sandbox", [Sandbox, ModalSandbox, ModalVmSandbox, E2BSandbox, SailVmSandbox])
    +@pytest.mark.parametrize("sandbox", [Sandbox, ModalSandbox, ModalVmSandbox, E2BSandbox, SailVmSandbox, TensorlakeSandbox])
  • uv.lock: take main's and run uv lock (it picks up tensorlake 0.5.149).

2. After the merge. CREATES_VMS no longer exists, and SANDBOX_ACCEPTS is now derived for a provider that implements create_vm, so both declarations go. And VmSandbox.load_docker_images now asks each image load_problem(on_this_machine=..., platform=...) and rebuilt_for(platform); without them four of the load tests fail with an AttributeError on the SimpleNamespace stand-in:

diff --git a/src/agent_env/providers/sandbox_providers/tensorlake/provider.py b/src/agent_env/providers/sandbox_providers/tensorlake/provider.py
index 6b72a08..d23f94a 100644
--- a/src/agent_env/providers/sandbox_providers/tensorlake/provider.py
+++ b/src/agent_env/providers/sandbox_providers/tensorlake/provider.py
@@ -20,7 +20,6 @@ from agent_env.attribution import PIPELINE_STEP_KEY, RUN_ID_KEY, Attribution
 from agent_env.config.errors import ConfigError
 from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy
 from agent_env.providers.sandbox_providers.sandbox_provider import (
-    Accepts,
     SandboxProvider,
     apply_default_attribution,
     refuse_unenforceable_policy,
@@ -63,9 +62,6 @@ class TensorlakeSandboxProvider(SandboxProvider):
     ``api_url`` defaults to the public Tensorlake API.
     """
 
-    CREATES_VMS = True
-    SANDBOX_ACCEPTS = Accepts.LOADABLE  # a sandbox is a VM, which loads the image; a container pulls it by name
-
     # Exposed ports are served at ``https://<port>-<sandbox-id>.sandbox.tensorlake.ai``.
     EGRESS_HOSTS: ClassVar[tuple[str, ...]] = ("*.sandbox.tensorlake.ai",)
 
diff --git a/tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py b/tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py
index a3c8cce..b78d859 100644
--- a/tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py
+++ b/tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py
@@ -23,7 +23,8 @@ from agent_env.store import set_object_store
 
 def _artifact(url: str | None = "s3://bucket/image.tar.gz", *, context_only: bool = False) -> SimpleNamespace:
     return SimpleNamespace(
-        tar_gz_object_url=url, context_only=context_only, image_name="img:latest", load_problem=lambda: None
+        tar_gz_object_url=url, context_only=context_only, image_name="img:latest", load_problem=lambda **_: None,
+        rebuilt_for=lambda *_, **__: False
     )
 
 

3. The download-host race isn't fixed yet. _allow_download_hosts reads await self._sandbox.info() under the lock to learn the policy another wrapper may have widened, but in the SDK (0.5.148 and 0.5.149 alike) AsyncSandbox._fetch_info returns _cached_info whenever it is set, and it is set by connect(), ready() and update() and never cleared. So info() is the handle's own snapshot, not a live read, and two wrappers of one sandbox (the deploy's and the one get_sandbox gives install_cli) still overwrite each other's allowed hosts. The unit test passes because its fake info is live. A fix that matches the other providers: have the provider hand TensorlakeSandbox a live-read callable, for example

async def _live_info(self, sandbox_id: str) -> SandboxInfo:
    async with self._client_cls.for_cloud(**self._client_kwargs()) as client:
        return (await client.get(sandbox_id)).value

passed in from create_vm and get_sandbox, and call that under the lock instead of self._sandbox.info(). For the test, give the fake sandbox an info() that returns the snapshot it was constructed with, so two wrappers of it reproduce the race.

Once you push, the fork CI needs a maintainer to approve the run again; ping here and we'll do it right away.

This branch has not been deployed

No deployments
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.

2 participants