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
10 changes: 10 additions & 0 deletions .agentenv/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ secret = "local" # process env vars, or an optional flat YAML file
default = "local" # envs run as containers on the host Docker daemon
agent_default = "local" # agents too

# Optional Vercel Sandboxes (install the 'vercel' extra):
# [sandbox.providers.vercel.config]
# image = "vercel/sandbox/universal"
# region = "iad1"
# failover_regions = ["cle1", "sfo1"]
# network_id = "secure-compute-network-id"
# token = "secret:vercel_token" # with team_id and project_id; omit all three for SDK OIDC
# team_id = "your-team"
# project_id = "your-project"

[agents]
default_a2a_agent_id = "a2a-default" # what a deploy_agent step that names no agent deploys

Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,44 @@ A plain install runs on local stores and needs no cloud SDK. The cloud store bac

The distribution is named `agentenv-framework`, the import package is `agent_env` and the command is `agent-env`. It depends on `agentenv-framework-protocol`, whose import package is `agentenv_protocol`, and installs it too.

### Sandbox providers

For a complete evaluation using your existing Vercel login, follow the [Vercel Sandbox demo](https://github.com/vercel-labs/agentenv-framework/tree/vercel-demo/examples/vercel-sandbox).

The default provider runs environments and agents as containers on the local Docker daemon. The `vercel` provider runs each environment or agent in an ephemeral, Docker-capable Vercel Sandbox and exposes declared ports through public `*.vercel.run` routes. From this source checkout, install the optional extra and select it in `.agentenv/config.toml`. This provider is not yet in a published PyPI release:

```bash
uv sync --frozen --extra vercel
```

```toml
[sandbox]
default = "vercel"
agent_default = "vercel"

[sandbox.providers.vercel.config]
# Optional. The default managed image installs Docker and Compose v2 when apt is available;
# a prepared image avoids that download under a restrictive network policy.
image = "vercel/sandbox/universal"
# region = "iad1"
# failover_regions = ["cle1", "sfo1"]
# network_id = "your-secure-compute-network"
# Omit all three credential fields to use the SDK's default OIDC credentials.
# token = "secret:vercel_token"
# team_id = "your-team"
# project_id = "your-project"
```

Credentials are resolved by AgentEnv's `env:` and `secret:` references before the provider builds its scoped Vercel SDK client; the provider never changes the process environment or makes its SDK session the active one. Each sandbox is ephemeral, its execution time follows AgentEnv's timeout, and teardown destroys the sandbox and its orphan snapshots. Vercel currently provisions 64 GB of ephemeral disk: requests above 64 GB are rejected rather than silently ignored. Resource sizing uses the smallest supported even vCPU count (or one vCPU) that covers both the requested CPU and memory (2048 MB per vCPU), up to 32 vCPUs and 64 GB at the provider-wide ceiling; the account's plan may impose lower limits.

See the [Vercel resource limits](https://vercel.com/docs/sandbox/pricing#resource-limits) and [firewall policy](https://vercel.com/docs/sandbox/concepts/firewall) documentation. The provider treats CPU and memory requests as minimums. If a request does not already match a supported shape, AgentEnv logs both the requested and allocated CPU and memory before provisioning. For example, `cpu = 0.5` and `memory = 5000` allocate four vCPUs and 8192 MB. A disk request below 64 GB is also ignored: every sandbox receives the fixed 64 GB disk, and the provider logs the ignored request. Configure all three of `token`, `team_id`, and `project_id` for an explicit scoped token, or omit all three to continue using the SDK's OIDC credentials.

Attribution is limited to five tags. When more are supplied, the provider keeps `pipeline_step` and `run_id` first, then fills the remaining slots by key name in alphabetical order. It warns with the omitted key names without logging their values. Invalid empty keys or values are still rejected.

A missing sandbox raises `SandboxNotFoundError` when reconnecting. Run cleanup and the `teardown_sandboxes` step treat this condition as already cleaned up; authentication and transport errors still fail cleanup.

Outbound policy supports AgentEnv's allow-all and allowlist forms, including hostname and CIDR entries. Vercel's custom transforms, forwarding rules, and denied CIDR exceptions are not reconstructed as ordinary allowlists: reconnect leaves such a policy unknown and image loading fails closed. At creation, public Vercel routes are added to a restricted workload's platform egress floor so it can reach another Vercel sandbox's declared route. Image and file downloads add their exact signed-URL hostnames to the current allowlist; these hosts remain allowed until the policy is replaced. A CIDR-only Vercel policy allows unrestricted DNS resolution; use domain rules to restrict DNS, or deny-all to block it.

To work on agent-env itself, install from a clone with [uv](https://docs.astral.sh/uv/):

```bash
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ gcp = [
sail = [
"sail~=0.12.8", # Sailbox exec/fs/listener/egress APIs validated against the 0.12 line
]
vercel = [
"anyio>=4,<5",
"httpx2>=2.13.1,<3",
"vercel-sandbox>=0.7.0,<0.8", # provides the vercel.sandbox namespace without the full vercel bundle
]
# 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
Expand All @@ -81,7 +86,7 @@ tenki = [
"tenki[async]~=1.4.1", # AsyncClient/AsyncSandbox exec, stdin, ports and egress APIs validated against the 1.4 line
]
dev = [
"agentenv-framework[aws,explorer,gcp,sail,tensorlake,tenki]", # exercise portable optional providers and stores in CI
"agentenv-framework[aws,explorer,gcp,sail,tensorlake,tenki,vercel]", # exercise portable optional providers and stores 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
11 changes: 10 additions & 1 deletion src/agent_env/providers/sandbox_providers/sandbox_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class SandboxProviderTypeError(ConfigError):
"""A config-registered provider produced a Sandbox whose ``.type`` != its ``[sandbox.providers.<name>]`` key."""


class SandboxNotFoundError(LookupError):
"""The requested sandbox no longer exists; cleanup may treat it as already done."""


class Accepts(Enum):
"""The images a sandbox provider can run, by the form the store keeps an image in."""

Expand Down Expand Up @@ -165,7 +169,11 @@ async def get_sandbox(self, sandbox_id: str) -> Sandbox:
async def terminate_sandbox(self, sandbox_id: str) -> None:
"""Terminate a sandbox this provider made, by id; one that has already finished or is gone is terminated.
A backend that can terminate without a live handle overrides this, so teardown doesn't need one."""
await (await self.get_sandbox(sandbox_id)).terminate()
try:
sandbox = await self.get_sandbox(sandbox_id)
except SandboxNotFoundError:
return
await sandbox.terminate()

async def close(self) -> None:
pass
Expand Down Expand Up @@ -309,6 +317,7 @@ def effective_network_policy(cls, policy: Optional[NetworkPolicy]) -> NetworkPol
"sail_vm": "agent_env.providers.sandbox_providers.sail_vm.provider:SailVmSandboxProvider",
"smol_vm": "agent_env.providers.sandbox_providers.smol_vm.provider:SmolVmSandboxProvider",
"tensorlake": "agent_env.providers.sandbox_providers.tensorlake.provider:TensorlakeSandboxProvider",
"vercel": "agent_env.providers.sandbox_providers.vercel.provider:VercelSandboxProvider",
"tenki": "agent_env.providers.sandbox_providers.tenki.provider:TenkiSandboxProvider",
"local": "agent_env.providers.sandbox_providers.local_sandbox:LocalSandboxProvider",
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Vercel Sandbox provider."""

from agent_env.providers.sandbox_providers.vercel.provider import VercelSandboxProvider
from agent_env.providers.sandbox_providers.vercel.sandbox import VercelSandbox

__all__ = ["VercelSandbox", "VercelSandboxProvider"]
Loading
Loading