From 223c6b5f195b86757f30cffdf975919ed724b3f2 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:39:08 +0000 Subject: [PATCH] Withdraw writable PHP launcher admissions until control mounts are corrected --- qualification/README.md | 32 ++- qualification/composer-container-trust.json | 244 ++---------------- .../retired-writable-runtime-setups.json | 228 ++++++++++++++++ tests/test_composer_trust_contract.py | 48 +++- 4 files changed, 304 insertions(+), 248 deletions(-) create mode 100644 tests/fixtures/retired-writable-runtime-setups.json diff --git a/qualification/README.md b/qualification/README.md index 3037ff7..72ddf9a 100644 --- a/qualification/README.md +++ b/qualification/README.md @@ -248,23 +248,21 @@ part of the Server hook's combined qualification. ### Composer through a reviewed PHP launcher -The same catalog has a separate `runtime_setups` entry for Sample App's PHP CI -launcher. It binds the exact setup command, target, workflow/jobs, literal PHP -matrix, build inputs, launcher and TLS control by file hash and mode. An -unreviewed Dockerfile exclusion file refuses. The setup probes and exports an -immutable local image config ID; subsequent PHP and Composer invocations use -that ID. The actual Composer TLS control runs in both PHP profiles: ambient -trust reaches a synthetic HTTPS repository, while the selected clean bundle -refuses before an HTTP request. - -This recognizes the PHP setup only. The reviewed clean bootstrap must still -precede it, and the reviewed live Composer verifier must run in the container -immediately before every dependency-request step. The launcher mounts the -selected CA file and empty additional-root directory read-only and inspects the -real Composer PHAR. Runtime selector overrides, changed source bytes or modes, -conditional/ignored setup, another target/job, and missing/intervening guards -refuse. This entry does not approve arbitrary Docker commands or another -repository's launcher. +The catalog currently admits no PHP CI launcher in `runtime_setups`. The earlier +Sample App, CLI and Waterline records were withdrawn after the +[writable control-path finding](https://github.com/durable-workflow/.github/issues/297#issuecomment-6854260334). +Their repository/state mounts allowed container code to replace a host launcher +or wrapper before a later nominal verifier invocation. Historical source or TLS +passes do not qualify that boundary. Whole-target Composer enforcement remains +in place, so the withdrawn setup cannot authorize dependency requests. + +Replacement source must mount repository/control and wrapper/launcher paths +read-only, expose only required data/cache/output paths as writable, and pass +actual-container predecessor-failing replacement controls. Each corrected recipe +needs exact file hashes/canonical Git modes, closed jobs/commands/matrix, +selector and exclusion boundaries, live in-container Composer verification and +its own caller qualification. Existing container-build recipes and the bound +host PHP helper are separate and remain unchanged. ### Composer on the runner diff --git a/qualification/composer-container-trust.json b/qualification/composer-container-trust.json index befd79a..beaeea2 100644 --- a/qualification/composer-container-trust.json +++ b/qualification/composer-container-trust.json @@ -7,10 +7,22 @@ "workflow": "ci.yml", "job": "cooperative-integration", "command": "docker compose --env-file /dev/null up -d --build --wait --wait-timeout 300", - "services": ["bootstrap", "server", "worker", "repair"], + "services": [ + "bootstrap", + "server", + "worker", + "repair" + ], "compose_files": [ - ["docker-compose.test.yml", "docker-compose.composer-trust.yml"], - ["docker-compose.test.yml", "docker-compose.composer-trust.yml", "docker-compose.child-policy.yml"] + [ + "docker-compose.test.yml", + "docker-compose.composer-trust.yml" + ], + [ + "docker-compose.test.yml", + "docker-compose.composer-trust.yml", + "docker-compose.child-policy.yml" + ] ], "dockerfile": "Dockerfile", "stage": "production", @@ -48,229 +60,5 @@ ] } }, - "runtime_setups": { - "sample-app-php-ci": { - "target": "sample-app", - "matrix": { - "php": ["8.4", "8.5"] - }, - "workflows": { - "ci.yml": [ - "test", - "microservice-test" - ], - "composer-lock-audit.yml": [ - "audit" - ], - "playground-journeys.yml": [ - "journeys" - ] - }, - "commands": [ - "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"${{ matrix.php }}\"", - "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"8.4\"" - ], - "environment": [ - "SAMPLE_APP_PHP_CI_IMAGE", - "SAMPLE_APP_PHP_CI_ROOT", - "SAMPLE_APP_PHP_CI_STATE" - ], - "absent_files": [ - "docker/php-ci/Dockerfile.dockerignore" - ], - "files": { - ".dockerignore": { - "sha256": "3d3b99f9bbf9d4ce35b7dc8462c4da1c87983b2b65a3a829d8ec9d80f43551cc", - "mode": 420 - }, - "docker/php-ci/Dockerfile": { - "sha256": "dcdd1e2f99db98825f9cf6b89ab2b0191252d2816001af09f391b6fb1a8b035a", - "mode": 420 - }, - "scripts/ci/php-container-images.json": { - "sha256": "fc4d410589d1fdb322107e404bfcf9f4593e6ac7e3fcce729b1d78fafd9be1c4", - "mode": 420 - }, - "scripts/ci/run-php-container.sh": { - "sha256": "56aef0c6ca67f277dce114eac3d5de396f5b751697df6c01e8f760cb18cc9f49", - "mode": 493 - }, - "scripts/ci/setup-php-container.sh": { - "sha256": "cf290fd8e9978d2a4a0edb894485e66df4e9ee695e53eae9da22bf9f35ca12ee", - "mode": 493 - }, - "tests/ci/test_php_container_tls.py": { - "sha256": "936a6584fbe2b3523a8835e4f18df272b9bf384b9d95419b6ff3368e986052e4", - "mode": 420 - }, - ".devcontainer/docker/verify-php-trust.php": { - "sha256": "bd8682984a757eb28887e996f77c9cafed32634cc7e19155b9885da1bb5b48c9", - "mode": 420 - }, - "docker/install-phpredis.sh": { - "sha256": "17b21ea9724199f502605abeeff6856f8af721027691e1de74d12cc6ccb7d33b", - "mode": 420 - }, - "docker/brotli/install-vendor.sh": { - "sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f", - "mode": 420 - }, - "docker/brotli/verify-runtime.php": { - "sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e", - "mode": 420 - }, - "docker/brotli/verify-system-brotli.c": { - "sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667", - "mode": 420 - }, - "docker/brotli/version": { - "sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2", - "mode": 420 - } - } - }, - "waterline-php82-build": { - "target": "waterline", - "workflows": { - "php.yml": [ - "build" - ] - }, - "commands": [ - "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"8.2\"" - ], - "environment": [ - "WATERLINE_PHP_CI_IMAGE", - "WATERLINE_PHP_CI_ROOT", - "WATERLINE_PHP_CI_STATE" - ], - "files": { - "docker/php-ci/Dockerfile": { - "sha256": "81d23fbbee7e4ee97ca14df9d17982aa664335b94499c5019662259fe0387eaa", - "mode": 384 - }, - "docker/php-ci/Dockerfile.dockerignore": { - "sha256": "a26d7f1f5ae2cac900e5e32bfaa0f08bd7a8435847095990c9bfe09d0edbf264", - "mode": 384 - }, - "docker/php-ci/verify-php-trust.php": { - "sha256": "529c282e7422202bf25565431a6391de388f86dfe5bfd6c75cf21daa56ffa75e", - "mode": 384 - }, - "docker/brotli/install-vendor.sh": { - "sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f", - "mode": 384 - }, - "docker/brotli/verify-runtime.php": { - "sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e", - "mode": 384 - }, - "docker/brotli/verify-system-brotli.c": { - "sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667", - "mode": 384 - }, - "docker/brotli/version": { - "sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2", - "mode": 384 - }, - "scripts/ci/php-container-images.json": { - "sha256": "3314ecfd6c32641af1d5475f8e57f4421979f67e05db1f98c79ef7b08d41b944", - "mode": 384 - }, - "scripts/ci/run-php-container.sh": { - "sha256": "456f9716edbfaa8429edba92f958c4e1b3551bcbfd849a722764ae47daffb8fd", - "mode": 448 - }, - "scripts/ci/setup-php-container.sh": { - "sha256": "5064d48df69940cdf609be7bccd82ee71624d1017a8486a29028c765bb311f13", - "mode": 448 - }, - "tests/ci/test_php_container_tls.py": { - "sha256": "7e365ff0f27c4efea0f7cf8ec3778f48fb1fe03e80a5b5b0c9500a6935f3d767", - "mode": 384 - } - } - }, - "cli-php-ci": { - "target": "cli", - "matrix": { - "php": [ - "8.2", - "8.4", - "8.5" - ] - }, - "workflows": { - "build.yml": [ - "test" - ] - }, - "commands": [ - "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"${{ matrix.php }}\"" - ], - "environment": [ - "CLI_PHP_CI_IMAGE", - "CLI_PHP_CI_ROOT", - "CLI_PHP_CI_STATE" - ], - "absent_files": [ - "docker/php-ci/Dockerfile.dockerignore", - "docker/php-ci/cli.Dockerfile.dockerignore" - ], - "files": { - ".dockerignore": { - "sha256": "19f259d2473129b500ed107780da5b5bc4a7dea47fde4500539bff4b919d4aee", - "mode": 420 - }, - "docker/php-ci/Dockerfile": { - "sha256": "81d23fbbee7e4ee97ca14df9d17982aa664335b94499c5019662259fe0387eaa", - "mode": 420 - }, - "docker/php-ci/cli.Dockerfile": { - "sha256": "c2b64bd82040083fbd812a470b266861f04303ea800ba58908f1e4601fe4179f", - "mode": 420 - }, - "docker/php-ci/verify-php-trust.php": { - "sha256": "529c282e7422202bf25565431a6391de388f86dfe5bfd6c75cf21daa56ffa75e", - "mode": 420 - }, - "docker/brotli/install-vendor.sh": { - "sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f", - "mode": 420 - }, - "docker/brotli/verify-runtime.php": { - "sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e", - "mode": 420 - }, - "docker/brotli/verify-system-brotli.c": { - "sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667", - "mode": 420 - }, - "docker/brotli/version": { - "sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2", - "mode": 420 - }, - "scripts/ci/php-container-images.json": { - "sha256": "85c87a2dff21225109581876befc00177c62824e44c044765dc921879ca41a4d", - "mode": 420 - }, - "scripts/ci/setup-php-container.sh": { - "sha256": "376959ca3cf6ab96600d6d8c1715c62de4236864496f594671f22592c27c985d", - "mode": 493 - }, - "scripts/ci/run-php-container.sh": { - "sha256": "97e87647dec2d102619114b6c836467f935180ec87f20e94052af5f02d0a1381", - "mode": 493 - }, - "scripts/ci/probe-php-test-runtime.php": { - "sha256": "a0bbef217128b8ffe81de5c126dfafbfd93ebed28244c2a69204a8f4991be264", - "mode": 420 - }, - "scripts/ci/test-php-container-tls.py": { - "sha256": "625b6d7cf444f27a34c0f63a3755d49dea09d397333240f46e29ce30149e519e", - "mode": 420 - } - } - } - } + "runtime_setups": {} } diff --git a/tests/fixtures/retired-writable-runtime-setups.json b/tests/fixtures/retired-writable-runtime-setups.json new file mode 100644 index 0000000..1836188 --- /dev/null +++ b/tests/fixtures/retired-writable-runtime-setups.json @@ -0,0 +1,228 @@ +{ + "sample-app-php-ci": { + "target": "sample-app", + "matrix": { + "php": [ + "8.4", + "8.5" + ] + }, + "workflows": { + "ci.yml": [ + "test", + "microservice-test" + ], + "composer-lock-audit.yml": [ + "audit" + ], + "playground-journeys.yml": [ + "journeys" + ] + }, + "commands": [ + "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"${{ matrix.php }}\"", + "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"8.4\"" + ], + "environment": [ + "SAMPLE_APP_PHP_CI_IMAGE", + "SAMPLE_APP_PHP_CI_ROOT", + "SAMPLE_APP_PHP_CI_STATE" + ], + "absent_files": [ + "docker/php-ci/Dockerfile.dockerignore" + ], + "files": { + ".dockerignore": { + "sha256": "3d3b99f9bbf9d4ce35b7dc8462c4da1c87983b2b65a3a829d8ec9d80f43551cc", + "mode": 420 + }, + "docker/php-ci/Dockerfile": { + "sha256": "dcdd1e2f99db98825f9cf6b89ab2b0191252d2816001af09f391b6fb1a8b035a", + "mode": 420 + }, + "scripts/ci/php-container-images.json": { + "sha256": "fc4d410589d1fdb322107e404bfcf9f4593e6ac7e3fcce729b1d78fafd9be1c4", + "mode": 420 + }, + "scripts/ci/run-php-container.sh": { + "sha256": "56aef0c6ca67f277dce114eac3d5de396f5b751697df6c01e8f760cb18cc9f49", + "mode": 493 + }, + "scripts/ci/setup-php-container.sh": { + "sha256": "cf290fd8e9978d2a4a0edb894485e66df4e9ee695e53eae9da22bf9f35ca12ee", + "mode": 493 + }, + "tests/ci/test_php_container_tls.py": { + "sha256": "936a6584fbe2b3523a8835e4f18df272b9bf384b9d95419b6ff3368e986052e4", + "mode": 420 + }, + ".devcontainer/docker/verify-php-trust.php": { + "sha256": "bd8682984a757eb28887e996f77c9cafed32634cc7e19155b9885da1bb5b48c9", + "mode": 420 + }, + "docker/install-phpredis.sh": { + "sha256": "17b21ea9724199f502605abeeff6856f8af721027691e1de74d12cc6ccb7d33b", + "mode": 420 + }, + "docker/brotli/install-vendor.sh": { + "sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f", + "mode": 420 + }, + "docker/brotli/verify-runtime.php": { + "sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e", + "mode": 420 + }, + "docker/brotli/verify-system-brotli.c": { + "sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667", + "mode": 420 + }, + "docker/brotli/version": { + "sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2", + "mode": 420 + } + } + }, + "waterline-php82-build": { + "target": "waterline", + "workflows": { + "php.yml": [ + "build" + ] + }, + "commands": [ + "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"8.2\"" + ], + "environment": [ + "WATERLINE_PHP_CI_IMAGE", + "WATERLINE_PHP_CI_ROOT", + "WATERLINE_PHP_CI_STATE" + ], + "files": { + "docker/php-ci/Dockerfile": { + "sha256": "81d23fbbee7e4ee97ca14df9d17982aa664335b94499c5019662259fe0387eaa", + "mode": 384 + }, + "docker/php-ci/Dockerfile.dockerignore": { + "sha256": "a26d7f1f5ae2cac900e5e32bfaa0f08bd7a8435847095990c9bfe09d0edbf264", + "mode": 384 + }, + "docker/php-ci/verify-php-trust.php": { + "sha256": "529c282e7422202bf25565431a6391de388f86dfe5bfd6c75cf21daa56ffa75e", + "mode": 384 + }, + "docker/brotli/install-vendor.sh": { + "sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f", + "mode": 384 + }, + "docker/brotli/verify-runtime.php": { + "sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e", + "mode": 384 + }, + "docker/brotli/verify-system-brotli.c": { + "sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667", + "mode": 384 + }, + "docker/brotli/version": { + "sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2", + "mode": 384 + }, + "scripts/ci/php-container-images.json": { + "sha256": "3314ecfd6c32641af1d5475f8e57f4421979f67e05db1f98c79ef7b08d41b944", + "mode": 384 + }, + "scripts/ci/run-php-container.sh": { + "sha256": "456f9716edbfaa8429edba92f958c4e1b3551bcbfd849a722764ae47daffb8fd", + "mode": 448 + }, + "scripts/ci/setup-php-container.sh": { + "sha256": "5064d48df69940cdf609be7bccd82ee71624d1017a8486a29028c765bb311f13", + "mode": 448 + }, + "tests/ci/test_php_container_tls.py": { + "sha256": "7e365ff0f27c4efea0f7cf8ec3778f48fb1fe03e80a5b5b0c9500a6935f3d767", + "mode": 384 + } + } + }, + "cli-php-ci": { + "target": "cli", + "matrix": { + "php": [ + "8.2", + "8.4", + "8.5" + ] + }, + "workflows": { + "build.yml": [ + "test" + ] + }, + "commands": [ + "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"${{ matrix.php }}\"" + ], + "environment": [ + "CLI_PHP_CI_IMAGE", + "CLI_PHP_CI_ROOT", + "CLI_PHP_CI_STATE" + ], + "absent_files": [ + "docker/php-ci/Dockerfile.dockerignore", + "docker/php-ci/cli.Dockerfile.dockerignore" + ], + "files": { + ".dockerignore": { + "sha256": "19f259d2473129b500ed107780da5b5bc4a7dea47fde4500539bff4b919d4aee", + "mode": 420 + }, + "docker/php-ci/Dockerfile": { + "sha256": "81d23fbbee7e4ee97ca14df9d17982aa664335b94499c5019662259fe0387eaa", + "mode": 420 + }, + "docker/php-ci/cli.Dockerfile": { + "sha256": "c2b64bd82040083fbd812a470b266861f04303ea800ba58908f1e4601fe4179f", + "mode": 420 + }, + "docker/php-ci/verify-php-trust.php": { + "sha256": "529c282e7422202bf25565431a6391de388f86dfe5bfd6c75cf21daa56ffa75e", + "mode": 420 + }, + "docker/brotli/install-vendor.sh": { + "sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f", + "mode": 420 + }, + "docker/brotli/verify-runtime.php": { + "sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e", + "mode": 420 + }, + "docker/brotli/verify-system-brotli.c": { + "sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667", + "mode": 420 + }, + "docker/brotli/version": { + "sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2", + "mode": 420 + }, + "scripts/ci/php-container-images.json": { + "sha256": "85c87a2dff21225109581876befc00177c62824e44c044765dc921879ca41a4d", + "mode": 420 + }, + "scripts/ci/setup-php-container.sh": { + "sha256": "376959ca3cf6ab96600d6d8c1715c62de4236864496f594671f22592c27c985d", + "mode": 493 + }, + "scripts/ci/run-php-container.sh": { + "sha256": "97e87647dec2d102619114b6c836467f935180ec87f20e94052af5f02d0a1381", + "mode": 493 + }, + "scripts/ci/probe-php-test-runtime.php": { + "sha256": "a0bbef217128b8ffe81de5c126dfafbfd93ebed28244c2a69204a8f4991be264", + "mode": 420 + }, + "scripts/ci/test-php-container-tls.py": { + "sha256": "625b6d7cf444f27a34c0f63a3755d49dea09d397333240f46e29ce30149e519e", + "mode": 420 + } + } + } +} diff --git a/tests/test_composer_trust_contract.py b/tests/test_composer_trust_contract.py index 280e65c..c4c329e 100644 --- a/tests/test_composer_trust_contract.py +++ b/tests/test_composer_trust_contract.py @@ -374,12 +374,13 @@ def setUp(self): repository = Path(__file__).resolve().parents[1] self.policy = json.loads((repository / "qualification/policy.json").read_text()) self.document = yaml.safe_load((repository / "tests/fixtures/waterline-php82-build.yml").read_text()) - self.catalog = json.loads((repository / "qualification/composer-container-trust.json").read_text()) + retired = json.loads((repository / "tests/fixtures/retired-writable-runtime-setups.json").read_text()) + self.catalog = {"requests": {}, "runtime_setups": {"waterline-php82-build": retired["waterline-php82-build"]}} temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.root = Path(temporary.name) - # Source hashing is covered independently. Bind harmless local bytes so - # this regression tests the real target policy and proposed caller offline. + # Exercise mandatory enumeration using inert historical recipe data. + # This fixture grants no admission in the current production catalog. recipe = self.catalog["runtime_setups"]["waterline-php82-build"] for name, expected in recipe["files"].items(): path = self.root / name @@ -414,6 +415,47 @@ def test_guardless_build_cannot_opt_out_before_loading_the_catalog(self): self.check(document) +class RetiredWritableRuntimeContractTest(unittest.TestCase): + def test_retired_callers_cannot_borrow_their_old_runtime_admission(self): + repository = Path(__file__).resolve().parents[1] + policy = json.loads((repository / "qualification/policy.json").read_text()) + current = json.loads((repository / "qualification/composer-container-trust.json").read_text()) + retired = json.loads((repository / "tests/fixtures/retired-writable-runtime-setups.json").read_text()) + revision = "75587fc33ff40c34f32ef777293dad56bb6f48c7" + for name, original in retired.items(): + with self.subTest(runtime=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + recipe = deepcopy(original) + for path, expected in recipe["files"].items(): + selected = root / path + selected.parent.mkdir(parents=True, exist_ok=True) + selected.write_text("# inert retired runtime input\n") + selected.chmod(expected["mode"]) + expected["sha256"] = hashlib.sha256(selected.read_bytes()).hexdigest() + filename, jobs = next(iter(recipe["workflows"].items())) + job = {"steps": [ + {"uses": PREFIX + "bootstrap-patched-pip@" + revision, + "with": {"clients": "python,composer,curl,git"}}, + {"run": recipe["commands"][0]}, + {"uses": PREFIX + "verify-composer-trust@" + revision}, + {"run": "composer install --no-interaction"}, + ]} + if "matrix" in recipe: + job["strategy"] = {"matrix": recipe["matrix"]} + sources = {filename: yaml.safe_dump({"jobs": {jobs[0]: job}})} + historical = {"requests": {}, "runtime_setups": {name: recipe}} + with mock.patch("scripts.qualification_policy._composer_container_catalog", return_value=historical): + self.assertEqual(1, len(verify_composer_network_sinks(policy, recipe["target"], sources, root))) + actual = deepcopy(current) + # A predecessor's real record is bound to inert bytes for this + # behavioral control. Corrected future records are not changed. + if actual.get("runtime_setups", {}).get(name) == original: + actual["runtime_setups"][name] = recipe + with mock.patch("scripts.qualification_policy._composer_container_catalog", return_value=actual): + with self.assertRaises(PolicyError): + verify_composer_network_sinks(policy, recipe["target"], sources, root) + + class ComposerRuntimeSetupContractTest(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory()