Skip to content

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

Merged
earakely-scale merged 3 commits into
scaleapi:mainfrom
tensorlakeai:feat/tensorlake_sandbox
Oct 13, 2026
Merged

earakely-scale merged 3 commits into
scaleapi:mainfrom
tensorlakeai:feat/tensorlake_sandbox

Conversation

@shanshantensorlake-spec

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

Copy link
Copy Markdown
Contributor

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: 5/5

No new blocking issue was established.

Summary

Adds tensorlake as an optional VM sandbox provider.

  • Tensorlake can run agent-env’s Docker-based sandboxes.
  • Restricted Tensorlake sandboxes can download files and verify large uploads.

The ancestry checks did not pass, so the full PR diff was reviewed as changes since the last review. All supplied previous threads are unnumbered; none require a previousFindings entry.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Select tensorlake] --> B[Resolve provider config]
  B --> C[Create MicroVM]
  C --> D[Expose ports and start Docker]
  D --> E[Load images and run workloads]
  E --> F[Reconnect by sandbox ID]
  F --> G[Restore ports and network policy]
  G --> H[Terminate sandbox]
  C -->|Startup fails| H
  D -->|Setup fails| H
Loading

Reviews (3) · Last reviewed commit: "fix(tensorlake): read the sandbox's poli..." · 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.14998100100100100

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

- 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>
…n's provider declarations

AsyncSandbox.info() returns the handle's cached copy, so two wrappers of one
sandbox (the deploy's and the one get_sandbox gives install_cli) overwrote each
other's download hosts. The provider now gives TensorlakeSandbox a reader that
asks the API, and _allow_download_hosts reads the applied policy through it.

CREATES_VMS is gone on main and SANDBOX_ACCEPTS is derived from create_vm, so
both declarations go. The load tests' image stand-in gains the load_problem
keywords and rebuilt_for that VmSandbox.load_docker_images now calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shanshantensorlake-spec

Copy link
Copy Markdown
Contributor Author

Hi @earakely-scale , thanks for the review. I've rebased this PR and fixed item 2 and 3 you mentioned above. Could u please take another look?

@earakely-scale
earakely-scale merged commit a0478ef into scaleapi:main Oct 13, 2026
11 of 13 checks passed
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