ENG-4847: send stable image build context hashes - #225
Conversation
🧪 Testing GuideWhat this PR addressesThis PR makes image build context ZIP archives byte-stable (deterministic) regardless of filesystem traversal order or file modification times, then computes a SHA-256 hash of the archive and passes it as a Steps to reproduce the original issue
What to verify (expected behavior)Deterministic archive output:
Context hash passed in API requests:
Backward compatibility:
Full test suite passes with no regressions: uv run pytest tests/core/test_image.py -vLinting: uv run ruff format --check src/blaxel/core/image/image.py tests/core/test_image.py
uv run ruff check src/blaxel/core/image/image.py tests/core/test_image.pyNote: The companion controlplane PR (blaxel-ai/controlplane#5215) consumes the new Note Posted by PR Testing Guide · Tag @mendral-app with feedback. |
|
✅ Linked to Linear issue ENG-4847 — status already In Progress. Note Posted by Linear Issue Enforcer · Tag @mendral-app with feedback. |
🔀 Interaction FlowHere's a sequence diagram showing how the new stable context hash flows through the sandbox build process: sequenceDiagram
participant Caller as Caller (build_sync / build)
participant Image as ImageInstance
participant ZIP as _create_zip
participant Hash as _context_hash
participant API as Controlplane API
participant Upload as Upload Endpoint
Caller->>Image: build_sync() / build()
Image->>Image: write_temp() (prepare build dir)
Image->>ZIP: _create_zip(build_dir)
Note over ZIP: Sorted traversal<br/>Pinned mtimes (1980-01-01)<br/>Pinned permissions
ZIP-->>Image: zip_content (deterministic bytes)
Image->>Hash: _context_hash(zip_content)
Hash-->>Image: "sha256-{hexdigest}"
Image->>API: PUT /sandboxes/{name}?upload=true&contextHash=sha256-...
alt 404 (sandbox doesn't exist)
API-->>Image: 404
Image->>API: POST /sandboxes?upload=true&contextHash=sha256-...
end
API-->>Image: Response + upload_url
Image->>Upload: PUT zip_content → upload_url
Upload-->>Image: 200 OK
Image->>Image: _wait_for_deployment()
SummaryThe PR makes ZIP archive creation byte-stable (sorted files, fixed timestamps/permissions) so that identical build contexts always produce the same archive. A SHA-256 hash of the archive is then computed and sent as a Note Posted by PR Sequence Diagram · Tag @mendral-app with feedback. |
There was a problem hiding this comment.
LGTM
The implementation is correct and well-tested. The riskiest hunk—the _create_zip rewrite—properly sorts paths, pins date_time to the ZIP epoch minimum, preserves file permissions via external_attr, and uses shutil.copyfileobj through ZipInfo to avoid leaking source metadata. The hash is computed over the final archive bytes (not file-by-file), which is the right approach for a content-addressable key. The if context_hash: truthiness guard is safe since the value is always either None or a 71-char non-empty string.
Tag @mendral-app with feedback or questions. View session
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8aaa496. Configure here.
| try: | ||
| # Create zip | ||
| zip_content = self._create_zip(build_dir) | ||
| context_hash = self._context_hash(zip_content) |
There was a problem hiding this comment.
Context hash still follows file mtimes
Medium Severity
contextHash is the digest of the zip from write_temp(), which still embeds manifest.json with Image.hash. That hash is built from local-file mtimes, so identical contents with different mtimes still produce different archives and cache keys on the real sandbox upload path. The new zip-header pinning does not make the uploaded payload byte-stable.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8aaa496. Configure here.
| info.compress_type = zipfile.ZIP_DEFLATED | ||
| info.external_attr = (resolved_path.stat().st_mode & 0xFFFF) << 16 | ||
| with resolved_path.open("rb") as source, zf.open(info, "w") as target: | ||
| shutil.copyfileobj(source, target, length=1024 * 1024) |
There was a problem hiding this comment.
Large files lose ZIP64 sizing
Low Severity
Manual ZipInfo entries leave file_size at 0 before ZipFile.open(..., "w"). Unlike the previous zf.write() path, ZIP64 is not selected up front, so a context file larger than 4 GiB can fail when the zip member is closed.
Reviewed by Cursor Bugbot for commit 8aaa496. Configure here.
|
Closing this companion PR. ENG-4847 will be implemented entirely in controlplane so existing Harbor and SDK clients require no upgrade. |


Summary
sha256-<digest>from the actual uploaded archiveDependency
Companion controlplane PR: https://github.com/blaxel-ai/controlplane/pull/5215
Validation
uv run ruff format --check src/blaxel/core/image/image.py tests/core/test_image.pyuv run ruff check src/blaxel/core/image/image.py tests/core/test_image.pyuv run --group test pytest tests/core/test_image.py -q(133 passed)uv run --group test pytest --import-mode=importlib tests -q(684 passed, 36 skipped)git diff --checkNote
Medium Risk
Touches sandbox deploy/upload request params and archive construction used for image builds. Hash/ZIP changes could skip or force rebuilds if the controlplane interprets
contextHashincorrectly, but the extra query param is backward-compatible.Overview
Makes sandbox image-context ZIPs byte-stable so identical content produces the same archive checksum, then sends that checksum as
contextHashon create/update upload requests._create_zipnow sorts files, uses POSIX arcnames, pins ZIP timestamps, and writes entries viaZipInfo(file modes still preserved)._context_hashhashes the uploaded bytes assha256-<digest>and both sync and async sandbox upload paths pass it as a query param. Older controlplanes can ignore the extra param.Tests cover ZIP/hash stability across mtime and traversal order, and that the upload request includes
contextHash.Reviewed by Cursor Bugbot for commit 8aaa496. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Makes image build context ZIP archives byte-stable by sorting file traversal order and pinning mtimes to a fixed epoch, then computes a SHA-256 hash of the archive and sends it as a
contextHashquery parameter alongside sandbox upload requests.Written by Mendral for commit 8aaa496.