Skip to content

fix(env): env snapshots and GitHub image builds work with an object store that can't presign - #90

Merged
earakely-scale merged 5 commits into
mainfrom
edgararakelyan/pull-back-sandbox-files
Oct 7, 2026
Merged

earakely-scale merged 5 commits into
mainfrom
edgararakelyan/pull-back-sandbox-files

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

Summary

Two flows on a remote VM sandbox refused to run when the object store can't presign, the local store for example:

  • Env snapshots raised before running any command.
  • DockerImageArtifact.put_from_github couldn't deliver its tarballs.

Both had the VM upload its file to a presigned URL. Now they deliver through one helper:

  • upload_vm_file(sandbox, vm_path, store, object_url). When the store signs a PUT URL, the VM uploads straight to it, as before; S3 is unchanged. Otherwise agent-env copies the file off the VM and put_file_ats it. A sandbox never connects to this machine.
  • read_vm_file(sandbox, vm_path, local_path). Copies a file off the VM range by range: tail -c +N | head -c M | base64 over exec, with 16 ranges of 4 MiB in flight.
    • Memory holds only the ranges in flight.
    • Each range's length is checked, and a short range fails the copy instead of leaving a truncated file.

Behavior changes worth a look

  • Every snapshot's tarball gets a key of its own (<image tag>-<16 hex>.tar.gz). The local store is write-once, so a second snapshot of the same env and universe would otherwise fail. It also means an artifact version's tarball never changes under it. On S3, a re-snapshot now adds an object where it used to overwrite one.
  • A GitHub build whose image store is a registry on this machine is refused on a remote provider before any VM is created. The build VM pushes the image there, and a remote VM can't reach localhost:5000. The local sandbox provider builds as before.
  • is_loopback_host moves next to registry_host_from_ref, so the bundle preflight and GitHub builds share it.

Testing

Unit: make unit-test passes. New and updated tests cover:

  • byte-for-byte copies at 0 B, under one range, exactly one range, and many ranges;
  • a short range failing the copy;
  • a signing store getting exactly the old curl upload;
  • an unsigning store getting the file with no curl;
  • GitHub builds against an unsigning store;
  • the loopback-registry refusal, and the local provider still building;
  • an @local env snapshot landing in the local store.

Against real VMs, on modal_vm and on another remote VM provider, with the local document and object stores and a hosted registry. Nothing shared was written.

Check modal_vm other provider
read_vm_file at 0 B, 1 B, 4 MiB ±1, 12 MiB and 200 MiB: sha256 matches ✅ ✅
upload_vm_file into the local store: sha256 matches ✅ ✅
VM terminated mid-copy fails in 6.4 s (Sandbox is shutting down) fails at once (see below)
↳ object or temp dir left behind none none
_snapshot_servicedb on a seeded servicedb (300k rows), twice: two 161 MB tarballs in the write-once store ✅ 87 s, 89 s ✅ 52 s, 46 s
Snapshot restored on a fresh VM from the local store: same row count and md5 not run (see below) ✅
put_from_github on a remote build VM ✅ 55 s ✅ 84 s
↳ tarball copied off the VM has the registry image's config digest ✅ ✅
↳ tarball loaded on a fresh VM: same image index digest as the registry tag, and the image runs not run ✅
Loopback image store with a remote provider refused in 0.0 s, no VM created
  • The length check is load-bearing. After the VM was terminated, the other provider answered an exec with success and empty output. The copy failed with read 0 of 4194304 bytes … refusing a truncated copy rather than storing a truncated tarball.

  • Ranges in flight (200 MiB, 4 MiB ranges):

    ranges in flight modal_vm other provider
    4 2.0 MiB/s 10.3 MiB/s
    8 3.7 MiB/s 16.1 MiB/s
    16 8.0 MiB/s 22.4 MiB/s

    2 MiB ranges did worse at 16 in flight (2.4 and 14.3 MiB/s).

  • The restore direction is unchanged and slow. It pushes an object the store can't sign onto a VM through the existing push path: one base64 chunk per exec, in sequence, from an object read whole into memory. The 161 MB snapshot took 1148 s to load on the other provider, and the 70 MB image took 502 s. Modal's chunks are a third of the size, which is why the Modal restores weren't run. Speeding that up is separate work.

Not in this PR

  • Plugin-provider env loads with a store that can't sign keep their refusal.
  • Faster pushes onto a VM from a store that can't sign.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no new issue or outstanding previous finding remains.

Fix All in CursorFindings

  1. P1 Long image IDs break builds ▶
