Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 # <date> <reason; awaiting fix>
# 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."
60 changes: 60 additions & 0 deletions .github/workflows/release-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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).
Expand All @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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**:
<https://pypi.org/project/agent-assembly/#files> β€” 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**:
<https://github.com/ai-agent-assembly/python-sdk/releases>

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.
Loading