Repository navigation
fix(local-sandbox): hold each container a local run starts to the sandbox's cpu and memory - #109
Open
earakely-scale wants to merge 4 commits into
Open
earakely-scale wants to merge 4 commits into
earakely-scale wants to merge 4 commits into
Conversation
…dbox's cpu and memory The local provider accepted cpu and memory and applied neither, so one runaway agent or env container could take the host, and every run on it. A local sandbox now records what it was created with, and Docker holds each container it starts to that: a container-mode sandbox's own container, an agent placed on a VM-mode sandbox and its Docker-in-Docker sidecar, every service of an env's compose stack, and run_docker_container's containers. The record is kept in the sandbox's work dir, so a handle rebuilt for a later step still applies it. A local VM-mode sandbox has no VM bounding everything it runs, so each container on it is held to the sandbox's budget. Docker refuses --cpus beyond its engine's CPU count, so cpu is held to that, with a warning. Remote providers are unchanged: a VM's size bounds its containers, and their commands and compose output stay as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eside its container-mode marker Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… make a sandbox whose limits can't be recorded Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| if container_limits is not None: | ||
| self.container_limits = container_limits | ||
| # Unrecorded, a handle a later step rebuilds would start containers unheld, so a failed write fails here. | ||
| (self._work_dir / _LIMITS_FILE).write_text(json.dumps(container_limits.to_dict())) |
There was a problem hiding this comment.
Failed deploy leaves a directory
If saving .container-limits.json fails, LocalSandbox raises after creating its work directory. No sandbox handle is returned, so cleanup cannot find that directory. Repeated failed attempts leave more directories behind. Remove the new directory when the write fails.
Knowledge Base Used: Provider integrations and sandbox deployment
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/providers/sandbox_providers/local_sandbox.py
Line: 185
Comment:
**Failed deploy leaves a directory**
If saving `.container-limits.json` fails, `LocalSandbox` raises after creating its work directory. No sandbox handle is returned, so cleanup cannot find that directory. Repeated failed attempts leave more directories behind. Remove the new directory when the write fails.
**Knowledge Base Used:** [Provider integrations and sandbox deployment](https://app.greptile.com/scale-ai/-/custom-context/knowledge-base/scaleapi/agentenv-framework/-/docs/provider-integrations.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Collaborator
Author
There was a problem hiding this comment.
Fixed in 636ebe6: a folder the constructor made is removed before the error propagates.
…s new folder with it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
The local sandbox provider accepted
cpuandmemorybut applied neither, so every container a local run started was unlimited. One memory hog or busy loop could take the host and every run on it.Now each container a local sandbox starts is held to the sandbox's cpu and memory with Docker's
--cpusand--memory. Swap is capped at the same value, so memory can't spill into swap. That covers:cpus,mem_limitandmemswap_limit);run_docker_container's containers.The limits are recorded in the sandbox's work dir, so a handle rebuilt by a later step (
get_sandbox) still applies them. A sandbox created before this change has no record and runs unlimited, as before.Semantics
Hard caps, like the VM providers (E2B, Sail, Scale sandbox), which size the VM itself.
LocalSandboxis aVmSandbox, and locally there is no VM to bound its containers, so Docker does it.Per container, not per stack. Each container on a VM-mode sandbox gets the sandbox's whole budget. That stops any single runaway, but a five-service env stack at 1 CPU / 8 GiB can still use 5 CPUs / 40 GiB in total. A combined cap would need a shared parent cgroup (Linux only); that's left for later.
CPU above the engine's count is lowered to it, with a warning. Docker refuses
--cpusabove the engine's CPU count (docker runexits 125, and compose fails the same way), so a request likedeploy_sandbox's default 2.0 on a 1-CPU engine would otherwise fail. Memory above the host's total is accepted by Docker and passed through unchanged.Behaviour change: what's requested is now enforced. With today's defaults:
deploy_sandboxcontainers get 2 CPUs / 8 GiB.Tasks that need more set
cpu/memory_mbon the step.Remote providers are unchanged. Their sandboxes record no limits, and a test checks that their docker commands and compose output are byte-identical.
Testing
Unit tier: 6434 passed. New tests cover:
All 11 mutants of the new code are killed.
Integration tiers on Linux (Ubuntu 24.04, Docker 29.1, cgroup v2, 8 CPUs), run as CI runs them:
test_container_limits_local.py. It caught one test regression, fixed in the second commit:test_verify_sandbox_localasserted that an agent's work dir holds only its container-mode marker, and it now also allows the limits record.Chaos on the same Linux host:
OOMKilled, exit 137, after holding 224 MiB). The sibling kept running; host memory available barely moved (14622 → 14462 MiB). Teardown removed both containers.cpu.max=50000 100000; measured 0.500 cores over 10 s. The same load with no limit took 3.96 cores.enable_dockeron a 1 CPU / 1 GiB sandboxagent-env run hellomacOS (Rancher Desktop, 4 CPUs): a container-mode sandbox at 0.5 CPU / 256 MiB got exactly that limit, and 64 CPUs were held to 4 with the warning. The memory-hog, clamp and old-sandbox scenarios above pass there too (8/8).
🤖 Generated with Claude Code
The PR appears safe to merge; the latest cleanup change leaves no outstanding finding.
Fix with agent prompt
Summary
Local sandboxes now apply requested CPU and memory limits to the containers they start, keeping each container within its own budget. The limits follow rebuilt sandbox handles and cover environment compose services as well as individual containers.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Create local sandbox] --> B[Save limits in work directory] B -->|Saved| C[Start containers with caps] B -->|Write fails| D[Remove new directory and raise] B --> E[Rebuild handle later] E --> CReviews (3) · Last reviewed commit: "fix(local-sandbox): a sandbox whose limi..." · Reviewed by Greptile