feat(release): pin first-party agent images
This commit is contained in:
@@ -84,6 +84,15 @@ def build_or_load_images(plan: DockerBottlePlan) -> BottleImages:
|
||||
)
|
||||
info(f"using cached agent image {plan.image!r}")
|
||||
return BottleImages(agent=plan.image)
|
||||
if "@sha256:" in plan.image:
|
||||
pulled = docker_mod.run_docker(["docker", "pull", plan.image])
|
||||
if pulled.returncode != 0:
|
||||
die(f"pulling packaged agent image {plan.image!r} failed: "
|
||||
f"{pulled.stderr.strip()}")
|
||||
docker_mod.verify_agent_image(
|
||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||
)
|
||||
return BottleImages(agent=plan.image)
|
||||
docker_mod.build_image(plan.image, str(resources.build_root()), dockerfile=plan.dockerfile_path)
|
||||
docker_mod.verify_agent_image(
|
||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||
|
||||
@@ -38,6 +38,7 @@ import subprocess
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from typing import Callable, Generator
|
||||
|
||||
from ...agent_provider import runtime_for
|
||||
from ...bottle_state import (
|
||||
egress_state_dir,
|
||||
git_gate_state_dir,
|
||||
@@ -93,7 +94,14 @@ def _agent_image(plan: MacosContainerBottlePlan) -> str:
|
||||
)
|
||||
info(f"using cached agent image {plan.image!r}")
|
||||
return plan.image
|
||||
container_mod.build_image(plan.image, str(resources.build_root()), dockerfile=plan.dockerfile_path)
|
||||
if "@sha256:" in plan.image:
|
||||
container_mod.pull_image(plan.image)
|
||||
container_mod.verify_agent_image(
|
||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||
)
|
||||
return plan.image
|
||||
container_mod.build_image(
|
||||
plan.image, str(resources.build_root()), dockerfile=plan.dockerfile_path)
|
||||
return plan.image
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ backend-specific final resolution.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from ..agent_provider import AgentProvisionPlan, build_agent_provision_plan, get_provider
|
||||
@@ -17,6 +17,7 @@ from ..egress import EgressPlan
|
||||
from ..env import ResolvedEnv, resolve_env
|
||||
from ..git_gate import GitGate, GitGatePlan
|
||||
from ..manifest import Manifest
|
||||
from ..release_manifest import oci_image
|
||||
from ..supervisor.plan import SupervisePlan
|
||||
from ..workspace import workspace_plan
|
||||
from .resolve_common import (
|
||||
@@ -112,6 +113,11 @@ class BottlePreparationPlanner:
|
||||
color=spec.color,
|
||||
provider_settings=provider_config.settings,
|
||||
)
|
||||
if not provider_config.dockerfile:
|
||||
image, local = oci_image(
|
||||
f"agent_{provider_config.template}", provision.image)
|
||||
if not local:
|
||||
provision = replace(provision, image=image)
|
||||
provision = merge_provision_env_vars(provision)
|
||||
return PreparedBottle(
|
||||
manifest=manifest,
|
||||
|
||||
@@ -51,6 +51,10 @@ def build_bundle_index(
|
||||
"oci": {
|
||||
"orchestrator": manifest.orchestrator_image,
|
||||
"gateway": manifest.gateway_image,
|
||||
**{
|
||||
f"agent_{role}": reference
|
||||
for role, reference in manifest.agent_images.items()
|
||||
},
|
||||
},
|
||||
"firecracker": {
|
||||
"orchestrator": {
|
||||
|
||||
@@ -32,6 +32,7 @@ class ReleaseManifest:
|
||||
source_commit: str
|
||||
orchestrator_image: str
|
||||
gateway_image: str
|
||||
agent_images: dict[str, str]
|
||||
firecracker_orchestrator: FirecrackerArtifact
|
||||
firecracker_gateway: FirecrackerArtifact
|
||||
|
||||
@@ -66,7 +67,7 @@ def parse_manifest(data: Any) -> ReleaseManifest:
|
||||
if not isinstance(oci, dict):
|
||||
raise ReleaseManifestError("release manifest oci must be an object")
|
||||
images: dict[str, str] = {}
|
||||
for role in ("orchestrator", "gateway"):
|
||||
for role in ("orchestrator", "gateway", "agent_claude", "agent_codex", "agent_pi"):
|
||||
ref = oci.get(role)
|
||||
if not isinstance(ref, str) or _OCI_RE.fullmatch(ref) is None:
|
||||
raise ReleaseManifestError(
|
||||
@@ -79,6 +80,9 @@ def parse_manifest(data: Any) -> ReleaseManifest:
|
||||
source_commit=commit,
|
||||
orchestrator_image=images["orchestrator"],
|
||||
gateway_image=images["gateway"],
|
||||
agent_images={
|
||||
role: images[f"agent_{role}"] for role in ("claude", "codex", "pi")
|
||||
},
|
||||
firecracker_orchestrator=_artifact(
|
||||
firecracker.get("orchestrator"), "orchestrator"),
|
||||
firecracker_gateway=_artifact(firecracker.get("gateway"), "gateway"),
|
||||
@@ -132,7 +136,8 @@ def oci_image(role: str, local_default: str) -> tuple[str, bool]:
|
||||
assert manifest is not None
|
||||
return (
|
||||
manifest.orchestrator_image if role == "orchestrator"
|
||||
else manifest.gateway_image,
|
||||
else manifest.gateway_image if role == "gateway"
|
||||
else manifest.agent_images[role.removeprefix("agent_")],
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user