fix(build): close remaining mutable image inputs
This commit is contained in:
@@ -116,7 +116,11 @@ def _layer_nested_containers(
|
||||
)
|
||||
info(f"using cached nested-container image {derived!r}")
|
||||
return derived
|
||||
return nested_containers_mod.build_image(agent_image, container_mod.build_image)
|
||||
return nested_containers_mod.build_image(
|
||||
agent_image,
|
||||
container_mod.build_image,
|
||||
container_mod.pinned_local_image_ref,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
||||
@@ -57,6 +57,7 @@ _GUEST_DEVICES = ("/dev/fuse", "/dev/net/tun")
|
||||
def build_image(
|
||||
base_image: str,
|
||||
build: Callable[..., None],
|
||||
pin_local_base: Callable[[str], str],
|
||||
) -> str:
|
||||
"""Layer the nested-container tooling onto an already-built agent image.
|
||||
|
||||
@@ -67,13 +68,15 @@ def build_image(
|
||||
# abstraction once that infrastructure exists.
|
||||
"""
|
||||
image = f"{base_image}{IMAGE_SUFFIX}"
|
||||
pinned_base = pin_local_base(base_image)
|
||||
init_script = Path(__file__).with_name("nested-containers-init.sh")
|
||||
with tempfile.TemporaryDirectory(prefix="bot-bottle-nested-containers.") as tmp:
|
||||
context = Path(tmp)
|
||||
shutil.copy2(init_script, context / "nested-containers-init.sh")
|
||||
(context / "Dockerfile").write_text(
|
||||
"FROM docker:28-cli AS docker_cli\n"
|
||||
f"FROM {base_image}\n"
|
||||
"ARG DOCKER_CLI_BASE_IMAGE\n"
|
||||
"FROM ${DOCKER_CLI_BASE_IMAGE} AS docker_cli\n"
|
||||
f"FROM {pinned_base}\n"
|
||||
"USER root\n"
|
||||
"COPY --from=docker_cli /usr/local/bin/docker /usr/local/bin/docker\n"
|
||||
"COPY --from=docker_cli /usr/local/libexec/docker/cli-plugins/"
|
||||
|
||||
@@ -682,6 +682,40 @@ def image_id(ref: str) -> str:
|
||||
raise AssertionError("unreachable")
|
||||
|
||||
|
||||
def pinned_local_image_ref(ref: str) -> str:
|
||||
"""Tag a local image with its complete content ID for a stable ``FROM``.
|
||||
|
||||
Agent images are immediately used as bases for the optional
|
||||
nested-containers layer. A content-derived tag prevents another concurrent
|
||||
build from moving the provider's ordinary ``:latest`` tag between those
|
||||
two builds.
|
||||
"""
|
||||
image = image_id(ref)
|
||||
digest = image.removeprefix("sha256:")
|
||||
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
|
||||
die(f"could not derive a local base tag from invalid image ID {image!r}")
|
||||
repository = ref.split("@", 1)[0]
|
||||
last_slash = repository.rfind("/")
|
||||
last_colon = repository.rfind(":")
|
||||
if last_colon > last_slash:
|
||||
repository = repository[:last_colon]
|
||||
pinned_ref = f"{repository}:sha256-{digest}"
|
||||
result = subprocess.run(
|
||||
[_CONTAINER, "image", "tag", image, pinned_ref],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
die(
|
||||
f"could not tag exact local image {image}: "
|
||||
f"{(result.stderr or result.stdout or '').strip() or '<no detail>'}"
|
||||
)
|
||||
if image_id(pinned_ref) != image:
|
||||
die(f"content-derived local tag {pinned_ref!r} did not resolve to {image}")
|
||||
return pinned_ref
|
||||
|
||||
|
||||
def image_created_at(ref: str) -> datetime | None:
|
||||
"""Return the image creation timestamp as an aware UTC datetime, or None
|
||||
when the field is absent or unparseable (e.g. FROM-scratch images, images
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
ARG NODE_BASE_IMAGE
|
||||
FROM ${NODE_BASE_IMAGE}
|
||||
|
||||
# The standalone installer is used below because remote-control requires its
|
||||
# managed package layout. Keep this exact release in sync with the verified
|
||||
# installer source and checksum.
|
||||
# Remote-control requires the standalone package layout. Keep this exact release
|
||||
# in sync with the committed upstream archive checksums.
|
||||
ARG CODEX_VERSION=0.145.0
|
||||
|
||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||
@@ -44,18 +43,29 @@ WORKDIR /home/node
|
||||
|
||||
ENV PATH="/home/node/.local/bin:${PATH}"
|
||||
|
||||
# Remote-control support requires the standalone Codex install layout under
|
||||
# ~/.codex/packages/standalone/current. Fetch the installer from the same
|
||||
# immutable release tag as the requested CLI, verify the committed checksum
|
||||
# before executing it, and let the official installer verify the selected
|
||||
# release archive against upstream release metadata.
|
||||
COPY --chown=node:node bot_bottle/contrib/codex/install.sh.sha256 /tmp/install.sh.sha256
|
||||
RUN curl -fsSL \
|
||||
"https://raw.githubusercontent.com/openai/codex/rust-v${CODEX_VERSION}/scripts/install/install.sh" \
|
||||
-o /tmp/install.sh \
|
||||
&& cd /tmp \
|
||||
&& sha256sum -c install.sh.sha256 \
|
||||
&& CODEX_NON_INTERACTIVE=1 sh /tmp/install.sh --release "${CODEX_VERSION}" \
|
||||
# Install the exact standalone release archive selected by the target
|
||||
# architecture. The checksum list is copied from the immutable upstream release
|
||||
# and committed so a rebuild cannot silently accept changed release bytes.
|
||||
COPY --chown=node:node bot_bottle/contrib/codex/codex-package_SHA256SUMS /tmp/codex-package_SHA256SUMS
|
||||
RUN case "$(dpkg --print-architecture)" in \
|
||||
amd64) codex_target=x86_64-unknown-linux-musl ;; \
|
||||
arm64) codex_target=aarch64-unknown-linux-musl ;; \
|
||||
*) echo "unsupported Codex architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
||||
esac \
|
||||
&& codex_asset="codex-package-${codex_target}.tar.gz" \
|
||||
&& codex_sha256="$(awk -v asset="${codex_asset}" '$2 == asset { print $1 }' /tmp/codex-package_SHA256SUMS)" \
|
||||
&& test -n "${codex_sha256}" \
|
||||
&& curl -fsSL \
|
||||
"https://github.com/openai/codex/releases/download/rust-v${CODEX_VERSION}/${codex_asset}" \
|
||||
-o "/tmp/${codex_asset}" \
|
||||
&& echo "${codex_sha256} /tmp/${codex_asset}" | sha256sum -c - \
|
||||
&& codex_release="/home/node/.codex/packages/standalone/releases/${CODEX_VERSION}-${codex_target}" \
|
||||
&& mkdir -p "${codex_release}" /home/node/.local/bin \
|
||||
&& tar -xzf "/tmp/${codex_asset}" -C "${codex_release}" \
|
||||
&& ln -s bin/codex "${codex_release}/codex" \
|
||||
&& ln -s "${codex_release}" /home/node/.codex/packages/standalone/current \
|
||||
&& ln -s /home/node/.codex/packages/standalone/current/bin/codex /home/node/.local/bin/codex \
|
||||
&& rm "/tmp/${codex_asset}" \
|
||||
&& test "$(codex --version)" = "codex-cli ${CODEX_VERSION}"
|
||||
|
||||
CMD ["codex"]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
54f79a05aba6f9abf8ef988abcae8bf2fcefba20beb549b4ff2b3acdb2cb6f54 codex-package-aarch64-unknown-linux-musl.tar.gz
|
||||
71a28d362c96ac9829bf8203a2c71be451aeb726adb843167fdaf0eae8fe7dd9 codex-package-x86_64-unknown-linux-musl.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
1154e9daf713aacd1534efca8042bfd6665ad24bc1d1dfd86b8f439fe60a7a5d install.sh
|
||||
+16
-2
@@ -52,7 +52,11 @@ BUNDLED_RESOURCES: tuple[str, ...] = (
|
||||
# tell for which layout we're in.
|
||||
_CHECKOUT_MARKER = "Dockerfile.gateway"
|
||||
_IMAGE_BUILD_ARGS_FILE = "image-build-args.json"
|
||||
_CENTRAL_BUILD_ARG_NAMES = frozenset({"NODE_BASE_IMAGE", "PYTHON_BASE_IMAGE"})
|
||||
_CENTRAL_BUILD_ARG_NAMES = frozenset({
|
||||
"DOCKER_CLI_BASE_IMAGE",
|
||||
"NODE_BASE_IMAGE",
|
||||
"PYTHON_BASE_IMAGE",
|
||||
})
|
||||
|
||||
|
||||
class ResourceError(RuntimeError):
|
||||
@@ -112,7 +116,17 @@ def image_build_args(
|
||||
wanted = declared & _CENTRAL_BUILD_ARG_NAMES
|
||||
if not wanted:
|
||||
return {}
|
||||
root = Path(context) if context is not None else build_root()
|
||||
# Generated Dockerfiles (notably the macOS nested-container layer) live in
|
||||
# temporary build contexts. Resolve their declaration there, but take the
|
||||
# centralized values from that context only when it carries its own input
|
||||
# file; otherwise use bot-bottle's staged build root.
|
||||
context_root = Path(context) if context is not None else None
|
||||
root = (
|
||||
context_root
|
||||
if context_root is not None
|
||||
and (context_root / _IMAGE_BUILD_ARGS_FILE).is_file()
|
||||
else build_root()
|
||||
)
|
||||
inputs_path = root / _IMAGE_BUILD_ARGS_FILE
|
||||
try:
|
||||
inputs = json.loads(inputs_path.read_text(encoding="utf-8"))
|
||||
|
||||
Reference in New Issue
Block a user