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 17 commits into
edgararakelyan/store-begin-writefrom
edgararakelyan/snapshot-parts-grant
Open

earakely-scale wants to merge 17 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.
  • 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
  • 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 the local backend 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 is local 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, mounted from a copy in the sandbox's work dir (a Docker-shared path, unlike the state dir);
  • LOCAL_TRUST_ENV;
  • the sandbox's extra_hosts (how host.docker.internal resolves on Linux).

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 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. The compose fixture now closes the grant server its tests start.

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.

make unit-test passes apart from tst/unit/bundle/parse_test.py::TestFiles::test_deep_nesting[json], which fails the same way on main locally.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no blocking issue remains.

Summary

snapshot_env offers parts-capable services their own upload object, completes successful uploads, and logs each export’s path and time.

  • Parts-ready services now upload snapshots straight to their own object.
  • Snapshot logs show each service’s export path and time.
  • Local compose services can trust and reach the local grant server.

Diagram

sequenceDiagram
  participant Snapshot
  participant Store
  participant Service
  Snapshot->>Store: Begin write for this service
  Store-->>Snapshot: Parts grant
  Snapshot->>Service: data/get with write_object
  Service->>Store: Upload parts
  Service-->>Snapshot: uploaded_object
  Snapshot->>Store: Complete write
  Snapshot->>Snapshot: Register bundle
  Note over Snapshot,Store: Abort an unfinished write on other replies or errors
Loading

Reviews (10) · Last reviewed commit: "fix(local): a local compose mounts the t..." · 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

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