Skip to content

fix(preflight)!: check images the way deploys do; a sandbox carries its platform, and SUPPORTED_SANDBOX_MODES replaces CREATES_VMS - #159

Open
earakely-scale wants to merge 11 commits into
mainfrom
preflight-reachability
Open

earakely-scale wants to merge 11 commits into
mainfrom
preflight-reachability

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

Preflight and deploys now make one image check, so preflight refuses only what a deploy would, in the deploy's words.

  • One check. Accepts.problem(image, on_this_machine=…) adds reachability: a sandbox elsewhere can't pull from this machine's registry (pull_problem). Preflight, the VM loader, agent deploys, lone servers, gateways (every image, servicedb and sidecars included, on the VM path and in containers) and container-mode deploy_sandbox all call it, each before creating anything.
  • No over-refusal. A tar.gz or build context that only this machine's object store holds is no longer refused on a remote provider: the VM loads it once agent-env pushes it. A local-registry name is refused only where a sandbox would pull it.
  • Platforms. Each sandbox carries platform, the platform of the images it runs, as it carries mode: linux/amd64 (what the CLI's --platform default and the in-VM build already assume of remote sandboxes) unless its provider sets another when it creates it, and None, any platform unchecked, for a local sandbox, whose Docker runs this machine's platform and emulates others. A VM refuses an image built for another platform as it loads it, before starting anything, saying to rebuild it for the VM's platform. Providers declare no platforms, so nothing checks one before a sandbox exists, the dry run included; a chain that gets a VM of another platform closes it and tries the next provider, at the cost of that VM. A platform compares equal however it's spelled (normalize_platform: os/arch by Docker's names, no variant but 32-bit arm's). Without this, lifting the local-store refusal let linux/arm64 images built on Apple silicon, the infra bootstrap's among them, reach amd64 VMs whose containers never started.
  • Sandbox modes (breaking). SUPPORTED_SANDBOX_MODES replaces CREATES_VMS, derived for each provider class from what it implements: a container when it implements create_sandbox, which is no longer abstract and, like create_vm, raises unless implemented, and a VM when it implements create_vm. A provider plugin that implements neither is refused at registration. The gateway's choice of a VM and the dry run's check of a deploy_sandbox mode read it. CREATES_VMS is removed: read SANDBOX_MODE_VM in SUPPORTED_SANDBOX_MODES, and a plugin that reads it has to switch before it pins this release.
  • Chains. A deploy on a comma-separated chain skips each provider that can't run its image (one elsewhere that would pull from this machine's registry, or one that runs an image by name when it's only a build context), with a warning, and is refused only when every provider refuses it, naming each refusal. Env deploys already fell through provider by provider; agent and container deploy_sandbox deploys now do too, and preflight matches: a deploy is refused only when no provider in its chain takes it, its gateway's sandboxes and infra included, and one whose chain skips the local provider needs no Docker here.
  • Agents placed on a sandbox. One on a VM sandbox is checked the way that VM loads its image, so a context-only agent runs on a local VM sandbox. deploy_agent's checks on the linked sandbox (VM mode, A2A port exposed) are shared with preflight.
  • install_agent refuses an agent with no build context at save as well as at deploy.
  • Builds in a VM each get a work folder of their own; one named after the context alone let two builds of it on one host delete each other's.

Hot path (the VM loader and the deploy paths), so it takes bump-version. Only refusals change, and where a chain deploys: a loopback-registry image never reaches a hosted deploy, an image a VM can't get is now refused before the VM is created instead of after, an image built for another platform is refused as the VM loads it instead of never starting, and a chain skips a provider that can't run an image instead of failing on it.

Live checks (local stores, the echo agent)

check this branch main
agent whose tar.gz only the local store holds, --sandbox modal_vm preflight passes; deployed in 16 s, card served refused at preflight
the same agent, --sandbox modal refused, naming --sandbox local or a VM provider refused
no tar.gz, named in the local registry, --sandbox modal_vm refused at preflight and at deploy (0.00 s, no VM), same words refused at preflight
agent built for linux/arm64 (as recorded), tar.gz in the local store, --sandbox modal_vm preflight passes; the VM refuses it as it loads it, 2 s after the deploy starts, saying to rebuild it for linux/amd64; before the platform check its VM never started (928 s) refused at preflight
the same agent, --sandbox modal_vm,local Modal's VM closed as linux/amd64, deployed on the local provider, 4 s in all refused at preflight
context-only agent placed on two local VM sandboxes preflight passes; built in each sandbox, both cards served (8 s) refused at preflight

Tests

Unit tier: 6,959 passed, 16 skipped. New: a parity test (preflight's refusal contains the deploy's own, for the same image on the real modal and modal_vm providers); Accepts.problem on and off this machine; the VM loader; agent, lone-server, gateway-container and deploy_sandbox refusals; linked sandboxes; install_agent at save; distinct build folders; platforms (a VM refusing another platform's image as it loads it, a sandbox's default and a local one's, spellings); sandbox modes derived from create_sandbox and create_vm, declared, and a plugin implementing neither refused; a chain closing a VM of another platform and trying the next provider; chains skipping a provider that refuses, at deploy and in preflight, and refused naming each when all do. Integration (run_bundle_test, agents on local sandboxes, verify-sandbox, container files): 17 passed, 1 skipped.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new blocking issue was found.

Summary

This PR shares image checks between preflight and deploys, checks platforms when a VM exists, and skips providers that cannot run an image.

  • Preflight and deploys skip providers that cannot run an image.
  • VMs check that each image matches the platform they run.
  • Each VM image build gets its own work folder.
  • Providers declare whether they can create containers, VMs, or both.
  • Agents placed on a task sandbox use the VM's image checks.
  • install_agent checks for a build context when a task is saved.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Deploy image] --> B[Filter providers by image and network policy]
  B --> C[Try next eligible provider]
  C --> D{Creation succeeds?}
  D -->|No| C
  D -->|Yes| E{VM can load image?}
  E -->|No| F[Close VM]
  F --> C
  E -->|Yes| G[Load image and start agent]
Loading

Reviews (10) · Last reviewed commit: "refactor(sandbox): one load check, one w..." · Reviewed by Greptile

… machine holds reach remote VMs

- Accepts.problem takes on_this_machine: a sandbox elsewhere can't pull from this machine's registry
  (pull_problem). Preflight, the VM loader, agent deploys, lone servers, gateway containers and container-mode
  deploy_sandbox all make that one check, so preflight refuses what a deploy would, in its words.
- Preflight no longer refuses a tar.gz or build context that only this machine's object store holds: the VM loads
  it once agent-env pushes it.
- An agent placed on a VM sandbox is checked the way that VM loads its image, and deploy_agent's checks on the
  sandbox it links (VM mode, the A2A port exposed) are shared with preflight.
- install_agent refuses an agent with no build context at save too.
- Each build in a VM gets a work folder of its own, so two builds of one context on one host can't delete each
  other's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/agent_env/providers/env_providers/env_gateway_provider.py Outdated
earakely-scale and others added 6 commits October 10, 2026 16:15
The VM path now assembles its images and checks them before the VM is created, and the container path also
checks the servicedb and sidecar images the state provider picks (LocalPostgresStateProvider.container_images).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hub runs Task.preflight on every task save, and a run's a2a_agent_id override can name another agent, so a
task saved naming a placeholder must still save. Preflight now refuses only an agent that exists with no build
context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… for another is refused before anything is created

A sandbox provider and its sandboxes declare PLATFORMS beside ON_THIS_MACHINE. Accepts.problem(image, on=...) reads
both from the provider or sandbox it's asked about: an image recording a platform the sandbox doesn't declare is
refused, at preflight and in every deploy path, before anything is created. Undeclared (None) is never checked: the
local provider declares none, since its Docker runs this machine's platform and emulates others where it's set up,
nor does a plugin's that hasn't opted in. Modal, Modal VM, E2B and Sail declare linux/amd64 (Modal's containers and
VMs checked on live sandboxes, Sail by its amd64 devbox, E2B by its x86-64 Firecracker VMs). A context-only image on a
provider that builds it, as Modal does, isn't checked, since that provider builds it for a platform of its own.

Lifting the refusal of tar.gzs only this machine's store holds had let linux/arm64 images built on Apple silicon, the
infra bootstrap's among them, reach linux/amd64 VMs, whose containers then never started.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rwise; the local provider runs any

PLATFORMS defaults to linux/amd64, which the CLI's --platform default and the in-VM build's fallback already assume
of remote sandboxes, so the remote providers' own declarations go and a plugin's or the sdk's provider is checked
without declaring. The local provider and its sandboxes declare None: they run any platform, unchecked, since this
machine's Docker runs its own and emulates others where it's set up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…refuses only when none can

An agent or container-sandbox deploy on a chain now drops each provider that refuses
its image, with a warning, and deploys on the rest; it's refused, naming each
provider's refusal, only when every provider refuses. Env deploys already fell
through provider by provider. The dry run matches: a deploy is refused only when
every provider in its chain refuses it, so a gateway's infra and its sandboxes are
checked per provider too, and a chain that falls back to the local provider isn't
refused for infra elsewhere.

The platform refusal says to rebuild the image for the sandbox's platform, and that a
Linux host of another platform can only do that with QEMU emulation set up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
normalize_platform names a platform as docker build --platform does: lower case, the
architecture by Docker's name (x86_64 is amd64, aarch64 arm64), and no variant but
32-bit arm's. A put records the platform that way, and the platform check compares
both the image's and the sandbox's that way, so a linux/amd64/v2 image runs on a
linux/amd64 sandbox and a plugin declaring linux/x86_64 runs linux/amd64 images.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/agent_env/bundle/preflight.py
earakely-scale and others added 2 commits October 11, 2026 04:17
… or local infra

A deploy whose chain skips the local provider, because it can't run the deploy's
image, never runs there, so preflight no longer counts it among the deploys that need
this machine's Docker, nor builds infra for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ox modes are derived

Providers no longer declare PLATFORMS. Each sandbox carries `platform`, the platform of
the images it runs, as it carries `mode`: linux/amd64 unless its provider sets another
when it creates it, and None, any platform unchecked, for a local sandbox. A VM refuses
an image built for another platform as it loads it, before starting anything. Nothing
checks a platform before a sandbox exists, so the dry run doesn't, and a chain doesn't
route by platform.

SUPPORTED_SANDBOX_MODES replaces CREATES_VMS, derived for each provider class: a VM when
it implements create_vm, and a container always; a provider whose create_sandbox can't
make a container declares its own. The gateway's choice of a VM and the dry run's
sandbox-mode check read it. CREATES_VMS stays, derived from it, for plugins that still
read it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/agent_env/providers/sandbox_providers/sandbox_provider.py
…, and CREATES_VMS is gone

SUPPORTED_SANDBOX_MODES no longer assumes containers: a provider supports
SANDBOX_MODE_CONTAINER when it implements create_sandbox and SANDBOX_MODE_VM when it
implements create_vm. create_sandbox is no longer abstract; like create_vm it raises
NotImplementedError unless implemented, and a provider plugin that implements neither is
refused at registration. CREATES_VMS is removed; read SANDBOX_MODE_VM in
SUPPORTED_SANDBOX_MODES instead.

A chain that gets a VM sandbox of another platform than the image it deploys closes it
and tries the next provider, so `--sandbox modal_vm,local` runs a linux/arm64 agent on the
local provider, at the cost of the VM it closed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale earakely-scale changed the title fix(preflight): check images the way deploys do, so tar.gzs only this machine holds reach remote VMs fix(preflight)!: check images the way deploys do; a sandbox carries its platform, and SUPPORTED_SANDBOX_MODES replaces CREATES_VMS Oct 11, 2026
…ainer image checks

- Accepts.problem takes a required on_this_machine instead of a provider or sandbox
  it read one flag from, so no caller skips the reachability check by leaving it out.
- VmSandbox.load_problems is the check load_docker_images refuses on, and the chain
  asks it of a VM it created, instead of repeating the platform half of it.
- skip_refusing raises the refusal itself, and it and the network-policy filter share
  one way to drop providers: the first, preferred, skipped with a warning, later ones
  noted.
- SUPPORTED_SANDBOX_MODES is always derived from what a provider implements.

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

Copy link
Copy Markdown
Collaborator Author

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

Offering cross-PR context only — nothing on the diff itself. Two open provider PRs sit on the other side of the CREATES_VMS → SUPPORTED_SANDBOX_MODES swap in src/agent_env/providers/sandbox_providers/sandbox_provider.py (the declaration block that now derives modes in __init_subclass__), and neither can see this branch:

  • feat(providers): add the tensorlake sandbox provider #155 (tensorlake) declares the attribute this PR removes: src/agent_env/providers/sandbox_providers/tensorlake/provider.py:66 CREATES_VMS = True, and it adds a TensorlakeSandboxProvider row to tst/unit/providers/sandbox_providers/provider_capabilities_test.py against the old assertions (cls.CREATES_VMS at :40 and :46) — the same file this PR rewrites to read SANDBOX_MODE_VM in cls.SUPPORTED_SANDBOX_MODES. Whichever lands second needs that row and the class attribute rewritten; the provider implements both create_sandbox and create_vm, so the derived set comes out right once the explicit declaration is dropped.
  • feat(sandbox): add Vercel Sandbox provider #148 (vercel) is the opposite shape and arguably gets fixed here for free: src/agent_env/providers/sandbox_providers/vercel/provider.py implements create_vm at :259 but declares no CREATES_VMS, so on main today it inherits the False default at sandbox_provider.py:175 and env_gateway_provider.py:131 would never pick a VM from it. Deriving the modes makes that declaration unnecessary rather than merely optional — probably worth saying so on that PR so nobody adds CREATES_VMS = True to it in the meantime.

Both are from outside contributors, so the heads-up likely has to come from here.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant