Repository navigation
feat(sandbox): add a Sail Sailbox VM sandbox provider - #83
Merged
Merged
Conversation
Adds `sail`, a built-in VM sandbox provider for Sail Research Sailboxes. A Sailbox boots from Sail's devbox image (Docker and Compose v2, root), so the gateway, agent, deploy_sandbox and image-build paths run on it unchanged through VmSandbox. - Config: [sandbox.providers.sail.config] api_key (secret:/env: ref, required), app, min_size, auto_sleep (off by default), auto_sleep_min_idle_seconds, runtime_threads. - Resources map to the smallest Sail size covering the CPU; memory and disk are ceilings rounded up into the size's range. - Ports are public *.sail.box listeners; reconnect restores them and the applied egress policy (fail closed when unrepresentable). - Egress allowlists of hostnames and IPv4 CIDRs, up to Sail's 128 entries; signed download hosts are added under a per-Sailbox lock. - Exec streams output with bounded backpressure; a lost host maps to exit -1 so exec_script retries. - A create cancelled by its caller terminates the Sailbox it yields. - Attribution goes in the Sailbox name and an agent_env.sail_sandbox_started log event keyed by sailbox_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- create_container terminates the VM when cancelled during login, pull or run (the cleanup caught Exception, not CancelledError), so a cancelled deploy no longer leaves a billed VM behind. - A Sail output stream cut by a lost host or transport ends instead of raising from read(), so wait() reports exit -1 and exec_script retries. - SAIL_RUNTIME_THREADS is restored after the SDK builds its client, like SAIL_API_KEY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- read_trajectory retries a read that exited -1 (lost exec transport, possibly partial output) and raises after three attempts instead of handing a partial tool-call history to the agent checks. Other exit codes keep their behaviour, so a gateway with no history is still []. - SailSandbox.exec_with_output returns no stdout for a command whose output stream was cut, so a caller that skips the exit code can't mistake partial output for the whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sail injects the key into the agent's model requests as they leave the Sailbox, so neither the VM disk (which Sail checkpoints) nor its memory ever holds it. On by default; [sandbox.providers.sail.config] inject_model_key = false passes the key in as before. - create_sandbox stores the env's LITELLM_API_KEY as a Sail secret named from its SHA-256 (one key, one secret; keys never share one) and creates the Sailbox with a saved egress policy whose rules set it as the authorization and x-api-key headers for LITELLM_BASE_URL's host, which a restricted allowlist also admits. An http endpoint is refused. - Every command and file the SailSandbox sends is scrubbed of the key, so docker run/exec -e carry a placeholder. - A docker shim on the Sailbox gives every container it runs or creates the VM's CA bundle, which holds the CA Sail terminates those requests' TLS with (SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS, CURL_CA_BUNDLE). - Widening the allowlist replaces the saved policy, keeping its rules. - Terminate deletes the policy, then the secret unless another Sailbox's policy still names it; a failed or cancelled create cleans up too. - Reconnect restores the injection from the applied policy, and scrubs the configured [model] key when it is the injected one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A handle reconnected in the process that injected a key recovers that key (kept in process memory, never persisted), so it keeps scrubbing an agent's own key even when it isn't the configured [model] key. - A create that fails, or is cancelled before the Sailbox create starts, deletes the model-key secret along with its policy; one abandoned in flight releases both once the create settles, if it fails. - A policy swap that fails or is cancelled deletes the replacement policy and keeps tracking the one still applied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Refuse rather than send: a Sailbox (with inject_model_key on) refuses any command or file write assigning LITELLM_API_KEY/ANTHROPIC_API_KEY a value other than the placeholder, so a key Sail doesn't inject (an agent deployed into an existing sandbox, or one a reconnected handle can't recover) never reaches its disk. Shell scripts are read as the shell reads them; other arguments and file bytes are literal. - Scrub the key in every form agent-env writes it: shlex-quoted, escaped inside single quotes, and raw. - Never delete the key's secret: it is named from the key, so a launch can no longer remove one a concurrent launch (here or in another process) still needs. The Sailbox's saved policy is still deleted. - Keys are held per launch and Sailbox in this process and forgotten once none needs them. - Policy changes and terminate are serialized per Sailbox; terminate deletes the policy actually applied. - Saved policies are named before creation, so one whose create response is lost is found by name and deleted, with retries. - The gateway's container lookup retries a lost exec transport and raises rather than read it as no container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
@greptile-apps re-review |
The gateway and sidecar log dumps after compose up go through _log_service, which reports a container lookup that keeps losing its exec transport instead of raising it into the deploy. read_trajectory still raises, since there a failed lookup must not read as no history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m API main now pushes such objects onto a VM as base64 chunks over exec, reading them through ObjectStore.open. A Sailbox takes them through its own filesystem API instead, streamed a chunk at a time with no base64 or per-chunk exec, which also keeps them out of the model-key guards meant for agent-env's own commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The provider, its sandbox type and its config table are now sail_vm ([sandbox.providers.sail_vm.config], --sandbox sail_vm), the package sandbox_providers/sail_vm, and the classes SailVmSandbox and SailVmSandboxProvider, matching modal_vm for a VM-mode provider. THIRD_PARTY_NOTICES: sail ships no license file and its source isn't public, so its entry says so and points at PyPI, as for other packages without one, rather than linking a generic Apache text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onal extra Review follow-ups for the sail_vm provider. - sail is an optional extra (pip install 'agentenv-framework[sail]'); dev includes it. The package's __init__ imports nothing and the registry points at sail_vm.provider, so core never loads the SDK and selecting sail_vm without the extra names the extra to install. - The source change stays in sandbox_providers/: the CLI help and the providers/__init__ exports are reverted, and the gateway exec-transport fixes leave this PR for their own. - sail_vm/README.md documents install, config, sizing, networking, model-key injection, attribution and caveats. - Guard: shell words are decoded as the shell reads them (a quoted or escaped name is still a key), a value passes only when it is wholly one variable reference, and YAML/JSON KEY: value is caught in files. Keys under 16 characters are refused, since scrubbing replaces every occurrence. - Policies: every saved policy for a Sailbox shares a name prefix, so teardown from any handle in any process deletes them all; a swap whose outcome Sail can't report keeps both ids, one that applied is settled even when cancelled, and a create whose response is lost terminates the Sailbox it made (by name, with retries) before releasing its policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
@greptile-apps re-review the pr |
Collaborator
Author
|
Follow-up (separate PR): gateway reads that lose their exec connection Greptile's remaining concern is a core gateway issue, which this PR leaves alone to keep its source changes inside
The fix will come in its own PR from
|
Set up in four steps (install the extra, get a key, a complete config.toml, a first run with its output), a reference table of every config key, and examples for a shared deployment, per-run model keys, a sandbox with restricted egress, auto-sleep, a non-HTTPS model endpoint, a fallback chain and the Python API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
approved these changes
Oct 7, 2026
nirvikbaruah
approved these changes
Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
sail_vm, a built-in VM sandbox provider for Sail Research Sailboxes (type = "sail_vm",mode = "vm"), alongsidemodal_vmande2b. Each Sailbox boots from Sail'sdevboximage, which ships Docker and Compose v2 and runs as root. That means the existing docker-in-VM flows work unchanged throughVmSandbox: gateway docker-compose, agentdocker run,deploy_sandbox, image builds and artifact collection. The gateway routes it to_deploy_via_vmwith no provider-specific branch.Install the optional extra with
pip install 'agentenv-framework[sail]', then select it with--sandbox sail_vmor[sandbox] default = "sail_vm". Usage is documented insandbox_providers/sail_vm/README.md.What's in it
sail_vm/_sdk.py: imports the Sail SDK (thesailextra) and installs the API key, described in the call-outs below. The package's__init__imports nothing and the registry points atsail_vm.provider, so core never loads the SDK.sail_vm/sandbox.py(SailVmSandbox):collect_artifactsstreams. Output is queued with bounded backpressure, and output nobody reads is drained bywait(). A leadingsudois dropped, a lost host or transport gives exit-1soexec_scriptretries, and other stream errors are raised rather than reported as success.docker infoand startsdockerdonce if it isn't running. Compose v2 is required.sail_vm/provider.py(SailVmSandboxProvider):timeoutbecomes the box'smax_lifetime_seconds.*.sail.boxlisteners. Create waits until every exposed port is routed.{}; an allowlist →{"allowlist": hosts + IPv4 CIDRs}. IPv6 entries and more than 128 entries are refused before provisioning.EGRESS_HOSTS = ("*.sail.box",).get_sandboxrestores the tunnel URLs from listeners and the applied egress policy. An unrepresentable policy becomesNone(fail closed).failedstatus, or a failure during setup or port routing, terminates the box.create_container: reuses the inherited login → pull → run, then deletes/root/.docker/config.json._BUILTIN_SANDBOX_PROVIDERS, asailextra inpyproject.toml(sail~=0.12.8, also pulled in bydev; the lock addssailandcloudpickle),THIRD_PARTY_NOTICES.md, and the AGENTS.md provider list.The agent's model key never enters the Sailbox
Sail's credential injection adds the key to the agent's model requests as they leave the Sailbox. The Sailbox disk (which Sail checkpoints, see below) and its memory hold only a placeholder. This lives entirely in
sail_vm/model_key.pyand thesail_vmprovider; no core code changes.create_sandboxreadsLITELLM_API_KEYandLITELLM_BASE_URLfrom the agent's env. It stores the key as a Sail secret namedAGENTENV_LITELLM_<sha256(key)[:32]>, so one key maps to one secret and different keys never share one. The secret is never deleted, so no launch can remove one that a concurrent launch (in this process or another) still needs. It holds only a key Sail already stores; sweepAGENTENV_LITELLM_*if you rotate keys.authorization: Bearer ${secrets.…}andx-api-key: ${secrets.…}on requests to the endpoint's host. A restricted allowlist also gets that host.SailVmSandboxreplaces the key withsail-injected-model-key, in every form agent-env writes it (raw, shell-quoted, escaped inside single quotes). Sodocker run -eanddocker exec -e(agent, judge, unit-test verifier) carry only the placeholder.LITELLM_API_KEY/ANTHROPIC_API_KEYa value other than the placeholder. In shell scripts each word is decoded as the shell reads it, quoted or escaped names included, and only a value that is wholly one$NAMEreference passes. Files are checked literally,KEY: value(YAML/JSON) included. Keys under 16 characters are refused, since scrubbing replaces every occurrence. That covers an agent deployed into an existing Sail sandbox, and a reconnected handle that can't recover the key. Shell scripts are read as the shell reads them ($VARreferences pass); other arguments and file contents are taken literally.dockershim at/usr/local/bin/dockergives every container it runs or creates the VM's CA bundle, plusSSL_CERT_FILE,REQUESTS_CA_BUNDLE,NODE_EXTRA_CA_CERTSandCURL_CA_BUNDLE.[model]key, if that is the one injected. A key it can't recover is refused, not sent.httpone is refused before anything is created, unlessinject_model_key = false.inject_model_key = false.docker run/docker createon the VM. Containers started by an agent's own Docker-in-Docker don't get the CA.deploy_sandboxand othercreate_vmboxes carry no model key and don't inject.Call-outs for reviewers
The Sail API key is written to
os.environ. This is the first core provider to do that. The Sail Python SDK has no client that takes a key explicitly; it readsSAIL_API_KEYonce, when it builds its process-wide client._sdk.connect:App.findcall that builds the client;A different key in the same process raises
ConfigError(one Sail key per process). An operator's ownSAIL_API_KEYis overridden only for that build, then restored. The key is never passed to workloads, exec env,CONTAINER_ENV,REQUEST_HEADERS, logs orrepr. A local-sandbox subprocess started during that window could inherit it, but the local provider already runs as the same user with the full host environment, so this adds no exposure.SAIL_API_KEYandSAIL_RUNTIME_THREADSare added to the env-var guard's not-configuration set: agent-env only writes them for the SDK and never reads them as configuration.Sail checkpoints every Sailbox automatically. A box with autosleep off was checkpointed about 2.5 minutes after creation with nothing on our side triggering it, and there is no setting to turn this off. The model key is kept out (above), and registry credentials are deleted after
create_container's pull. Other values deployments put in container env still land on disk and in checkpoints:share_credentials = true: frozen host AWS credentials, so keep this off on Sail or use a scoped, short-lived role;extra_env_vars;Retention, whether checkpoints include memory, and deletion on terminate are open questions for Sail.
A cancelled create is cleaned up. If the caller is cancelled while a create is in flight (for example by
ChainedSandboxProvider's 180s deadline), the box it produces is terminated: three attempts with backoff, then an error log naming the box. Otherwise it would leak a billed Sailbox.Attribution is not on the Sailbox. Sail has no labels or tags. Attribution goes in the box name (
ae-<random>-<slugged values in key order>, ≤128 chars) and in a structuredagent_env.sail_vm_sandbox_startedlog event keyed bysailbox_id, the same pattern as Modal'smodal_sandbox_started. Per-box cost comes from Sail's spend API joined onsailbox_id.Autosleep is off by default (
AutoSleep.never()), so idle MCP servers and gateways don't sleep or come back cold. Config can turn it on.Resource requests round up. Requests are ceilings on Sail (billing is for observed usage), so CPU rounds up to a size and memory and disk round up into the size's range. This raises a cap, not the bill.
SAIL_MODEis not supported. Core must not read a stage variable, so the provider uses Sail's production endpoint.Optional and self-contained.
sailis an optional extra likegcp. Without it, selectingsail_vmfails withload_impl's standardConfigErrornaming the extra, and the contribution helpers (egress hosts, container env) skip the provider. The source change is confined tosandbox_providers/. The gateway exec-transport hardening (retryingread_trajectory/ container lookups that lose their exec connection) was split out for a separate PR.Testing
make unit-testgives 6602 passed, 13 skipped, with the SDK faked and nothing offline touching the network. New tests aresail_vm_sandbox_provider_test.py,sail_vm_sandbox_test.pyandsail_vm_model_key_test.py(secret naming, the rules, the shim's argument rewriting, create, scrubbing, policy replacement, cleanup order and reconnect).sail_vmis also added to the network-policy stamping test, the egress-host floor, config interpolation, the capability gate and the gateway VM-path routing test.int_test_slow):sail_vm_sandbox_smoke_test.py: Docker in the box, nginx served through a*.sail.boxURL, an exact binary write round-trip, reconnect restoring ports and policy, and an allowlist enforced for containers. A third test checks model-key injection end to end: a container's request reaches a header-echo endpoint with the real key, while the container's env anddocker inspectshow only the placeholder, and the policy is gone after terminate.sail_vmis added to thesandbox_providerparametrisations ingateway_test.pyandtask_steps_test.py.agentenv-capability-missing: remote_sandbox.sail==0.12.8, a local key, on the final code):agent-env run hello --sandbox sail_vm: passed;gateway_test.py::test_gateway_with_multi_mcp_server[sail_vm]: passed in about 1m40s on the Sailbox, covering two MCP servers, a universe load, MCP tool calls and/step;task_steps_test[sail_vm](needs a model endpoint), autosleep staying off through a long idle period, and how promptly a box expires at its max lifetime.🤖 Generated with Claude Code
The changes since the previous review appear safe to merge.
What we checked:
--sandbox, and the package registers the bundledhellotask.Summary
Adds
sail_vm, a built-in VM sandbox provider for Sail Research Sailboxes, so agent-env workloads can run on Sail’s Docker-ready VMs. It also keeps agent model keys out of those VMs by having Sail add them to outgoing requests.Reviews (13) · Last reviewed commit: "docs(sail_vm): walk through setup with c..." · Reviewed by Greptile