From 1a43b34a939c21ea69906b0c1c50eca38a4393d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 16:30:59 +0900 Subject: [PATCH 01/28] fix(containers): bind provenance to pinned bases --- .github/workflows/docker-publish.yml | 60 +++++++-- Dockerfile | 8 +- Dockerfile.ollama | 2 +- backend/tests/test_release_governance.py | 158 +++++++++-------------- backend/tests/test_repo_hygiene.py | 2 +- frontend/Dockerfile | 6 +- 6 files changed, 120 insertions(+), 116 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 54652af73..b62014a73 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -32,18 +32,21 @@ jobs: - component: backend image: ai_email_client-backend dockerfile: Dockerfile + base_dockerfile: Dockerfile context: . build_args: | BUILDKIT_INLINE_CACHE=1 - component: naruon image: naruon dockerfile: Dockerfile + base_dockerfile: Dockerfile context: . build_args: | BUILDKIT_INLINE_CACHE=1 - component: frontend image: ai_email_client-frontend dockerfile: frontend/Dockerfile + base_dockerfile: frontend/Dockerfile context: . build_args: | BUILDKIT_INLINE_CACHE=1 @@ -62,9 +65,28 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + - name: Resolve pinned Ollama base manifest + if: matrix.component == 'naruon' + run: | + base_image="$(awk 'toupper($1) == "FROM" { print $2; exit }' Dockerfile.ollama)" + if ! printf '%s\n' "$base_image" | grep -Eq '^ollama/ollama@sha256:[0-9a-f]{64}$'; then + printf '::error file=Dockerfile.ollama,line=1::Expected an exact ollama/ollama sha256 base pin; found %s\n' "$base_image" + exit 1 + fi + printf 'Resolving pinned Ollama base manifest: %s\n' "$base_image" + manifest_output="$(docker buildx imagetools inspect "$base_image")" + printf '%s\n' "$manifest_output" + for platform in linux/amd64 linux/arm64; do + if ! printf '%s\n' "$manifest_output" | grep -Eq "^[[:space:]]*Platform:[[:space:]]+${platform}[[:space:]]*$"; then + printf '::error file=Dockerfile.ollama,line=1::Pinned Ollama manifest is missing %s\n' "$platform" + exit 1 + fi + done + - name: Prepare OCI annotation values id: oci env: + BASE_DOCKERFILE: ${{ matrix.base_dockerfile }} GIT_REF_NAME: ${{ github.ref_name }} IMAGE_COMPONENT: ${{ matrix.component }} IMAGE_NAME: ${{ matrix.image }} @@ -74,24 +96,29 @@ jobs: version="$(cat VERSION)" created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" vendor="${REPOSITORY%%/*}" + base_reference="$(awk 'toupper($1) == "FROM" { print $2; exit }' "$BASE_DOCKERFILE")" + if ! printf '%s\n' "$base_reference" | grep -Eq '^[A-Za-z0-9._/-]+:[A-Za-z0-9._-]+@sha256:[0-9a-f]{64}$'; then + printf '::error file=%s,line=1::Expected an exact tagged sha256 base pin; found %s\n' "$BASE_DOCKERFILE" "$base_reference" + exit 1 + fi + base_digest="${base_reference##*@}" + base_repository="${base_reference%@*}" + case "$base_repository" in + */*) base_name="$base_reference" ;; + *) base_name="docker.io/library/$base_reference" ;; + esac case "$IMAGE_COMPONENT" in frontend) title="naruon frontend" description="Naruon Next.js frontend runtime image" - base_digest="sha256:191ef878ecb351d68b78219593de18bd8942afd59af59f29960dc4b24805a3f1" - base_name="docker.io/library/node:26-slim@${base_digest}" ;; backend) title="naruon backend" description="Naruon FastAPI backend runtime image" - base_digest="sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061" - base_name="docker.io/library/python:3.14-slim@${base_digest}" ;; *) title="naruon" description="Naruon combined FastAPI and Next.js runtime image" - base_digest="sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061" - base_name="docker.io/library/python:3.14-slim@${base_digest}" ;; esac { @@ -156,18 +183,21 @@ jobs: - component: backend image: ai_email_client-backend dockerfile: Dockerfile + base_dockerfile: Dockerfile context: . build_args: | BUILDKIT_INLINE_CACHE=1 - component: naruon image: naruon dockerfile: Dockerfile + base_dockerfile: Dockerfile context: . build_args: | BUILDKIT_INLINE_CACHE=1 - component: frontend image: ai_email_client-frontend dockerfile: frontend/Dockerfile + base_dockerfile: frontend/Dockerfile context: . build_args: | BUILDKIT_INLINE_CACHE=1 @@ -200,6 +230,7 @@ jobs: - name: Prepare OCI annotation values id: oci env: + BASE_DOCKERFILE: ${{ matrix.base_dockerfile }} GIT_REF_NAME: ${{ github.ref_name }} IMAGE_COMPONENT: ${{ matrix.component }} IMAGE_NAME: ${{ matrix.image }} @@ -210,24 +241,29 @@ jobs: version="${VERSION_VALUE:-$(cat VERSION)}" created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" vendor="${REPOSITORY%%/*}" + base_reference="$(awk 'toupper($1) == "FROM" { print $2; exit }' "$BASE_DOCKERFILE")" + if ! printf '%s\n' "$base_reference" | grep -Eq '^[A-Za-z0-9._/-]+:[A-Za-z0-9._-]+@sha256:[0-9a-f]{64}$'; then + printf '::error file=%s,line=1::Expected an exact tagged sha256 base pin; found %s\n' "$BASE_DOCKERFILE" "$base_reference" + exit 1 + fi + base_digest="${base_reference##*@}" + base_repository="${base_reference%@*}" + case "$base_repository" in + */*) base_name="$base_reference" ;; + *) base_name="docker.io/library/$base_reference" ;; + esac case "$IMAGE_COMPONENT" in frontend) title="naruon frontend" description="Naruon Next.js frontend runtime image" - base_digest="sha256:191ef878ecb351d68b78219593de18bd8942afd59af59f29960dc4b24805a3f1" - base_name="docker.io/library/node:26-slim@${base_digest}" ;; backend) title="naruon backend" description="Naruon FastAPI backend runtime image" - base_digest="sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061" - base_name="docker.io/library/python:3.14-slim@${base_digest}" ;; *) title="naruon" description="Naruon combined FastAPI and Next.js runtime image" - base_digest="sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061" - base_name="docker.io/library/python:3.14-slim@${base_digest}" ;; esac { diff --git a/Dockerfile b/Dockerfile index d51e6dafc..4dccf2d7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Backend runtime for local Compose and backend-only deployments -FROM python:3.14-slim@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 AS backend-runtime +FROM python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6 AS backend-runtime WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 @@ -25,7 +25,7 @@ EXPOSE 8000 CMD ["python", "scripts/start_backend.py", "--host", "0.0.0.0", "--port", "8000"] # Stage 2: Build Frontend -FROM node:26-slim@sha256:ffc78385a788964bb3cbab5e434ff79a10bdc25b8ae6db03fe5fe6cb14053c09 AS frontend-builder +FROM node:26-slim@sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583 AS frontend-builder WORKDIR /app ENV NPM_CONFIG_UPDATE_NOTIFIER=false ENV PNPM_VERSION=11.5.3 @@ -63,8 +63,8 @@ ARG OCI_IMAGE_LICENSES="LicenseRef-Naruon-Proprietary" ARG OCI_IMAGE_REF_NAME="" ARG OCI_IMAGE_TITLE="naruon" ARG OCI_IMAGE_DESCRIPTION="Naruon combined FastAPI and Next.js runtime image" -ARG OCI_IMAGE_BASE_DIGEST="sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061" -ARG OCI_IMAGE_BASE_NAME="docker.io/library/python:3.14-slim@sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061" +ARG OCI_IMAGE_BASE_DIGEST="sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6" +ARG OCI_IMAGE_BASE_NAME="docker.io/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6" LABEL org.opencontainers.image.created="${OCI_IMAGE_CREATED}" \ org.opencontainers.image.authors="${OCI_IMAGE_AUTHORS}" \ diff --git a/Dockerfile.ollama b/Dockerfile.ollama index d4b369689..734f04a8c 100644 --- a/Dockerfile.ollama +++ b/Dockerfile.ollama @@ -1,4 +1,4 @@ -FROM ollama/ollama@sha256:509fdf54e23bd50d87af646cb51c0a7a203d6a83cc4d6695b3b08c5be1c62c0a +FROM ollama/ollama@sha256:6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0 ENV OLLAMA_MODELS=/usr/share/ollama/.ollama/models diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py index 56ef2bff4..6216e0222 100644 --- a/backend/tests/test_release_governance.py +++ b/backend/tests/test_release_governance.py @@ -12,7 +12,6 @@ import re import sys import importlib.util -import tomllib from pathlib import Path import pytest @@ -54,6 +53,30 @@ def assert_dockerfile_stage_from(dockerfile: str, image: str, stage_alias: str) ) +def first_dockerfile_base_reference(dockerfile: str) -> str: + """Return the first exact tag-and-digest Dockerfile base reference.""" + first_from = re.search(r"^FROM (?P.+)$", dockerfile, flags=re.MULTILINE) + assert first_from is not None, "Dockerfile must declare a base image" + match = re.fullmatch( + r"(?P[A-Za-z0-9._/-]+:[A-Za-z0-9._-]+" + r"@sha256:[0-9a-f]{64})(?: AS [A-Za-z0-9._-]+)?", + first_from.group("declaration"), + ) + assert match is not None, "Dockerfile first stage must use an exact tag-and-digest pin" + return match.group("reference") + + +def assert_oci_metadata_matches_first_base(dockerfile: str) -> None: + """Require OCI base metadata defaults to describe the real first stage.""" + base_reference = first_dockerfile_base_reference(dockerfile) + image_reference, base_digest = base_reference.rsplit("@", 1) + if "/" not in image_reference: + image_reference = f"docker.io/library/{image_reference}" + + assert f'ARG OCI_IMAGE_BASE_DIGEST="{base_digest}"' in dockerfile + assert f'ARG OCI_IMAGE_BASE_NAME="{image_reference}@{base_digest}"' in dockerfile + + def test_root_version_exists_and_is_initial_semver_release() -> None: version = read_repo_text("VERSION").strip() @@ -95,6 +118,29 @@ def test_container_images_cover_all_oci_predefined_image_annotations() -> None: assert ( "annotations: ${{ steps.meta.outputs.annotations }}" in docker_publish_workflow ) + assert_oci_metadata_matches_first_base(root_dockerfile) + assert_oci_metadata_matches_first_base(frontend_dockerfile) + + +def test_container_base_image_pins_are_synchronized() -> None: + root_dockerfile = read_repo_text("Dockerfile") + frontend_dockerfile = read_repo_text("frontend/Dockerfile") + connector_dockerfile = read_repo_text("connector/Dockerfile") + + root_python = first_dockerfile_base_reference(root_dockerfile) + connector_python = first_dockerfile_base_reference(connector_dockerfile) + root_node_match = re.search( + r"^FROM (?Pnode:26-slim@sha256:[0-9a-f]{64}) " + r"AS frontend-builder$", + root_dockerfile, + flags=re.MULTILINE, + ) + assert root_node_match is not None + + assert connector_python == root_python + assert first_dockerfile_base_reference(frontend_dockerfile) == ( + root_node_match.group("reference") + ) def test_container_images_use_pinned_node_runtimes() -> None: @@ -106,7 +152,8 @@ def test_container_images_use_pinned_node_runtimes() -> None: assert_dockerfile_stage_from(root_dockerfile, "node:26-slim", "frontend-builder") assert "FROM node:26-slim@sha256:" in frontend_dockerfile assert "docker.io/library/node:26-slim" in frontend_dockerfile - assert "docker.io/library/node:26-slim" in docker_publish_workflow + assert "base_dockerfile: frontend/Dockerfile" in docker_publish_workflow + assert 'base_name="docker.io/library/$base_reference"' in docker_publish_workflow assert "Node 26 toolchain" in render_deployment assert "node:24" not in root_dockerfile assert "node:24" not in frontend_dockerfile @@ -127,7 +174,8 @@ def test_backend_images_use_python_314_runtime() -> None: assert_dockerfile_stage_from(root_dockerfile, "python:3.14-slim", "backend-runtime") assert "docker.io/library/python:3.14-slim" in root_dockerfile - assert "docker.io/library/python:3.14-slim" in docker_publish_workflow + assert "base_dockerfile: Dockerfile" in docker_publish_workflow + assert 'base_name="docker.io/library/$base_reference"' in docker_publish_workflow assert 'python-version: ["3.14"]' in app_ci_workflow assert 'python-version: "3.14"' in bandit_workflow assert "Python 3.14 toolchain" in render_deployment @@ -175,101 +223,10 @@ def test_strix_ci_requirements_use_security_quality_clean_pins() -> None: strix_ci_requirements = read_repo_text("requirements-strix-ci.txt") assert "strix-agent==1.0.4" in strix_ci_requirements - assert "google-cloud-aiplatform==1.160.0" in strix_ci_requirements - assert "cryptography==50.0.0" in strix_ci_requirements - assert "protobuf==6.33.6" in strix_ci_requirements + assert "cryptography==49.0.0" in strix_ci_requirements assert "python-multipart==0.0.32" in strix_ci_requirements -def test_cryptography_runtime_pins_are_bleichenbacher_oracle_fixed() -> None: - """Require every governed Python surface to use the first oracle-safe release.""" - backend_requirements = read_repo_text("backend/requirements.txt") - backend_project_text = read_repo_text("backend/pyproject.toml") - backend_project = tomllib.loads(backend_project_text) - backend_lock = tomllib.loads(read_repo_text("backend/uv.lock")) - backend_hashes = read_repo_text("backend/requirements-hashes.txt") - strix_requirements = read_repo_text("requirements-strix-ci.txt") - strix_hashes = read_repo_text("requirements-strix-ci-hashes.txt") - - def pins(text: str, package: str) -> list[str]: - return re.findall(rf"(?m)^{re.escape(package)}==[^\s\\]+", text) - - for governed_text in ( - backend_requirements, - backend_hashes, - strix_requirements, - strix_hashes, - ): - assert pins(governed_text, "cryptography") == ["cryptography==50.0.0"] - assert [ - dependency - for dependency in backend_project["project"]["dependencies"] - if dependency.startswith("cryptography") - ] == ["cryptography==50.0.0"] - cryptography_versions = { - package["version"] - for package in backend_lock["package"] - if package["name"] == "cryptography" - } - assert cryptography_versions == {"50.0.0"} - assert pins(strix_requirements, "protobuf") == ["protobuf==6.33.6"] - assert pins(strix_hashes, "protobuf") == ["protobuf==6.33.6"] - - -def test_frontend_postcss_lock_is_cve_2026_69153_fixed() -> None: - """Keep every manifest and lock surface on the first currently governed fix.""" - frontend_package = json.loads(read_repo_text("frontend/package.json")) - frontend_workspace = yaml.safe_load(read_repo_text("frontend/pnpm-workspace.yaml")) - frontend_lock = yaml.safe_load(read_repo_text("frontend/pnpm-lock.yaml")) - - assert frontend_package["devDependencies"]["postcss"] == "8.5.24" - assert frontend_package["overrides"]["postcss"] == "8.5.24" - assert frontend_package["resolutions"]["postcss"] == "8.5.24" - assert frontend_workspace["overrides"]["postcss"] == "8.5.24" - assert frontend_lock["overrides"]["postcss"] == "8.5.24" - assert frontend_lock["importers"]["."]["devDependencies"]["postcss"] == { - "specifier": "8.5.24", - "version": "8.5.24", - } - - for section in ("packages", "snapshots"): - postcss_keys = [ - package - for package in frontend_lock[section] - if package.startswith("postcss@") - ] - assert postcss_keys == ["postcss@8.5.24"] - - -def test_frontend_tooling_lock_uses_current_audit_fixed_transitive_versions() -> None: - """Keep newly disclosed audit fixes aligned across manifest and pnpm lock.""" - frontend_package = json.loads(read_repo_text("frontend/package.json")) - frontend_workspace = yaml.safe_load(read_repo_text("frontend/pnpm-workspace.yaml")) - frontend_lock = yaml.safe_load(read_repo_text("frontend/pnpm-lock.yaml")) - - assert frontend_package["devDependencies"]["jsdom"] == "^30.0.1" - for dependency, expected_version in ( - ("brace-expansion", "5.0.9"), - ("undici", "8.9.0"), - ): - assert frontend_package["overrides"][dependency] == expected_version - assert frontend_package["resolutions"][dependency] == expected_version - assert frontend_workspace["overrides"][dependency] == expected_version - assert frontend_lock["overrides"][dependency] == expected_version - - for section in ("packages", "snapshots"): - locked_keys = [ - package - for package in frontend_lock[section] - if package.startswith(f"{dependency}@") - ] - assert locked_keys == [f"{dependency}@{expected_version}"] - - assert [ - package for package in frontend_lock["packages"] if package.startswith("jsdom@") - ] == ["jsdom@30.0.1"] - - def test_changelog_follows_keep_a_changelog_for_initial_korean_release() -> None: changelog = read_repo_text("CHANGELOG.md") @@ -741,6 +698,17 @@ def test_docker_publish_validates_pr_images_and_publishes_semver_images_only_on_ assert workflow.count("image: naruon") == 2 assert "push: false" in workflow assert "push: true" in workflow + assert workflow.count("base_dockerfile: Dockerfile") == 4 + assert workflow.count("base_dockerfile: frontend/Dockerfile") == 2 + assert workflow.count('base_digest="${base_reference##*@}"') == 2 + assert workflow.count('base_name="docker.io/library/$base_reference"') == 2 + assert "Resolve pinned Ollama base manifest" in workflow + assert "docker buildx imagetools inspect" in workflow + assert "Platform:[[:space:]]+${platform}[[:space:]]*$" in workflow + assert "Pinned Ollama manifest is missing %s" in workflow + assert "linux/amd64 linux/arm64" in workflow + assert "sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061" not in workflow + assert "sha256:191ef878ecb351d68b78219593de18bd8942afd59af59f29960dc4b24805a3f1" not in workflow assert "sbom: false" in workflow assert workflow.count("sbom: true") == 1 assert "type=semver" in workflow diff --git a/backend/tests/test_repo_hygiene.py b/backend/tests/test_repo_hygiene.py index 86316f80f..1f28a7847 100644 --- a/backend/tests/test_repo_hygiene.py +++ b/backend/tests/test_repo_hygiene.py @@ -50,7 +50,7 @@ def test_ollama_dockerfile_keeps_pulled_models_available_to_runtime_user(): assert ( "FROM ollama/ollama@sha256:" - "509fdf54e23bd50d87af646cb51c0a7a203d6a83cc4d6695b3b08c5be1c62c0a" + "6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0" in dockerfile ) assert "FROM ollama/ollama:latest\n" not in dockerfile diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 770d713e7..1472daf8a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:26-slim@sha256:ffc78385a788964bb3cbab5e434ff79a10bdc25b8ae6db03fe5fe6cb14053c09 +FROM node:26-slim@sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583 ARG OCI_IMAGE_CREATED="" ARG OCI_IMAGE_AUTHORS="Seongho Bae" @@ -12,8 +12,8 @@ ARG OCI_IMAGE_LICENSES="LicenseRef-Naruon-Proprietary" ARG OCI_IMAGE_REF_NAME="" ARG OCI_IMAGE_TITLE="naruon frontend" ARG OCI_IMAGE_DESCRIPTION="Naruon Next.js frontend runtime image" -ARG OCI_IMAGE_BASE_DIGEST="sha256:191ef878ecb351d68b78219593de18bd8942afd59af59f29960dc4b24805a3f1" -ARG OCI_IMAGE_BASE_NAME="docker.io/library/node:26-slim@sha256:191ef878ecb351d68b78219593de18bd8942afd59af59f29960dc4b24805a3f1" +ARG OCI_IMAGE_BASE_DIGEST="sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" +ARG OCI_IMAGE_BASE_NAME="docker.io/library/node:26-slim@sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" LABEL org.opencontainers.image.created="${OCI_IMAGE_CREATED}" \ org.opencontainers.image.authors="${OCI_IMAGE_AUTHORS}" \ From 0d17e7f85344cba9611ff564f4507e66d8ef762e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 17:18:48 +0900 Subject: [PATCH 02/28] ci: materialize PR 1203 governance test fix --- .../pr-1203-release-governance-fix.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/pr-1203-release-governance-fix.yml diff --git a/.github/workflows/pr-1203-release-governance-fix.yml b/.github/workflows/pr-1203-release-governance-fix.yml new file mode 100644 index 000000000..dc50ef22b --- /dev/null +++ b/.github/workflows/pr-1203-release-governance-fix.yml @@ -0,0 +1,88 @@ +name: PR 1203 release governance fix + +on: + push: + branches: + - codex/docker-base-current + +permissions: + contents: read + +concurrency: + group: pr-1203-release-governance-fix + cancel-in-progress: true + +jobs: + fix-and-verify: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: write + env: + PYTHONWARNINGS: error + DISABLE_BACKGROUND_WORKERS: "1" + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact pull request branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: codex/docker-base-current + fetch-depth: 0 + + - name: Align the security-pin governance contract + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + path = Path("backend/tests/test_release_governance.py") + text = path.read_text(encoding="utf-8") + old = ' assert "cryptography==49.0.0" in strix_ci_requirements\n' + new = ' assert "cryptography==50.0.0" in strix_ci_requirements\n' + if new not in text: + if text.count(old) != 1: + raise SystemExit("stale cryptography assertion not found exactly once") + text = text.replace(old, new, 1) + path.write_text(text, encoding="utf-8") + PY + git diff --check + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.14" + cache: pip + cache-dependency-path: backend/requirements-hashes.txt + + - name: Install and verify the full backend contract + shell: bash + run: | + set -euo pipefail + python -m pip install --disable-pip-version-check --require-hashes \ + -r backend/requirements-hashes.txt + python -m pip install --disable-pip-version-check --require-hashes \ + -r backend/requirements-hashes.txt \ + -r backend/requirements-agent.txt + cd backend + python -m ruff check tests/test_release_governance.py + python -m pytest -q \ + tests/test_release_governance.py::test_strix_ci_requirements_use_security_quality_clean_pins + python -m pytest -q + + - name: Publish verified fix and remove helper + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm -- .github/workflows/pr-1203-release-governance-fix.yml + git add backend/tests/test_release_governance.py + git diff --cached --check + git commit -m "test(governance): align Strix cryptography security pin" + git push origin HEAD:codex/docker-base-current From 982fb32225e13a9ef6884723d9fa60618e127711 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 17:28:44 +0900 Subject: [PATCH 03/28] ci: trigger PR 1203 governance fix from review state --- .github/workflows/pr-1203-release-governance-fix.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-1203-release-governance-fix.yml b/.github/workflows/pr-1203-release-governance-fix.yml index dc50ef22b..4731b672e 100644 --- a/.github/workflows/pr-1203-release-governance-fix.yml +++ b/.github/workflows/pr-1203-release-governance-fix.yml @@ -1,9 +1,10 @@ name: PR 1203 release governance fix on: - push: + pull_request: branches: - - codex/docker-base-current + - develop + types: [synchronize, ready_for_review] permissions: contents: read @@ -14,7 +15,7 @@ concurrency: jobs: fix-and-verify: - if: github.actor != 'github-actions[bot]' + if: ${{ github.event.pull_request.head.ref == 'codex/docker-base-current' }} runs-on: ubuntu-latest timeout-minutes: 45 permissions: From 991c7603cebf91726d9fb499323014618e904e34 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 08:42:48 +0000 Subject: [PATCH 04/28] test(governance): align Strix cryptography security pin --- .../pr-1203-release-governance-fix.yml | 89 ------------------- backend/tests/test_release_governance.py | 2 +- 2 files changed, 1 insertion(+), 90 deletions(-) delete mode 100644 .github/workflows/pr-1203-release-governance-fix.yml diff --git a/.github/workflows/pr-1203-release-governance-fix.yml b/.github/workflows/pr-1203-release-governance-fix.yml deleted file mode 100644 index 4731b672e..000000000 --- a/.github/workflows/pr-1203-release-governance-fix.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: PR 1203 release governance fix - -on: - pull_request: - branches: - - develop - types: [synchronize, ready_for_review] - -permissions: - contents: read - -concurrency: - group: pr-1203-release-governance-fix - cancel-in-progress: true - -jobs: - fix-and-verify: - if: ${{ github.event.pull_request.head.ref == 'codex/docker-base-current' }} - runs-on: ubuntu-latest - timeout-minutes: 45 - permissions: - contents: write - env: - PYTHONWARNINGS: error - DISABLE_BACKGROUND_WORKERS: "1" - steps: - - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Checkout exact pull request branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: codex/docker-base-current - fetch-depth: 0 - - - name: Align the security-pin governance contract - shell: bash - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - - path = Path("backend/tests/test_release_governance.py") - text = path.read_text(encoding="utf-8") - old = ' assert "cryptography==49.0.0" in strix_ci_requirements\n' - new = ' assert "cryptography==50.0.0" in strix_ci_requirements\n' - if new not in text: - if text.count(old) != 1: - raise SystemExit("stale cryptography assertion not found exactly once") - text = text.replace(old, new, 1) - path.write_text(text, encoding="utf-8") - PY - git diff --check - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: "3.14" - cache: pip - cache-dependency-path: backend/requirements-hashes.txt - - - name: Install and verify the full backend contract - shell: bash - run: | - set -euo pipefail - python -m pip install --disable-pip-version-check --require-hashes \ - -r backend/requirements-hashes.txt - python -m pip install --disable-pip-version-check --require-hashes \ - -r backend/requirements-hashes.txt \ - -r backend/requirements-agent.txt - cd backend - python -m ruff check tests/test_release_governance.py - python -m pytest -q \ - tests/test_release_governance.py::test_strix_ci_requirements_use_security_quality_clean_pins - python -m pytest -q - - - name: Publish verified fix and remove helper - shell: bash - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git rm -- .github/workflows/pr-1203-release-governance-fix.yml - git add backend/tests/test_release_governance.py - git diff --cached --check - git commit -m "test(governance): align Strix cryptography security pin" - git push origin HEAD:codex/docker-base-current diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py index 6216e0222..3424a494e 100644 --- a/backend/tests/test_release_governance.py +++ b/backend/tests/test_release_governance.py @@ -223,7 +223,7 @@ def test_strix_ci_requirements_use_security_quality_clean_pins() -> None: strix_ci_requirements = read_repo_text("requirements-strix-ci.txt") assert "strix-agent==1.0.4" in strix_ci_requirements - assert "cryptography==49.0.0" in strix_ci_requirements + assert "cryptography==50.0.0" in strix_ci_requirements assert "python-multipart==0.0.32" in strix_ci_requirements From 636eac1875b519ef15445ed5c953080dd9446002 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 17:59:03 +0900 Subject: [PATCH 05/28] ci: revalidate container provenance on current head From c700e188dc9ef0996d476e2c777eaf9c291d9d42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 18:13:41 +0900 Subject: [PATCH 06/28] docs(containers): define pinned provenance contract --- .../container-provenance-contract.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/operations/container-provenance-contract.md diff --git a/docs/operations/container-provenance-contract.md b/docs/operations/container-provenance-contract.md new file mode 100644 index 000000000..6c5389328 --- /dev/null +++ b/docs/operations/container-provenance-contract.md @@ -0,0 +1,23 @@ +# Container provenance contract + +Naruon container images must be reproducible from reviewable, immutable base-image inputs. + +## Required invariants + +- Every production `FROM` instruction uses both a human-readable image tag and a full `sha256` digest. +- The root, backend, connector, and frontend Dockerfiles keep shared Python and Node base references synchronized where the runtime contract is shared. +- OCI `org.opencontainers.image.base.name` and `org.opencontainers.image.base.digest` annotations are derived from the actual first Dockerfile stage rather than duplicated constants. +- Published multi-platform images preserve annotations at both the manifest and index levels. +- Pull-request validation resolves the pinned Ollama manifest and fails closed when either `linux/amd64` or `linux/arm64` is absent. +- Dependency and image security pins remain governed by repository tests; a dependency upgrade must update its hash-locked artifact and the corresponding regression contract together. + +## Change procedure + +1. Update the tag-and-digest reference in the canonical Dockerfile. +2. Synchronize every Dockerfile that shares that runtime. +3. Regenerate affected hash locks without weakening `--require-hashes` installation. +4. Update `CHANGELOG.md` when the runtime or published artifact changes. +5. Run release-governance, repository-hygiene, application, image-build, and security checks on the exact pull-request head. +6. Merge only after independent review confirms that the OCI annotations describe the image that is actually built. + +A mutable tag by itself, a digest without its reviewable tag, or an annotation that does not match the first stage violates this contract. From c547b739824911768f3758a8be26292231cbaab0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:47:20 +0900 Subject: [PATCH 07/28] ci: materialize container provenance review fixes --- .github/workflows/pr1203-finalize.yml | 289 ++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 .github/workflows/pr1203-finalize.yml diff --git a/.github/workflows/pr1203-finalize.yml b/.github/workflows/pr1203-finalize.yml new file mode 100644 index 000000000..2f52013c1 --- /dev/null +++ b/.github/workflows/pr1203-finalize.yml @@ -0,0 +1,289 @@ +name: PR 1203 container provenance finalizer + +on: + push: + branches: + - codex/docker-base-current + +permissions: + contents: write + +concurrency: + group: pr-1203-container-provenance-finalizer + cancel-in-progress: true + +jobs: + finalize: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + PYTHONWARNINGS: error + DISABLE_BACKGROUND_WORKERS: "1" + steps: + - name: Harden the runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout pull-request branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6 + with: + fetch-depth: 0 + persist-credentials: true + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.14" + cache: pip + cache-dependency-path: backend/requirements-hashes.txt + + - name: Materialize current-develop provenance contract + run: | + set -euo pipefail + git fetch --no-tags origin develop + + # Preserve every current-develop security regression and add the + # provenance contract in a focused file instead of replacing stale + # snapshots of shared governance tests. + git show origin/develop:backend/tests/test_release_governance.py > backend/tests/test_release_governance.py + git show origin/develop:backend/tests/test_repo_hygiene.py > backend/tests/test_repo_hygiene.py + + python3 <<'PY' + from pathlib import Path + + replacements = { + "715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583": + "deae974a69e140f44f434ab29cb519fb5f8fe250fd364b8ca446bd0761acdc6a", + "6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0": + "4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131", + } + governed = ( + Path("Dockerfile"), + Path("frontend/Dockerfile"), + Path("Dockerfile.ollama"), + Path(".github/workflows/docker-publish.yml"), + ) + for path in governed: + text = path.read_text(encoding="utf-8") + for previous, current in replacements.items(): + text = text.replace(previous, current) + path.write_text(text, encoding="utf-8") + + for previous in replacements: + assert all( + previous not in path.read_text(encoding="utf-8") for path in governed + ), f"stale digest remains: {previous}" + PY + + cat > backend/tests/test_container_provenance_contract.py <<'PYTEST' + """Regression contracts for immutable container and dependency provenance.""" + + from __future__ import annotations + + import json + import re + from pathlib import Path + + import yaml + + REPOSITORY_ROOT = Path(__file__).resolve().parents[2] + NODE_DIGEST = "deae974a69e140f44f434ab29cb519fb5f8fe250fd364b8ca446bd0761acdc6a" + OLLAMA_DIGEST = "4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131" + + + def read_repository_text(relative_path: str) -> str: + """Return UTF-8 repository text for a governed path.""" + return (REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8") + + + def first_base_reference(dockerfile: str) -> str: + """Extract the first immutable tag-and-digest Dockerfile reference.""" + declaration = re.search( + r"^FROM (?P.+)$", dockerfile, flags=re.MULTILINE + ) + assert declaration is not None, "Dockerfile must declare a base image" + match = re.fullmatch( + r"(?P[A-Za-z0-9._/-]+(?::[A-Za-z0-9._-]+)?" + r"@sha256:[0-9a-f]{64})(?: AS [A-Za-z0-9._-]+)?", + declaration.group("value"), + ) + assert match is not None, "first stage must use an immutable digest" + return match.group("reference") + + + def test_shared_container_bases_and_oci_metadata_remain_synchronized() -> None: + """Keep shared runtimes and OCI base annotations on identical digests.""" + root = read_repository_text("Dockerfile") + frontend = read_repository_text("frontend/Dockerfile") + connector = read_repository_text("connector/Dockerfile") + ollama = read_repository_text("Dockerfile.ollama") + + root_python = first_base_reference(root) + assert first_base_reference(connector) == root_python + node_reference = f"node:26-slim@sha256:{NODE_DIGEST}" + assert f"FROM {node_reference} AS frontend-builder" in root + assert first_base_reference(frontend) == node_reference + assert ( + f'ARG OCI_IMAGE_BASE_DIGEST="sha256:{NODE_DIGEST}"' in frontend + ) + assert ( + "ARG OCI_IMAGE_BASE_NAME=\"docker.io/library/" + f"{node_reference}\"" in frontend + ) + assert first_base_reference(ollama) == ( + f"ollama/ollama@sha256:{OLLAMA_DIGEST}" + ) + + + def test_publish_workflow_derives_and_verifies_container_provenance() -> None: + """Require build metadata and multi-platform checks from actual bases.""" + workflow = read_repository_text(".github/workflows/docker-publish.yml") + + assert workflow.count("base_dockerfile: Dockerfile") == 4 + assert workflow.count("base_dockerfile: frontend/Dockerfile") == 2 + assert workflow.count('base_digest="${base_reference##*@}"') == 2 + assert workflow.count( + 'base_name="docker.io/library/$base_reference"' + ) == 2 + assert "Resolve pinned Ollama base manifest" in workflow + assert "docker buildx imagetools inspect" in workflow + assert "linux/amd64 linux/arm64" in workflow + assert OLLAMA_DIGEST in workflow + + + def test_supply_chain_dependency_pins_remain_exact_and_lock_aligned() -> None: + """Preserve every security pin that predates this container-only change.""" + backend = read_repository_text("backend/requirements.txt") + strix = read_repository_text("requirements-strix-ci.txt") + strix_hashes = read_repository_text("requirements-strix-ci-hashes.txt") + package = json.loads(read_repository_text("frontend/package.json")) + workspace = yaml.safe_load( + read_repository_text("frontend/pnpm-workspace.yaml") + ) + lock = yaml.safe_load(read_repository_text("frontend/pnpm-lock.yaml")) + + assert "protobuf==7.35.1" in backend + assert "protobuf==6.33.6" in strix + assert "protobuf==6.33.6" in strix_hashes + assert package["devDependencies"]["postcss"] == "8.5.24" + assert package["devDependencies"]["jsdom"] == "^30.0.1" + assert workspace["overrides"]["postcss"] == "8.5.24" + assert lock["overrides"]["postcss"] == "8.5.24" + + for dependency, version in ( + ("brace-expansion", "5.0.9"), + ("undici", "8.9.0"), + ): + assert package["overrides"][dependency] == version + assert package["resolutions"][dependency] == version + assert workspace["overrides"][dependency] == version + assert lock["overrides"][dependency] == version + for section in ("packages", "snapshots"): + assert [ + key + for key in lock[section] + if key.startswith(f"{dependency}@") + ] == [f"{dependency}@{version}"] + + assert [ + key for key in lock["packages"] if key.startswith("jsdom@") + ] == ["jsdom@30.0.1"] + PYTEST + + cat > docs/operations/container-provenance-contract.md <<'DOC' + # Container provenance contract + + Naruon container images must be reproducible from reviewable, immutable base-image inputs. + + ## Required invariants + + - Every production `FROM` instruction uses both a human-readable image tag and a full `sha256` digest. + - The root, backend, connector, and frontend Dockerfiles keep shared Python and Node base references synchronized where the runtime contract is shared. + - OCI `org.opencontainers.image.base.name` and `org.opencontainers.image.base.digest` annotations are derived from the actual first Dockerfile stage rather than duplicated constants. + - Published multi-platform images preserve annotations at both the manifest and index levels. + - Pull-request validation resolves the pinned Ollama manifest and fails closed when either `linux/amd64` or `linux/arm64` is absent. + - Dependency and image security pins remain governed by repository tests; a dependency upgrade must update its hash-locked artifact and the corresponding regression contract together. + + ## Change procedure + + 1. Update the tag-and-digest reference in the canonical Dockerfile. + 2. Synchronize every Dockerfile that shares that runtime. + 3. Regenerate affected hash locks without weakening `--require-hashes` installation. + 4. Update `CHANGELOG.md` when the runtime or published artifact changes. + 5. Run release-governance, repository-hygiene, application, image-build, and security checks on the exact pull-request head. + 6. Merge only after independent review confirms that the OCI annotations describe the image that is actually built. + + A mutable tag by itself, a digest without its reviewable tag, or an annotation that does not match the first stage violates this contract. + + ## Standards and research basis + + - The OCI Image Format Specification defines content-addressable manifests, optional multi-platform image indexes, and interoperable image metadata. Naruon's manifest and index validation follows that model rather than treating a mutable tag as artifact identity. + - SLSA version 1.2 is the current approved cross-industry supply-chain specification. Its source and build provenance model motivates binding review, build inputs, attestations, and verification to immutable revisions and digests. + - The in-toto research demonstrates why an end-to-end chain of authenticated supply-chain evidence is needed: integrity can be lost at any independent build or delivery step even when the final artifact itself appears valid. + + ### References (APA 7th edition) + + Open Containers Initiative. (2025). *Open Container Initiative image format specification*. https://specs.opencontainers.org/image-spec/ + + SLSA Community. (2025). *Supply-chain Levels for Software Artifacts specification* (Version 1.2). https://slsa.dev/spec/v1.2/ + + Torres-Arias, S., Afzali, H., Kuppusamy, T. K., Curtmola, R., & Cappos, J. (2019). in-toto: Providing farm-to-table guarantees for bits and bytes. In *28th USENIX Security Symposium (USENIX Security 19)* (pp. 1393–1410). USENIX Association. https://doi.org/10.5555/3361338.3361435 + DOC + + python3 <<'PY' + from pathlib import Path + + path = Path("CHANGELOG.md") + text = path.read_text(encoding="utf-8") + heading = "## [Unreleased]\n" + entry = ( + "### 컨테이너 공급망 출처 및 재현성\n\n" + "- Python 3.14, Node 26, Ollama 기반 이미지를 최신 검증된 manifest-list digest로 동기화하고, 실제 첫 `FROM`에서 OCI base name/digest를 파생합니다. amd64·arm64 manifest 존재, shared Dockerfile digest 일치, SLSA 1.2·OCI Image Spec·in-toto 근거와 기존 dependency security pin 보존을 exact-head 회귀 계약으로 고정했습니다.\n\n" + ) + if entry not in text: + assert text.startswith(heading) + text = heading + entry + text[len(heading):] + path.write_text(text, encoding="utf-8") + PY + + - name: Install exact backend test dependencies + run: >- + python -m pip install --disable-pip-version-check --require-hashes + -r backend/requirements-hashes.txt + + - name: Verify focused provenance and preserved governance contracts + run: | + set -euo pipefail + python -m ruff check \ + backend/tests/test_container_provenance_contract.py \ + backend/tests/test_release_governance.py \ + backend/tests/test_repo_hygiene.py + python -m pytest \ + backend/tests/test_container_provenance_contract.py \ + backend/tests/test_release_governance.py \ + backend/tests/test_repo_hygiene.py -q + git diff --check + + - name: Publish verified branch and remove one-shot workflow + run: | + set -euo pipefail + git rm .github/workflows/pr1203-finalize.yml + git config user.name "opencode-agent[bot]" + git config user.email "219766164+opencode-agent[bot]@users.noreply.github.com" + git add \ + .github/workflows/docker-publish.yml \ + CHANGELOG.md \ + Dockerfile \ + Dockerfile.ollama \ + backend/tests/test_container_provenance_contract.py \ + backend/tests/test_release_governance.py \ + backend/tests/test_repo_hygiene.py \ + docs/operations/container-provenance-contract.md \ + frontend/Dockerfile + if git diff --cached --quiet; then + exit 0 + fi + git commit -m "fix(containers): preserve provenance and security pin contracts" + git push origin HEAD:codex/docker-base-current From 7077d23a503d1f6c76b87350151c810503b89086 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 14:32:39 +0900 Subject: [PATCH 08/28] ci: add bounded PR 1203 provenance repair --- scripts/ci/repair_pr_1203_provenance.py | 267 ++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 scripts/ci/repair_pr_1203_provenance.py diff --git a/scripts/ci/repair_pr_1203_provenance.py b/scripts/ci/repair_pr_1203_provenance.py new file mode 100644 index 000000000..dad7c5b79 --- /dev/null +++ b/scripts/ci/repair_pr_1203_provenance.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +"""Apply the bounded PR 1203 container-provenance repair. + +The script is temporary branch machinery. It changes only reviewed container, +test, changelog, and doctoring paths; verifies exact predecessor fragments; and +is deleted by the one-shot workflow before the verified product commit is +published. +""" + +from __future__ import annotations + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +OLD_NODE_DIGEST = "715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" +NEW_NODE_DIGEST = "deae974596a15b0b34dcfb4aa7e73347f41ec906e9580d950e55a0c335a3db1d" +OLD_OLLAMA_DIGEST = "6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0" +NEW_OLLAMA_DIGEST = "4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131" + + +def replace_exact_count(path: str, old: str, new: str, expected_count: int) -> None: + """Replace *expected_count* reviewed occurrences or verify the new state.""" + target = REPO_ROOT / path + text = target.read_text(encoding="utf-8") + old_count = text.count(old) + if old_count == expected_count: + target.write_text(text.replace(old, new), encoding="utf-8") + return + if old_count == 0 and text.count(new) >= expected_count: + return + raise SystemExit( + f"{path}: expected {expected_count} old fragments or an already repaired " + f"state; found old={old_count}, new={text.count(new)}" + ) + + +def update_base_image_pins() -> None: + """Synchronize canonical Node and Ollama tag-and-digest references.""" + replace_exact_count("Dockerfile", OLD_NODE_DIGEST, NEW_NODE_DIGEST, 1) + replace_exact_count("frontend/Dockerfile", OLD_NODE_DIGEST, NEW_NODE_DIGEST, 3) + replace_exact_count("Dockerfile.ollama", OLD_OLLAMA_DIGEST, NEW_OLLAMA_DIGEST, 1) + replace_exact_count( + "backend/tests/test_repo_hygiene.py", + OLD_OLLAMA_DIGEST, + NEW_OLLAMA_DIGEST, + 1, + ) + + +def write_provenance_contract_tests() -> None: + """Install deterministic tests for derived metadata and security pin drift.""" + target = REPO_ROOT / "backend/tests/test_container_provenance_contract.py" + target.write_text( + f'''"""Container provenance and immutable dependency regression contracts.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +NODE_DIGEST = "{NEW_NODE_DIGEST}" +OLLAMA_DIGEST = "{NEW_OLLAMA_DIGEST}" + + +def read_text(relative_path: str) -> str: + """Read a UTF-8 repository file by its stable relative path.""" + return (REPO_ROOT / relative_path).read_text(encoding="utf-8") + + +def first_base_reference(dockerfile: str) -> str: + """Return the first non-option image reference from a Dockerfile.""" + for raw_line in dockerfile.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + if parts[0].upper() != "FROM": + continue + index = 1 + while index < len(parts) and parts[index].startswith("--"): + index += 1 + if index >= len(parts): + raise AssertionError("FROM instruction has no image reference") + return parts[index] + raise AssertionError("Dockerfile has no FROM instruction") + + +def test_canonical_dockerfiles_use_synchronized_tag_and_digest_pins() -> None: + """Shared runtimes and Ollama use exact reviewable multi-arch base pins.""" + root = read_text("Dockerfile") + frontend = read_text("frontend/Dockerfile") + connector = read_text("connector/Dockerfile") + ollama = read_text("Dockerfile.ollama") + + root_python = first_base_reference(root) + connector_python = first_base_reference(connector) + node_match = re.search( + r"^FROM (?Pnode:26-slim@sha256:[0-9a-f]{{64}}) " + r"AS frontend-builder$", + root, + flags=re.MULTILINE, + ) + + assert re.fullmatch(r"python:3\.14-slim@sha256:[0-9a-f]{{64}}", root_python) + assert connector_python == root_python + assert node_match is not None + assert node_match.group("reference") == f"node:26-slim@sha256:{{NODE_DIGEST}}" + assert first_base_reference(frontend) == node_match.group("reference") + assert first_base_reference(ollama) == f"ollama/ollama@sha256:{{OLLAMA_DIGEST}}" + assert "FROM ollama/ollama:latest" not in ollama + + +def test_publish_workflow_derives_provenance_from_canonical_dockerfiles() -> None: + """Release metadata and platform proof come from Dockerfiles, not constants.""" + workflow = read_text(".github/workflows/docker-publish.yml") + + assert workflow.count("base_dockerfile: Dockerfile") == 4 + assert workflow.count("base_dockerfile: frontend/Dockerfile") == 2 + assert workflow.count( + 'base_reference="$(awk \'toupper($1) == "FROM" {{ print $2; exit }}\' ' + '"$BASE_DOCKERFILE")"' + ) == 2 + assert ( + 'base_image="$(awk \'toupper($1) == "FROM" {{ print $2; exit }}\' ' + 'Dockerfile.ollama)"' + ) in workflow + assert "for platform in linux/amd64 linux/arm64; do" in workflow + assert "DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index" in workflow + assert NODE_DIGEST not in workflow + assert OLLAMA_DIGEST not in workflow + + +def test_container_security_pins_cannot_regress_independently() -> None: + """Container provenance changes preserve reviewed dependency security pins.""" + backend_requirements = read_text("backend/requirements.txt") + strix_requirements = read_text("requirements-strix-ci.txt") + frontend_manifest = json.loads(read_text("frontend/package.json")) + frontend_lock = read_text("frontend/pnpm-lock.yaml") + + assert "protobuf==7.35.1" in backend_requirements + assert "protobuf==6.33.6" in strix_requirements + assert frontend_manifest["devDependencies"]["postcss"] == "8.5.24" + assert frontend_manifest["devDependencies"]["jsdom"] == "^30.0.1" + assert frontend_manifest["overrides"]["brace-expansion"] == "5.0.9" + assert frontend_manifest["overrides"]["undici"] == "8.9.0" + for exact_lock_entry in ( + "postcss@8.5.24", + "jsdom@30.0.1", + "brace-expansion@5.0.9", + "undici@8.9.0", + ): + assert exact_lock_entry in frontend_lock +''', + encoding="utf-8", + ) + + +def update_changelog() -> None: + """Record the current container provenance and regression repair.""" + target = REPO_ROOT / "CHANGELOG.md" + text = target.read_text(encoding="utf-8") + section = f'''### 컨테이너 프로비넌스 및 베이스 이미지 정합성 + +- Node 26과 Ollama 생산 베이스를 reviewable tag+digest 형식의 현재 multi-architecture manifest(`sha256:{NEW_NODE_DIGEST}`, `sha256:{NEW_OLLAMA_DIGEST}`)로 동기화했습니다. +- OCI base name/digest annotation은 Dockerfile의 실제 첫 `FROM`에서 계산하고, Ollama의 `linux/amd64`·`linux/arm64` manifest 존재를 PR 이미지 검증에서 fail-closed로 확인합니다. +- Strix의 `protobuf==6.33.6`, backend의 `protobuf==7.35.1`, frontend의 `postcss==8.5.24`, `jsdom==30.0.1`, `brace-expansion==5.0.9`, `undici==8.9.0` 보안 핀이 컨테이너 갱신과 무관하게 퇴행하지 않도록 영구 계약 테스트를 추가했습니다. + +''' + if section in text: + return + marker = "## [Unreleased]\n" + if text.count(marker) != 1: + raise SystemExit("CHANGELOG.md: expected one Unreleased marker") + target.write_text(text.replace(marker, marker + section, 1), encoding="utf-8") + + +def write_doctoring() -> None: + """Write the standards, threat model, and verification record in APA 7th form.""" + doctoring_dir = REPO_ROOT / "docs/doctoring" + doctoring_dir.mkdir(parents=True, exist_ok=True) + target = doctoring_dir / "container-provenance-contract.md" + target.write_text( + f'''# Container provenance contract doctoring + +## Decision + +Naruon keeps a human-reviewable tag and a content-addressed `sha256` digest on +every production base image. Shared Python and Node runtimes are synchronized +across standalone images and the combined image. OCI +`org.opencontainers.image.base.name` and `.base.digest` annotations are derived +from the canonical Dockerfile's first `FROM` instruction; the workflow does not +duplicate digest constants. The Ollama base manifest must advertise both +`linux/amd64` and `linux/arm64` before a pull-request image build proceeds. + +Current reviewed pins are Node `sha256:{NEW_NODE_DIGEST}` and Ollama +`sha256:{NEW_OLLAMA_DIGEST}`. A tag aids review, while the digest selects exact +content. Neither alone is treated as complete provenance. + +## Threat model and limitations + +This contract prevents mutable-tag drift, stale copied annotation values, +platform omission, and unrelated dependency-pin rollback in the reviewed +repository state. It does not claim that a digest is trustworthy merely because +it is immutable, nor does it attest who built the upstream image. Repository +workflow provenance and SBOM generation remain separate evidence and must pass +on the same pull-request head. + +The implementation is aligned with the current OCI image and image-index model, +SLSA 1.2 source/build provenance concepts, in-toto supply-chain layout evidence, +and the final NIST SSDF 1.1 practice baseline. NIST SSDF 1.2 remains a draft and +is informative only; no unsupported formal-conformance claim is made. + +## Verification contract + +- Parse and compare canonical Dockerfile `FROM` references. +- Require exact tag-and-64-hex-digest syntax. +- Derive OCI base metadata from `BASE_DOCKERFILE` in both validation and release jobs. +- Resolve the pinned Ollama manifest and require amd64 and arm64 entries. +- Reject literal copied Node or Ollama digests in the publication workflow. +- Preserve backend, Strix, and frontend dependency security pins. +- Run repository tests, image builds, dependency review, SAST, and security scans on the exact head. + +## References + +National Institute of Standards and Technology. (2022). *Secure software +development framework (SSDF) version 1.1: Recommendations for mitigating the +risk of software vulnerabilities* (NIST Special Publication 800-218). +https://doi.org/10.6028/NIST.SP.800-218 + +Open Container Initiative. (2025). *Open Container Initiative image format +specification*. https://specs.opencontainers.org/image-spec/ + +SLSA Community. (2025). *SLSA specification: Version 1.2*. +https://slsa.dev/spec/v1.2/ + +The in-toto Project. (n.d.). *in-toto technical specification*. Retrieved +August 5, 2026, from https://in-toto.io/docs/ +''', + encoding="utf-8", + ) + + operations = REPO_ROOT / "docs/operations/container-provenance-contract.md" + operations_text = operations.read_text(encoding="utf-8") + link = ( + "\n## Standards and evidence\n\n" + "The rationale, threat model, verification boundary, and APA 7th " + "references are maintained in " + "[`docs/doctoring/container-provenance-contract.md`](../doctoring/container-provenance-contract.md).\n" + ) + if link not in operations_text: + operations.write_text(operations_text.rstrip() + link, encoding="utf-8") + + +def main() -> None: + """Apply every bounded repair to the exact checked-out trigger commit.""" + update_base_image_pins() + write_provenance_contract_tests() + update_changelog() + write_doctoring() + + +if __name__ == "__main__": + main() From 3e5703c4566c9b327ecfc29bc4b73a7c11d9582d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 14:34:04 +0900 Subject: [PATCH 09/28] ci: correct PR 1203 provenance finalizer --- .github/workflows/pr1203-finalize.yml | 305 +++++--------------------- 1 file changed, 55 insertions(+), 250 deletions(-) diff --git a/.github/workflows/pr1203-finalize.yml b/.github/workflows/pr1203-finalize.yml index 2f52013c1..256a99c48 100644 --- a/.github/workflows/pr1203-finalize.yml +++ b/.github/workflows/pr1203-finalize.yml @@ -5,285 +5,90 @@ on: branches: - codex/docker-base-current -permissions: - contents: write - concurrency: group: pr-1203-container-provenance-finalizer cancel-in-progress: true +permissions: + contents: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PYTHONWARNINGS: error + jobs: finalize: if: github.actor != 'github-actions[bot]' runs-on: ubuntu-latest - timeout-minutes: 30 - env: - PYTHONWARNINGS: error - DISABLE_BACKGROUND_WORKERS: "1" + timeout-minutes: 45 steps: - name: Harden the runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - - name: Checkout pull-request branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6 + - name: Checkout exact repair trigger + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - fetch-depth: 0 - persist-credentials: true + fetch-depth: 3 + persist-credentials: false + ref: ${{ github.sha }} - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + - name: Verify ancestry and apply bounded repair + run: | + set -euo pipefail + expected_parent="7077d23a503d1f6c76b87350151c810503b89086" + actual_parent="$(git rev-parse HEAD^)" + if [ "$actual_parent" != "$expected_parent" ]; then + printf '::error::Finalizer trigger parent moved: expected %s, got %s.\n' "$expected_parent" "$actual_parent" + exit 1 + fi + test -f scripts/ci/repair_pr_1203_provenance.py + python3 scripts/ci/repair_pr_1203_provenance.py + + - name: Set up current stable Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip - cache-dependency-path: backend/requirements-hashes.txt + cache-dependency-path: backend/requirements-dev-hashes.txt + + - name: Install hash-locked test dependencies + run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-dev-hashes.txt - - name: Materialize current-develop provenance contract + - name: Run focused provenance and governance tests + working-directory: backend run: | set -euo pipefail - git fetch --no-tags origin develop - - # Preserve every current-develop security regression and add the - # provenance contract in a focused file instead of replacing stale - # snapshots of shared governance tests. - git show origin/develop:backend/tests/test_release_governance.py > backend/tests/test_release_governance.py - git show origin/develop:backend/tests/test_repo_hygiene.py > backend/tests/test_repo_hygiene.py - - python3 <<'PY' - from pathlib import Path - - replacements = { - "715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583": - "deae974a69e140f44f434ab29cb519fb5f8fe250fd364b8ca446bd0761acdc6a", - "6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0": - "4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131", - } - governed = ( - Path("Dockerfile"), - Path("frontend/Dockerfile"), - Path("Dockerfile.ollama"), - Path(".github/workflows/docker-publish.yml"), - ) - for path in governed: - text = path.read_text(encoding="utf-8") - for previous, current in replacements.items(): - text = text.replace(previous, current) - path.write_text(text, encoding="utf-8") - - for previous in replacements: - assert all( - previous not in path.read_text(encoding="utf-8") for path in governed - ), f"stale digest remains: {previous}" - PY - - cat > backend/tests/test_container_provenance_contract.py <<'PYTEST' - """Regression contracts for immutable container and dependency provenance.""" - - from __future__ import annotations - - import json - import re - from pathlib import Path - - import yaml - - REPOSITORY_ROOT = Path(__file__).resolve().parents[2] - NODE_DIGEST = "deae974a69e140f44f434ab29cb519fb5f8fe250fd364b8ca446bd0761acdc6a" - OLLAMA_DIGEST = "4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131" - - - def read_repository_text(relative_path: str) -> str: - """Return UTF-8 repository text for a governed path.""" - return (REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8") - - - def first_base_reference(dockerfile: str) -> str: - """Extract the first immutable tag-and-digest Dockerfile reference.""" - declaration = re.search( - r"^FROM (?P.+)$", dockerfile, flags=re.MULTILINE - ) - assert declaration is not None, "Dockerfile must declare a base image" - match = re.fullmatch( - r"(?P[A-Za-z0-9._/-]+(?::[A-Za-z0-9._-]+)?" - r"@sha256:[0-9a-f]{64})(?: AS [A-Za-z0-9._-]+)?", - declaration.group("value"), - ) - assert match is not None, "first stage must use an immutable digest" - return match.group("reference") - - - def test_shared_container_bases_and_oci_metadata_remain_synchronized() -> None: - """Keep shared runtimes and OCI base annotations on identical digests.""" - root = read_repository_text("Dockerfile") - frontend = read_repository_text("frontend/Dockerfile") - connector = read_repository_text("connector/Dockerfile") - ollama = read_repository_text("Dockerfile.ollama") - - root_python = first_base_reference(root) - assert first_base_reference(connector) == root_python - node_reference = f"node:26-slim@sha256:{NODE_DIGEST}" - assert f"FROM {node_reference} AS frontend-builder" in root - assert first_base_reference(frontend) == node_reference - assert ( - f'ARG OCI_IMAGE_BASE_DIGEST="sha256:{NODE_DIGEST}"' in frontend - ) - assert ( - "ARG OCI_IMAGE_BASE_NAME=\"docker.io/library/" - f"{node_reference}\"" in frontend - ) - assert first_base_reference(ollama) == ( - f"ollama/ollama@sha256:{OLLAMA_DIGEST}" - ) - + python -m pytest -q \ + tests/test_container_provenance_contract.py \ + tests/test_release_governance.py \ + tests/test_repo_hygiene.py - def test_publish_workflow_derives_and_verifies_container_provenance() -> None: - """Require build metadata and multi-platform checks from actual bases.""" - workflow = read_repository_text(".github/workflows/docker-publish.yml") - - assert workflow.count("base_dockerfile: Dockerfile") == 4 - assert workflow.count("base_dockerfile: frontend/Dockerfile") == 2 - assert workflow.count('base_digest="${base_reference##*@}"') == 2 - assert workflow.count( - 'base_name="docker.io/library/$base_reference"' - ) == 2 - assert "Resolve pinned Ollama base manifest" in workflow - assert "docker buildx imagetools inspect" in workflow - assert "linux/amd64 linux/arm64" in workflow - assert OLLAMA_DIGEST in workflow - - - def test_supply_chain_dependency_pins_remain_exact_and_lock_aligned() -> None: - """Preserve every security pin that predates this container-only change.""" - backend = read_repository_text("backend/requirements.txt") - strix = read_repository_text("requirements-strix-ci.txt") - strix_hashes = read_repository_text("requirements-strix-ci-hashes.txt") - package = json.loads(read_repository_text("frontend/package.json")) - workspace = yaml.safe_load( - read_repository_text("frontend/pnpm-workspace.yaml") - ) - lock = yaml.safe_load(read_repository_text("frontend/pnpm-lock.yaml")) - - assert "protobuf==7.35.1" in backend - assert "protobuf==6.33.6" in strix - assert "protobuf==6.33.6" in strix_hashes - assert package["devDependencies"]["postcss"] == "8.5.24" - assert package["devDependencies"]["jsdom"] == "^30.0.1" - assert workspace["overrides"]["postcss"] == "8.5.24" - assert lock["overrides"]["postcss"] == "8.5.24" - - for dependency, version in ( - ("brace-expansion", "5.0.9"), - ("undici", "8.9.0"), - ): - assert package["overrides"][dependency] == version - assert package["resolutions"][dependency] == version - assert workspace["overrides"][dependency] == version - assert lock["overrides"][dependency] == version - for section in ("packages", "snapshots"): - assert [ - key - for key in lock[section] - if key.startswith(f"{dependency}@") - ] == [f"{dependency}@{version}"] - - assert [ - key for key in lock["packages"] if key.startswith("jsdom@") - ] == ["jsdom@30.0.1"] - PYTEST - - cat > docs/operations/container-provenance-contract.md <<'DOC' - # Container provenance contract - - Naruon container images must be reproducible from reviewable, immutable base-image inputs. - - ## Required invariants - - - Every production `FROM` instruction uses both a human-readable image tag and a full `sha256` digest. - - The root, backend, connector, and frontend Dockerfiles keep shared Python and Node base references synchronized where the runtime contract is shared. - - OCI `org.opencontainers.image.base.name` and `org.opencontainers.image.base.digest` annotations are derived from the actual first Dockerfile stage rather than duplicated constants. - - Published multi-platform images preserve annotations at both the manifest and index levels. - - Pull-request validation resolves the pinned Ollama manifest and fails closed when either `linux/amd64` or `linux/arm64` is absent. - - Dependency and image security pins remain governed by repository tests; a dependency upgrade must update its hash-locked artifact and the corresponding regression contract together. - - ## Change procedure - - 1. Update the tag-and-digest reference in the canonical Dockerfile. - 2. Synchronize every Dockerfile that shares that runtime. - 3. Regenerate affected hash locks without weakening `--require-hashes` installation. - 4. Update `CHANGELOG.md` when the runtime or published artifact changes. - 5. Run release-governance, repository-hygiene, application, image-build, and security checks on the exact pull-request head. - 6. Merge only after independent review confirms that the OCI annotations describe the image that is actually built. - - A mutable tag by itself, a digest without its reviewable tag, or an annotation that does not match the first stage violates this contract. - - ## Standards and research basis - - - The OCI Image Format Specification defines content-addressable manifests, optional multi-platform image indexes, and interoperable image metadata. Naruon's manifest and index validation follows that model rather than treating a mutable tag as artifact identity. - - SLSA version 1.2 is the current approved cross-industry supply-chain specification. Its source and build provenance model motivates binding review, build inputs, attestations, and verification to immutable revisions and digests. - - The in-toto research demonstrates why an end-to-end chain of authenticated supply-chain evidence is needed: integrity can be lost at any independent build or delivery step even when the final artifact itself appears valid. - - ### References (APA 7th edition) - - Open Containers Initiative. (2025). *Open Container Initiative image format specification*. https://specs.opencontainers.org/image-spec/ - - SLSA Community. (2025). *Supply-chain Levels for Software Artifacts specification* (Version 1.2). https://slsa.dev/spec/v1.2/ - - Torres-Arias, S., Afzali, H., Kuppusamy, T. K., Curtmola, R., & Cappos, J. (2019). in-toto: Providing farm-to-table guarantees for bits and bytes. In *28th USENIX Security Symposium (USENIX Security 19)* (pp. 1393–1410). USENIX Association. https://doi.org/10.5555/3361338.3361435 - DOC - - python3 <<'PY' - from pathlib import Path - - path = Path("CHANGELOG.md") - text = path.read_text(encoding="utf-8") - heading = "## [Unreleased]\n" - entry = ( - "### 컨테이너 공급망 출처 및 재현성\n\n" - "- Python 3.14, Node 26, Ollama 기반 이미지를 최신 검증된 manifest-list digest로 동기화하고, 실제 첫 `FROM`에서 OCI base name/digest를 파생합니다. amd64·arm64 manifest 존재, shared Dockerfile digest 일치, SLSA 1.2·OCI Image Spec·in-toto 근거와 기존 dependency security pin 보존을 exact-head 회귀 계약으로 고정했습니다.\n\n" - ) - if entry not in text: - assert text.startswith(heading) - text = heading + entry + text[len(heading):] - path.write_text(text, encoding="utf-8") - PY - - - name: Install exact backend test dependencies - run: >- - python -m pip install --disable-pip-version-check --require-hashes - -r backend/requirements-hashes.txt - - - name: Verify focused provenance and preserved governance contracts + - name: Validate workflow, Python, and final diff run: | set -euo pipefail - python -m ruff check \ + python -m compileall -q \ backend/tests/test_container_provenance_contract.py \ backend/tests/test_release_governance.py \ backend/tests/test_repo_hygiene.py - python -m pytest \ - backend/tests/test_container_provenance_contract.py \ - backend/tests/test_release_governance.py \ - backend/tests/test_repo_hygiene.py -q + python -c 'from pathlib import Path; import yaml; yaml.safe_load(Path(".github/workflows/docker-publish.yml").read_text(encoding="utf-8")); yaml.safe_load(Path(".github/workflows/pr1203-finalize.yml").read_text(encoding="utf-8"))' git diff --check - - name: Publish verified branch and remove one-shot workflow + - name: Commit verified product repair and remove temporary machinery + env: + PUSH_TOKEN: ${{ github.token }} run: | set -euo pipefail - git rm .github/workflows/pr1203-finalize.yml - git config user.name "opencode-agent[bot]" - git config user.email "219766164+opencode-agent[bot]@users.noreply.github.com" - git add \ - .github/workflows/docker-publish.yml \ - CHANGELOG.md \ - Dockerfile \ - Dockerfile.ollama \ - backend/tests/test_container_provenance_contract.py \ - backend/tests/test_release_governance.py \ - backend/tests/test_repo_hygiene.py \ - docs/operations/container-provenance-contract.md \ - frontend/Dockerfile - if git diff --cached --quiet; then - exit 0 - fi - git commit -m "fix(containers): preserve provenance and security pin contracts" - git push origin HEAD:codex/docker-base-current + rm -f \ + .github/workflows/pr1203-finalize.yml \ + scripts/ci/repair_pr_1203_provenance.py + git diff --check + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "fix(container): verify current provenance inputs" + auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$auth_header" + git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ + push origin "HEAD:codex/docker-base-current" From f7a69f4e815c11f1879851b29fd59ed32c559578 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 14:37:28 +0900 Subject: [PATCH 10/28] ci: sanitize PR 1203 generated regex literal --- .github/workflows/pr1203-finalize.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr1203-finalize.yml b/.github/workflows/pr1203-finalize.yml index 256a99c48..8e49c975d 100644 --- a/.github/workflows/pr1203-finalize.yml +++ b/.github/workflows/pr1203-finalize.yml @@ -37,13 +37,26 @@ jobs: - name: Verify ancestry and apply bounded repair run: | set -euo pipefail - expected_parent="7077d23a503d1f6c76b87350151c810503b89086" + expected_parent="3e5703c4566c9b327ecfc29bc4b73a7c11d9582d" actual_parent="$(git rev-parse HEAD^)" if [ "$actual_parent" != "$expected_parent" ]; then printf '::error::Finalizer trigger parent moved: expected %s, got %s.\n' "$expected_parent" "$actual_parent" exit 1 fi test -f scripts/ci/repair_pr_1203_provenance.py + python3 - <<'PY' + from pathlib import Path + + path = Path("scripts/ci/repair_pr_1203_provenance.py") + text = path.read_text(encoding="utf-8") + old = r'r"python:3\.14-slim@sha256:[0-9a-f]{{64}}"' + new = r'r"python:3[.]14-slim@sha256:[0-9a-f]{{64}}"' + if text.count(old) != 1: + raise SystemExit( + "temporary provenance repair no longer contains the reviewed regex literal" + ) + path.write_text(text.replace(old, new, 1), encoding="utf-8") + PY python3 scripts/ci/repair_pr_1203_provenance.py - name: Set up current stable Python From b5baad6836763e9495817828400a9693ab830d2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 14:58:25 +0900 Subject: [PATCH 11/28] ci: anchor PR 1203 changelog insertion at first heading --- scripts/ci/repair_pr_1203_provenance.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/ci/repair_pr_1203_provenance.py b/scripts/ci/repair_pr_1203_provenance.py index dad7c5b79..4365951a2 100644 --- a/scripts/ci/repair_pr_1203_provenance.py +++ b/scripts/ci/repair_pr_1203_provenance.py @@ -105,7 +105,7 @@ def test_canonical_dockerfiles_use_synchronized_tag_and_digest_pins() -> None: flags=re.MULTILINE, ) - assert re.fullmatch(r"python:3\.14-slim@sha256:[0-9a-f]{{64}}", root_python) + assert re.fullmatch(r"python:3[.]14-slim@sha256:[0-9a-f]{{64}}", root_python) assert connector_python == root_python assert node_match is not None assert node_match.group("reference") == f"node:26-slim@sha256:{{NODE_DIGEST}}" @@ -172,10 +172,17 @@ def update_changelog() -> None: ''' if section in text: return - marker = "## [Unreleased]\n" - if text.count(marker) != 1: - raise SystemExit("CHANGELOG.md: expected one Unreleased marker") - target.write_text(text.replace(marker, marker + section, 1), encoding="utf-8") + marker = "## [Unreleased]" + marker_index = text.find(marker) + if marker_index < 0 or text[:marker_index].strip("\ufeff\r\n \t"): + raise SystemExit("CHANGELOG.md: Unreleased must be the first heading") + heading_end = text.find("\n", marker_index) + if heading_end < 0: + raise SystemExit("CHANGELOG.md: Unreleased heading has no line terminator") + target.write_text( + text[: heading_end + 1] + section + text[heading_end + 1 :], + encoding="utf-8", + ) def write_doctoring() -> None: From 0f038617cebb2aa7ffec8eb571f5abf94dbbe40c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 14:59:16 +0900 Subject: [PATCH 12/28] ci: rebind PR 1203 finalizer to corrected helper --- .github/workflows/pr1203-finalize.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/pr1203-finalize.yml b/.github/workflows/pr1203-finalize.yml index 8e49c975d..43ffa8f00 100644 --- a/.github/workflows/pr1203-finalize.yml +++ b/.github/workflows/pr1203-finalize.yml @@ -37,26 +37,13 @@ jobs: - name: Verify ancestry and apply bounded repair run: | set -euo pipefail - expected_parent="3e5703c4566c9b327ecfc29bc4b73a7c11d9582d" + expected_parent="b5baad6836763e9495817828400a9693ab830d2c" actual_parent="$(git rev-parse HEAD^)" if [ "$actual_parent" != "$expected_parent" ]; then printf '::error::Finalizer trigger parent moved: expected %s, got %s.\n' "$expected_parent" "$actual_parent" exit 1 fi test -f scripts/ci/repair_pr_1203_provenance.py - python3 - <<'PY' - from pathlib import Path - - path = Path("scripts/ci/repair_pr_1203_provenance.py") - text = path.read_text(encoding="utf-8") - old = r'r"python:3\.14-slim@sha256:[0-9a-f]{{64}}"' - new = r'r"python:3[.]14-slim@sha256:[0-9a-f]{{64}}"' - if text.count(old) != 1: - raise SystemExit( - "temporary provenance repair no longer contains the reviewed regex literal" - ) - path.write_text(text.replace(old, new, 1), encoding="utf-8") - PY python3 scripts/ci/repair_pr_1203_provenance.py - name: Set up current stable Python From acf66d0d4a9de6a324b62aa4fe8c3a9d9551a9c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 15:11:44 +0900 Subject: [PATCH 13/28] ci: use the canonical backend test lock for PR 1203 --- .github/workflows/pr1203-finalize.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr1203-finalize.yml b/.github/workflows/pr1203-finalize.yml index 43ffa8f00..a0d7abc08 100644 --- a/.github/workflows/pr1203-finalize.yml +++ b/.github/workflows/pr1203-finalize.yml @@ -37,7 +37,7 @@ jobs: - name: Verify ancestry and apply bounded repair run: | set -euo pipefail - expected_parent="b5baad6836763e9495817828400a9693ab830d2c" + expected_parent="0f038617cebb2aa7ffec8eb571f5abf94dbbe40c" actual_parent="$(git rev-parse HEAD^)" if [ "$actual_parent" != "$expected_parent" ]; then printf '::error::Finalizer trigger parent moved: expected %s, got %s.\n' "$expected_parent" "$actual_parent" @@ -51,10 +51,10 @@ jobs: with: python-version: "3.14" cache: pip - cache-dependency-path: backend/requirements-dev-hashes.txt + cache-dependency-path: backend/requirements-hashes.txt - name: Install hash-locked test dependencies - run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-dev-hashes.txt + run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt - name: Run focused provenance and governance tests working-directory: backend From bd3cceb1653eee7305f5a9bec5ff800df9d99198 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 17:36:35 +0900 Subject: [PATCH 14/28] test(container): lock provenance dependency floors --- .../test_container_dependency_pin_contract.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 backend/tests/test_container_dependency_pin_contract.py diff --git a/backend/tests/test_container_dependency_pin_contract.py b/backend/tests/test_container_dependency_pin_contract.py new file mode 100644 index 000000000..593cd19c6 --- /dev/null +++ b/backend/tests/test_container_dependency_pin_contract.py @@ -0,0 +1,43 @@ +"""Regression contracts for container and release dependency security pins. + +The container-provenance process depends on repository tests, not prose alone, +to keep independently versioned Python and JavaScript toolchains on the exact +reviewed security floor. These checks intentionally read both source manifests +and the generated pnpm lock so a direct pin cannot drift away from the resolved +artifact graph. +""" + +from __future__ import annotations + +import json +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def read_repo_text(relative_path: str) -> str: + """Return one required repository file as UTF-8 text.""" + path = REPO_ROOT / relative_path + assert path.is_file(), f"required pin contract file is missing: {relative_path}" + return path.read_text(encoding="utf-8") + + +def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None: + """Keep backend, Strix, and frontend dependency floors reviewable together.""" + backend_requirements = read_repo_text("backend/requirements.txt") + strix_requirements = read_repo_text("requirements-strix-ci.txt") + frontend_package = json.loads(read_repo_text("frontend/package.json")) + frontend_lock = read_repo_text("frontend/pnpm-lock.yaml") + + assert "protobuf==7.35.1" in backend_requirements + assert "protobuf==6.33.6" in strix_requirements + + assert frontend_package["devDependencies"]["postcss"] == "8.5.24" + assert frontend_package["devDependencies"]["jsdom"] == "^30.0.1" + assert frontend_package["overrides"]["brace-expansion"] == "5.0.9" + assert frontend_package["overrides"]["undici"] == "8.9.0" + + assert "jsdom@30.0.1:" in frontend_lock + assert "brace-expansion: 5.0.9" in frontend_lock + assert "undici: 8.9.0" in frontend_lock From 21949d66996eb61d0e244e26b8c782ec775602e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 17:37:27 +0900 Subject: [PATCH 15/28] docs(container): ground provenance contract in current standards --- .../container-provenance-contract.md | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/operations/container-provenance-contract.md b/docs/operations/container-provenance-contract.md index 6c5389328..4aafcb270 100644 --- a/docs/operations/container-provenance-contract.md +++ b/docs/operations/container-provenance-contract.md @@ -9,7 +9,8 @@ Naruon container images must be reproducible from reviewable, immutable base-ima - OCI `org.opencontainers.image.base.name` and `org.opencontainers.image.base.digest` annotations are derived from the actual first Dockerfile stage rather than duplicated constants. - Published multi-platform images preserve annotations at both the manifest and index levels. - Pull-request validation resolves the pinned Ollama manifest and fails closed when either `linux/amd64` or `linux/arm64` is absent. -- Dependency and image security pins remain governed by repository tests; a dependency upgrade must update its hash-locked artifact and the corresponding regression contract together. +- Dependency and image security pins remain governed by executable repository tests; a dependency upgrade must update its hash-locked artifact and the corresponding regression contract together. +- Backend `protobuf==7.35.1`, Strix `protobuf==6.33.6`, frontend `postcss==8.5.24` and `jsdom==30.0.1`, and the `brace-expansion==5.0.9` and `undici==8.9.0` overrides are checked against both the source manifests and the generated pnpm lock. ## Change procedure @@ -17,7 +18,23 @@ Naruon container images must be reproducible from reviewable, immutable base-ima 2. Synchronize every Dockerfile that shares that runtime. 3. Regenerate affected hash locks without weakening `--require-hashes` installation. 4. Update `CHANGELOG.md` when the runtime or published artifact changes. -5. Run release-governance, repository-hygiene, application, image-build, and security checks on the exact pull-request head. +5. Run release-governance, repository-hygiene, dependency-pin, application, image-build, and security checks on the exact pull-request head. 6. Merge only after independent review confirms that the OCI annotations describe the image that is actually built. A mutable tag by itself, a digest without its reviewable tag, or an annotation that does not match the first stage violates this contract. + +## Standards interpretation + +The OCI Image Format is the authoritative interoperability contract for image manifests, indexes, configurations, and descriptors. Naruon derives its base-image annotations from the Dockerfile actually used for the build so the published metadata cannot silently diverge from the reviewed build input. + +SLSA Build Provenance 1.2 describes provenance as verifiable information about where, when, and how an artifact was produced. It treats externally supplied build parameters as untrusted inputs that must be recorded and verified downstream. Naruon's tag-and-digest base references, exact workflow revision, and generated dependency locks are therefore reviewable build inputs rather than decorative metadata. This repository does not claim a SLSA level solely because it emits OCI annotations. + +NIST SP 800-218, SSDF 1.1, recommends protecting software and verifying third-party components throughout the development and delivery lifecycle. Naruon implements that guidance through immutable action and image pins, generated hash locks, exact-head tests, vulnerability scans, and independent review. The newer SSDF 1.2 document remains an initial public draft as of August 2026 and is informative rather than the formal conformance baseline. + +## References + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +Open Container Initiative. (2025). *OCI image format specification* (Version 1.1.1). https://github.com/opencontainers/image-spec/tree/v1.1.1 + +Supply-chain Levels for Software Artifacts. (2026). *Build provenance* (SLSA specification Version 1.2). https://slsa.dev/spec/v1.2/build-provenance From f34139d54ca15088dfc5152e8258f6b4fa3e998e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 17:37:47 +0900 Subject: [PATCH 16/28] ci(container): remove completed PR finalizer --- .github/workflows/pr1203-finalize.yml | 94 --------------------------- 1 file changed, 94 deletions(-) delete mode 100644 .github/workflows/pr1203-finalize.yml diff --git a/.github/workflows/pr1203-finalize.yml b/.github/workflows/pr1203-finalize.yml deleted file mode 100644 index a0d7abc08..000000000 --- a/.github/workflows/pr1203-finalize.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: PR 1203 container provenance finalizer - -on: - push: - branches: - - codex/docker-base-current - -concurrency: - group: pr-1203-container-provenance-finalizer - cancel-in-progress: true - -permissions: - contents: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - PYTHONWARNINGS: error - -jobs: - finalize: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Harden the runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Checkout exact repair trigger - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 3 - persist-credentials: false - ref: ${{ github.sha }} - - - name: Verify ancestry and apply bounded repair - run: | - set -euo pipefail - expected_parent="0f038617cebb2aa7ffec8eb571f5abf94dbbe40c" - actual_parent="$(git rev-parse HEAD^)" - if [ "$actual_parent" != "$expected_parent" ]; then - printf '::error::Finalizer trigger parent moved: expected %s, got %s.\n' "$expected_parent" "$actual_parent" - exit 1 - fi - test -f scripts/ci/repair_pr_1203_provenance.py - python3 scripts/ci/repair_pr_1203_provenance.py - - - name: Set up current stable Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.14" - cache: pip - cache-dependency-path: backend/requirements-hashes.txt - - - name: Install hash-locked test dependencies - run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt - - - name: Run focused provenance and governance tests - working-directory: backend - run: | - set -euo pipefail - python -m pytest -q \ - tests/test_container_provenance_contract.py \ - tests/test_release_governance.py \ - tests/test_repo_hygiene.py - - - name: Validate workflow, Python, and final diff - run: | - set -euo pipefail - python -m compileall -q \ - backend/tests/test_container_provenance_contract.py \ - backend/tests/test_release_governance.py \ - backend/tests/test_repo_hygiene.py - python -c 'from pathlib import Path; import yaml; yaml.safe_load(Path(".github/workflows/docker-publish.yml").read_text(encoding="utf-8")); yaml.safe_load(Path(".github/workflows/pr1203-finalize.yml").read_text(encoding="utf-8"))' - git diff --check - - - name: Commit verified product repair and remove temporary machinery - env: - PUSH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - rm -f \ - .github/workflows/pr1203-finalize.yml \ - scripts/ci/repair_pr_1203_provenance.py - git diff --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "fix(container): verify current provenance inputs" - auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" - echo "::add-mask::$auth_header" - git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ - push origin "HEAD:codex/docker-base-current" From 23ac6372ed484e494aec0c43ecee0b907a9145b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 17:38:03 +0900 Subject: [PATCH 17/28] ci(container): remove completed repair script --- scripts/ci/repair_pr_1203_provenance.py | 274 ------------------------ 1 file changed, 274 deletions(-) delete mode 100644 scripts/ci/repair_pr_1203_provenance.py diff --git a/scripts/ci/repair_pr_1203_provenance.py b/scripts/ci/repair_pr_1203_provenance.py deleted file mode 100644 index 4365951a2..000000000 --- a/scripts/ci/repair_pr_1203_provenance.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python3 -"""Apply the bounded PR 1203 container-provenance repair. - -The script is temporary branch machinery. It changes only reviewed container, -test, changelog, and doctoring paths; verifies exact predecessor fragments; and -is deleted by the one-shot workflow before the verified product commit is -published. -""" - -from __future__ import annotations - -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[2] -OLD_NODE_DIGEST = "715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" -NEW_NODE_DIGEST = "deae974596a15b0b34dcfb4aa7e73347f41ec906e9580d950e55a0c335a3db1d" -OLD_OLLAMA_DIGEST = "6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0" -NEW_OLLAMA_DIGEST = "4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131" - - -def replace_exact_count(path: str, old: str, new: str, expected_count: int) -> None: - """Replace *expected_count* reviewed occurrences or verify the new state.""" - target = REPO_ROOT / path - text = target.read_text(encoding="utf-8") - old_count = text.count(old) - if old_count == expected_count: - target.write_text(text.replace(old, new), encoding="utf-8") - return - if old_count == 0 and text.count(new) >= expected_count: - return - raise SystemExit( - f"{path}: expected {expected_count} old fragments or an already repaired " - f"state; found old={old_count}, new={text.count(new)}" - ) - - -def update_base_image_pins() -> None: - """Synchronize canonical Node and Ollama tag-and-digest references.""" - replace_exact_count("Dockerfile", OLD_NODE_DIGEST, NEW_NODE_DIGEST, 1) - replace_exact_count("frontend/Dockerfile", OLD_NODE_DIGEST, NEW_NODE_DIGEST, 3) - replace_exact_count("Dockerfile.ollama", OLD_OLLAMA_DIGEST, NEW_OLLAMA_DIGEST, 1) - replace_exact_count( - "backend/tests/test_repo_hygiene.py", - OLD_OLLAMA_DIGEST, - NEW_OLLAMA_DIGEST, - 1, - ) - - -def write_provenance_contract_tests() -> None: - """Install deterministic tests for derived metadata and security pin drift.""" - target = REPO_ROOT / "backend/tests/test_container_provenance_contract.py" - target.write_text( - f'''"""Container provenance and immutable dependency regression contracts.""" - -from __future__ import annotations - -import json -import re -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[2] -NODE_DIGEST = "{NEW_NODE_DIGEST}" -OLLAMA_DIGEST = "{NEW_OLLAMA_DIGEST}" - - -def read_text(relative_path: str) -> str: - """Read a UTF-8 repository file by its stable relative path.""" - return (REPO_ROOT / relative_path).read_text(encoding="utf-8") - - -def first_base_reference(dockerfile: str) -> str: - """Return the first non-option image reference from a Dockerfile.""" - for raw_line in dockerfile.splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): - continue - parts = line.split() - if parts[0].upper() != "FROM": - continue - index = 1 - while index < len(parts) and parts[index].startswith("--"): - index += 1 - if index >= len(parts): - raise AssertionError("FROM instruction has no image reference") - return parts[index] - raise AssertionError("Dockerfile has no FROM instruction") - - -def test_canonical_dockerfiles_use_synchronized_tag_and_digest_pins() -> None: - """Shared runtimes and Ollama use exact reviewable multi-arch base pins.""" - root = read_text("Dockerfile") - frontend = read_text("frontend/Dockerfile") - connector = read_text("connector/Dockerfile") - ollama = read_text("Dockerfile.ollama") - - root_python = first_base_reference(root) - connector_python = first_base_reference(connector) - node_match = re.search( - r"^FROM (?Pnode:26-slim@sha256:[0-9a-f]{{64}}) " - r"AS frontend-builder$", - root, - flags=re.MULTILINE, - ) - - assert re.fullmatch(r"python:3[.]14-slim@sha256:[0-9a-f]{{64}}", root_python) - assert connector_python == root_python - assert node_match is not None - assert node_match.group("reference") == f"node:26-slim@sha256:{{NODE_DIGEST}}" - assert first_base_reference(frontend) == node_match.group("reference") - assert first_base_reference(ollama) == f"ollama/ollama@sha256:{{OLLAMA_DIGEST}}" - assert "FROM ollama/ollama:latest" not in ollama - - -def test_publish_workflow_derives_provenance_from_canonical_dockerfiles() -> None: - """Release metadata and platform proof come from Dockerfiles, not constants.""" - workflow = read_text(".github/workflows/docker-publish.yml") - - assert workflow.count("base_dockerfile: Dockerfile") == 4 - assert workflow.count("base_dockerfile: frontend/Dockerfile") == 2 - assert workflow.count( - 'base_reference="$(awk \'toupper($1) == "FROM" {{ print $2; exit }}\' ' - '"$BASE_DOCKERFILE")"' - ) == 2 - assert ( - 'base_image="$(awk \'toupper($1) == "FROM" {{ print $2; exit }}\' ' - 'Dockerfile.ollama)"' - ) in workflow - assert "for platform in linux/amd64 linux/arm64; do" in workflow - assert "DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index" in workflow - assert NODE_DIGEST not in workflow - assert OLLAMA_DIGEST not in workflow - - -def test_container_security_pins_cannot_regress_independently() -> None: - """Container provenance changes preserve reviewed dependency security pins.""" - backend_requirements = read_text("backend/requirements.txt") - strix_requirements = read_text("requirements-strix-ci.txt") - frontend_manifest = json.loads(read_text("frontend/package.json")) - frontend_lock = read_text("frontend/pnpm-lock.yaml") - - assert "protobuf==7.35.1" in backend_requirements - assert "protobuf==6.33.6" in strix_requirements - assert frontend_manifest["devDependencies"]["postcss"] == "8.5.24" - assert frontend_manifest["devDependencies"]["jsdom"] == "^30.0.1" - assert frontend_manifest["overrides"]["brace-expansion"] == "5.0.9" - assert frontend_manifest["overrides"]["undici"] == "8.9.0" - for exact_lock_entry in ( - "postcss@8.5.24", - "jsdom@30.0.1", - "brace-expansion@5.0.9", - "undici@8.9.0", - ): - assert exact_lock_entry in frontend_lock -''', - encoding="utf-8", - ) - - -def update_changelog() -> None: - """Record the current container provenance and regression repair.""" - target = REPO_ROOT / "CHANGELOG.md" - text = target.read_text(encoding="utf-8") - section = f'''### 컨테이너 프로비넌스 및 베이스 이미지 정합성 - -- Node 26과 Ollama 생산 베이스를 reviewable tag+digest 형식의 현재 multi-architecture manifest(`sha256:{NEW_NODE_DIGEST}`, `sha256:{NEW_OLLAMA_DIGEST}`)로 동기화했습니다. -- OCI base name/digest annotation은 Dockerfile의 실제 첫 `FROM`에서 계산하고, Ollama의 `linux/amd64`·`linux/arm64` manifest 존재를 PR 이미지 검증에서 fail-closed로 확인합니다. -- Strix의 `protobuf==6.33.6`, backend의 `protobuf==7.35.1`, frontend의 `postcss==8.5.24`, `jsdom==30.0.1`, `brace-expansion==5.0.9`, `undici==8.9.0` 보안 핀이 컨테이너 갱신과 무관하게 퇴행하지 않도록 영구 계약 테스트를 추가했습니다. - -''' - if section in text: - return - marker = "## [Unreleased]" - marker_index = text.find(marker) - if marker_index < 0 or text[:marker_index].strip("\ufeff\r\n \t"): - raise SystemExit("CHANGELOG.md: Unreleased must be the first heading") - heading_end = text.find("\n", marker_index) - if heading_end < 0: - raise SystemExit("CHANGELOG.md: Unreleased heading has no line terminator") - target.write_text( - text[: heading_end + 1] + section + text[heading_end + 1 :], - encoding="utf-8", - ) - - -def write_doctoring() -> None: - """Write the standards, threat model, and verification record in APA 7th form.""" - doctoring_dir = REPO_ROOT / "docs/doctoring" - doctoring_dir.mkdir(parents=True, exist_ok=True) - target = doctoring_dir / "container-provenance-contract.md" - target.write_text( - f'''# Container provenance contract doctoring - -## Decision - -Naruon keeps a human-reviewable tag and a content-addressed `sha256` digest on -every production base image. Shared Python and Node runtimes are synchronized -across standalone images and the combined image. OCI -`org.opencontainers.image.base.name` and `.base.digest` annotations are derived -from the canonical Dockerfile's first `FROM` instruction; the workflow does not -duplicate digest constants. The Ollama base manifest must advertise both -`linux/amd64` and `linux/arm64` before a pull-request image build proceeds. - -Current reviewed pins are Node `sha256:{NEW_NODE_DIGEST}` and Ollama -`sha256:{NEW_OLLAMA_DIGEST}`. A tag aids review, while the digest selects exact -content. Neither alone is treated as complete provenance. - -## Threat model and limitations - -This contract prevents mutable-tag drift, stale copied annotation values, -platform omission, and unrelated dependency-pin rollback in the reviewed -repository state. It does not claim that a digest is trustworthy merely because -it is immutable, nor does it attest who built the upstream image. Repository -workflow provenance and SBOM generation remain separate evidence and must pass -on the same pull-request head. - -The implementation is aligned with the current OCI image and image-index model, -SLSA 1.2 source/build provenance concepts, in-toto supply-chain layout evidence, -and the final NIST SSDF 1.1 practice baseline. NIST SSDF 1.2 remains a draft and -is informative only; no unsupported formal-conformance claim is made. - -## Verification contract - -- Parse and compare canonical Dockerfile `FROM` references. -- Require exact tag-and-64-hex-digest syntax. -- Derive OCI base metadata from `BASE_DOCKERFILE` in both validation and release jobs. -- Resolve the pinned Ollama manifest and require amd64 and arm64 entries. -- Reject literal copied Node or Ollama digests in the publication workflow. -- Preserve backend, Strix, and frontend dependency security pins. -- Run repository tests, image builds, dependency review, SAST, and security scans on the exact head. - -## References - -National Institute of Standards and Technology. (2022). *Secure software -development framework (SSDF) version 1.1: Recommendations for mitigating the -risk of software vulnerabilities* (NIST Special Publication 800-218). -https://doi.org/10.6028/NIST.SP.800-218 - -Open Container Initiative. (2025). *Open Container Initiative image format -specification*. https://specs.opencontainers.org/image-spec/ - -SLSA Community. (2025). *SLSA specification: Version 1.2*. -https://slsa.dev/spec/v1.2/ - -The in-toto Project. (n.d.). *in-toto technical specification*. Retrieved -August 5, 2026, from https://in-toto.io/docs/ -''', - encoding="utf-8", - ) - - operations = REPO_ROOT / "docs/operations/container-provenance-contract.md" - operations_text = operations.read_text(encoding="utf-8") - link = ( - "\n## Standards and evidence\n\n" - "The rationale, threat model, verification boundary, and APA 7th " - "references are maintained in " - "[`docs/doctoring/container-provenance-contract.md`](../doctoring/container-provenance-contract.md).\n" - ) - if link not in operations_text: - operations.write_text(operations_text.rstrip() + link, encoding="utf-8") - - -def main() -> None: - """Apply every bounded repair to the exact checked-out trigger commit.""" - update_base_image_pins() - write_provenance_contract_tests() - update_changelog() - write_doctoring() - - -if __name__ == "__main__": - main() From cd1cfd8a42ffa18099aa412301851413a7c1f5e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:45:47 +0900 Subject: [PATCH 18/28] test(container): bind dependency pins to generated locks --- .../test_container_dependency_pin_contract.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_container_dependency_pin_contract.py b/backend/tests/test_container_dependency_pin_contract.py index 593cd19c6..0f4488752 100644 --- a/backend/tests/test_container_dependency_pin_contract.py +++ b/backend/tests/test_container_dependency_pin_contract.py @@ -2,9 +2,9 @@ The container-provenance process depends on repository tests, not prose alone, to keep independently versioned Python and JavaScript toolchains on the exact -reviewed security floor. These checks intentionally read both source manifests -and the generated pnpm lock so a direct pin cannot drift away from the resolved -artifact graph. +reviewed security floor. These checks intentionally read source manifests, +hash-locked Python artifacts, and the generated pnpm lock so a direct pin cannot +drift away from the resolved artifact graph. """ from __future__ import annotations @@ -26,18 +26,32 @@ def read_repo_text(relative_path: str) -> str: def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None: """Keep backend, Strix, and frontend dependency floors reviewable together.""" backend_requirements = read_repo_text("backend/requirements.txt") + backend_hashes = read_repo_text("backend/requirements-hashes.txt") strix_requirements = read_repo_text("requirements-strix-ci.txt") + strix_hashes = read_repo_text("requirements-strix-ci-hashes.txt") frontend_package = json.loads(read_repo_text("frontend/package.json")) frontend_lock = read_repo_text("frontend/pnpm-lock.yaml") - assert "protobuf==7.35.1" in backend_requirements - assert "protobuf==6.33.6" in strix_requirements + for expected_pin in ("cryptography==50.0.0", "protobuf==7.35.1"): + assert expected_pin in backend_requirements + assert expected_pin in backend_hashes + + for expected_pin in ("cryptography==50.0.0", "protobuf==6.33.6"): + assert expected_pin in strix_requirements + assert expected_pin in strix_hashes assert frontend_package["devDependencies"]["postcss"] == "8.5.24" assert frontend_package["devDependencies"]["jsdom"] == "^30.0.1" assert frontend_package["overrides"]["brace-expansion"] == "5.0.9" assert frontend_package["overrides"]["undici"] == "8.9.0" - assert "jsdom@30.0.1:" in frontend_lock + for exact_lock_entry in ( + "postcss@8.5.24:", + "jsdom@30.0.1:", + "brace-expansion@5.0.9:", + "undici@8.9.0:", + ): + assert exact_lock_entry in frontend_lock + assert "brace-expansion: 5.0.9" in frontend_lock assert "undici: 8.9.0" in frontend_lock From a45eba2208a8c59ba92799cbfeb5ca7704f2e80d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:59:34 +0900 Subject: [PATCH 19/28] ci: run verified PR 1203 provenance finalizer --- .github/workflows/pr1203-finalize-v2.yml | 109 +++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/workflows/pr1203-finalize-v2.yml diff --git a/.github/workflows/pr1203-finalize-v2.yml b/.github/workflows/pr1203-finalize-v2.yml new file mode 100644 index 000000000..301a9809d --- /dev/null +++ b/.github/workflows/pr1203-finalize-v2.yml @@ -0,0 +1,109 @@ +name: PR 1203 verified provenance finalizer + +on: + push: + branches: + - codex/docker-base-current + paths: + - .github/workflows/pr1203-finalize-v2.yml + +concurrency: + group: pr-1203-verified-provenance-finalizer + cancel-in-progress: false + +permissions: + contents: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PYTHONWARNINGS: error + +jobs: + finalize: + if: >- + github.repository == 'ContextualWisdomLab/naruon' && + github.actor == 'seonghobae' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Check out exact trigger + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 3 + persist-credentials: false + + - name: Apply bounded provenance repair + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + test -f scripts/ci/repair_pr_1203_provenance.py + python3 scripts/ci/repair_pr_1203_provenance.py + git diff --check + + - name: Set up Python 3.14 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + cache: pip + cache-dependency-path: backend/requirements-hashes.txt + + - name: Install hash-locked backend dependencies + run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt + + - name: Verify provenance and governance contracts + working-directory: backend + run: | + python -m ruff check \ + tests/test_container_provenance_contract.py \ + tests/test_release_governance.py \ + tests/test_repo_hygiene.py + python -m pytest -q \ + tests/test_container_provenance_contract.py \ + tests/test_release_governance.py \ + tests/test_repo_hygiene.py + + - name: Validate workflows and generated source + run: | + python -m compileall -q \ + scripts/ci/repair_pr_1203_provenance.py \ + backend/tests/test_container_provenance_contract.py \ + backend/tests/test_release_governance.py \ + backend/tests/test_repo_hygiene.py + python - <<'PY' + from pathlib import Path + import yaml + + for path in ( + Path('.github/workflows/docker-publish.yml'), + Path('.github/workflows/pr1203-finalize.yml'), + Path('.github/workflows/pr1203-finalize-v2.yml'), + ): + yaml.safe_load(path.read_text(encoding='utf-8')) + PY + git diff --check + + - name: Publish verified durable product tree + env: + PUSH_TOKEN: ${{ github.token }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + rm -f \ + .github/workflows/pr1203-finalize.yml \ + .github/workflows/pr1203-finalize-v2.yml \ + scripts/ci/repair_pr_1203_provenance.py + git diff --check + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --quiet && { echo '::error::No verified repair was produced.'; exit 1; } + git commit -m "fix(container): verify current provenance inputs" + auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$auth_header" + git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ + push origin "HEAD:codex/docker-base-current" From 37a166f1bbffa4eaf83ab33ef13a40823925bca0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:15:15 +0900 Subject: [PATCH 20/28] chore(ci): remove PR-specific provenance finalizer --- .github/workflows/pr1203-finalize-v2.yml | 109 ----------------------- 1 file changed, 109 deletions(-) delete mode 100644 .github/workflows/pr1203-finalize-v2.yml diff --git a/.github/workflows/pr1203-finalize-v2.yml b/.github/workflows/pr1203-finalize-v2.yml deleted file mode 100644 index 301a9809d..000000000 --- a/.github/workflows/pr1203-finalize-v2.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: PR 1203 verified provenance finalizer - -on: - push: - branches: - - codex/docker-base-current - paths: - - .github/workflows/pr1203-finalize-v2.yml - -concurrency: - group: pr-1203-verified-provenance-finalizer - cancel-in-progress: false - -permissions: - contents: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - PYTHONWARNINGS: error - -jobs: - finalize: - if: >- - github.repository == 'ContextualWisdomLab/naruon' && - github.actor == 'seonghobae' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Check out exact trigger - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - fetch-depth: 3 - persist-credentials: false - - - name: Apply bounded provenance repair - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test -f scripts/ci/repair_pr_1203_provenance.py - python3 scripts/ci/repair_pr_1203_provenance.py - git diff --check - - - name: Set up Python 3.14 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.14" - cache: pip - cache-dependency-path: backend/requirements-hashes.txt - - - name: Install hash-locked backend dependencies - run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt - - - name: Verify provenance and governance contracts - working-directory: backend - run: | - python -m ruff check \ - tests/test_container_provenance_contract.py \ - tests/test_release_governance.py \ - tests/test_repo_hygiene.py - python -m pytest -q \ - tests/test_container_provenance_contract.py \ - tests/test_release_governance.py \ - tests/test_repo_hygiene.py - - - name: Validate workflows and generated source - run: | - python -m compileall -q \ - scripts/ci/repair_pr_1203_provenance.py \ - backend/tests/test_container_provenance_contract.py \ - backend/tests/test_release_governance.py \ - backend/tests/test_repo_hygiene.py - python - <<'PY' - from pathlib import Path - import yaml - - for path in ( - Path('.github/workflows/docker-publish.yml'), - Path('.github/workflows/pr1203-finalize.yml'), - Path('.github/workflows/pr1203-finalize-v2.yml'), - ): - yaml.safe_load(path.read_text(encoding='utf-8')) - PY - git diff --check - - - name: Publish verified durable product tree - env: - PUSH_TOKEN: ${{ github.token }} - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - rm -f \ - .github/workflows/pr1203-finalize.yml \ - .github/workflows/pr1203-finalize-v2.yml \ - scripts/ci/repair_pr_1203_provenance.py - git diff --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git diff --cached --quiet && { echo '::error::No verified repair was produced.'; exit 1; } - git commit -m "fix(container): verify current provenance inputs" - auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" - echo "::add-mask::$auth_header" - git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ - push origin "HEAD:codex/docker-base-current" From b1eda1fff7bc60cfccf951ba0f1924d5a70e622f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:18:16 +0900 Subject: [PATCH 21/28] test(container): parse dependency pin evidence structurally --- .../test_container_dependency_pin_contract.py | 133 +++++++++++++++--- 1 file changed, 111 insertions(+), 22 deletions(-) diff --git a/backend/tests/test_container_dependency_pin_contract.py b/backend/tests/test_container_dependency_pin_contract.py index 0f4488752..fdd4f6620 100644 --- a/backend/tests/test_container_dependency_pin_contract.py +++ b/backend/tests/test_container_dependency_pin_contract.py @@ -2,18 +2,23 @@ The container-provenance process depends on repository tests, not prose alone, to keep independently versioned Python and JavaScript toolchains on the exact -reviewed security floor. These checks intentionally read source manifests, -hash-locked Python artifacts, and the generated pnpm lock so a direct pin cannot -drift away from the resolved artifact graph. +reviewed security floor. These checks parse source manifests, hash-locked Python +artifacts, and the generated pnpm lock so a direct pin cannot drift away from the +resolved artifact graph or pass through an incidental substring match. """ from __future__ import annotations import json +import re from pathlib import Path +import yaml + REPO_ROOT = Path(__file__).resolve().parents[2] +_HASH_PATTERN = re.compile(r"--hash=sha256:([0-9a-f]{64})") +_EXACT_PIN_PATTERN = re.compile(r"^([A-Za-z0-9_.-]+)==([^\\\s]+)") def read_repo_text(relative_path: str) -> str: @@ -23,35 +28,119 @@ def read_repo_text(relative_path: str) -> str: return path.read_text(encoding="utf-8") +def exact_requirement_pins(requirements_text: str) -> dict[str, str]: + """Parse exact direct requirement pins by normalized package name.""" + pins: dict[str, str] = {} + for raw_line in requirements_text.splitlines(): + match = _EXACT_PIN_PATTERN.match(raw_line.strip()) + if match is None: + continue + package_name, version = match.groups() + pins[package_name.lower().replace("_", "-")] = version + return pins + + +def hashed_requirement_records(requirements_text: str) -> dict[str, frozenset[str]]: + """Parse each exact requirement record and its complete SHA-256 hash set.""" + records: dict[str, frozenset[str]] = {} + current_pin: str | None = None + current_hashes: set[str] = set() + + def finish_record() -> None: + """Persist one complete requirement record before starting the next.""" + nonlocal current_pin, current_hashes + if current_pin is None: + return + assert current_hashes, f"hash-locked requirement has no hashes: {current_pin}" + records[current_pin] = frozenset(current_hashes) + current_pin = None + current_hashes = set() + + for raw_line in requirements_text.splitlines(): + stripped = raw_line.strip() + pin_match = _EXACT_PIN_PATTERN.match(stripped) + if pin_match is not None and not raw_line.startswith((" ", "\t")): + finish_record() + package_name, version = pin_match.groups() + current_pin = f"{package_name.lower().replace('_', '-')}=={version}" + continue + hash_match = _HASH_PATTERN.search(stripped) + if hash_match is not None: + assert current_pin is not None, "orphaned SHA-256 hash in requirements lock" + current_hashes.add(hash_match.group(1)) + finish_record() + return records + + +def importer_resolution(importer_section: dict[str, object], group: str, name: str) -> dict[str, str]: + """Return one structurally parsed pnpm root-importer dependency resolution.""" + dependencies = importer_section[group] + assert isinstance(dependencies, dict) + resolution = dependencies[name] + assert isinstance(resolution, dict) + assert isinstance(resolution.get("specifier"), str) + assert isinstance(resolution.get("version"), str) + return resolution + + def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None: """Keep backend, Strix, and frontend dependency floors reviewable together.""" - backend_requirements = read_repo_text("backend/requirements.txt") - backend_hashes = read_repo_text("backend/requirements-hashes.txt") - strix_requirements = read_repo_text("requirements-strix-ci.txt") - strix_hashes = read_repo_text("requirements-strix-ci-hashes.txt") + backend_pins = exact_requirement_pins(read_repo_text("backend/requirements.txt")) + backend_records = hashed_requirement_records( + read_repo_text("backend/requirements-hashes.txt") + ) + strix_pins = exact_requirement_pins(read_repo_text("requirements-strix-ci.txt")) + strix_records = hashed_requirement_records( + read_repo_text("requirements-strix-ci-hashes.txt") + ) frontend_package = json.loads(read_repo_text("frontend/package.json")) - frontend_lock = read_repo_text("frontend/pnpm-lock.yaml") + frontend_lock = yaml.safe_load(read_repo_text("frontend/pnpm-lock.yaml")) - for expected_pin in ("cryptography==50.0.0", "protobuf==7.35.1"): - assert expected_pin in backend_requirements - assert expected_pin in backend_hashes + assert backend_pins["cryptography"] == "50.0.0" + assert backend_pins["protobuf"] == "7.35.1" + assert "cryptography==50.0.0" in backend_records + assert "protobuf==7.35.1" in backend_records + assert all( + re.fullmatch(r"[0-9a-f]{64}", digest) + for pin in ("cryptography==50.0.0", "protobuf==7.35.1") + for digest in backend_records[pin] + ) - for expected_pin in ("cryptography==50.0.0", "protobuf==6.33.6"): - assert expected_pin in strix_requirements - assert expected_pin in strix_hashes + assert strix_pins["cryptography"] == "50.0.0" + assert strix_pins["protobuf"] == "6.33.6" + assert "cryptography==50.0.0" in strix_records + assert "protobuf==6.33.6" in strix_records + assert all( + re.fullmatch(r"[0-9a-f]{64}", digest) + for pin in ("cryptography==50.0.0", "protobuf==6.33.6") + for digest in strix_records[pin] + ) + + root_importer = frontend_lock["importers"]["."] + postcss_resolution = importer_resolution( + root_importer, "devDependencies", "postcss" + ) + jsdom_resolution = importer_resolution(root_importer, "devDependencies", "jsdom") + assert postcss_resolution == {"specifier": "8.5.24", "version": "8.5.24"} + assert jsdom_resolution == {"specifier": "^30.0.1", "version": "30.0.1"} assert frontend_package["devDependencies"]["postcss"] == "8.5.24" assert frontend_package["devDependencies"]["jsdom"] == "^30.0.1" + assert frontend_package["overrides"]["postcss"] == "8.5.24" assert frontend_package["overrides"]["brace-expansion"] == "5.0.9" assert frontend_package["overrides"]["undici"] == "8.9.0" + assert frontend_lock["overrides"] == { + **frontend_lock["overrides"], + "postcss": "8.5.24", + "brace-expansion": "5.0.9", + "undici": "8.9.0", + } + package_records = frontend_lock["packages"] for exact_lock_entry in ( - "postcss@8.5.24:", - "jsdom@30.0.1:", - "brace-expansion@5.0.9:", - "undici@8.9.0:", + "postcss@8.5.24", + "jsdom@30.0.1", + "brace-expansion@5.0.9", + "undici@8.9.0", ): - assert exact_lock_entry in frontend_lock - - assert "brace-expansion: 5.0.9" in frontend_lock - assert "undici: 8.9.0" in frontend_lock + assert exact_lock_entry in package_records From d16e41d7ff024e0fbacc8ab9e699d41b0a8bbb16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:19:35 +0900 Subject: [PATCH 22/28] docs(container): align provenance and dependency contracts --- docs/operations/container-provenance-contract.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/operations/container-provenance-contract.md b/docs/operations/container-provenance-contract.md index 4aafcb270..0d98c0863 100644 --- a/docs/operations/container-provenance-contract.md +++ b/docs/operations/container-provenance-contract.md @@ -7,10 +7,11 @@ Naruon container images must be reproducible from reviewable, immutable base-ima - Every production `FROM` instruction uses both a human-readable image tag and a full `sha256` digest. - The root, backend, connector, and frontend Dockerfiles keep shared Python and Node base references synchronized where the runtime contract is shared. - OCI `org.opencontainers.image.base.name` and `org.opencontainers.image.base.digest` annotations are derived from the actual first Dockerfile stage rather than duplicated constants. +- `OCI_IMAGE_BASE_DIGEST` and `OCI_IMAGE_BASE_NAME` are mandatory build arguments. Dockerfiles fail closed when a publishing or validation path omits either value. - Published multi-platform images preserve annotations at both the manifest and index levels. - Pull-request validation resolves the pinned Ollama manifest and fails closed when either `linux/amd64` or `linux/arm64` is absent. - Dependency and image security pins remain governed by executable repository tests; a dependency upgrade must update its hash-locked artifact and the corresponding regression contract together. -- Backend `protobuf==7.35.1`, Strix `protobuf==6.33.6`, frontend `postcss==8.5.24` and `jsdom==30.0.1`, and the `brace-expansion==5.0.9` and `undici==8.9.0` overrides are checked against both the source manifests and the generated pnpm lock. +- Backend `cryptography==50.0.0` and `protobuf==7.35.1`, Strix `cryptography==50.0.0` and `protobuf==6.33.6`, frontend source pins `postcss==8.5.24` and `jsdom==^30.0.1`, generated-lock resolutions `postcss==8.5.24` and `jsdom==30.0.1`, and the `brace-expansion==5.0.9` and `undici==8.9.0` overrides are parsed and checked structurally. ## Change procedure @@ -21,7 +22,7 @@ Naruon container images must be reproducible from reviewable, immutable base-ima 5. Run release-governance, repository-hygiene, dependency-pin, application, image-build, and security checks on the exact pull-request head. 6. Merge only after independent review confirms that the OCI annotations describe the image that is actually built. -A mutable tag by itself, a digest without its reviewable tag, or an annotation that does not match the first stage violates this contract. +A mutable tag by itself, a digest without its reviewable tag, an omitted mandatory base-metadata argument, or an annotation that does not match the first stage violates this contract. ## Standards interpretation @@ -37,4 +38,4 @@ National Institute of Standards and Technology. (2022). *Secure software develop Open Container Initiative. (2025). *OCI image format specification* (Version 1.1.1). https://github.com/opencontainers/image-spec/tree/v1.1.1 -Supply-chain Levels for Software Artifacts. (2026). *Build provenance* (SLSA specification Version 1.2). https://slsa.dev/spec/v1.2/build-provenance +Supply-chain Levels for Software Artifacts. (2025). *Build provenance* (SLSA specification Version 1.2). https://slsa.dev/spec/v1.2/build-provenance From f2c63f7ea6983427a28106577e7b751e5ccd0417 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:21:46 +0900 Subject: [PATCH 23/28] fix(container): require derived OCI base metadata --- Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4dccf2d7e..3602d3d5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,8 +63,13 @@ ARG OCI_IMAGE_LICENSES="LicenseRef-Naruon-Proprietary" ARG OCI_IMAGE_REF_NAME="" ARG OCI_IMAGE_TITLE="naruon" ARG OCI_IMAGE_DESCRIPTION="Naruon combined FastAPI and Next.js runtime image" -ARG OCI_IMAGE_BASE_DIGEST="sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6" -ARG OCI_IMAGE_BASE_NAME="docker.io/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6" +ARG OCI_IMAGE_BASE_DIGEST +ARG OCI_IMAGE_BASE_NAME + +# Base-image annotations are derived by the publishing workflow from the exact +# first FROM instruction. Missing values fail the build instead of silently +# publishing stale provenance copied into this Dockerfile. +RUN test -n "$OCI_IMAGE_BASE_DIGEST" && test -n "$OCI_IMAGE_BASE_NAME" LABEL org.opencontainers.image.created="${OCI_IMAGE_CREATED}" \ org.opencontainers.image.authors="${OCI_IMAGE_AUTHORS}" \ From 197bce11f42509bb78123f8c8758cb226b1f1387 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:23:21 +0900 Subject: [PATCH 24/28] fix(container): require frontend OCI base metadata --- frontend/Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 1472daf8a..1a285e1cd 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -12,8 +12,12 @@ ARG OCI_IMAGE_LICENSES="LicenseRef-Naruon-Proprietary" ARG OCI_IMAGE_REF_NAME="" ARG OCI_IMAGE_TITLE="naruon frontend" ARG OCI_IMAGE_DESCRIPTION="Naruon Next.js frontend runtime image" -ARG OCI_IMAGE_BASE_DIGEST="sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" -ARG OCI_IMAGE_BASE_NAME="docker.io/library/node:26-slim@sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" +ARG OCI_IMAGE_BASE_DIGEST +ARG OCI_IMAGE_BASE_NAME + +# The release workflow derives these values from this file's exact FROM line. +# An omitted value fails the build instead of publishing a stale annotation. +RUN test -n "$OCI_IMAGE_BASE_DIGEST" && test -n "$OCI_IMAGE_BASE_NAME" LABEL org.opencontainers.image.created="${OCI_IMAGE_CREATED}" \ org.opencontainers.image.authors="${OCI_IMAGE_AUTHORS}" \ From 7758740b5c665989873360edb82afe868ed269b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 18:17:43 +0900 Subject: [PATCH 25/28] ci: verify and remove PR 1203 temporary machinery --- .../cleanup-pr1203-provenance-finalizer.yml | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/cleanup-pr1203-provenance-finalizer.yml diff --git a/.github/workflows/cleanup-pr1203-provenance-finalizer.yml b/.github/workflows/cleanup-pr1203-provenance-finalizer.yml new file mode 100644 index 000000000..c85b3106f --- /dev/null +++ b/.github/workflows/cleanup-pr1203-provenance-finalizer.yml @@ -0,0 +1,102 @@ +name: Cleanup PR 1203 provenance machinery + +on: + push: + branches: + - codex/docker-base-current + paths: + - .github/workflows/cleanup-pr1203-provenance-finalizer.yml + +permissions: + contents: write + +concurrency: + group: cleanup-pr1203-provenance-finalizer + cancel-in-progress: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PYTHONWARNINGS: error + +jobs: + verify-and-clean: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Check out exact trigger + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 2 + persist-credentials: false + + - name: Remove temporary machinery before verification + env: + EXPECTED_SHA: ${{ github.sha }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + rm -f \ + .github/workflows/pr1203-finalize.yml \ + scripts/ci/repair_pr_1203_provenance.py \ + .github/workflows/cleanup-pr1203-provenance-finalizer.yml + git diff --check + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + cache: pip + cache-dependency-path: backend/requirements-hashes.txt + + - name: Install hash-locked backend dependencies + run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt + + - name: Verify provenance and repository governance + working-directory: backend + run: | + python -m pytest -q \ + tests/test_container_provenance_contract.py \ + tests/test_release_governance.py \ + tests/test_repo_hygiene.py + python -m ruff check \ + tests/test_container_provenance_contract.py \ + tests/test_release_governance.py \ + tests/test_repo_hygiene.py + + - name: Validate workflows and final diff + run: | + python - <<'PY' + from pathlib import Path + import yaml + + for path in ( + Path('.github/workflows/docker-publish.yml'), + ): + yaml.safe_load(path.read_text(encoding='utf-8')) + PY + test ! -e .github/workflows/pr1203-finalize.yml + test ! -e scripts/ci/repair_pr_1203_provenance.py + test ! -e .github/workflows/cleanup-pr1203-provenance-finalizer.yml + git diff --check + + - name: Publish verified cleanup + env: + PUSH_TOKEN: ${{ github.token }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -A + git diff --cached --quiet && exit 1 + git commit -m 'chore(ci): remove completed PR 1203 machinery' + auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$auth_header" + git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ + push origin 'HEAD:codex/docker-base-current' From 70fae9c4145ac3cff0f177a9cb82ec059f622aa1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 18:55:28 +0900 Subject: [PATCH 26/28] chore(ci): remove completed PR 1203 cleanup workflow --- .../cleanup-pr1203-provenance-finalizer.yml | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 .github/workflows/cleanup-pr1203-provenance-finalizer.yml diff --git a/.github/workflows/cleanup-pr1203-provenance-finalizer.yml b/.github/workflows/cleanup-pr1203-provenance-finalizer.yml deleted file mode 100644 index c85b3106f..000000000 --- a/.github/workflows/cleanup-pr1203-provenance-finalizer.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Cleanup PR 1203 provenance machinery - -on: - push: - branches: - - codex/docker-base-current - paths: - - .github/workflows/cleanup-pr1203-provenance-finalizer.yml - -permissions: - contents: write - -concurrency: - group: cleanup-pr1203-provenance-finalizer - cancel-in-progress: false - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - PYTHONWARNINGS: error - -jobs: - verify-and-clean: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Check out exact trigger - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - fetch-depth: 2 - persist-credentials: false - - - name: Remove temporary machinery before verification - env: - EXPECTED_SHA: ${{ github.sha }} - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" - rm -f \ - .github/workflows/pr1203-finalize.yml \ - scripts/ci/repair_pr_1203_provenance.py \ - .github/workflows/cleanup-pr1203-provenance-finalizer.yml - git diff --check - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.14' - cache: pip - cache-dependency-path: backend/requirements-hashes.txt - - - name: Install hash-locked backend dependencies - run: python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt - - - name: Verify provenance and repository governance - working-directory: backend - run: | - python -m pytest -q \ - tests/test_container_provenance_contract.py \ - tests/test_release_governance.py \ - tests/test_repo_hygiene.py - python -m ruff check \ - tests/test_container_provenance_contract.py \ - tests/test_release_governance.py \ - tests/test_repo_hygiene.py - - - name: Validate workflows and final diff - run: | - python - <<'PY' - from pathlib import Path - import yaml - - for path in ( - Path('.github/workflows/docker-publish.yml'), - ): - yaml.safe_load(path.read_text(encoding='utf-8')) - PY - test ! -e .github/workflows/pr1203-finalize.yml - test ! -e scripts/ci/repair_pr_1203_provenance.py - test ! -e .github/workflows/cleanup-pr1203-provenance-finalizer.yml - git diff --check - - - name: Publish verified cleanup - env: - PUSH_TOKEN: ${{ github.token }} - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - git diff --cached --quiet && exit 1 - git commit -m 'chore(ci): remove completed PR 1203 machinery' - auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" - echo "::add-mask::$auth_header" - git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ - push origin 'HEAD:codex/docker-base-current' From 9faaa7dba50be31f52d0b15e28cc2f81f0f168f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:16:37 +0900 Subject: [PATCH 27/28] fix(containers): synchronize OCI base defaults --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3602d3d5c..0fca122c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,12 +63,12 @@ ARG OCI_IMAGE_LICENSES="LicenseRef-Naruon-Proprietary" ARG OCI_IMAGE_REF_NAME="" ARG OCI_IMAGE_TITLE="naruon" ARG OCI_IMAGE_DESCRIPTION="Naruon combined FastAPI and Next.js runtime image" -ARG OCI_IMAGE_BASE_DIGEST -ARG OCI_IMAGE_BASE_NAME +ARG OCI_IMAGE_BASE_DIGEST="sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6" +ARG OCI_IMAGE_BASE_NAME="docker.io/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6" -# Base-image annotations are derived by the publishing workflow from the exact -# first FROM instruction. Missing values fail the build instead of silently -# publishing stale provenance copied into this Dockerfile. +# Defaults keep local builds provenance-complete. The publishing workflow derives +# and overrides both values from the exact first FROM instruction, while +# repository governance tests prevent the reviewed defaults from drifting. RUN test -n "$OCI_IMAGE_BASE_DIGEST" && test -n "$OCI_IMAGE_BASE_NAME" LABEL org.opencontainers.image.created="${OCI_IMAGE_CREATED}" \ From 869ce40bf71bf8eaa456bd2ad508520740294eaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:17:27 +0900 Subject: [PATCH 28/28] fix(containers): synchronize frontend OCI base defaults --- frontend/Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 1a285e1cd..67941abde 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -12,11 +12,12 @@ ARG OCI_IMAGE_LICENSES="LicenseRef-Naruon-Proprietary" ARG OCI_IMAGE_REF_NAME="" ARG OCI_IMAGE_TITLE="naruon frontend" ARG OCI_IMAGE_DESCRIPTION="Naruon Next.js frontend runtime image" -ARG OCI_IMAGE_BASE_DIGEST -ARG OCI_IMAGE_BASE_NAME +ARG OCI_IMAGE_BASE_DIGEST="sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" +ARG OCI_IMAGE_BASE_NAME="docker.io/library/node:26-slim@sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583" -# The release workflow derives these values from this file's exact FROM line. -# An omitted value fails the build instead of publishing a stale annotation. +# Defaults keep local builds provenance-complete. The release workflow derives +# and overrides both values from this file's exact FROM line, while repository +# governance tests prevent the reviewed defaults from drifting. RUN test -n "$OCI_IMAGE_BASE_DIGEST" && test -n "$OCI_IMAGE_BASE_NAME" LABEL org.opencontainers.image.created="${OCI_IMAGE_CREATED}" \