Repository navigation
feat(builder): a recipe declares the locked git sources it fetches (contract v6) - #832
Merged
antoinetoussaint-byte merged 6 commits intoOct 23, 2026
Conversation
…ontract v6) A build whose lockfile pins a dependency to a commit in a private git repository fetches it inside BuildKit, where the credentials of the machine running the build do not reach. DockerBuildRecipe.git_source_downloads lets a recipe declare each locked repository URL and commit, and the named build context it reads them from, so the caller can fetch every commit once, before any image build, into a bare mirror the Dockerfile maps the URL onto. The build then needs no credential, and the lockfile is unchanged. GitSourceMirrorPath names each mirror inside the context (the hex SHA-256 of the URL plus ".git"), so the caller that fills the context and the template that reads it share one layout. Validation accepts https, ssh and git+ssh URLs with no password, token, query or fragment (an ssh login name is allowed), a full 40- or 64-character lowercase commit, and a build-context name that is not also a Go module proxy context. One repository may be declared at several commits. A recipe that declares git sources emits the v6 contract; v6 digests the declaration on top of v5's Go module downloads and v4's explicit context root. A host that predates the field expects v3 to v5 for the same recipes and refuses the plan rather than building without supplying the mirrors. Recipes without git sources keep v3 to v5. SingleImageBuildResponse takes the same variadic recipe options as SingleImageBuildPlan; existing callers emit exactly what they did. Regenerated with the pinned generators (Go and Python). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ord recipe v5 and v6 A refused git source repository may be a credential pasted into a URL, and a builder error reaches the caller's RPC status and logs. Errors now name the declaration by position and the reason, never the URL or any part of it. The repository must be spelled exactly https:// or ssh:// (git rejects the spellings url.Parse normalizes, such as SSH://), with a host, an optional port and a path whose every character is in [A-Za-z0-9._~/@:%+-], so it can be written into a Dockerfile or a git config value verbatim. git+ssh is no longer accepted: a contract widens more easily than it narrows. GitSourceMirrorRef names the ref a mirror holds a commit under, beside GitSourceMirrorPath, so a host does not hard-code it. The option and proto documentation state that a host fetches every declared source and refuses an unfetched declaration before it accepts a v6 plan, and that declarations are digested in order and exact duplicates refused, so they are declared sorted and de-duplicated. agents/contract/contract.json and docs/agent-contract.md now record docker-build-recipe v5, which was never recorded, and v6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
git decodes percent-encoding before it splits an ssh URL into user, host and path, so ssh://host/team/x@%5B::1%5D/repo passed validation and plan verification while git read the user as host/team/x, the host as ::1 and the path as /repo. A git source repository now carries no "%" at all, for https as well as ssh: no lockfile URL needs an escape, and refusing every one is simpler to reason about than refusing the escapes of "/", "@", "[", "]", ":" and "%". No "@" may appear after the authority either; the only "@" accepted is an ssh login name's. Both refusals are tested through plan construction (WithGitSourceDownloads) and through VerifyDockerBuildPlan. A recipe declaring both Go module downloads and git sources is now also tested to be refused by a host that expects v5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in or host starting with - git cannot fetch either: it rejects the port, and blocks a login or host that would read as an option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… agents An agent reading a lockfile can check a source before declaring it, and leave one the contract cannot carry to the build's own fetch instead of failing the plan after rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
antoinetoussaint-byte
enabled auto-merge
October 21, 2026 23:40
lukazhupa
approved these changes
Oct 22, 2026
Merged
6 tasks
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 free
to 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.
What
A build recipe can now declare the locked git sources its build needs:
DockerBuildRecipe.git_source_downloads, a list ofGitSourceDownload { repository, commit, mirror_context }. The caller fetches each commit into a bare mirror on its own machine, with its own credentials, before any image build, and supplies the mirrors as a named build context. The build then resolves those sources from the mirrors and needs no credential.This is the git counterpart of
GoModuleDownload(core#677, contract v5), built the same way:DockerBuildRecipe.go_module_downloads(field 10),WithGoModuleDownloadsDockerBuildRecipe.git_source_downloads(field 11),WithGitSourceDownloadsgomodproxy), no secretuvsources), no secretvalidateGoModuleDownloads, invalidateRecipesvalidateGitSourceDownloads, invalidateRecipesAlso in this PR:
GitSourceMirrorPath(repository), which returns<sha256 hex>.git, andGitSourceMirrorRef(commit), which returnsrefs/commit/<commit>. They give the host and the Dockerfile one definition of the mirror layout.GitSourceRepositoryProblem(repository)andIsGitSourceCommit(commit), the validator's own checks exported. An agent reading a lockfile checks a source before declaring it, and leaves one the contract cannot carry to the build's own fetch, instead of failing the plan after rendering.SingleImageBuildResponsetakes variadicRecipeOptions; existing callers are unchanged.agents/contract/contract.jsonanddocs/agent-contract.mdrecord recipe v5 and v6.Why
Issue: codefly-dev/service-python-fastapi#102. A Python service whose
uv.lockpins a private git source cannot be built into an image today. The builder stage runsuv sync --frozenwith no credential and no prefetched input. The caller usually holds the credential, but nothing that credential can read reaches the build.Go services with private modules build today because the caller prefetches the module graph. This PR adds the same declaration for locked git sources.
We tested carrying a prefetched uv cache into the build first. It works only when the host's uv matches the image's uv exactly and the wheels are fetched per target platform; caches from uv 0.9 and 0.13 both failed in a 0.12 image. A bare git mirror at the locked commit, mapped in with
url.<mirror>.insteadOf=<locked url>, worked with an empty cache, no network route to the source and the lockfile untouched. It depends on nothing in uv's internals, and it carries only the part that needs the credential. Public package indexes stay on the network, as in every build today.The message is generic rather than Python-specific. Any lockfile that pins git sources by commit (Cargo, npm, Go
replaceto a git URL) can declare the same thing.Deviations from the Go module download path, with reasons
https://andssh://are accepted, with no userinfo over https, no password, and no query or fragment.[A-Za-z0-9._~/:+-]. Percent-escapes are refused anywhere in the URL, and@is refused after the authority, because git decodes escapes before it splits an ssh URL into user, host and path; an encoded@,[or/could otherwise change the host after validation.git+sshis not accepted. Lockfiles record the URL git is handed, without agit+prefix, and the build maps exactly that URL onto the mirror, so an alias would never match.SingleImageBuildResponsegains variadicRecipeOptions. Existing callers compile unchanged, and agents that use the single-image helper can declare downloads without copying the plan code.GitSourceMirrorPath/GitSourceMirrorRefhelpers. Go's proxy layout is defined by Go. A mirror layout is not, so the host and the Dockerfile share one definition.contract.jsonanddocs/agent-contract.mdrecord v5 as well as v6. v5 was supported but never recorded.Release order (please read)
A CLI must not pick up this core without the matching host fetch. Otherwise it would accept v6 plans without supplying the mirrors, and the build would fall back to the network. The Go change closed the same gap by landing core#677 and the CLI change together. Companion PRs:
uv.lockgit sources and maps them onto the mirror: feat(builder): declare uv.lock git sources for the caller to mirror service-python-fastapi#103 (draft).Testing
go test ./agents/services/ ./agents/contract/andgo test -race ./agents/services/. The newdocker_recipe_git_sources_test.gocovers:mainfor recipes that declare no git sources.make buf-lint, andmake buf-breaking("no breaking change against origin/main").test_*_contract.py).codefly generate proto --proto ./proto --output ./generated --local(plus the Python template).Not in this PR, and not verified here:
proto/buf.lock(a googleapis pin) and regeneratesruntime_pb2.py, which is already out of date onmain. Both were reverted, to keep this change focused.agents/managerTestNewNixStoreFromEnv_SetReturnsStoreneeds nix, and threeinternal/ciguardtests need bash 4 (mapfile). They fail the same way onmainon this machine.CI notes (run on the fork, head
abdc7077: https://github.com/saurabhmaheshwari-obin/core/actions/runs/39681236215)Every test step in Build passes, and pnpm source evidence passes. Two steps fail, neither because of this change:
originis this repository ("BASE_REMOTE=origin is not codefly-dev/core"), so it can only pass here. Locally, against this repository'smain, it reports "proto: no breaking change"; the change is additive (field 11 and a new message).go.ymlinstalls1.27.2. The finding is in code this PR does not touch.main's last run predates the advisory, somainwould report the same today. Bumping the CI Go version is left to a separate change.Review
-) fixed with tests.Classification: a fix. It closes a gap where a declared build input could not reach the build.
🤖 Generated with Claude Code