diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d33ac0..ad09edf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,94 +83,6 @@ jobs: - name: SDK hardening contract audit (spec-only + drift guards) run: bash intentproof-spec/scripts/check-consumer-hardening.sh "${{ github.workspace }}" - intentproof-spec: - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - PUBLISH_CONFORMANCE_ROOT: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master') && github.actor != 'github-actions[bot]' && github.actor != 'intentproof-cert-bot[bot]' }} - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - - - name: Resolve pinned intentproof-spec revision - id: spec_pin - run: | - set -euo pipefail - python3 - <<'PY' - import os - import tomllib - from pathlib import Path - c = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["tool"]["intentproof"]["spec-commit"] - with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: - f.write(f"sha={c}\n") - PY - - - uses: actions/checkout@v6 - with: - repository: IntentProof/intentproof-spec - ref: ${{ steps.spec_pin.outputs.sha }} - fetch-depth: 0 - path: intentproof-spec - - - uses: actions/setup-node@v6 - with: - node-version-file: intentproof-spec/.nvmrc - cache: npm - cache-dependency-path: intentproof-spec/package-lock.json - - - name: Canonical spec conformance (pin check + oracle + replay) - env: - INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec - INTENTPROOF_SDK_ID: python - INTENTPROOF_REPLAY_VERIFY: "1" - INTENTPROOF_CONFORMANCE_JSON: "1" - INTENTPROOF_CERTIFICATE_SCHEMA_VERSION: "v2" - INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PATH: ${{ github.workspace }}/intentproof-spec/keys/spec-integrity.public.pem - run: bash scripts/spec-conformance.sh - - - name: Upload conformance report artifact - uses: actions/upload-artifact@v7 - with: - name: conformance-report-python - path: intentproof-spec/conformance-report.json - if-no-files-found: error - - - name: Upload conformance certificate artifact - uses: actions/upload-artifact@v7 - with: - name: conformance-certificate-python - path: intentproof-spec/conformance-certificate.json - if-no-files-found: error - - - name: Mint cert-bot installation token - id: cert_bot_token - if: ${{ env.PUBLISH_CONFORMANCE_ROOT == 'true' }} - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ vars.INTENTPROOF_CERT_BOT_ID }} - private-key: ${{ secrets.INTENTPROOF_CERT_BOT_PRIVATE_KEY }} - - - name: Publish conformance certificate and report at repo root - if: ${{ env.PUBLISH_CONFORMANCE_ROOT == 'true' }} - env: - INTENTPROOF_CERT_BOT_TOKEN: ${{ steps.cert_bot_token.outputs.token }} - run: | - set -euo pipefail - cp intentproof-spec/conformance-certificate.json conformance-certificate.json - cp intentproof-spec/conformance-report.json conformance-report.json - if git diff --quiet -- conformance-certificate.json conformance-report.json; then - echo "No conformance artifact updates to commit." - exit 0 - fi - git config user.name "intentproof-cert-bot[bot]" - git config user.email "intentproof-cert-bot[bot]@users.noreply.github.com" - git config --local --unset-all http.https://github.com/.extraheader || true - git remote set-url origin "https://x-access-token:${INTENTPROOF_CERT_BOT_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - git add conformance-certificate.json conformance-report.json - git commit -m "chore(ci): update conformance certificate and report" - git push - spec-golden-parity: runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/conformance-attestation.yml b/.github/workflows/conformance-attestation.yml new file mode 100644 index 0000000..cb59f9d --- /dev/null +++ b/.github/workflows/conformance-attestation.yml @@ -0,0 +1,142 @@ +# Trusted-path attestation: signed conformance certificate + validation. +# Mirrors intentproof-api/.github/workflows/conformance-attestation.yml. +# PR checks stay on spec-conformance.yml (no signing secrets required). +name: Conformance Attestation + +on: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: conformance-attestation-${{ github.ref }} + cancel-in-progress: true + +jobs: + conformance-attestation: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + PUBLISH_CONFORMANCE_ROOT: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master') && github.actor != 'github-actions[bot]' && github.actor != 'intentproof-cert-bot[bot]' }} + steps: + - name: Checkout SDK repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Resolve pinned intentproof-spec revision + id: spec_pin + run: | + set -euo pipefail + python3 - <<'PY' + import os + import tomllib + from pathlib import Path + c = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["tool"]["intentproof"]["spec-commit"] + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: + f.write(f"sha={c}\n") + PY + + - name: Checkout intentproof-spec + uses: actions/checkout@v6 + with: + repository: IntentProof/intentproof-spec + ref: ${{ steps.spec_pin.outputs.sha }} + fetch-depth: 0 + path: intentproof-spec + + - uses: ./.github/actions/setup-python-pip + with: + python-version: "3.x" + check-latest: true + + - name: Install tox + run: pip install "tox>=4" + + # secrets context is not allowed in step `if:`; pass PEMs via env and verify in shell. + - name: Require attestation secrets + env: + INTENTPROOF_CERTIFICATE_SIGNING_KEY_PEM: ${{ secrets.INTENTPROOF_CERTIFICATE_SIGNING_KEY_PEM }} + INTENTPROOF_CERTIFICATE_PUBLIC_KEY_PEM: ${{ secrets.INTENTPROOF_CERTIFICATE_PUBLIC_KEY_PEM }} + INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PEM: ${{ secrets.INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PEM }} + run: | + if [[ -z "${INTENTPROOF_CERTIFICATE_SIGNING_KEY_PEM}" || -z "${INTENTPROOF_CERTIFICATE_PUBLIC_KEY_PEM}" ]]; then + echo '::error::Configure repository secrets INTENTPROOF_CERTIFICATE_SIGNING_KEY_PEM and INTENTPROOF_CERTIFICATE_PUBLIC_KEY_PEM.' + exit 1 + fi + if [[ -z "${INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PEM}" ]]; then + echo '::error::Configure repository secret INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PEM (spec integrity verify during oracle).' + exit 1 + fi + + - name: SDK quality gates (tox static + cov) + env: + INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec + run: tox run -e static,cov + + - name: Set up Node.js for spec oracle + uses: actions/setup-node@v6 + with: + node-version-file: intentproof-spec/.nvmrc + cache: npm + cache-dependency-path: intentproof-spec/package-lock.json + + - name: Conformance (trusted attestation gate) + env: + INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec + INTENTPROOF_SDK_ID: python + INTENTPROOF_REPLAY_VERIFY: "1" + INTENTPROOF_CONFORMANCE_JSON: "1" + INTENTPROOF_CERTIFICATE_SCHEMA_VERSION: "v2" + INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PEM: ${{ secrets.INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PEM }} + INTENTPROOF_CERTIFICATE_SIGNING_KEY_PEM: ${{ secrets.INTENTPROOF_CERTIFICATE_SIGNING_KEY_PEM }} + INTENTPROOF_CERTIFICATE_SIGNING_KEY_ID: intentproof-ci-ed25519-v1 + run: bash scripts/spec-conformance.sh + + - name: Validate conformance certificate + working-directory: intentproof-spec + env: + INTENTPROOF_CERTIFICATE_PUBLIC_KEY_PEM: ${{ secrets.INTENTPROOF_CERTIFICATE_PUBLIC_KEY_PEM }} + INTENTPROOF_CERTIFICATE_REQUIRE_SIGNATURE: "1" + INTENTPROOF_CERTIFICATE_SCHEMA_VERSION: "v2" + run: npm run validate:conformance-certificate + + - name: Upload conformance report and certificate + uses: actions/upload-artifact@v7 + with: + name: conformance-artifacts + path: | + intentproof-spec/conformance-report.json + intentproof-spec/conformance-certificate.json + if-no-files-found: error + + - name: Mint cert-bot installation token + id: cert_bot_token + if: ${{ env.PUBLISH_CONFORMANCE_ROOT == 'true' }} + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.INTENTPROOF_CERT_BOT_ID }} + private-key: ${{ secrets.INTENTPROOF_CERT_BOT_PRIVATE_KEY }} + + - name: Publish conformance certificate and report at repo root + if: ${{ env.PUBLISH_CONFORMANCE_ROOT == 'true' }} + env: + INTENTPROOF_CERT_BOT_TOKEN: ${{ steps.cert_bot_token.outputs.token }} + run: | + set -euo pipefail + cp intentproof-spec/conformance-certificate.json conformance-certificate.json + cp intentproof-spec/conformance-report.json conformance-report.json + if git diff --quiet -- conformance-certificate.json conformance-report.json; then + echo "No conformance artifact updates to commit." + exit 0 + fi + git config user.name "intentproof-cert-bot[bot]" + git config user.email "intentproof-cert-bot[bot]@users.noreply.github.com" + git config --local --unset-all http.https://github.com/.extraheader || true + git remote set-url origin "https://x-access-token:${INTENTPROOF_CERT_BOT_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git add conformance-certificate.json conformance-report.json + git commit -m "chore(ci): update conformance certificate and report" + git push diff --git a/.github/workflows/spec-conformance.yml b/.github/workflows/spec-conformance.yml new file mode 100644 index 0000000..d2fbd5f --- /dev/null +++ b/.github/workflows/spec-conformance.yml @@ -0,0 +1,84 @@ +# PR / manual: SDK quality gates + canonical spec oracle (no signing secrets). +# Trusted signed attestation + repo-root certificate publish lives in conformance-attestation.yml. +name: Spec Conformance + +on: + workflow_dispatch: + pull_request: + +permissions: + contents: read + +concurrency: + group: spec-conformance-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + spec-conformance: + name: Spec conformance + SDK quality gates + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - name: Checkout SDK repository + uses: actions/checkout@v6 + + - name: Resolve pinned intentproof-spec revision + id: spec_pin + run: | + set -euo pipefail + python3 - <<'PY' + import os + import tomllib + from pathlib import Path + c = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["tool"]["intentproof"]["spec-commit"] + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: + f.write(f"sha={c}\n") + PY + + - name: Checkout intentproof-spec + uses: actions/checkout@v6 + with: + repository: IntentProof/intentproof-spec + ref: ${{ steps.spec_pin.outputs.sha }} + fetch-depth: 0 + path: intentproof-spec + + - uses: ./.github/actions/setup-python-pip + with: + python-version: "3.x" + check-latest: true + + - name: Install tox + run: pip install "tox>=4" + + - name: SDK quality gates (tox static + cov) + env: + INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec + run: tox run -e static,cov + + - name: Set up Node.js for spec oracle + uses: actions/setup-node@v6 + with: + node-version-file: intentproof-spec/.nvmrc + cache: npm + cache-dependency-path: intentproof-spec/package-lock.json + + - name: Run canonical spec conformance + env: + INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec + INTENTPROOF_SDK_ID: python + INTENTPROOF_REPLAY_VERIFY: "1" + INTENTPROOF_CONFORMANCE_JSON: "1" + INTENTPROOF_CERTIFICATE_SCHEMA_VERSION: "v2" + INTENTPROOF_SPEC_INTEGRITY_PUBLIC_KEY_PATH: ${{ github.workspace }}/intentproof-spec/keys/spec-integrity.public.pem + run: | + bash scripts/spec-conformance.sh + cp intentproof-spec/conformance-report.json conformance-report.json + + - name: Upload conformance report artifact + uses: actions/upload-artifact@v7 + with: + name: conformance-report-python + path: conformance-report.json + if-no-files-found: error diff --git a/CHANGELOG.md b/CHANGELOG.md index ede45c1..b26205b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this repository are documented here. **PyPI** releases us ## Unreleased -- None yet. +- **CI / attestation alignment:** split canonical conformance out of **`ci.yml`** into **`spec-conformance.yml`** (PR + manual) and **`conformance-attestation.yml`** (trusted **`main`** signing + validation + root publish), matching **`intentproof-api`** and the Node SDK layout. ## 0.1.3 — 2026-05-08 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d83017b..73b479c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,4 +10,7 @@ tox run -e static tox run -e cov ``` +- **Spec Conformance** (`.github/workflows/spec-conformance.yml`): pull requests and `workflow_dispatch` — **`tox run -e static,cov`**, then **`scripts/spec-conformance.sh`**; uploads a conformance report artifact (no signing secrets). +- **Conformance Attestation** (`.github/workflows/conformance-attestation.yml`): trusted runs on **`main`**/**`master`** — same shape as **`intentproof-api`**: PEM secret checks, signed certificate, validation, artifact upload, optional cert-bot commit of root conformance JSON. + PyPI releases use **`release.yml`** (trusted publisher / OIDC). For undisclosed security issues, use [**Security**](https://github.com/IntentProof/intentproof-sdk-python/security) advisories. diff --git a/README.md b/README.md index 6d52109..61ed083 100644 --- a/README.md +++ b/README.md @@ -347,8 +347,9 @@ Custom **`body`** serializers: if **`body(event)`** raises, **`HttpExporter`** n - **Version pin:** **`[tool.intentproof].spec-version`** and **`spec-commit`** in **`pyproject.toml`** match **`spec.json`** and the spec **`HEAD`** checkout; **`scripts/check-consumer-spec-pin.sh`** delegates to **`intentproof-spec`** **`scripts/check-consumer-spec-pins.sh`** before conformance. -- **CI:** every push/PR checks out this SDK plus **`intentproof-spec`** and runs **`scripts/spec-conformance.sh`** (pin check + full oracle; see `.github/workflows/ci.yml`). The **`spec-golden-parity`** job runs **`tests/unit/test_spec_golden_conformance.py`** against the same **`golden/execution_event_cases.jsonl`** using **`jsonschema`** + semantics mirrored from the spec (`tests/spec_semantics.py`). -- **Repo-root certificates:** each run uploads **`conformance-report.json`** and **`conformance-certificate.json`** as workflow artifacts; after a green default-branch push, the conformance GitHub App commits the same files at the repo root when they differ from **`main`**. +- **CI:** `.github/workflows/ci.yml` runs hardening, golden parity, audit, and the **`tox`** matrix against a pinned **`intentproof-spec`** checkout. +- **Spec conformance (PR):** `.github/workflows/spec-conformance.yml` runs **`tox run -e static,cov`** then **`scripts/spec-conformance.sh`** and uploads **`conformance-report.json`** (committed spec public key path; no signing secrets on PRs). The **`spec-golden-parity`** job in CI runs **`tests/unit/test_spec_golden_conformance.py`** against **`golden/execution_event_cases.jsonl`** using **`jsonschema`** + semantics mirrored from the spec (`tests/spec_semantics.py`). +- **Trusted attestation (`main`):** `.github/workflows/conformance-attestation.yml` follows **`intentproof-api`**: tox gates, signed oracle output, **`npm run validate:conformance-certificate`** in the spec checkout, **`conformance-artifacts`** upload, and cert-bot publish of root **`conformance-certificate.json`** / **`conformance-report.json`** when they change. - **Local:** clone `intentproof-spec` **next to** this repository (`../intentproof-spec`), then: ```bash