@@ -69,7 +69,8 @@ def _git_clone_commands(owner: str, repo: str, ref: str | None, token: str | Non
6969
7070class DockerImageArtifact (Artifact ):
7171 """A Docker image artifact: a tar.gz of the image in the object store; without one, a registry reference a
72- sandbox pulls; or, with neither, only its build context."""
72+ sandbox pulls, by its ``image_digest`` when it records one, or builds from the build context beside it when it can't; or,
73+ with neither, only its build context."""
7374
7475 model_config = ConfigDict (populate_by_name = True )
7576
@@ -87,6 +88,10 @@ class DockerImageArtifact(Artifact):
8788 source_digest : str | None = Field (
8889 default = None , description = "sha256 over the build context's files and modes, the Dockerfile's path and the platform"
8990 )
91+ image_digest : str | None = Field (
92+ default = None , description = "The registry's digest of the built image, not of its sources (source_digest): a "
93+ "sandbox pulls the image by it and runs it as image_name"
94+ )
9095
9196 # No return annotation: pydantic builds the serialization schema from one, and a dict drops the fields.
9297 @model_serializer (mode = "wrap" )
@@ -207,16 +212,16 @@ def put_tar(
207212 return store .put_document (instance )
208213
209214 @classmethod
210- def put_ref (cls , id : str , * , description : str , image_name : str ,
211- earlier_pin : str | None = None ) -> "DockerImageArtifact" :
212- """Register ``image_name``, an image already in a registry, writing this document only: nothing is pushed,
213- saved or uploaded, and a sandbox pulls the image. A tag is pinned to the digest the registry serves for it
214- now, recorded as ``name:tag@sha256:...``, so every run gets the same image; a digest is checked to exist.
215- ``earlier_pin``, the pin an earlier version recorded, is kept instead when it pins ``image_name``.
216-
217- ``image_name`` must name its registry. The registry is read with the configured image store's credentials
218- where it holds that registry, anonymously otherwise. One on this machine is refused unless ``id``'s image
219- store is the local registry, since nothing else could pull from it ."""
215+ def put_ref (cls , id : str , * , description : str , image_name : str , earlier_pin : str | None = None ,
216+ image_digest : str | None = None , context_path : str | None = None ,
217+ dockerfile_path : str | None = None ) -> "DockerImageArtifact" :
218+ """Register ``image_name``, an image already in a registry, which a sandbox pulls; nothing is built, pushed or
219+ saved. A tag is pinned to the digest the registry serves now, as ``name:tag@sha256:...`` (``earlier_pin`` kept
220+ when it pins the same ref). Given the image's build context and ``image_digest``, the tag stays the name, the
221+ digest is recorded beside it, and the context is uploaded for a sandbox that can't pull the image to build it.
222+
223+ ``image_name`` must name its registry, read with the configured image store's credentials where it holds them;
224+ one on this machine is refused unless ``id``'s image store is the local registry ."""
220225 from agent_env .artifact .store import get_artifact_store
221226 from agent_env .config import get_config
222227
@@ -228,6 +233,21 @@ def put_ref(cls, id: str, *, description: str, image_name: str,
228233 and not isinstance (config .get_image_store_for (id ), LocalRegistryImageStore )):
229234 raise ValueError (f"{ image_name } is in a registry on this machine, which only an image in the local "
230235 "registry's store, such as an @local one, can name" )
236+ if context_path is not None :
237+ context = _context_of (Path (context_path ), dockerfile_path )
238+ store = get_artifact_store ()
239+ version = store .next_version (id )
240+ prefix = store .attempt_prefix ("docker_image" , id )
241+ return store .put_document (cls (
242+ id = id ,
243+ version = version ,
244+ description = description ,
245+ image_name = image_name ,
246+ image_digest = image_digest ,
247+ build_context_object_url = _upload_context (config .get_object_store_to_write (prefix , id ), prefix , context ),
248+ dockerfile_path = context .dockerfile ,
249+ source_digest = context .source_digest (None ),
250+ ))
231251 if earlier_pin is not None and pinned_from (earlier_pin , image_name ):
232252 pinned = earlier_pin
233253 else :
@@ -281,6 +301,11 @@ def load(self) -> bytes:
281301 raise ValueError (f"{ self .id !r} v{ self .version } has no tar.gz; its image is { how } " )
282302 return get_artifact_store ().get_object (self .tar_gz_object_url )
283303
304+ @property
305+ def pinned_name (self ) -> str :
306+ """What a sandbox pulls: ``image_name`` pinned to the ``image_digest`` when it records one."""
307+ return f"{ self .image_name } @{ self .image_digest } " if self .image_digest else self .image_name
308+
284309 @property
285310 def context_only (self ) -> bool :
286311 """Whether only the image's build context is stored: no tar.gz, and an image name that names no registry, the
0 commit comments