Uh oh!
There was an error while loading. Please reload this page.
fix(builder): close the recipe-contract verification gaps - #334
Merged
antoinetoussaint-byte merged 2 commits intoAug 21, 2026
Conversation
The Docker build-recipe contract validated only the recipe *files* and left the recipes — the half that decides what gets built and where it is pushed — outside its integrity boundary. Gaps closed: - A passing VerifyDockerBuildPlan did not mean the plan was buildable: recipe Dockerfile/context/dockerignore were never checked to exist in the emitted tree, so a green verify was followed by a confusing buildx "Dockerfile not found". validateRecipes now requires every recipe path to resolve to a real tree entry (Dockerfile/dockerignore files, context a directory), enforced in both Build and Verify. - The aggregate digest covered only the file inventory, so a plan whose image reference, build args, target, or paths were altered still passed verification when the files were byte-identical — the digest advertised as the plan's fingerprint protected half the plan. aggregateRecipeDigest now covers the recipes too, so digest pinning actually detects recipe tampering. - Recipe path fields were unconstrained, letting a recipe point buildx at a context or Dockerfile outside the caller-owned output directory (context: "/", dockerfile: "../.."). recipeRelPath now rejects absolute and escaping paths at the verification gate. - Recipe names, documented as the unique logical identity within a service, were not enforced: an empty or duplicated name was accepted and would silently build two recipes under one identity. validateRecipes now rejects both. Also stream file hashing (io.Copy instead of os.ReadFile) so inventorying a large context no longer buffers each file whole, and length-prefix every string in the digest (netstring form) so a path containing a separator or newline cannot be confused with a field boundary. Tests added for each: missing-file rejection, path-escape rejection, duplicate-name rejection, and tampered-image detection through verification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SbdJpccJnztweMo1G7W7Sx
The recipe-contract verification still had three holes where a plan could pass VerifyDockerBuildPlan yet not be the artifact the agent emitted: - Digest covered content but not file mode. Flipping a recipe file's executable bit (content byte-identical) — a change buildx carries into the image — passed verification. Add RecipeFile.mode to the inventory and cover it in aggregateRecipeDigest so a mode change is detected. - Path-escape prevention was lexical only, and a symlink defeated it: a Dockerfile symlinked to an out-of-tree target was accepted (fileDigest followed it), and the higher-value context-directory escape was blocked only by accident, via an unrelated "is a directory" io.Copy crash. Reject symlinks in inventoryRecipeFiles so the tree is contained by construction, not by accident — this also replaces that cryptic error with a clear "symlinks are not permitted" message. - The aggregate-digest algorithm changed but the contract version stayed v1, so a stale-algorithm digest surfaced as a digest mismatch (indistinguishable from tampering). Bump to v2 — since this commit again changes the algorithm (mode coverage) — so an older digest is reported as a contract mismatch, a clear and actionable error. Tests added for each: mode-tamper detection, symlink rejection, and stale-contract-version rejection. Full agents/services suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiCpejtWC2kQurJMREuK1e
antoinetoussaint-byteforce-pushed
the
fix/recipe-contract-verification-gaps
branch
from
August 21, 2026 18:30
cf06580 to
94ad1d6CompareUh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #332. That PR merged the Docker build-recipe contract, but the verification-gap fixes reviewed on the branch did not make it into the merge —
maincurrently has the contract with the gaps still open. This lands them.The contract validated only the recipe files and left the recipes — the half that decides what gets built and where it is pushed — outside its integrity boundary.
Gaps closed
dockerfile/context/dockerignorewere never checked to exist in the emitted tree, so a greenVerifyDockerBuildPlanwas followed by a confusing buildx "Dockerfile not found".validateRecipesnow requires every recipe path to resolve to a real tree entry (Dockerfile/dockerignore files, context a directory), enforced in bothBuildandVerify.aggregateRecipeDigestnow covers the recipes too, so digest pinning actually detects recipe tampering.context: "/",dockerfile: "../..").recipeRelPathnow rejects absolute and escaping paths at the verification gate.nameis documented as the unique logical identity within a service; an empty or duplicated name was accepted and would silently build two recipes under one identity. Now rejected.Also: stream file hashing (
io.Copyinstead ofos.ReadFile) so inventorying a large context no longer buffers each file whole, and length-prefix every string in the digest (netstring form) so a path containing a separator or newline cannot be confused with a field boundary.Tests
Added for each: missing-file rejection, path-escape rejection, duplicate-name rejection, and tampered-image detection through verification. Full
agents/servicessuite green.Related follow-up: #333 (enforce
output_directoryabsoluteness at the CLI boundary).🤖 Generated with Claude Code