From 512977fdd6b1179eefc04953d8d5f31ceebbe542 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Tue, 11 Aug 2026 09:00:47 -0700 Subject: [PATCH 1/2] ci: verify Python release artifacts Signed-off-by: Imran Siddique --- .github/workflows/publish.yml | 23 +++++++++ CHANGELOG.md | 8 ++- CONTRIBUTING.md | 9 ++++ scripts/verify_python_distribution.py | 73 +++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 scripts/verify_python_distribution.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 713e974..1f587d5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,6 +27,29 @@ jobs: - name: Verify dist working-directory: python run: twine check dist/* + - name: Install and smoke-test wheel and sdist + shell: bash + run: | + expected_version=$(python -c 'import tomllib; print(tomllib.load(open("python/pyproject.toml", "rb"))["project"]["version"])') + if [[ "$GITHUB_REF" == refs/tags/python-v* ]] && [[ "${GITHUB_REF_NAME#python-v}" != "$expected_version" ]]; then + echo "tag version ${GITHUB_REF_NAME#python-v} does not match package version $expected_version" >&2 + exit 1 + fi + index=0 + for artifact in python/dist/*.whl python/dist/*.tar.gz; do + index=$((index + 1)) + venv="$RUNNER_TEMP/agent-manifest-dist-$index" + python -m venv "$venv" + "$venv/bin/python" -m pip install --disable-pip-version-check "${artifact}[cli]" + ( + cd "$RUNNER_TEMP" + "$venv/bin/python" "$GITHUB_WORKSPACE/scripts/verify_python_distribution.py" \ + --expected-version "$expected_version" \ + --forbidden-source-root "$GITHUB_WORKSPACE/python/src" + "$venv/bin/manifest" --help >/dev/null + ) + done + test "$index" -eq 2 - uses: actions/upload-artifact@v7 with: name: dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b40896..c1e55f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ All notable changes to Agent Manifest are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Spec changes are marked **[SPEC]**; SDK changes are marked **[SDK]**. -## [Unreleased] +## [Unreleased] + +### Security + +- The PyPI release workflow now installs and smoke-tests both the exact wheel and source distribution before upload, including version/tag agreement, import provenance, a public cryptographic verification roundtrip, and the packaged CLI entry point. ## [0.11.0] — 2026-08-11 @@ -317,4 +321,4 @@ Initial developer preview. Launching at Confidential Computing Summit, June 23 2 - CLI: `manifest keygen`, `create`, `sign`, `attest`, `verify`, `revoke` - Post-quantum support via `pyoqs`: `pip install "agent-manifest[pq]"` - Verification server: `pip install "agent-manifest[server]"` -- Python 3.11, 3.12, 3.13 support \ No newline at end of file +- Python 3.11, 3.12, 3.13 support diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 351d82b..324ada3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,15 @@ Run security scan: bandit -r src/agent_manifest ``` +### Release artifact verification + +The PyPI workflow builds one wheel and one source distribution, installs each +with the declared `cli` extra into a separate clean virtual environment, and runs +`scripts/verify_python_distribution.py` outside the checkout. The gate checks +the installed metadata version, proves imports do not resolve to `python/src`, +exercises the public signing and verification API, and invokes the packaged +`manifest` console entry point. Neither artifact is uploaded unless both pass. + ## Submitting a PR 1. Fork the repo and create a branch from `main`. diff --git a/scripts/verify_python_distribution.py b/scripts/verify_python_distribution.py new file mode 100644 index 0000000..2097f06 --- /dev/null +++ b/scripts/verify_python_distribution.py @@ -0,0 +1,73 @@ +"""Smoke-test an installed agent-manifest distribution outside the checkout.""" + +from __future__ import annotations + +import argparse +from datetime import datetime, timedelta, timezone +from importlib.metadata import version +from pathlib import Path + +import agent_manifest + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--expected-version", required=True) + parser.add_argument("--forbidden-source-root", required=True, type=Path) + args = parser.parse_args() + + installed_version = version("agent-manifest") + if installed_version != args.expected_version: + raise SystemExit( + f"installed version {installed_version!r} != expected {args.expected_version!r}" + ) + + module_path = Path(agent_manifest.__file__).resolve() + forbidden_root = args.forbidden_source_root.resolve() + if module_path.is_relative_to(forbidden_root): + raise SystemExit( + f"smoke test imported checkout source {module_path}, not the distribution" + ) + + now = datetime.now(timezone.utc) + prompt_hash = "sha256:" + "a" * 64 + policy_hash = "sha256:" + "b" * 64 + keypair = agent_manifest.generate_ed25519() + manifest = { + "manifest_id": "018f4a3b-2c1d-7e5f-a8b9-0d1e2f3a4b5c", + "agent_id": "spiffe://trust.example/agent/release-smoke/prod", + "issuer": "spiffe://trust.example/issuer/release", + "version": "0.1", + "issued_at": now.isoformat().replace("+00:00", "Z"), + "expires_at": (now + timedelta(days=1)).isoformat().replace("+00:00", "Z"), + "crypto_profile": "standard", + "artifacts": { + "system_prompt": {"hash": prompt_hash}, + "policy_bundle": { + "hash": policy_hash, + "enforcement_mode": "enforce", + }, + "model_identity": { + "version": "release-smoke", + "deployment_type": "api", + }, + }, + } + manifest["signature"] = agent_manifest.Ed25519Signer(keypair).sign(manifest) + context = agent_manifest.VerificationContext( + system_prompt_hash=prompt_hash, + policy_bundle_hash=policy_hash, + model_version="release-smoke", + trusted_keys={keypair.key_id: keypair.public_b64url()}, + ) + result = agent_manifest.verify_manifest( + manifest, context, agent_manifest.RevocationStore() + ) + if result.result != agent_manifest.OverallResult.VALID: + raise SystemExit(f"installed-package verification roundtrip failed: {result}") + + print(f"verified agent-manifest {installed_version} from {module_path}") + + +if __name__ == "__main__": + main() From 9e762ce85d28e94d58fba77854e52280d1d3e8b7 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Tue, 11 Aug 2026 09:03:27 -0700 Subject: [PATCH 2/2] ci: cover release gate changes Signed-off-by: Imran Siddique --- .github/workflows/ci.yml | 5 +++- CONTRIBUTING.md | 2 ++ .../tests/test_release_distribution_smoke.py | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 python/tests/test_release_distribution_smoke.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5de16a..c02c9a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,16 @@ on: - "python/**" - "spec/**" - "examples/**" - - ".github/workflows/ci.yml" + - "scripts/**" + - ".github/workflows/*.yml" pull_request: branches: [main] paths: - "python/**" - "spec/**" - "examples/**" + - "scripts/**" + - ".github/workflows/*.yml" permissions: contents: read diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 324ada3..1db0c7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,8 @@ with the declared `cli` extra into a separate clean virtual environment, and run the installed metadata version, proves imports do not resolve to `python/src`, exercises the public signing and verification API, and invokes the packaged `manifest` console entry point. Neither artifact is uploaded unless both pass. +The main CI path filters include release scripts and workflow definitions so +changes to this gate cannot bypass the repository's normal review checks. ## Submitting a PR diff --git a/python/tests/test_release_distribution_smoke.py b/python/tests/test_release_distribution_smoke.py new file mode 100644 index 0000000..144a8b4 --- /dev/null +++ b/python/tests/test_release_distribution_smoke.py @@ -0,0 +1,27 @@ +"""Tests for the standalone installed-distribution release smoke check.""" + +from importlib.metadata import version +from pathlib import Path +import subprocess +import sys + + +def test_distribution_smoke_script_exercises_installed_public_api(tmp_path: Path) -> None: + script = Path(__file__).parents[2] / "scripts" / "verify_python_distribution.py" + + completed = subprocess.run( + [ + sys.executable, + str(script), + "--expected-version", + version("agent-manifest"), + "--forbidden-source-root", + str(tmp_path), + ], + check=False, + capture_output=True, + text=True, + ) + + assert completed.returncode == 0, completed.stderr + assert "verified agent-manifest" in completed.stdout