Skip to content

refactor(providers)!: each sandbox provider declares what it runs, and nothing outside the providers asks which one it is - #143

Merged
earakely-scale merged 7 commits into
mainfrom
edgararakelyan/provider-capabilities
Oct 10, 2026
Merged

earakely-scale merged 7 commits into
mainfrom
edgararakelyan/provider-capabilities

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #137 (merged).

Code outside the sandbox providers used to choose by checking a provider's class (isinstance(LocalSandboxProvider), isinstance(ModalSandboxProvider), isinstance(ChainedSandboxProvider)) or by guessing from which methods a class overrides. That happened in more than a dozen places across preflight, the agent and server deploys, the gateway, chain handling, teardown and put_from_github. Now each provider declares what it runs, each deploy says what it runs on a provider, and everything else reads those.

What a provider declares (SandboxProvider)

The sandbox layer knows sandboxes, containers, VMs and networks, never what a caller deploys (an agent, a server, a gateway).

meaning default local modal modal_vm, e2b, sail_vm
ON_THIS_MACHINE its sandboxes run on this machine: images only this machine holds reach them, its containers need this machine's Docker no yes no no
CREATES_VMS create_vm gives a VM no yes no yes
SANDBOX_ACCEPTS / CONTAINER_ACCEPTS the images create_sandbox / create_container run NAME / NAME NAME / NAME NAME_OR_CONTEXT / NAME_OR_CONTEXT LOADABLE / NAME
PRIVATE_NETWORK create_container(private_network=True) puts containers on a network they share, each reached at its sandbox's private_host no no yes: i6pn in the configured region no
links the providers a deploy on it may run on itself; a chain, its providers
url_from_sandbox(url) how its sandboxes reach a URL this process reaches as is loopback → host.docker.internal

Accepts names the forms of image a provider takes, and Accepts.problem(image) says why one is refused:

  • LOADABLE: anything a VM can get, whether a tar.gz it loads, a registry name it pulls, or a build context it builds.
  • NAME: an image it starts by name, so not one that is only a build context.
  • NAME_OR_CONTEXT: by name, or a build context it builds first, through prepare_image.

The declarations are explicit, not inferred from which methods a class overrides. A provider that declares nothing runs images by name and creates no VM and no private network, so a provider that hasn't said what it does is refused up front rather than misdescribed.

What each deploy says it runs

  • Agent: A2AAgent.accepts(provider) is the provider's SANDBOX_ACCEPTS.
  • Lone server: EnvironmentServerProvider.accepts(provider) is its CONTAINER_ACCEPTS.
  • Gateway: EnvironmentGatewayProvider owns its rule. in_vm(provider) is true where the provider creates VMs; elsewhere the gateway runs a container per server on the provider's private network. accepts and sandbox_problem follow from that: a container gateway needs the private network and serves no websites.

Preflight passes these to one check instead of re-deriving each deploy's rules, and the deploys read the same answers, so the two can't drift.

Breaking for plugins

  • create_container gains private_network, which an override without **kwargs doesn't accept. The built-ins and agentenvhub-sdk's one override take it.
  • A third-party provider that implements create_vm must now declare CREATES_VMS = True, and SANDBOX_ACCEPTS = Accepts.LOADABLE when its sandboxes are VMs. Undeclared, it is taken for one that creates no VM: its gateways are refused, and so are context-only images for its agents.
  • Modal's create_container drops i6pn and region, which nothing passed any more: private_network is the one way to ask for i6pn, in the configured region.
  • agentenvhub-sdk's beta_scale and arp need those two declarations, and modal_gpu needs PRIVATE_NETWORK = False and an override that no longer takes i6pn. That sdk change rides the pin bump to this release, with its CUA env reading a provider's links and CREATES_VMS instead of checking its class.

Also here

  • Modal's build of a context-only image moves into modal_image_build.py, unchanged: the cache and its locks, the build, the Dockerfile rewrite and fetching the context.
  • Modal refuses a GPU container the private network before any Modal call.
  • A gateway on a provider with no VM and no private network is refused in preflight and in the deploy, before anything is created.
  • Every deploy calls prepare_image for every image, as its contract says; a provider with nothing to prepare (Modal, for an image that isn't only a build context) returns at once.
  • Two leftovers go: the container gateway's own websites refusal, which the deploy now makes first, and EnvironmentProvider._sandbox_providers, a hook nothing overrode (it reads a chain's links directly).

