Skip to content

feat(snapshot_env): hand a service that takes parts grants an object of its own to upload to - #151

Open
earakely-scale wants to merge 22 commits into
edgararakelyan/store-begin-writefrom
edgararakelyan/snapshot-parts-grant
Open

earakely-scale wants to merge 22 commits into
edgararakelyan/store-begin-writefrom
edgararakelyan/snapshot-parts-grant

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

Stacked on #150 → #149 → #147: merge those in order, then this retargets to main.

Summary

snapshot_env becomes the first issuer of http-put-parts grants.

When a service's card lists the kind (client.write_kinds, from #149):

  • Offer: it begins a write (feat(store): add begin_write, a write made through a grant, with S3 multipart parts #150) of agentenv-snapshots/<uuid>/<env>.zip, as application/zip, up to ENV_SNAPSHOT_MAX_BYTES (16 GiB, about 256 part URLs), and sends it as data/get(write_object=...).
  • On an uploaded_object reply: it completes the write and registers the bundle in place, with the same key shape, file name and content type a service self-upload gets. A store may answer with one PUT instead of parts; that write is completed and registered the same way.
  • On anything else (another kind of reply, an error, a timeout): it aborts the write. Nothing leaks a pending upload.
  • Cancelled midway (the snapshot timeout): an export cancelled while the write begins waits for it, then aborts it. One cancelled while the write completes lets the completion finish before any abort runs. The credentials push sits inside the same try, so a cancellation there aborts the write too.
  • A completion failure is UploadFailedError, a ValueError, so export_one fails only that service, as with any other bad export.

Services that don't declare parts are offered exactly what they get today.

Rollout choice to review: a parts-declaring service that also advertises the credentials extension still gets the pushed credentials. A parts upload that fails therefore falls back to them, which keeps the change regression-free while services move over. Dropping the push is the later removal.

Per-service timing log. Each export now logs one line naming how its bundle came and how long it took, with bytes and parts for a parts upload. The ways are:

  • parts
  • object (one PUT)
  • namespace
  • service upload
  • inline
  • stream
  • data
  • export-state

Nothing recorded per-service export time before, and it's the comparison a rollout needs. The part count comes from the protocol's part_ranges.

Fix: services in a local compose stack can reach and trust the local grant server. A MultiEnv on a sandbox on this machine renders its MCP servers into a compose file, which never gave them the local transfer CA or the host mapping. So every local grant handed to one failed with CERTIFICATE_VERIFY_FAILED, write_namespace grants included. The e2e below found it. When the sandbox runs on this machine (ON_THIS_MACHINE: local Docker, a local VM) and the store issues local grants, each MCP server now gets three things, as a single local container already does:

  • the CA's trust files, written through the sandbox's own host beside the compose file (/app/trust, which local Docker keeps in the sandbox's Docker-shared work dir and a local VM in the VM), readable by any user, and mounted read-only where LOCAL_TRUST_ENV points;
  • LOCAL_TRUST_ENV, under the server's own variables, which win, as they do for a single container;
  • the sandbox's extra_hosts (how host.docker.internal resolves on Linux and in a local VM).

A MultiEnv's snapshot reload, which renders the compose again, keeps them. A sandbox elsewhere, or a store without local grants, renders exactly the compose it did.

Fix: a large host write on a Mac. Staging the trust bundle (a few hundred KB) is the first host write on a local sandbox past one chunk, and the chunked write decoded with base64 -d FILE, which macOS's base64 refuses. It now decodes from stdin, the same on Linux and macOS.

Labelled bump-version: this is the release issuers pin to start offering parts grants.

Verification

An end-to-end prototype of this stack ran in a real sandbox: a 2 vCPU / 8 GB VM, with an env service holding an 8.6 GB dataset.

  • Upload: 8.66 GB (over a single PUT's 5 GiB) went up in 129 parts with no credentials pushed.
  • Bundle content: identical to the input on 255,035/255,036 files, with only the service's regenerated state file differing.
  • Restore: loading it back and snapshotting again was identical on all 255,036 files.

End-to-end A/B on the same VM size, same service, data and bucket, 4 runs each:

Path Mean Container CPU
pushed credentials (boto3 multipart) 160.4 s 62.5 s
this path 159.7 s higher

That higher CPU came from per-chunk hashing in the uploader, since fixed in #147; upload-only CPU is now about 0.58× boto3's.

Tests

snapshot_env_object_write_test.py covers:

  • a parts-declaring service's grant shape, its object key, and its in-place registration and load-back, with credentials still pushed and the parts log line;
  • a store answering a parts-declaring service with one PUT, registered as well;
  • a fresh object per export;
  • a non-upload reply aborting the write and exporting as before;
  • a misreported upload failing only that service while the other is exported;
  • undeclared, unreachable-sandbox and no-object-write stores offered today's request;
  • an export cancelled while beginning, pushing credentials or completing: no write is left open, and none is aborted mid-completion.

The existing snapshot_env tests pass unchanged.

End to end (slow tier). multi_env_snapshot_local_test.py does the following:

  1. Deploy a MultiEnv on local compose whose items server takes write_object.
  2. Run the snapshot_env step, and check the bundle came through the parts tier.
  3. Check the registered object is agentenv-snapshots/<uuid>/items.zip, holds the state, and is application/zip.
  4. Change the state, load the snapshot back, and check the state it held is restored.

env_gateway_provider_test.py checks that MCP servers get the trust files, env and host mappings only when there is a local CA, with a server's own variables winning; that any sandbox on this machine, and no other, has the files written beside its compose and the deploy mounts them; and, on a real local sandbox under a 077 umask, that a bundle past one chunk lands whole and readable. The MultiEnv reload keeps them, and the compose fixture now closes the grant server its tests start. Locally, main's MultiEnv, env-trigger and server-env integration tests pass with the change.

Mutants, all killed:

  • no object write;
  • no complete;
  • no abort;
  • credentials withheld from parts services;
  • kinds ignored;
  • a shared key;
  • no timing log;
  • UploadFailedError not a ValueError;
  • plain to_thread for beginning and for completing;
  • the push before the try;
  • no shield;
  • no abort of a write begun after cancellation.

The unit tier passes.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new actionable issues remain.

Summary

Adds object-write grants to service snapshots while keeping existing export paths available.

  • Eligible services can upload snapshot bundles directly to a fresh object.
  • Snapshot export logs show each service’s route and elapsed time.
  • Local compose services can trust and reach the local grant server.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Read service card] --> B{Takes parts grants?}
  B -->|Yes, store can issue one| C[Begin object write]
  B -->|No| D[Use existing export offer]
  C --> E[Request service data]
  D --> E
  E --> F{Reports object upload?}
  F -->|Yes| G[Complete write]
  G --> H[Register uploaded bundle]
  F -->|No| I[Abort unused write]
  I --> J[Read existing reply format]
Loading

Reviews (11) · Last reviewed commit: "fix(snapshot_env): log a write a store m..." · Reviewed by Greptile

…of its own to upload to

When a service's card lists http-put-parts (client.write_kinds), snapshot_env
begins a write of agentenv-snapshots/<uuid>/<env>.zip (application/zip, up to
16 GiB) and sends it as data/get's write_object. On an uploaded_object reply
it completes the write and registers the bundle in place, with the same key
shape and file name as a self-upload; any other reply, an error or a timeout
aborts it. A completion failure is an UploadFailedError, a ValueError, so it
fails only that service.

Services that do not declare parts are offered what they are today. A
service that also advertises the credentials extension still gets the pushed
credentials, so a parts upload that fails can fall back to them.

Each export now logs one line naming how the bundle came (parts, namespace,
service upload, inline, stream, data or export-state) and how long it took,
with the bytes and parts of a parts upload.

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 9, 2026 20:32
Comment thread src/agent_env/task_step/task_steps/snapshot_env.py Outdated
earakely-scale and others added 2 commits October 9, 2026 13:49
… abort racing its completion

A write begun or completing in a thread when the export is cancelled is now seen through: the export
waits for the thread, aborts a write that began after the cancellation, and lets a completion finish
before its cleanup runs. The creds push moved inside the try, so a cancellation there aborts the write.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/agent_env/task_step/task_steps/snapshot_env.py
earakely-scale and others added 12 commits October 9, 2026 15:29
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r CA; snapshot e2e through a parts grant

A MultiEnv on the local backend runs its MCP servers from a compose file that never gave them the local
grant server's CA, so every grant handed to one failed TLS verification. Each MCP server now mounts the
CA's trust files where LOCAL_TRUST_ENV points, as a single local container already gets them. The new
slow-tier test snapshots a MultiEnv whose items server takes write_object, checks the bundle it uploaded
is registered and holds its state, and loads the snapshot back.

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

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

On Linux, host.docker.internal resolves in a container only through extra_hosts, which the compose
gave the gateway alone. A service handed the local CA's trust files now gets the mapping as well, so
it reaches the grant server it trusts.

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
… work dir

The local CA's folder lives under the state dir, which can sit outside what Docker shares (an
XDG_STATE_HOME elsewhere), so mounting it from there could fail. LocalSandbox.stage_local_trust
copies the trust files into the sandbox's work dir, which is kept on a shared path, and the compose
mounts that copy by a path relative to itself.

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 5 commits October 28, 2026 23:11
…napshot-parts-grant

# Conflicts:
#	src/agent_env/providers/env_providers/env_gateway_provider.py
#	src/agent_env/providers/sandbox_providers/local_docker/provider.py
#	src/agent_env/task_step/task_steps/snapshot_env.py
#	tst/unit/providers/env_providers/env_gateway_provider_test.py
#	tst/unit/providers/sandbox_providers/local_sandbox_test.py
…chine

A local VM's compose services are handed local grants as local Docker's are, so the trust files are written through
the sandbox's own host beside the compose file, for every sandbox that runs on this machine, instead of copied into
local Docker's work dir alone. A server's own variables win over the trust ones, as they do for a container, and a
snapshot reload's compose keeps the files mounted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ase64 also reads

A host write past one chunk decoded `base64 -d FILE`, which macOS's base64 refuses, so a local sandbox on a Mac failed
any large write_host_file, as staging a compose's trust bundle does on every local MultiEnv deploy. Reading the file
from stdin decodes the same on Linux and macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… staged trust files readable

A store may answer a service that takes parts with one PUT; the export's log line no longer reads a part size that
grant lacks, which failed the whole snapshot after the object was made. The trust files staged for a compose are made
755/644 whatever the deploy's umask, so a service running as any user reads them, and the deploy's mount of them is
pinned in a unit test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Merged the updated #150 in. main renamed local_sandbox.py to local_docker/provider.py and forbids provider checks outside the providers, so the compose trust fix was redone.

  • Trust files for any sandbox on this machine. The trust files are written through the sandbox's own host to /app/trust, for every sandbox on this machine (ON_THIS_MACHINE: local Docker and now smol_vm), instead of isinstance(sandbox, LocalSandbox) and a work-dir copy. The files are 755/644 whatever the umask. A server's own variables win over the trust ones. A MultiEnv snapshot reload, which renders the compose again, keeps them.
  • Large host writes on a Mac. The chunked host write decoded with base64 -d FILE, which macOS's base64 refuses. Every local MultiEnv deploy on a Mac failed staging the CA bundle. It now decodes from stdin.
  • One-PUT writes. A store that answers a parts-declaring service with one PUT is completed and logged as an object export. The parts log line used to fail the snapshot there.

Locally, on top of main, these pass: the unit tier, plus multi_env_snapshot_local_test, multi_env_local_test, test_register_env_triggers_local and server_env_local_test.

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