From 3d1ee7c6699a4a0caca3a95f1ed6ab735663ef23 Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Thu, 11 Jun 2026 09:53:54 -0400 Subject: [PATCH 1/2] fix(OMN-12965): stamp runtime image identity (version + revision) in workspace builds The main runtime image stamped org.opencontainers.image.version=0.1.0 with a blank org.opencontainers.image.revision after workspace rebuilds. A blank identity degrades every proof packet (runtime SHA + image digest are required citations in accepted evidence). Root causes (three build paths under-stamped identity): - onex up --build (cmd_up) passed only GIT_SHA; the runtime-stage OCI labels read VCS_REF (-> blank revision) and RUNTIME_VERSION (-> placeholder 0.1.0). - deploy-runtime.sh passed VCS_REF but not RUNTIME_VERSION/GIT_SHA. - Dockerfile silently allowed blank/placeholder identity in workspace mode. Fix: - cli._image_identity_build_args() stamps the full quad (GIT_SHA/VCS_REF/ RUNTIME_VERSION/BUILD_DATE) and fails fast on an unresolved git revision. - deploy-runtime.sh stamps RUNTIME_VERSION + GIT_SHA and verifies the version label is non-placeholder post-deploy. - Dockerfile.runtime fails workspace builds with blank VCS_REF or placeholder RUNTIME_VERSION=0.1.0 (release mode unaffected). Enforcement ratchet (same PR): - scripts/check_runtime_image_identity.py static check, wired as pre-commit hook + CI gate (ci.yml). - tests/unit/infra/test_runtime_image_identity_labels.py pins the cli helpers + Dockerfile guard; deploy-agent test extended for the quad. Proven locally via throwaway docker builds: workspace+args -> populated labels; workspace without args -> guard fails (exit 64); release without args -> 0.1.0 placeholder allowed (no regression). Evidence-Ticket: OMN-12965 --- .github/workflows/ci.yml | 7 + .pre-commit-config.yaml | 10 ++ docker/Dockerfile.runtime | 19 +++ scripts/check_runtime_image_identity.py | 141 ++++++++++++++++ .../tests/unit/test_executor_build_source.py | 12 ++ scripts/deploy-runtime.sh | 23 ++- src/omnibase_infra/docker/catalog/cli.py | 66 +++++++- .../test_runtime_image_identity_labels.py | 157 ++++++++++++++++++ 8 files changed, 431 insertions(+), 4 deletions(-) create mode 100644 scripts/check_runtime_image_identity.py create mode 100644 tests/unit/infra/test_runtime_image_identity_labels.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e01360e9ff..35d32fa7fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -982,6 +982,13 @@ jobs: echo "================================================================" uv run python scripts/check_dockerfile_pins.py + - name: Check runtime image identity stamping + run: | + echo "================================================================" + echo "Runtime Image Identity Check (OMN-12965)" + echo "================================================================" + uv run python scripts/check_runtime_image_identity.py + # ============================================================================ # ONEX Compliance Check # Runs `onex compliance check` to validate contracts, handlers, and schemas. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2862358dc..ce6d53a771 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -106,6 +106,16 @@ repos: files: ^src/omnibase_infra/nodes/[^/]+/handlers/.*\.py$ pass_filenames: false stages: [pre-commit] + # OMN-12965: enforce runtime image identity stamping (version + revision). + # A blank/placeholder identity degrades every proof packet. Guards both the + # Dockerfile workspace-mode identity guard and the cmd_up build-arg wiring. + - id: check-runtime-image-identity + name: Enforce runtime image identity labels (OMN-12965) + entry: uv run --frozen python scripts/check_runtime_image_identity.py + language: system + files: ^(docker/Dockerfile\.runtime|src/omnibase_infra/docker/catalog/cli\.py)$ + pass_filenames: false + stages: [pre-commit] # OMN-11069: Block new os.environ/os.getenv reads outside approved overlay modules - id: check-env-reads name: Block new os.environ reads (use overlay-resolved config) diff --git a/docker/Dockerfile.runtime b/docker/Dockerfile.runtime index ba6de76b33..926b5c281e 100644 --- a/docker/Dockerfile.runtime +++ b/docker/Dockerfile.runtime @@ -434,6 +434,25 @@ RUN set -eu; \ exit 64; \ fi +# OMN-12965: workspace-mode builds must stamp a real image identity. A blank +# VCS_REF (-> blank org.opencontainers.image.revision) or the placeholder +# RUNTIME_VERSION default (0.1.0) degrades every proof packet, since the runtime +# SHA + image digest are required citations in accepted evidence. Fail the build +# here rather than ship a blank-identity image. Release-mode builds may rely on +# the placeholder default (operator-driven release tooling pins them explicitly), +# so the guard is scoped to workspace mode. +RUN set -eu; \ + if [ "${BUILD_SOURCE}" = "workspace" ]; then \ + if [ -z "${VCS_REF}" ] || [ "${VCS_REF}" = "unknown" ]; then \ + echo "workspace build requires a non-empty VCS_REF for org.opencontainers.image.revision (OMN-12965)" >&2; \ + exit 64; \ + fi; \ + if [ -z "${RUNTIME_VERSION}" ] || [ "${RUNTIME_VERSION}" = "0.1.0" ]; then \ + echo "workspace build requires RUNTIME_VERSION from pyproject (not placeholder 0.1.0) for org.opencontainers.image.version (OMN-12965)" >&2; \ + exit 64; \ + fi; \ + fi + # OCI Image Labels (https://github.com/opencontainers/image-spec/blob/main/annotations.md) LABEL org.opencontainers.image.title="OmniBase Infra Runtime" \ org.opencontainers.image.description="ONEX Infrastructure Runtime Service - Contract-driven kernel for event-based infrastructure orchestration" \ diff --git a/scripts/check_runtime_image_identity.py b/scripts/check_runtime_image_identity.py new file mode 100644 index 0000000000..6b2b8a998f --- /dev/null +++ b/scripts/check_runtime_image_identity.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT + +"""Enforce runtime image identity stamping (OMN-12965). + +The deep dive found the main runtime image stamped +``org.opencontainers.image.version=0.1.0`` with a blank +``org.opencontainers.image.revision`` after workspace rebuilds. A blank identity +degrades every proof packet — the runtime SHA + image digest are required +citations in accepted evidence. + +This is the enforcement ratchet (pre-commit + CI) that prevents the build paths +from regressing to a blank/placeholder identity. It is a static check: no docker +build, no network. It verifies: + +1. ``docker/Dockerfile.runtime`` labels ``org.opencontainers.image.revision`` + from ``VCS_REF`` and ``org.opencontainers.image.version`` from + ``RUNTIME_VERSION``, and carries a workspace-mode guard that fails the build + when either would be blank/``unknown``/placeholder ``0.1.0``. +2. The operator rebuild path ``cmd_up`` in + ``src/omnibase_infra/docker/catalog/cli.py`` stamps the identity quad via + ``_image_identity_build_args()`` rather than a bare ``GIT_SHA`` build-arg. + +Usage:: + + # From the repo root + uv run python scripts/check_runtime_image_identity.py + +Exit codes: + 0 — all checks passed + 1 — one or more checks failed (stderr has details) +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[1] +_DOCKERFILE = _REPO_ROOT / "docker" / "Dockerfile.runtime" +_CLI = _REPO_ROOT / "src" / "omnibase_infra" / "docker" / "catalog" / "cli.py" + + +def _collapse_continuations(text: str) -> str: + """Collapse shell ``\\``-continued physical lines into single logical lines.""" + return re.sub(r"\\\n\s*", " ", text) + + +def _check_dockerfile() -> list[str]: + errors: list[str] = [] + if not _DOCKERFILE.is_file(): + return [f"missing {_DOCKERFILE}"] + collapsed = _collapse_continuations(_DOCKERFILE.read_text(encoding="utf-8")) + + if 'org.opencontainers.image.revision="${VCS_REF}"' not in collapsed: + errors.append( + "Dockerfile.runtime must label org.opencontainers.image.revision " + 'from "${VCS_REF}" (OMN-12965).' + ) + if 'org.opencontainers.image.version="${RUNTIME_VERSION}"' not in collapsed: + errors.append( + "Dockerfile.runtime must label org.opencontainers.image.version " + 'from "${RUNTIME_VERSION}" (OMN-12965).' + ) + + guard = re.search( + r'if \[ "\$\{BUILD_SOURCE\}" = "workspace" \];.*?VCS_REF.*?' + r"exit 64.*?RUNTIME_VERSION.*?0\.1\.0.*?exit 64", + collapsed, + re.DOTALL, + ) + if not guard: + errors.append( + "Dockerfile.runtime must guard workspace builds against blank VCS_REF " + "and placeholder RUNTIME_VERSION=0.1.0 (OMN-12965)." + ) + return errors + + +def _function_body(source: str, name: str) -> str | None: + lines = source.splitlines() + start = next( + (i for i, line in enumerate(lines) if line.startswith(f"def {name}(")), + None, + ) + if start is None: + return None + end = next( + ( + i + for i in range(start + 1, len(lines)) + if lines[i] and not lines[i][0].isspace() + ), + len(lines), + ) + return "\n".join(lines[start:end]) + + +def _check_cli() -> list[str]: + errors: list[str] = [] + if not _CLI.is_file(): + return [f"missing {_CLI}"] + source = _CLI.read_text(encoding="utf-8") + + body = _function_body(source, "cmd_up") + if body is None: + return ["cmd_up not found in cli.py"] + + if "_image_identity_build_args()" not in body: + errors.append( + "cmd_up --build must stamp the identity quad via " + "_image_identity_build_args() (OMN-12965)." + ) + if "GIT_SHA=" in body: + errors.append( + "cmd_up must not hand-roll a GIT_SHA build-arg; delegate to " + "_image_identity_build_args() so VCS_REF + RUNTIME_VERSION are " + "stamped (OMN-12965)." + ) + return errors + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.parse_args(argv) + + errors = _check_dockerfile() + _check_cli() + if errors: + print("Runtime image identity check FAILED (OMN-12965):", file=sys.stderr) + for err in errors: + print(f" - {err}", file=sys.stderr) + return 1 + print("Runtime image identity check passed (OMN-12965).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/deploy-agent/tests/unit/test_executor_build_source.py b/scripts/deploy-agent/tests/unit/test_executor_build_source.py index 97237a7024..eaca3c868c 100644 --- a/scripts/deploy-agent/tests/unit/test_executor_build_source.py +++ b/scripts/deploy-agent/tests/unit/test_executor_build_source.py @@ -85,6 +85,18 @@ def fake_run(cmd: list[str], timeout: int, **kwargs) -> subprocess.CompletedProc assert "EXPECTED_BUILD_SOURCE=workspace" in build_cmd assert "OMNI_HOME=/data/omninode/omni_home" in build_cmd + # OMN-12965: the workspace build must stamp the full OCI image-identity quad + # so org.opencontainers.image.{version,revision,created} populate. A blank + # revision / placeholder version degrades every proof packet. + assert "GIT_SHA=abc1234" in build_cmd + assert "VCS_REF=abc1234" in build_cmd + runtime_version_args = [a for a in build_cmd if a.startswith("RUNTIME_VERSION=")] + assert runtime_version_args, "missing RUNTIME_VERSION build-arg (OMN-12965)" + assert runtime_version_args[0] != "RUNTIME_VERSION=0.1.0", ( + "RUNTIME_VERSION must come from pyproject, not the Dockerfile placeholder" + ) + assert any(a.startswith("BUILD_DATE=") for a in build_cmd) + def test_unknown_build_source_fails_before_compose_build( monkeypatch: pytest.MonkeyPatch, diff --git a/scripts/deploy-runtime.sh b/scripts/deploy-runtime.sh index 5082c4de7a..b72e1a3487 100755 --- a/scripts/deploy-runtime.sh +++ b/scripts/deploy-runtime.sh @@ -1349,6 +1349,11 @@ build_images() { local build_date build_date="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + # OMN-12965: stamp org.opencontainers.image.version from pyproject so the + # runtime image carries a real version instead of the Dockerfile placeholder + # (0.1.0). A placeholder version degrades every proof packet. + local runtime_version + runtime_version="$(read_version "${deploy_target}")" local omni_home="${OMNI_HOME:-}" local build_source build_source="$(resolve_build_source)" @@ -1375,7 +1380,9 @@ build_images() { --profile "${COMPOSE_PROFILE}" build --progress=plain + --build-arg "GIT_SHA=${git_sha}" --build-arg "VCS_REF=${git_sha}" + --build-arg "RUNTIME_VERSION=${runtime_version}" --build-arg "BUILD_DATE=${build_date}" --build-arg "RUNTIME_SOURCE_HASH=${git_sha}" --build-arg "COMPOSE_PROJECT=${compose_project}" @@ -1387,7 +1394,7 @@ build_images() { --build-arg "ONEX_CHANGE_CONTROL_REF=${occ_ref}" ) - log_info "Building images with VCS_REF=${git_sha} RUNTIME_SOURCE_HASH=${git_sha} COMPOSE_PROJECT=${compose_project}..." + log_info "Building images with VCS_REF=${git_sha} RUNTIME_VERSION=${runtime_version} RUNTIME_SOURCE_HASH=${git_sha} COMPOSE_PROJECT=${compose_project}..." log_info "Build source: BUILD_SOURCE=${build_source} EXPECTED_BUILD_SOURCE=${expected_build_source} OMNI_HOME=${omni_home}" log_info "Plugin refs: OMNIBASE_COMPAT_REF=${compat_ref} OMNIMARKET_REF=${omnimarket_ref} ONEX_CHANGE_CONTROL_REF=${occ_ref}" log_info "Build timeout: ${build_timeout}s (set DOCKER_BUILD_TIMEOUT_SECONDS to override)" @@ -1552,6 +1559,20 @@ verify_deployment() { log_warn "Could not read image label (container may not exist yet)." fi + # OMN-12965: verify org.opencontainers.image.version is a real version, not + # the Dockerfile placeholder (0.1.0) or blank. A placeholder/blank identity + # degrades every proof packet (runtime SHA + image digest are required + # citations in accepted evidence). + local version_label + version_label="$(docker inspect "${container_id}" \ + --format='{{index .Config.Labels "org.opencontainers.image.version"}}' 2>/dev/null || true)" + if [[ -z "${version_label}" || "${version_label}" == "0.1.0" ]]; then + log_error "Image version label is blank/placeholder: org.opencontainers.image.version='${version_label}'" + log_error "Runtime image identity is degraded (OMN-12965). Rebuild with RUNTIME_VERSION from pyproject." + exit 1 + fi + log_info "Image version label OK: org.opencontainers.image.version=${version_label}" + # 4. Log sentinel: entrypoint ran log_info "Checking log sentinels..." local logs diff --git a/src/omnibase_infra/docker/catalog/cli.py b/src/omnibase_infra/docker/catalog/cli.py index 17b49337fd..c592fa22c4 100644 --- a/src/omnibase_infra/docker/catalog/cli.py +++ b/src/omnibase_infra/docker/catalog/cli.py @@ -17,9 +17,11 @@ from __future__ import annotations +import datetime import os import subprocess import sys +import tomllib from pathlib import Path import yaml @@ -183,6 +185,66 @@ def _current_git_sha() -> str: return "unknown" +def _runtime_version() -> str: + """Return the runtime package version stamped as the OCI image version. + + Read from the repo ``pyproject.toml`` so ``org.opencontainers.image.version`` + reflects the real package version rather than the Dockerfile placeholder + default (``0.1.0``). Fails fast if the version cannot be resolved so a build + never silently stamps a placeholder identity (OMN-12965). + """ + pyproject = _REPO_ROOT / "pyproject.toml" + try: + data = tomllib.loads(pyproject.read_text(encoding="utf-8")) + version = str(data["project"]["version"]).strip() + except (FileNotFoundError, KeyError, TypeError, tomllib.TOMLDecodeError) as exc: + raise RuntimeError( + f"Could not resolve RUNTIME_VERSION from {pyproject}; " + "refusing to build an image with placeholder identity (OMN-12965)." + ) from exc + if not version: + raise RuntimeError( + f"Empty RUNTIME_VERSION in {pyproject}; " + "refusing to build an image with placeholder identity (OMN-12965)." + ) + return version + + +def _image_identity_build_args() -> list[str]: + """Return the OCI image-identity ``--build-arg`` pairs for a runtime build. + + Stamps the full quad consumed by the runtime-stage OCI labels and provenance + manifest: ``GIT_SHA`` (busts the COPY src/ layer + builder-stage revision + label), ``VCS_REF`` (runtime-stage ``org.opencontainers.image.revision``), + ``RUNTIME_VERSION`` (``org.opencontainers.image.version``), and ``BUILD_DATE`` + (``org.opencontainers.image.created``). + + Fails fast when the git SHA is unresolved: a blank/``unknown`` revision + produces a blank-identity image that degrades every proof packet (the runtime + SHA + image digest are required citations in accepted evidence). This is the + enforcement point for OMN-12965 — never stamp a placeholder identity. + """ + git_sha = _current_git_sha() + if not git_sha or git_sha == "unknown": + raise RuntimeError( + "Cannot resolve git revision for the runtime image build. A blank or " + "'unknown' org.opencontainers.image.revision degrades every proof " + "packet (OMN-12965). Run the build from a clean git checkout of " + f"{_REPO_ROOT}." + ) + build_date = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") + return [ + "--build-arg", + f"GIT_SHA={git_sha}", + "--build-arg", + f"VCS_REF={git_sha}", + "--build-arg", + f"RUNTIME_VERSION={_runtime_version()}", + "--build-arg", + f"BUILD_DATE={build_date}", + ] + + def cmd_seed(_args: list[str]) -> int: """Seed Infisical with config keys from contracts.""" return _run_seed() @@ -236,7 +298,6 @@ def cmd_up(args: list[str]) -> int: # Build if requested (OMN-7214) if force_build: print("Rebuilding images (--build)...") - git_sha = _current_git_sha() build_proc = subprocess.run( [ "docker", @@ -244,8 +305,7 @@ def cmd_up(args: list[str]) -> int: "-f", _DEFAULT_OUTPUT, "build", - "--build-arg", - f"GIT_SHA={git_sha}", + *_image_identity_build_args(), ], cwd=str(_REPO_ROOT), check=False, diff --git a/tests/unit/infra/test_runtime_image_identity_labels.py b/tests/unit/infra/test_runtime_image_identity_labels.py new file mode 100644 index 0000000000..289cb27777 --- /dev/null +++ b/tests/unit/infra/test_runtime_image_identity_labels.py @@ -0,0 +1,157 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Enforcement ratchet for OMN-12965 — runtime image identity labels. + +The deep dive found the main runtime image stamped +``org.opencontainers.image.version=0.1.0`` with a blank +``org.opencontainers.image.revision`` after workspace rebuilds. A blank identity +degrades EVERY proof packet, because the runtime SHA + image digest are required +citations in accepted evidence. + +Root cause: the operator rebuild path ``onex up --build`` (``cmd_up`` in +``omnibase_infra.docker.catalog.cli``) passed only ``GIT_SHA`` as a build arg. +The runtime-stage OCI labels read ``VCS_REF`` (-> blank revision) and +``RUNTIME_VERSION`` (-> placeholder ``0.1.0``), neither of which were passed. + +These tests pin the fix so it cannot regress: + +1. ``_image_identity_build_args`` stamps the full identity quad + (GIT_SHA / VCS_REF / RUNTIME_VERSION / BUILD_DATE). +2. ``_image_identity_build_args`` fails fast when the git revision cannot be + resolved, rather than stamping a blank/``unknown`` revision. +3. ``_runtime_version`` resolves the real package version (never the Dockerfile + placeholder). +4. ``Dockerfile.runtime`` carries a workspace-mode guard that fails the build + when VCS_REF or RUNTIME_VERSION would be blank/placeholder. +""" + +from __future__ import annotations + +import re +import tomllib +from pathlib import Path + +import pytest + +from omnibase_infra.docker.catalog import cli + +pytestmark = pytest.mark.unit + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_DOCKERFILE = _REPO_ROOT / "docker" / "Dockerfile.runtime" + +_IDENTITY_ARGS = ("GIT_SHA", "VCS_REF", "RUNTIME_VERSION", "BUILD_DATE") + + +def _parse_build_args(args: list[str]) -> dict[str, str]: + """Collapse a ``--build-arg KEY=VALUE`` list into a dict.""" + parsed: dict[str, str] = {} + it = iter(args) + for token in it: + if token == "--build-arg": + key, _, value = next(it).partition("=") + parsed[key] = value + return parsed + + +def test_runtime_version_matches_pyproject() -> None: + """_runtime_version must return the real package version, not the placeholder.""" + data = tomllib.loads((_REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + expected = str(data["project"]["version"]).strip() + assert cli._runtime_version() == expected + assert cli._runtime_version() != "0.1.0", ( + "runtime version must not be the Dockerfile placeholder 0.1.0 (OMN-12965)" + ) + + +def test_identity_build_args_stamp_full_quad( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The identity quad must be present so the runtime-stage labels populate.""" + monkeypatch.setattr(cli, "_current_git_sha", lambda: "deadbeefcafef00d") + parsed = _parse_build_args(cli._image_identity_build_args()) + + for key in _IDENTITY_ARGS: + assert key in parsed, f"missing --build-arg {key} (OMN-12965)" + + # VCS_REF (-> org.opencontainers.image.revision) must carry the real SHA. + assert parsed["VCS_REF"] == "deadbeefcafef00d" + assert parsed["GIT_SHA"] == "deadbeefcafef00d" + # RUNTIME_VERSION (-> org.opencontainers.image.version) must be the real version. + assert parsed["RUNTIME_VERSION"] == cli._runtime_version() + assert parsed["RUNTIME_VERSION"] != "0.1.0" + # BUILD_DATE (-> org.opencontainers.image.created) must be an RFC3339 stamp. + assert re.fullmatch( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", parsed["BUILD_DATE"] + ), parsed["BUILD_DATE"] + + +@pytest.mark.parametrize("bad_sha", ["", "unknown"]) +def test_identity_build_args_fail_fast_on_unresolved_revision( + monkeypatch: pytest.MonkeyPatch, bad_sha: str +) -> None: + """A blank/unknown revision must abort the build, not ship blank identity.""" + monkeypatch.setattr(cli, "_current_git_sha", lambda: bad_sha) + with pytest.raises(RuntimeError, match="git revision"): + cli._image_identity_build_args() + + +def test_cmd_up_build_path_uses_identity_args() -> None: + """cmd_up's --build branch must call the identity-arg helper, not bare GIT_SHA. + + Static guard scoped to the ``cmd_up`` function body: the build branch must + spread ``_image_identity_build_args()`` and must NOT hand-roll a lone + ``GIT_SHA`` build-arg (the pre-fix regression that left VCS_REF + + RUNTIME_VERSION unstamped → blank-identity image, OMN-12965). + """ + source = Path(cli.__file__).read_text(encoding="utf-8") + body = _function_body(source, "cmd_up") + assert "_image_identity_build_args()" in body, ( + "cmd_up --build must stamp the identity quad via _image_identity_build_args()" + ) + assert "GIT_SHA=" not in body, ( + "cmd_up must not hand-roll a GIT_SHA build-arg; delegate to " + "_image_identity_build_args() so VCS_REF + RUNTIME_VERSION are stamped " + "(OMN-12965)." + ) + + +def _function_body(source: str, name: str) -> str: + """Return the source text of a top-level ``def name(...)`` block.""" + lines = source.splitlines() + start = next( + (i for i, line in enumerate(lines) if line.startswith(f"def {name}(")), + None, + ) + assert start is not None, f"function {name} not found in cli.py" + end = next( + ( + i + for i in range(start + 1, len(lines)) + if lines[i] and not lines[i][0].isspace() + ), + len(lines), + ) + return "\n".join(lines[start:end]) + + +def test_dockerfile_workspace_identity_guard_present() -> None: + """Dockerfile.runtime must fail workspace builds with blank/placeholder identity.""" + text = _DOCKERFILE.read_text(encoding="utf-8") + collapsed = re.sub(r"\\\n\s*", " ", text) + + # The runtime stage must label revision from VCS_REF and version from RUNTIME_VERSION. + assert 'org.opencontainers.image.revision="${VCS_REF}"' in collapsed + assert 'org.opencontainers.image.version="${RUNTIME_VERSION}"' in collapsed + + # Workspace-mode guard: reject blank/unknown VCS_REF. + guard = re.search( + r'if \[ "\$\{BUILD_SOURCE\}" = "workspace" \];.*?VCS_REF.*?' + r"exit 64.*?RUNTIME_VERSION.*?0\.1\.0.*?exit 64", + collapsed, + re.DOTALL, + ) + assert guard, ( + "Dockerfile.runtime must guard workspace builds against blank VCS_REF and " + "placeholder RUNTIME_VERSION=0.1.0 (OMN-12965)." + ) From fbccd984db1d559d2f8f0d34ba611c0d533eac14 Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Thu, 11 Jun 2026 10:08:55 -0400 Subject: [PATCH 2/2] test(OMN-12965): integration build proof for runtime image identity labels Builds the real runtime-stage ARG/LABEL/guard block against busybox and asserts via docker inspect: workspace+args -> populated version/revision; workspace without args -> guard fails (exit 64); release -> placeholder allowed. Satisfies the integration-test hard gate and makes the throwaway proof permanent. Evidence-Ticket: OMN-12965 --- .../test_runtime_image_identity_build.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/integration/docker/test_runtime_image_identity_build.py diff --git a/tests/integration/docker/test_runtime_image_identity_build.py b/tests/integration/docker/test_runtime_image_identity_build.py new file mode 100644 index 0000000000..064cc53206 --- /dev/null +++ b/tests/integration/docker/test_runtime_image_identity_build.py @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Integration coverage for runtime image identity stamping (OMN-12965). + +These tests build the runtime-stage identity block (ARG + LABEL + workspace +guard) extracted verbatim from ``docker/Dockerfile.runtime`` against a tiny base +image and assert, via ``docker inspect``, that: + +1. A workspace build WITH the identity quad populates + ``org.opencontainers.image.version`` and ``.revision`` (the fixed behavior). +2. A workspace build WITHOUT the identity args FAILS the guard (the broken + blank-identity image is refused, exit 64). +3. A release build without args is allowed to carry the placeholder default + (no regression for release tooling). + +This makes the local throwaway-build proof a permanent gate. The block under +test is the real runtime-stage logic, so a regression in the Dockerfile guard or +label wiring is caught here. +""" + +from __future__ import annotations + +import re +import subprocess +import uuid +from pathlib import Path + +import pytest + +pytestmark = [pytest.mark.integration, pytest.mark.slow] + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_DOCKERFILE = _REPO_ROOT / "docker" / "Dockerfile.runtime" + + +def _runtime_identity_block() -> str: + """Extract the runtime-stage ARG/guard/LABEL identity block from the real Dockerfile. + + Pulls the exact lines under test so the integration build exercises the + shipped logic, not a hand-copied paraphrase. Spans from the runtime-stage + ``ARG RUNTIME_VERSION`` line through the end of the OCI ``LABEL`` block. + """ + text = _DOCKERFILE.read_text(encoding="utf-8") + # Runtime stage begins at the second `FROM python:...-slim`. Slice from the + # runtime-stage RUNTIME_VERSION arg to the end of the first LABEL block after + # it (the OCI image labels including version/revision). + runtime_start = text.index("FROM python:${PYTHON_VERSION}-slim AS runtime") + runtime_text = text[runtime_start:] + arg_idx = runtime_text.index("ARG RUNTIME_VERSION=0.1.0") + label_idx = runtime_text.index( + 'com.omninode.workspace_provenance_manifest="/app/build-provenance.json"' + ) + # Extend to the end of that physical line. + label_end = runtime_text.index("\n", label_idx) + return runtime_text[arg_idx : label_end + 1] + + +def _write_proof_dockerfile(tmp_path: Path) -> Path: + """Write a throwaway Dockerfile wrapping the real identity block on busybox.""" + block = _runtime_identity_block() + # The block references BUILD_SOURCE/EXPECTED_BUILD_SOURCE/OMNI_HOME args that + # the runtime stage declares earlier; declare them here so the slice builds. + proof = "FROM busybox:latest AS runtime\n" + block + dockerfile = tmp_path / "Dockerfile.identity-proof" + dockerfile.write_text(proof, encoding="utf-8") + return dockerfile + + +def _build( + dockerfile: Path, context: Path, tag: str, build_args: dict[str, str] +) -> subprocess.CompletedProcess[str]: + cmd = ["docker", "build", "-f", str(dockerfile), "-t", tag] + for key, value in build_args.items(): + cmd += ["--build-arg", f"{key}={value}"] + cmd.append(str(context)) + return subprocess.run(cmd, capture_output=True, text=True, check=False) + + +def _inspect_label(tag: str, label: str) -> str: + result = subprocess.run( + [ + "docker", + "inspect", + tag, + "--format", + f'{{{{index .Config.Labels "{label}"}}}}', + ], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +@pytest.mark.integration +def test_workspace_build_with_identity_args_populates_labels( + skip_if_no_docker: None, tmp_path: Path +) -> None: + """Workspace build + identity quad → populated version + revision labels.""" + dockerfile = _write_proof_dockerfile(tmp_path) + tag = f"omn12965-good-{uuid.uuid4().hex[:8]}" + try: + result = _build( + dockerfile, + tmp_path, + tag, + { + "BUILD_SOURCE": "workspace", + "EXPECTED_BUILD_SOURCE": "workspace", + "OMNI_HOME": "/x", + "RUNTIME_VERSION": "0.38.3", + "VCS_REF": "abc123def456", + "BUILD_DATE": "2026-06-11T00:00:00Z", + }, + ) + assert result.returncode == 0, f"build failed: {result.stderr}" + assert _inspect_label(tag, "org.opencontainers.image.version") == "0.38.3" + assert ( + _inspect_label(tag, "org.opencontainers.image.revision") == "abc123def456" + ) + assert re.fullmatch( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", + _inspect_label(tag, "org.opencontainers.image.created"), + ) + finally: + subprocess.run(["docker", "rmi", "-f", tag], capture_output=True, check=False) + + +@pytest.mark.integration +def test_workspace_build_without_identity_args_fails_guard( + skip_if_no_docker: None, tmp_path: Path +) -> None: + """Workspace build without identity args → guard fails (blank identity refused).""" + dockerfile = _write_proof_dockerfile(tmp_path) + tag = f"omn12965-bad-{uuid.uuid4().hex[:8]}" + try: + result = _build( + dockerfile, + tmp_path, + tag, + { + "BUILD_SOURCE": "workspace", + "EXPECTED_BUILD_SOURCE": "workspace", + "OMNI_HOME": "/x", + }, + ) + assert result.returncode != 0, ( + "workspace build with blank identity must fail the guard (OMN-12965)" + ) + assert "OMN-12965" in (result.stderr + result.stdout) + finally: + subprocess.run(["docker", "rmi", "-f", tag], capture_output=True, check=False) + + +@pytest.mark.integration +def test_release_build_allows_placeholder_default( + skip_if_no_docker: None, tmp_path: Path +) -> None: + """Release build without args keeps the placeholder default (no regression).""" + dockerfile = _write_proof_dockerfile(tmp_path) + tag = f"omn12965-rel-{uuid.uuid4().hex[:8]}" + try: + result = _build(dockerfile, tmp_path, tag, {"BUILD_SOURCE": "release"}) + assert result.returncode == 0, f"release build failed: {result.stderr}" + assert _inspect_label(tag, "org.opencontainers.image.version") == "0.1.0" + finally: + subprocess.run(["docker", "rmi", "-f", tag], capture_output=True, check=False)