feat(release): pin first-party agent images
lint / lint (push) Successful in 1m15s
test / image-input-builds (pull_request) Successful in 1m12s
test / integration-docker (pull_request) Failing after 2m47s
tracker-policy-pr / check-pr (pull_request) Failing after 11m49s
test / unit (pull_request) Failing after 12m1s
prd-number-check / require-numbered-prds (pull_request) Failing after 12m6s
test / coverage (pull_request) Has been skipped
lint / lint (push) Successful in 1m15s
test / image-input-builds (pull_request) Successful in 1m12s
test / integration-docker (pull_request) Failing after 2m47s
tracker-policy-pr / check-pr (pull_request) Failing after 11m49s
test / unit (pull_request) Failing after 12m1s
prd-number-check / require-numbered-prds (pull_request) Failing after 12m6s
test / coverage (pull_request) Has been skipped
This commit is contained in:
@@ -13,6 +13,15 @@ on:
|
|||||||
gateway_image:
|
gateway_image:
|
||||||
description: Digest-pinned gateway OCI reference
|
description: Digest-pinned gateway OCI reference
|
||||||
required: true
|
required: true
|
||||||
|
agent_claude_image:
|
||||||
|
description: Digest-pinned Claude agent OCI reference
|
||||||
|
required: true
|
||||||
|
agent_codex_image:
|
||||||
|
description: Digest-pinned Codex agent OCI reference
|
||||||
|
required: true
|
||||||
|
agent_pi_image:
|
||||||
|
description: Digest-pinned Pi agent OCI reference
|
||||||
|
required: true
|
||||||
firecracker_orchestrator_version:
|
firecracker_orchestrator_version:
|
||||||
description: Published orchestrator rootfs package version
|
description: Published orchestrator rootfs package version
|
||||||
required: true
|
required: true
|
||||||
@@ -42,6 +51,9 @@ jobs:
|
|||||||
--source-commit "$GITHUB_SHA" \
|
--source-commit "$GITHUB_SHA" \
|
||||||
--orchestrator-image '${{ inputs.orchestrator_image }}' \
|
--orchestrator-image '${{ inputs.orchestrator_image }}' \
|
||||||
--gateway-image '${{ inputs.gateway_image }}' \
|
--gateway-image '${{ inputs.gateway_image }}' \
|
||||||
|
--agent-claude-image '${{ inputs.agent_claude_image }}' \
|
||||||
|
--agent-codex-image '${{ inputs.agent_codex_image }}' \
|
||||||
|
--agent-pi-image '${{ inputs.agent_pi_image }}' \
|
||||||
--firecracker-orchestrator-version '${{ inputs.firecracker_orchestrator_version }}' \
|
--firecracker-orchestrator-version '${{ inputs.firecracker_orchestrator_version }}' \
|
||||||
--firecracker-orchestrator-sha256 '${{ inputs.firecracker_orchestrator_sha256 }}' \
|
--firecracker-orchestrator-sha256 '${{ inputs.firecracker_orchestrator_sha256 }}' \
|
||||||
--firecracker-gateway-version '${{ inputs.firecracker_gateway_version }}' \
|
--firecracker-gateway-version '${{ inputs.firecracker_gateway_version }}' \
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ def build_or_load_images(plan: DockerBottlePlan) -> BottleImages:
|
|||||||
)
|
)
|
||||||
info(f"using cached agent image {plan.image!r}")
|
info(f"using cached agent image {plan.image!r}")
|
||||||
return BottleImages(agent=plan.image)
|
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.build_image(plan.image, str(resources.build_root()), dockerfile=plan.dockerfile_path)
|
||||||
docker_mod.verify_agent_image(
|
docker_mod.verify_agent_image(
|
||||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import subprocess
|
|||||||
from contextlib import ExitStack, contextmanager
|
from contextlib import ExitStack, contextmanager
|
||||||
from typing import Callable, Generator
|
from typing import Callable, Generator
|
||||||
|
|
||||||
|
from ...agent_provider import runtime_for
|
||||||
from ...bottle_state import (
|
from ...bottle_state import (
|
||||||
egress_state_dir,
|
egress_state_dir,
|
||||||
git_gate_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}")
|
info(f"using cached agent image {plan.image!r}")
|
||||||
return plan.image
|
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
|
return plan.image
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ backend-specific final resolution.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from typing import TYPE_CHECKING, Protocol
|
from typing import TYPE_CHECKING, Protocol
|
||||||
|
|
||||||
from ..agent_provider import AgentProvisionPlan, build_agent_provision_plan, get_provider
|
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 ..env import ResolvedEnv, resolve_env
|
||||||
from ..git_gate import GitGate, GitGatePlan
|
from ..git_gate import GitGate, GitGatePlan
|
||||||
from ..manifest import Manifest
|
from ..manifest import Manifest
|
||||||
|
from ..release_manifest import oci_image
|
||||||
from ..supervisor.plan import SupervisePlan
|
from ..supervisor.plan import SupervisePlan
|
||||||
from ..workspace import workspace_plan
|
from ..workspace import workspace_plan
|
||||||
from .resolve_common import (
|
from .resolve_common import (
|
||||||
@@ -112,6 +113,11 @@ class BottlePreparationPlanner:
|
|||||||
color=spec.color,
|
color=spec.color,
|
||||||
provider_settings=provider_config.settings,
|
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)
|
provision = merge_provision_env_vars(provision)
|
||||||
return PreparedBottle(
|
return PreparedBottle(
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ def build_bundle_index(
|
|||||||
"oci": {
|
"oci": {
|
||||||
"orchestrator": manifest.orchestrator_image,
|
"orchestrator": manifest.orchestrator_image,
|
||||||
"gateway": manifest.gateway_image,
|
"gateway": manifest.gateway_image,
|
||||||
|
**{
|
||||||
|
f"agent_{role}": reference
|
||||||
|
for role, reference in manifest.agent_images.items()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"firecracker": {
|
"firecracker": {
|
||||||
"orchestrator": {
|
"orchestrator": {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class ReleaseManifest:
|
|||||||
source_commit: str
|
source_commit: str
|
||||||
orchestrator_image: str
|
orchestrator_image: str
|
||||||
gateway_image: str
|
gateway_image: str
|
||||||
|
agent_images: dict[str, str]
|
||||||
firecracker_orchestrator: FirecrackerArtifact
|
firecracker_orchestrator: FirecrackerArtifact
|
||||||
firecracker_gateway: FirecrackerArtifact
|
firecracker_gateway: FirecrackerArtifact
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ def parse_manifest(data: Any) -> ReleaseManifest:
|
|||||||
if not isinstance(oci, dict):
|
if not isinstance(oci, dict):
|
||||||
raise ReleaseManifestError("release manifest oci must be an object")
|
raise ReleaseManifestError("release manifest oci must be an object")
|
||||||
images: dict[str, str] = {}
|
images: dict[str, str] = {}
|
||||||
for role in ("orchestrator", "gateway"):
|
for role in ("orchestrator", "gateway", "agent_claude", "agent_codex", "agent_pi"):
|
||||||
ref = oci.get(role)
|
ref = oci.get(role)
|
||||||
if not isinstance(ref, str) or _OCI_RE.fullmatch(ref) is None:
|
if not isinstance(ref, str) or _OCI_RE.fullmatch(ref) is None:
|
||||||
raise ReleaseManifestError(
|
raise ReleaseManifestError(
|
||||||
@@ -79,6 +80,9 @@ def parse_manifest(data: Any) -> ReleaseManifest:
|
|||||||
source_commit=commit,
|
source_commit=commit,
|
||||||
orchestrator_image=images["orchestrator"],
|
orchestrator_image=images["orchestrator"],
|
||||||
gateway_image=images["gateway"],
|
gateway_image=images["gateway"],
|
||||||
|
agent_images={
|
||||||
|
role: images[f"agent_{role}"] for role in ("claude", "codex", "pi")
|
||||||
|
},
|
||||||
firecracker_orchestrator=_artifact(
|
firecracker_orchestrator=_artifact(
|
||||||
firecracker.get("orchestrator"), "orchestrator"),
|
firecracker.get("orchestrator"), "orchestrator"),
|
||||||
firecracker_gateway=_artifact(firecracker.get("gateway"), "gateway"),
|
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
|
assert manifest is not None
|
||||||
return (
|
return (
|
||||||
manifest.orchestrator_image if role == "orchestrator"
|
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,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ supply-chain boundary between release production and runtime.
|
|||||||
immutable orchestrator and gateway identities.
|
immutable orchestrator and gateway identities.
|
||||||
- Docker and Apple Container acquire and run the manifest's digest-pinned OCI
|
- Docker and Apple Container acquire and run the manifest's digest-pinned OCI
|
||||||
images without invoking `docker build` or `container build`.
|
images without invoking `docker build` or `container build`.
|
||||||
|
- The shipped Claude, Codex, and Pi agent images are built, smoke-tested,
|
||||||
|
published, and selected by digest as part of the same commit bundle.
|
||||||
- Firecracker acquires the manifest's versioned, checksum-verified
|
- Firecracker acquires the manifest's versioned, checksum-verified
|
||||||
orchestrator and gateway rootfs artifacts rather than deriving versions at
|
orchestrator and gateway rootfs artifacts rather than deriving versions at
|
||||||
launch.
|
launch.
|
||||||
@@ -75,7 +77,7 @@ supply-chain boundary between release production and runtime.
|
|||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- Changing agent/provider image builds. User Dockerfiles continue to build
|
- Prebuilding user-supplied Dockerfiles. Custom agent images continue to build
|
||||||
through the backend-specific agent-image paths.
|
through the backend-specific agent-image paths.
|
||||||
- Installing Docker, Apple Container, Firecracker, or other host prerequisites.
|
- Installing Docker, Apple Container, Firecracker, or other host prerequisites.
|
||||||
- Replacing Gitea's OCI or generic-package registries.
|
- Replacing Gitea's OCI or generic-package registries.
|
||||||
@@ -101,7 +103,10 @@ shipped as package data:
|
|||||||
"source_commit": "<40 lowercase hex>",
|
"source_commit": "<40 lowercase hex>",
|
||||||
"oci": {
|
"oci": {
|
||||||
"orchestrator": "gitea.dideric.is/didericis/bot-bottle-orchestrator@sha256:<64 hex>",
|
"orchestrator": "gitea.dideric.is/didericis/bot-bottle-orchestrator@sha256:<64 hex>",
|
||||||
"gateway": "gitea.dideric.is/didericis/bot-bottle-gateway@sha256:<64 hex>"
|
"gateway": "gitea.dideric.is/didericis/bot-bottle-gateway@sha256:<64 hex>",
|
||||||
|
"agent_claude": "gitea.dideric.is/didericis/bot-bottle-claude@sha256:<64 hex>",
|
||||||
|
"agent_codex": "gitea.dideric.is/didericis/bot-bottle-codex@sha256:<64 hex>",
|
||||||
|
"agent_pi": "gitea.dideric.is/didericis/bot-bottle-pi@sha256:<64 hex>"
|
||||||
},
|
},
|
||||||
"firecracker": {
|
"firecracker": {
|
||||||
"orchestrator": {
|
"orchestrator": {
|
||||||
@@ -131,7 +136,8 @@ it refuses mutable or incomplete release inputs.
|
|||||||
|
|
||||||
The release job operates on one tested commit:
|
The release job operates on one tested commit:
|
||||||
|
|
||||||
1. Build the multi-architecture orchestrator and gateway OCI images.
|
1. Build and smoke-test the multi-architecture orchestrator, gateway, Claude,
|
||||||
|
Codex, and Pi OCI images.
|
||||||
2. Push them and resolve their registry manifest digests.
|
2. Push them and resolve their registry manifest digests.
|
||||||
3. Build Firecracker's orchestrator and gateway rootfs artifacts from the same
|
3. Build Firecracker's orchestrator and gateway rootfs artifacts from the same
|
||||||
source and pinned OCI bases.
|
source and pinned OCI bases.
|
||||||
@@ -158,6 +164,7 @@ immutable external bundle index under that SHA. The index contains:
|
|||||||
- schema version and source SHA;
|
- schema version and source SHA;
|
||||||
- wheel URL and SHA-256;
|
- wheel URL and SHA-256;
|
||||||
- orchestrator and gateway OCI digest references;
|
- orchestrator and gateway OCI digest references;
|
||||||
|
- Claude, Codex, and Pi agent OCI digest references;
|
||||||
- Firecracker package versions and compressed-file SHA-256 values;
|
- Firecracker package versions and compressed-file SHA-256 values;
|
||||||
- producing workflow/run identity and publication timestamp; and
|
- producing workflow/run identity and publication timestamp; and
|
||||||
- qualification records, if any, naming the test workflow and release tag.
|
- qualification records, if any, naming the test workflow and release tag.
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ def parser() -> argparse.ArgumentParser:
|
|||||||
result.add_argument("--source-commit", required=True)
|
result.add_argument("--source-commit", required=True)
|
||||||
result.add_argument("--orchestrator-image", required=True)
|
result.add_argument("--orchestrator-image", required=True)
|
||||||
result.add_argument("--gateway-image", required=True)
|
result.add_argument("--gateway-image", required=True)
|
||||||
|
for provider in ("claude", "codex", "pi"):
|
||||||
|
result.add_argument(f"--agent-{provider}-image", required=True)
|
||||||
for role in ("orchestrator", "gateway"):
|
for role in ("orchestrator", "gateway"):
|
||||||
result.add_argument(f"--firecracker-{role}-version", required=True)
|
result.add_argument(f"--firecracker-{role}-version", required=True)
|
||||||
result.add_argument(f"--firecracker-{role}-sha256", required=True)
|
result.add_argument(f"--firecracker-{role}-sha256", required=True)
|
||||||
@@ -29,6 +31,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
"oci": {
|
"oci": {
|
||||||
"orchestrator": args.orchestrator_image,
|
"orchestrator": args.orchestrator_image,
|
||||||
"gateway": args.gateway_image,
|
"gateway": args.gateway_image,
|
||||||
|
**{
|
||||||
|
f"agent_{provider}": getattr(args, f"agent_{provider}_image")
|
||||||
|
for provider in ("claude", "codex", "pi")
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"firecracker": {
|
"firecracker": {
|
||||||
role: {
|
role: {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class TestGenerateReleaseManifest(unittest.TestCase):
|
|||||||
"--orchestrator-image",
|
"--orchestrator-image",
|
||||||
"registry/orchestrator@sha256:" + "a" * 64,
|
"registry/orchestrator@sha256:" + "a" * 64,
|
||||||
"--gateway-image", "registry/gateway@sha256:" + "b" * 64,
|
"--gateway-image", "registry/gateway@sha256:" + "b" * 64,
|
||||||
|
"--agent-claude-image", "registry/claude@sha256:" + "e" * 64,
|
||||||
|
"--agent-codex-image", "registry/codex@sha256:" + "f" * 64,
|
||||||
|
"--agent-pi-image", "registry/pi@sha256:" + "0" * 64,
|
||||||
"--firecracker-orchestrator-version", "orch-v1",
|
"--firecracker-orchestrator-version", "orch-v1",
|
||||||
"--firecracker-orchestrator-sha256", "c" * 64,
|
"--firecracker-orchestrator-sha256", "c" * 64,
|
||||||
"--firecracker-gateway-version", "gateway-v1",
|
"--firecracker-gateway-version", "gateway-v1",
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ def manifest() -> dict[str, object]:
|
|||||||
"oci": {
|
"oci": {
|
||||||
"orchestrator": "registry/orchestrator@sha256:" + "a" * 64,
|
"orchestrator": "registry/orchestrator@sha256:" + "a" * 64,
|
||||||
"gateway": "registry/gateway@sha256:" + "b" * 64,
|
"gateway": "registry/gateway@sha256:" + "b" * 64,
|
||||||
|
"agent_claude": "registry/agent-claude@sha256:" + "e" * 64,
|
||||||
|
"agent_codex": "registry/agent-codex@sha256:" + "f" * 64,
|
||||||
|
"agent_pi": "registry/agent-pi@sha256:" + "0" * 64,
|
||||||
},
|
},
|
||||||
"firecracker": {
|
"firecracker": {
|
||||||
"orchestrator": {"version": "orch-v1", "sha256": "c" * 64},
|
"orchestrator": {"version": "orch-v1", "sha256": "c" * 64},
|
||||||
@@ -31,6 +34,7 @@ class TestParseManifest(unittest.TestCase):
|
|||||||
parsed = parse_manifest(manifest())
|
parsed = parse_manifest(manifest())
|
||||||
self.assertTrue(parsed.orchestrator_image.endswith("a" * 64))
|
self.assertTrue(parsed.orchestrator_image.endswith("a" * 64))
|
||||||
self.assertEqual("orch-v1", parsed.firecracker_orchestrator.version)
|
self.assertEqual("orch-v1", parsed.firecracker_orchestrator.version)
|
||||||
|
self.assertTrue(parsed.agent_images["codex"].endswith("f" * 64))
|
||||||
|
|
||||||
def test_rejects_mutable_oci_reference(self) -> None:
|
def test_rejects_mutable_oci_reference(self) -> None:
|
||||||
data = manifest()
|
data = manifest()
|
||||||
@@ -100,6 +104,17 @@ class TestImageSelection(unittest.TestCase):
|
|||||||
self.assertEqual(parsed.orchestrator_image, ref)
|
self.assertEqual(parsed.orchestrator_image, ref)
|
||||||
self.assertFalse(local)
|
self.assertFalse(local)
|
||||||
|
|
||||||
|
def test_packaged_install_selects_agent_reference(self) -> None:
|
||||||
|
parsed = parse_manifest(manifest())
|
||||||
|
with patch(
|
||||||
|
"bot_bottle.release_manifest.packaged_manifest", return_value=parsed,
|
||||||
|
), patch(
|
||||||
|
"bot_bottle.release_manifest.local_build_requested", return_value=False,
|
||||||
|
), patch.dict("os.environ", {}, clear=True):
|
||||||
|
ref, local = oci_image("agent_codex", "local:latest")
|
||||||
|
self.assertEqual(parsed.agent_images["codex"], ref)
|
||||||
|
self.assertFalse(local)
|
||||||
|
|
||||||
def test_mutable_override_fails_outside_local_mode(self) -> None:
|
def test_mutable_override_fails_outside_local_mode(self) -> None:
|
||||||
parsed = parse_manifest(manifest())
|
parsed = parse_manifest(manifest())
|
||||||
with patch(
|
with patch(
|
||||||
|
|||||||
Reference in New Issue
Block a user