Fix with agent prompt
### Issue 1
src/agent_env/artifact/artifacts/docker_image.py:undefined-234
A bare image ID with 224 characters could build with the old suffix. The new 16-character suffix makes its build-context filename 256 bytes long. The local object store cannot create that filename, so the GitHub build fails after the VM has done its work. Keep the generated filename within the store’s limit.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR lets environment snapshots and GitHub image builds upload VM files to stores that cannot sign PUT URLs. It also gives snapshot tarballs unique keys and checks that remote build VMs do not need to reach a registry on this machine.

  • VM files reach object stores even when they cannot sign upload URLs.
  • Each environment snapshot gets its own tarball in the object store.
  • GitHub image builds upload tarballs through the shared file-transfer path.

Reviews (4) · Last reviewed commit: "fix(artifact): name GitHub build tarball..." · Reviewed by Greptile

earakely-scale and others added 2 commits October 6, 2026 20:29
…tore that can't presign

A remote VM sandbox delivered its file by uploading it to a presigned URL.
The local object store signs nothing, so env snapshots and GitHub image
builds both refused it.

- read_vm_file copies a file off a VM host range by range: tail | head |
  base64 over exec, four ranges in flight, each length-checked.
- upload_vm_file puts a VM file into a store. When the store signs a PUT
  URL, the VM uploads straight to it, as before. Otherwise agent-env copies
  the file off and put_file_at's it.
- Env snapshots and GitHub builds deliver through upload_vm_file.
- Each snapshot's tarball gets a key of its own. A write-once store then
  takes a second snapshot of the same env and universe, and an artifact
  version's tarball never changes under it. On S3, a re-snapshot now adds
  an object instead of overwriting one.
- A GitHub build is refused before a remote build VM is created when its
  image store is a registry on this machine, which that VM can't push to.
  The local provider builds as before.
- is_loopback_host moves next to registry_host_from_ref, so the bundle
  preflight and GitHub builds share it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Measured on 200 MiB files: each exec gets a small share of a provider's
bandwidth, so throughput scales with the number of ranges in flight.

| ranges in flight (4 MiB each) | beta_scale | modal_vm  |
| ----------------------------- | ---------- | --------- |
| 4                             | 10.3 MiB/s | 2.0 MiB/s |
| 8                             | 16.1 MiB/s | 3.7 MiB/s |
| 16                            | 22.4 MiB/s | 8.0 MiB/s |

That holds 64 MiB of ranges in memory at once. The path only runs when
the store can't presign.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 7, 2026 04:03
Comment thread src/agent_env/providers/sandbox_providers/sandbox.py
Comment thread src/agent_env/env/snapshot_store.py Outdated
earakely-scale and others added 2 commits October 6, 2026 21:17
Eight hex characters left a small chance that two snapshots of one env and
universe draw the same key. In the write-once local store that fails the
second upload after its image is built. On S3 it overwrites a tarball an
earlier artifact version still names. Sixteen keep the longest filename
at 224 bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lengthen GitHub build suffixes

- read_vm_file fails with the VM's answer when `wc -c` doesn't return a
  number. A terminated VM can answer an exec with success and no output,
  which surfaced as a bare `invalid literal for int()`.
- GitHub build names take a 16-hex suffix, as snapshot tarballs now do.
  A repeat would fail the upload into a write-once store and overwrite an
  earlier version's tarball on S3.
- _write_bytes_to_vm_path drops its local base64 import; the module
  imports it now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dockerfile_repo_path = df_parts.path
config = get_config()
suffix = uuid.uuid4().hex[:8]
suffix = uuid.uuid4().hex[:16]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Long image IDs break builds

A bare image ID with 224 characters could build with the old suffix. The new 16-character suffix makes its build-context filename 256 bytes long. The local object store cannot create that filename, so the GitHub build fails after the VM has done its work. Keep the generated filename within the store’s limit.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/artifact/artifacts/docker_image.py
Line: 234

Comment:
**Long image IDs break builds**

A bare image ID with 224 characters could build with the old suffix. The new 16-character suffix makes its build-context filename 256 bytes long. The local object store cannot create that filename, so the GitHub build fails after the VM has done its work. Keep the generated filename within the store’s limit.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9eba072: the tarballs are named {suffix}.tar.gz and {suffix}-context.tar.gz under github-builds/{id}/, so their names are at most 31 bytes whatever the id. The local-store test now also builds a 238-character id, the longest Docker accepts for the build name {id}-{suffix}.

The build's directory already names the image id, so the tarball names
don't repeat it. With the longer suffix, an id of 224 characters or more
made the build context's filename longer than the 255 bytes the local
store's filesystem allows. Now the names are 31 bytes at most, whatever
the id, and an id is limited only by Docker's name length.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 7ce9353 into main Oct 7, 2026
13 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/pull-back-sandbox-files branch October 7, 2026 05:19
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