From 84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 16 Oct 2026 19:52:09 +0200 Subject: [PATCH] Verify image publication without Python and bind local managed package Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786 --- docs/native-signer-workload.json | 28 ++++++-- tests/test_image_publisher.py | 64 +++++++++++++++++++ tools/publish-runtime-images.sh | 18 +++++- ...-WP-0024-decision-envelope-authenticity.md | 19 ++++++ 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 tests/test_image_publisher.py diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json index 5673647..7bea881 100644 --- a/docs/native-signer-workload.json +++ b/docs/native-signer-workload.json @@ -33,17 +33,33 @@ "kid" ], "proposed_catalog_workload_reference": { - "name": "flex-auth", - "declaration_ref": "access-engine/tenancy.yaml#/workload_identity", + "rapp_id": "rapp-flex-auth-secrets-engine", + "name": "flex-auth-secrets-engine", + "deployable": "flex-auth-secrets-engine", + "declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml", + "declaration_commit": "d52f811ed5ec0be27c9c367e71d476125b3228be", "exact_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine", - "resolution": "unknown until owner custody and managed-package admission; do not mark resolvable merely from source binding" + "resolution": "Local source tuple resolves draft. This is an owner-proposed catalog pointer only; actual Platform catalog/custody is unchanged and remains pending admission." }, "key_created_or_admitted": false }, "managed_package_limit": { - "rapp_declaration_ref": null, - "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.", - "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule." + "rapp_declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml", + "status": "Canonical local package declaration now exists at exact recorded commit/hash. Repo Manager resolves the exact tuple/deployable under /home/worsch with ownership_repo access-engine and readiness draft. Existing unrelated rapp-forgejo declaration_missing remains visible. This source resolution does not claim central registration, private source access/placement, reef admission, signer custody, native deployment or independent consumer trust.", + "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule.", + "owner_selected_reference": { + "rapp_id": "rapp-flex-auth-secrets-engine", + "name": "flex-auth-secrets-engine", + "deployable": "flex-auth-secrets-engine", + "declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml", + "declaration_commit": "d52f811ed5ec0be27c9c367e71d476125b3228be", + "declaration_tree": "d4fbad71bdc8ef870603526ccd1f53195d0322af", + "declaration_sha256": "0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c", + "local_resolver_root": "/home/worsch", + "local_resolution": "resolved", + "readiness_state": "draft", + "scope": "Committed canonical local declaration and exact local source resolution only; no central/private read-placement, reef binding, custody, live deployment or consumer trust admission." + } }, "unchanged_limits": [ "tenancy and zones grades are unchanged", diff --git a/tests/test_image_publisher.py b/tests/test_image_publisher.py new file mode 100644 index 0000000..c87209d --- /dev/null +++ b/tests/test_image_publisher.py @@ -0,0 +1,64 @@ +"""Execute publisher against a Docker metadata fixture with no Python on PATH.""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +REVISION = '0a05383195213e7eaacd08388ce1d7892ce756df' +DIGEST = 'sha256:' + 'a'*64 +DOCKER = '''#!/bin/bash +set -eu +if [[ "$1" == image && "$2" == inspect ]]; then + [[ "${CASE:-}" != inspect_error ]] || exit 29 + if [[ "$4" == *RepoDigests* ]]; then + image=${5%:main-*} + case "${CASE:-}" in + missing) printf '%s@sha256:%064d\\n' other/repository 0 ;; + duplicate) printf '%s@%s\\n%s@%s\\n' "$image" "$DIGEST" "$image" "$DIGEST" ;; + malformed) printf '%s@sha256:BAD\\n' "$image" ;; + prefix_collision) printf '%s-extra@%s\\n' "$image" "$DIGEST" ;; + mismatch) if [[ "$image" == */access-engine ]]; then printf '%s@sha256:%064d\\n' "$image" 0; else printf '%s@%s\\n' "$image" "$DIGEST"; fi ;; + *) printf '%s@%s\\n' "$image" "$DIGEST" ;; + esac + else + if [[ "${CASE:-}" == wrong_revision ]]; then printf '%040d\\n' 0; else printf '%s\\n' "$REVISION"; fi + fi +fi +''' + + +class PublisherMetadataChecks(unittest.TestCase): + def invoke(self, case='', mode='legacy'): + with tempfile.TemporaryDirectory() as tmp: + docker = Path(tmp)/'docker' + docker.write_text(DOCKER) + docker.chmod(0o755) + env = {'PATH':tmp, 'CASE':case, 'REVISION':REVISION, 'DIGEST':DIGEST} + return subprocess.run(['/bin/bash',str(ROOT/'tools/publish-runtime-images.sh'), + REVISION,mode,tmp],env=env,capture_output=True,text=True) + + def test_existing_bash_docker_only_runner_and_exact_receipt(self): + for mode,count in [('legacy',1),('paired',2)]: + result = self.invoke(mode=mode) + self.assertEqual(result.returncode,0,result.stderr) + self.assertIn('manifest_digest='+DIGEST,result.stdout) + self.assertIn('verified_repositories='+str(count),result.stdout) + + def test_missing_duplicate_malformed_foreign_revision_and_inspect_failure_refuse(self): + for case in ('missing','duplicate','malformed','prefix_collision','wrong_revision','inspect_error'): + with self.subTest(case=case): + result = self.invoke(case) + self.assertNotEqual(result.returncode,0) + self.assertNotIn('publication mode=',result.stdout) + + def test_existing_paired_digest_equality_is_preserved(self): + result = self.invoke('mismatch','paired') + self.assertNotEqual(result.returncode,0) + self.assertIn('manifest digests differ',result.stderr) + self.assertNotIn('publication mode=',result.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/publish-runtime-images.sh b/tools/publish-runtime-images.sh index c9337e0..dbeaeed 100644 --- a/tools/publish-runtime-images.sh +++ b/tools/publish-runtime-images.sh @@ -27,8 +27,22 @@ done digests=() for image in "${images[@]}"; do docker pull "$image:main-$short" - raw=$(docker image inspect --format '{{json .RepoDigests}}' "$image:main-$short") - digest=$(printf '%s' "$raw" | python3 -c 'import json,re,sys; x=json.load(sys.stdin); assert isinstance(x,list); matches=[v.removeprefix(sys.argv[1]+"@") for v in x if isinstance(v,str) and v.startswith(sys.argv[1]+"@")]; assert len(matches)==1 and re.fullmatch(r"sha256:[a-f0-9]{64}",matches[0]); print(matches[0])' "$image") + # The existing container-build runner supplies Bash and Docker, not Python. + # Capture inspect first so command failure is not hidden by process substitution. + raw=$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$image:main-$short") + digest="" + matches=0 + while IFS= read -r reference; do + if [[ "$reference" == "$image@"* ]]; then + candidate=${reference#"$image@"} + [[ "$candidate" =~ ^sha256:[a-f0-9]{64}$ ]] || { echo 'invalid published registry manifest digest' >&2; exit 1; } + digest=$candidate + matches=$((matches + 1)) + fi + done <<< "$raw" + [[ "$matches" == 1 ]] || { echo 'expected exactly one published repository digest' >&2; exit 1; } + actual_revision=$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$image:main-$short") + [[ "$actual_revision" == "$revision" ]] || { echo 'published image revision differs from exact source' >&2; exit 1; } digests+=("$digest") done if [[ "$mode" == paired && "${digests[0]}" != "${digests[1]}" ]]; then diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md index bd7a83c..1481c1d 100644 --- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md @@ -283,3 +283,22 @@ process contention/crash release, capacity/prefix/restart and ENOSPC/fsync failu proofs are part of the owner package. No new service, live mount, deletion or custody grant; exact published successor, new PVC/PV retention/managed-package owner disposition and independent consumer admission remain prerequisites. + +2026-10-16 owner package extraction record: canonical local package +`rapp-flex-auth-secrets-engine` now exists at commit +`d52f811ed5ec0be27c9c367e71d476125b3228be`, tree +`d4fbad71bdc8ef870603526ccd1f53195d0322af`; authoritative declaration +`declarations/rapp.yaml` SHA256 `0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c`. +Repo Manager exact local resolution under `/home/worsch` returns +`rapp-flex-auth-secrets-engine` / workload `flex-auth-secrets-engine` / +deployable `flex-auth-secrets-engine`, ownership `access-engine`, readiness +`draft`. The retained lifecycle PDP is the only rollback boundary claimed; +tenant/user/canary and retired `rapp-secrets-engine` remain excluded. Normative +focused family validation passes; existing unrelated global rapp-forgejo error +and reef projection warning remain visible. Package extraction keeps source +`2b064eee510640224274ccdc7cefceb1ae13162e` and current image +`sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd` +unchanged. Exact local source resolution is no central/private read-placement, +reef binding, custody, native rollout or consumer trust admission. Future signer +image/config adoption remains separate. T06 remains `progress`; no second work +item duplicates this bounded dependency.