Skip to content

ENG-4847: send stable image build context hashes - #225

Closed
SystemSculpt wants to merge 1 commit into
mainfrom
codex/eng-4847-image-context-hash
Closed

ENG-4847: send stable image build context hashes#225
SystemSculpt wants to merge 1 commit into
mainfrom
codex/eng-4847-image-context-hash

Conversation

@SystemSculpt

@SystemSculpt SystemSculpt commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

  • make generated image-context ZIPs byte-stable across traversal order and source mtimes
  • compute sha256-<digest> from the actual uploaded archive
  • pass that context hash through synchronous and asynchronous sandbox upload requests
  • preserve compatibility with older controlplanes, which ignore the additional query parameter

Dependency

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.py
  • uv run ruff check src/blaxel/core/image/image.py tests/core/test_image.py
  • uv 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 --check

Note

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 contextHash incorrectly, 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 contextHash on create/update upload requests.

_create_zip now sorts files, uses POSIX arcnames, pins ZIP timestamps, and writes entries via ZipInfo (file modes still preserved). _context_hash hashes the uploaded bytes as sha256-<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 contextHash query parameter alongside sandbox upload requests.

Written by Mendral for commit 8aaa496.

@mendral-app

mendral-app Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 Testing Guide

What this PR addresses

This 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 contextHash query parameter to the controlplane during sandbox creation/upload. This enables the controlplane to skip re-processing identical build contexts.

Steps to reproduce the original issue

  1. Create a project with an ImageInstance and a build directory containing multiple files.
  2. Run build() or build_sync() twice on the same unmodified source tree — ideally on different machines or after touching a file's mtime (e.g., touch file.txt).
  3. Observe that previously the generated ZIP would differ in bytes each time (different file order, embedded mtimes), meaning the controlplane could not deduplicate identical contexts.

What to verify (expected behavior)

Deterministic archive output:

  • Run the new test directly:
    uv run pytest tests/core/test_image.py::test_build_archive_and_context_hash_are_content_stable -v
  • Confirm that two directories with identical file contents but different mtimes produce byte-identical ZIPs and identical sha256-<hex> hashes (71 chars total).

Context hash passed in API requests:

  • Run:
    uv run pytest tests/core/test_image.py::test_upload_request_carries_context_hash -v
  • Confirm the sandbox creation request includes params={"upload": "true", "contextHash": "sha256-..."}.

Backward compatibility:

  • When context_hash is None (the default), verify that contextHash is not included in the params dict — only {"upload": "true"} is sent.

Full test suite passes with no regressions:

uv run pytest tests/core/test_image.py -v

Linting:

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.py

Note: The companion controlplane PR (blaxel-ai/controlplane#5215) consumes the new contextHash parameter — this SDK change is safe to merge independently since older controlplanes simply ignore unknown query params.

Note

Posted by PR Testing Guide · Tag @mendral-app with feedback.

@mendral-app

mendral-app Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

✅ Linked to Linear issue ENG-4847 — status already In Progress.

Note

Posted by Linear Issue Enforcer · Tag @mendral-app with feedback.

@mendral-app

mendral-app Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🔀 Interaction Flow

Here'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()
Loading

Summary

The 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 contextHash query parameter to the controlplane, enabling content-based deduplication of image builds on the server side.

Note

Posted by PR Sequence Diagram · Tag @mendral-app with feedback.

@mendral-app mendral-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8aaa496. Configure here.

@SystemSculpt

Copy link
Copy Markdown
Member Author

Closing this companion PR. ENG-4847 will be implemented entirely in controlplane so existing Harbor and SDK clients require no upgrade.

@SystemSculpt
SystemSculpt deleted the codex/eng-4847-image-context-hash branch August 20, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant