Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1a43b34
fix(containers): bind provenance to pinned bases
seonghobae Aug 4, 2026
0d17e7f
ci: materialize PR 1203 governance test fix
seonghobae Aug 4, 2026
982fb32
ci: trigger PR 1203 governance fix from review state
seonghobae Aug 4, 2026
991c760
test(governance): align Strix cryptography security pin
github-actions[bot] Aug 4, 2026
636eac1
ci: revalidate container provenance on current head
seonghobae Aug 4, 2026
c700e18
docs(containers): define pinned provenance contract
seonghobae Aug 4, 2026
e2a5eb1
Merge branch 'develop' into codex/docker-base-current
opencode-agent[bot] Aug 4, 2026
c547b73
ci: materialize container provenance review fixes
seonghobae Aug 4, 2026
7077d23
ci: add bounded PR 1203 provenance repair
seonghobae Aug 5, 2026
3e5703c
ci: correct PR 1203 provenance finalizer
seonghobae Aug 5, 2026
f7a69f4
ci: sanitize PR 1203 generated regex literal
seonghobae Aug 5, 2026
b5baad6
ci: anchor PR 1203 changelog insertion at first heading
seonghobae Aug 5, 2026
0f03861
ci: rebind PR 1203 finalizer to corrected helper
seonghobae Aug 5, 2026
acf66d0
ci: use the canonical backend test lock for PR 1203
seonghobae Aug 5, 2026
bd3cceb
test(container): lock provenance dependency floors
seonghobae Aug 5, 2026
21949d6
docs(container): ground provenance contract in current standards
seonghobae Aug 5, 2026
f34139d
ci(container): remove completed PR finalizer
seonghobae Aug 5, 2026
23ac637
ci(container): remove completed repair script
seonghobae Aug 5, 2026
cd1cfd8
test(container): bind dependency pins to generated locks
seonghobae Aug 5, 2026
a45eba2
ci: run verified PR 1203 provenance finalizer
seonghobae Aug 5, 2026
37a166f
chore(ci): remove PR-specific provenance finalizer
seonghobae Aug 6, 2026
35b6780
Merge branch 'develop' into codex/docker-base-current
opencode-agent[bot] Aug 6, 2026
b1eda1f
test(container): parse dependency pin evidence structurally
seonghobae Aug 6, 2026
d16e41d
docs(container): align provenance and dependency contracts
seonghobae Aug 6, 2026
f2c63f7
fix(container): require derived OCI base metadata
seonghobae Aug 6, 2026
92e7692
Merge branch 'develop' into codex/docker-base-current
opencode-agent[bot] Aug 6, 2026
197bce1
fix(container): require frontend OCI base metadata
seonghobae Aug 6, 2026
7758740
ci: verify and remove PR 1203 temporary machinery
seonghobae Aug 6, 2026
8cd0471
Merge branch 'develop' into codex/docker-base-current
opencode-agent[bot] Aug 7, 2026
70fae9c
chore(ci): remove completed PR 1203 cleanup workflow
seonghobae Aug 7, 2026
9faaa7d
fix(containers): synchronize OCI base defaults
seonghobae Aug 7, 2026
869ce40
fix(containers): synchronize frontend OCI base defaults
seonghobae Aug 7, 2026
b694980
Merge branch 'develop' into codex/docker-base-current
opencode-agent[bot] Aug 7, 2026
cb3946c
Merge branch 'develop' into codex/docker-base-current
opencode-agent[bot] Aug 7, 2026
a6cce6f
Consolidate current container pins with provenance contract
seonghobae Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 48 additions & 12 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -64,9 +67,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 }}
Expand All @@ -76,24 +98,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
{
Expand Down Expand Up @@ -158,18 +185,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
Expand Down Expand Up @@ -204,6 +234,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 }}
Expand All @@ -214,24 +245,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
{
Expand Down
13 changes: 9 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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: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"

# 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}" \
org.opencontainers.image.authors="${OCI_IMAGE_AUTHORS}" \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.ollama
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ollama/ollama@sha256:509fdf54e23bd50d87af646cb51c0a7a203d6a83cc4d6695b3b08c5be1c62c0a
FROM ollama/ollama@sha256:6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0

ENV OLLAMA_MODELS=/usr/share/ollama/.ollama/models

Expand Down
146 changes: 146 additions & 0 deletions backend/tests/test_container_dependency_pin_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""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 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:
"""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 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_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 = yaml.safe_load(read_repo_text("frontend/pnpm-lock.yaml"))

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]
)

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",
):
assert exact_lock_entry in package_records
Loading
Loading