From c4b344ccf01de3af4376d84a3c0b5462504bdf2b Mon Sep 17 00:00:00 2001 From: Desolate-Jix Date: Fri, 23 Oct 2026 19:45:35 +1300 Subject: [PATCH] fix(ci): permit data-only fork checkout in catalog admission --- .github/workflows/catalog-admission.yml | 4 +++ tests/test_catalog_submission.py | 46 ++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/catalog-admission.yml b/.github/workflows/catalog-admission.yml index 2cd42e4..7df7ff8 100644 --- a/.github/workflows/catalog-admission.yml +++ b/.github/workflows/catalog-admission.yml @@ -22,6 +22,10 @@ jobs: path: proposal fetch-depth: 0 persist-credentials: false + # Fork contents are inspection data only. Run no scripts, actions, + # dependency installs or wheels from proposal in this trusted job. + # Checkers/requirements below come from protected base or the fixed Host. + allow-unsafe-pr-checkout: true - uses: actions/setup-python@v7 with: python-version: '3.12.10' diff --git a/tests/test_catalog_submission.py b/tests/test_catalog_submission.py index 08e1a07..032c3f1 100644 --- a/tests/test_catalog_submission.py +++ b/tests/test_catalog_submission.py @@ -1,18 +1,62 @@ import copy import io +import json +import os from pathlib import Path +import shutil +import subprocess +import sys import pytest import test_catalog import verify_publication as delivery from preflight_submission import preflight -from test_catalog import index +from test_catalog import ROOT, index from catalog import canonical host_environment = test_catalog.host_environment +def test_trusted_admission_does_not_execute_proposal_python(host_environment, tmp_path): + host, python, _ = host_environment + proposal = tmp_path / "proposal" + (proposal / "catalog/v1").mkdir(parents=True) + (proposal / "catalog/packages").mkdir() + for relative in ("catalog/security.json", "catalog/v1/index.json"): + shutil.copyfile(ROOT / relative, proposal / relative) + + marker = tmp_path / "proposal-code-executed" + payload = ( + "from pathlib import Path\n" + f"Path({str(marker)!r}).write_text('unexpected execution')\n" + "raise RuntimeError('proposal Python must not execute')\n" + ) + for relative in ("catalog.py", "sitecustomize.py", "tools/check_catalog_ci.py"): + target = proposal / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(payload, encoding="utf-8") + # The trusted job must not install this proposal's dependencies either. + (proposal / "requirements-dev.txt").write_text("not-a-real-dependency\n", encoding="utf-8") + + environment = dict(os.environ) + environment.pop("PYTHONPATH", None) + environment.pop("PYTHONHOME", None) + result = subprocess.run( + [ + sys.executable, str(ROOT / "tools/check_catalog_ci.py"), + "--proposal", str(proposal), "--base", "HEAD", + "--host", str(host), "--python", str(python), + "--artifacts", str(tmp_path / "artifacts"), + "--evidence", str(tmp_path / "evidence"), + ], + cwd=tmp_path, env=environment, capture_output=True, text=True, timeout=90, + ) + assert result.returncode == 0, (result.stdout, result.stderr) + assert json.loads(result.stdout) == {"ok": True, "publication": False} + assert not marker.exists(), "untrusted proposal Python was executed" + + @pytest.mark.parametrize("fragment,admitted", [("notes_mcp", True), ("fish_audio", False)]) def test_real_author_preflight_separates_host_policy_and_review(host_environment, fragment, admitted): host, python, _ = host_environment