Tests

  • New:
    • each provider's declarations, and that it declares VMs and a private network only where it implements them;
    • the base defaults and a chain's links;
    • Accepts.problem;
    • the gateway's rule, and its refusal of a provider with no VM and no private network, in preflight and in the deploy;
    • a guard test that parses src/ and fails on any isinstance/issubclass check for a concrete sandbox or provider class outside the provider package.
  • Unit tier: 6,787 passed.
  • agentenvhub-sdk against this branch: its tests pass (1,866 with the sdk change, which adds the same no-class-checks guard test to the sdk). With the sdk's declarations, beta_scale runs gateways in a VM; without them it would be refused, which is why they ride the pin bump.
  • Live on Modal, with @local ids: the integration bundle (four agents, plus a server through Modal's container gateway) and the Slack env through the gateway. Earlier commits on this PR also ran the bundle on beta_scale,modal, a lone server, a project App build, concurrent builds and a stale image id.
  • Local integration: the gateway test failed twice on my machine on Docker, not on this change: a dropped daemon connection, then a host-port race in the local port picker, with 127 other containers running. CI's local integration jobs are the gate.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge with the acknowledged SDK rollout requirement.

Summary

Providers now declare where they run, which images they accept, and whether they offer VMs or a private network. Deploys and preflight read those declarations instead of checking provider classes.

  • Deploy code uses declared sandbox provider capabilities.
  • Gateway containers share a private network and use each server’s private address.
  • Modal’s context-image build now lives in its own module.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  P[Provider declarations] --> F[Preflight checks]
  P --> A[Agent image choice]
  P --> S[Server image choice]
  P --> G[Gateway choice]
  G --> V{Creates VMs?}
  V -->|Yes| VM[Run services in one VM]
  V -->|No| N{Has private network and no websites?}
  N -->|Yes| C[Run separate containers]
  N -->|No| R[Refuse before creating containers]
Loading

Reviews (6) · Last reviewed commit: "refactor(providers): one way to ask Moda..." · Reviewed by Greptile

@earakely-scale
earakely-scale requested a review from a team as a code owner October 9, 2026 19:21
Base automatically changed from edgararakelyan/modal-from-dockerfile to main October 9, 2026 19:22
Comment thread src/agent_env/providers/env_providers/env_gateway_provider.py Outdated
earakely-scale and others added 3 commits October 9, 2026 13:16
…build

The build cache and its locks, the build, the Dockerfile rewrite and fetching the context move out of the
provider module unchanged; the provider asks context_image for the built image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… nothing outside the providers asks which one it is

A provider declares whether its sandboxes run on this machine (ON_THIS_MACHINE), how it runs an image for an
agent, a lone server and a gateway's servers (runs: in a VM it creates, by name, or building it first), the
providers a deploy on it may run on (links), how its sandboxes reach a URL (url_from_sandbox) and the options
a gateway's containers take on it (gateway_container_options). Preflight, the agent and server deploys, the
gateway's choice between containers and a VM, chain unwrapping, teardown and put_from_github read those
instead of checking for Local, Modal or chain classes; image_problem is the one rule a deploy and preflight
share. A test fails on any class check outside the providers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ovider their declared facts

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale force-pushed the edgararakelyan/provider-capabilities branch from 12cc409 to 620cb59 Compare October 9, 2026 20:19
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

@greptileai review

…s create_sandbox and create_container, and containers share a private network

A provider now declares how it runs an image it's given through create_sandbox or create_container,
rather than for an agent, a lone server or a gateway. The gateway takes a VM on a provider that creates
VMs and a container per server on one that doesn't, and its containers ask create_container for
private_network and reach one another at their sandbox's private_host. Modal maps that to i6pn in its
configured region, so the i6pn vocabulary stays in the Modal provider.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

@greptileai review

earakely-scale and others added 2 commits October 9, 2026 14:13
…al call

The gateway now asks for private_network rather than i6pn, which an override that refuses i6pn up front no
longer sees, so the provider refuses the combination before it looks up its App or builds an image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ch deploy says what it runs there

A provider declares CREATES_VMS, SANDBOX_ACCEPTS, CONTAINER_ACCEPTS and PRIVATE_NETWORK with ON_THIS_MACHINE,
rather than having them inferred from the methods it overrides; one that declares nothing runs images by name
and creates no VM and no private network. Accepts names the forms of image it takes: LOADABLE (a VM's tar.gz,
registry name or build context), NAME, and NAME_OR_CONTEXT (built first, by prepare_image).

The agent, the lone server and the gateway each say what they accept on a provider, and preflight asks them
rather than re-deriving it. The gateway owns its rule (a VM where its provider creates VMs, else a container
per server on the provider's private network), so preflight and the deploy read one answer, and both refuse a
gateway on a provider with neither.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale earakely-scale changed the title refactor(providers): each sandbox provider declares what it runs, and nothing outside the providers asks which one it is refactor(providers)!: each sandbox provider declares what it runs, and nothing outside the providers asks which one it is Oct 9, 2026
Comment thread src/agent_env/providers/sandbox_providers/sandbox_provider.py
…d every deploy prepares every image

Modal's create_container drops i6pn and region, which nothing passed any more: private_network is the one way
to ask for i6pn, in the configured region. The agent, the lone server and the gateway call prepare_image for
every image, as its contract says; a provider that has nothing to prepare, as Modal for an image that isn't
only a build context, returns at once. The container gateway's own refusal of websites, which the deploy now
refuses first, goes, and EnvironmentProvider reads a chain's links directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit f88b313 into main Oct 10, 2026
14 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/provider-capabilities branch October 10, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant