diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 803765b..6e000b9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -73,22 +73,57 @@ jobs: codecov_token: ${{ secrets.CODECOV_TOKEN }} sonar_token: ${{ secrets.SONAR_TOKEN }} + # AAASM-3612: advisory gate. Fail CI when a (possibly transitive) dependency + # in the locked set carries a known advisory, so a poisoned dep cannot ride + # into a release through uv.lock. pip-audit resolves the synced environment + # against the PyPI Advisory + OSV databases; a non-empty result exits + # non-zero and fails the job. + dependency-audit: + name: Dependency advisory audit (pip-audit) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Sync locked environment + run: uv sync --frozen + - name: Run pip-audit advisory gate + # Documented allowlist for advisories with NO available fix, mirroring + # go-sdk's KNOWN_UNFIXED. Add an entry ONLY when there is no fixed + # release, with a dated rationale, in the form: + # --ignore-vuln GHSA-xxxx-xxxx-xxxx # + # The list starts empty: every known-vuln dependency fails the gate. + run: | + set -euo pipefail + uvx pip-audit \ + --strict \ + --desc + # To allowlist an unfixable advisory, append flags above, e.g.: + # --ignore-vuln GHSA-xxxx-xxxx-xxxx # 2026-06-23 no fixed release yet + # Single aggregate required check. Collapses the reusable-workflow fan-out into one # stable status so branch protection needs only this check, and it still reports a # result (success on skip) instead of staying pending when path filters skip the # underlying test jobs. ci-success: name: CI Success - needs: [build-and-test_all] + needs: [build-and-test_all, dependency-audit] if: always() runs-on: ubuntu-latest steps: - name: Verify upstream jobs did not fail or get cancelled run: | - result="${{ needs.build-and-test_all.result }}" - echo "build-and-test_all result: ${result}" - if [ "${result}" = "failure" ] || [ "${result}" = "cancelled" ]; then - echo "::error::CI failed: build-and-test_all=${result}" - exit 1 - fi + test_result="${{ needs.build-and-test_all.result }}" + audit_result="${{ needs.dependency-audit.result }}" + echo "build-and-test_all result: ${test_result}" + echo "dependency-audit result: ${audit_result}" + for result in "${test_result}" "${audit_result}"; do + if [ "${result}" = "failure" ] || [ "${result}" = "cancelled" ]; then + echo "::error::CI failed: build-and-test_all=${test_result} dependency-audit=${audit_result}" + exit 1 + fi + done echo "CI Success: all required upstream jobs passed or were skipped." diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index 52f28dc..e78cc1a 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -142,6 +142,38 @@ jobs: } >> "$GITHUB_OUTPUT" echo "Resolved binary_source_tag=${binary_source_tag} pypi_version=${pypi_version} dry_run=${dry_run} release_tag=${release_tag}" + # AAASM-3615: produce a CycloneDX SBOM of the SDK's resolved dependency set + # so consumers get a machine-readable manifest to cross-check against + # advisory feeds (and detect a poisoned transitive dep after the fact). + # Runs on BOTH dry-run and real dispatches — it is a build artifact, always + # worth producing. The create-github-release job attaches it to the Release + # on the real-publish path only. + sbom: + name: Generate CycloneDX SBOM + needs: resolve + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Sync resolved environment + # Resolve the locked dependency set into .venv so the SBOM reflects the + # exact versions a consumer gets, not just the declared ranges. + run: uv sync --frozen + - name: Generate CycloneDX SBOM + # cyclonedx-py walks the resolved virtualenv and emits a CycloneDX + # JSON BOM of every installed distribution. + run: uvx --from cyclonedx-bom cyclonedx-py environment .venv --output-format JSON --outfile sbom.cdx.json + - name: Upload SBOM artifact + uses: actions/upload-artifact@v7 + with: + name: sbom + path: sbom.cdx.json + if-no-files-found: error + build-sdist: name: Build sdist needs: resolve @@ -464,6 +496,16 @@ jobs: - name: Publish via PyPI Trusted Publisher uses: pypa/gh-action-pypi-publish@release/v1 # No `with: password:` — Trusted Publisher uses OIDC, no token stored. + with: + # PEP 740: mint a Sigstore-backed digital attestation for every + # wheel/sdist, reusing the Trusted Publisher OIDC identity already + # granted on this job (id-token: write) — no new secret. A wheel + # pushed outside this OIDC-gated workflow carries no attestation, so + # PyPI (and consumers) can detect an artifact not produced by the + # sanctioned pipeline. `true` is the action default on recent + # versions, but pin it explicitly so it cannot silently regress. + # AAASM-3611. + attestations: true # Cut python-sdk's own GitHub Release at the just-published version so the # repo's release line tracks PyPI (and the README `github/v/release` badge @@ -475,6 +517,7 @@ jobs: needs: - resolve - publish + - sbom runs-on: ubuntu-latest # Real publish only, and only when resolve produced a SemVer tag (empty # for .post/.dev republishes, which do not get their own Release). @@ -483,6 +526,12 @@ jobs: contents: write # create the git tag + GitHub Release steps: - uses: actions/checkout@v7 + - name: Download CycloneDX SBOM + # AAASM-3615: pull the SBOM built by the `sbom` job so it can be + # attached to the Release below. + uses: actions/download-artifact@v8 + with: + name: sbom - name: Create tag and GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -517,6 +566,17 @@ jobs: --title "$RELEASE_TAG" \ --notes "python-sdk agent-assembly==${PYPI_VERSION} — coordinated with agent-assembly ${RELEASE_TAG}." echo "Created GitHub Release $RELEASE_TAG (agent-assembly==${PYPI_VERSION})" + - name: Attach CycloneDX SBOM to the Release + # AAASM-3615: attach the SBOM as a Release asset so consumers can audit + # the dependency set of exactly this published version. `--clobber` + # keeps the step idempotent on a workflow_dispatch re-run. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }} + run: | + set -euo pipefail + gh release upload "$RELEASE_TAG" sbom.cdx.json --repo "$GITHUB_REPOSITORY" --clobber + echo "Attached sbom.cdx.json to GitHub Release $RELEASE_TAG" # Publish the *real* release tag as a tiny artifact so the documentation # workflow (which is triggered by `workflow_run` after this workflow diff --git a/README.md b/README.md index ea5d5ed..3bfc54c 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,12 @@ pip install 'agent-assembly[runtime]' # SDK + bundled aasm runtime binary (platf `aasm` sidecar binary, so you don't need a separate runtime install. Plain `agent-assembly` is the pure-Python client and expects an `aasm` runtime to be reachable some other way. +> **Supply-chain verification.** The only official PyPI package is `agent-assembly` +> (anything else is a typosquat). Every release ships PEP 740 attestations on its +> [PyPI files page](https://pypi.org/project/agent-assembly/#files) and a CycloneDX +> SBOM attached to its [GitHub Release](https://github.com/ai-agent-assembly/python-sdk/releases). +> See [`SECURITY.md`](./SECURITY.md) for how to verify what you installed. + With [`uv`](https://docs.astral.sh/uv/): ```bash diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8e0b0b3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ +# Security Policy + +## Reporting a vulnerability + +Report suspected vulnerabilities privately to **security@agent-assembly.dev**. +Please do not open public issues for security reports. + +## Canonical package name + +The **only** official distribution of this SDK on PyPI is: + +| Ecosystem | Canonical name | +|---|---| +| PyPI | [`agent-assembly`](https://pypi.org/project/agent-assembly/) | + +Anything else — a hyphen/underscore variant, a near-miss spelling, or a +look-alike namespace — is **not us**. The published package is a trust path +straight into your application's process: by the time runtime policy evaluates, +the package's code is already executing. Treat a typosquat as hostile and +install only the name above. + +## Verifying what you installed + +Every release is published exclusively through the operator-gated, OIDC-backed +release pipeline (`.github/workflows/release-python.yml`), which ships two +consumer-verifiable signals: + +### 1. PEP 740 attestations (provenance) + +Wheels and sdists are uploaded via PyPI Trusted Publishing with PEP 740 digital +attestations enabled. The attestation is minted from the release workflow's +OIDC identity — an artifact uploaded outside that pipeline cannot carry one. + +- Open the project's **PyPI files page**: + — each file lists its + **Attestations** (Sigstore provenance) when produced by the sanctioned + pipeline. +- An artifact with **no attestation** was not produced by our release workflow. + Do not trust it. + +### 2. CycloneDX SBOM + +Each tagged release attaches a CycloneDX Software Bill of Materials, +`sbom.cdx.json`, to its **GitHub Release**: + + +The SBOM enumerates the resolved dependency set so you can cross-check it +against advisory feeds and detect a poisoned transitive dependency. Download it +from the matching release tag and audit it with any CycloneDX-aware scanner. + +## Supply-chain controls in CI + +- **Advisory gate:** `ci.yaml` runs `pip-audit` against the locked environment + on every push and pull request; a known-vuln dependency fails CI. +- **Locked dependencies:** the resolved versions in `uv.lock` are what the + SBOM and the published wheels are built from.