Skip to content

Commit c8f96f2

Browse files
authored
Merge pull request #5 from negillett/hardening
feat(spec): enforce generated models and release hardening
2 parents e33c538 + a0cd9bc commit c8f96f2

30 files changed

Lines changed: 1315 additions & 96 deletions

.github/workflows/ci.yml

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,97 @@ concurrency:
1010
cancel-in-progress: true
1111

1212
jobs:
13+
no-handwritten-model-types:
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 10
16+
steps:
17+
- uses: actions/checkout@v6
18+
19+
- uses: actions/checkout@v6
20+
with:
21+
repository: IntentProof/intentproof-spec
22+
path: intentproof-spec
23+
24+
- name: No handwritten model/types outside generated schema package
25+
env:
26+
INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec
27+
run: bash scripts/check-no-handwritten-model-types.sh
28+
29+
hardening:
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 10
32+
steps:
33+
- uses: actions/checkout@v6
34+
35+
- uses: actions/checkout@v6
36+
with:
37+
repository: IntentProof/intentproof-spec
38+
path: intentproof-spec
39+
40+
- uses: actions/setup-node@v6
41+
with:
42+
node-version-file: intentproof-spec/.nvmrc
43+
cache: npm
44+
cache-dependency-path: intentproof-spec/package-lock.json
45+
46+
- name: SDK hardening contract audit (spec-only + drift guards)
47+
run: bash intentproof-spec/scripts/check-sdk-hardening.sh "${{ github.workspace }}"
48+
1349
intentproof-spec:
1450
runs-on: ubuntu-latest
1551
timeout-minutes: 15
1652
steps:
17-
- uses: actions/checkout@v4
53+
- uses: actions/checkout@v6
54+
55+
- uses: actions/checkout@v6
1856
with:
19-
repository: intentproof/intentproof-spec
57+
repository: IntentProof/intentproof-spec
2058
path: intentproof-spec
2159

22-
- uses: actions/setup-node@v4
60+
- uses: actions/setup-node@v6
2361
with:
2462
node-version-file: intentproof-spec/.nvmrc
2563
cache: npm
2664
cache-dependency-path: intentproof-spec/package-lock.json
2765

28-
- name: Canonical spec conformance (Vitest oracle)
29-
run: bash intentproof-spec/scripts/run-conformance.sh intentproof-spec
66+
- name: Canonical spec conformance (pin check + oracle + replay)
67+
env:
68+
INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec
69+
INTENTPROOF_SDK_ID: python
70+
INTENTPROOF_REPLAY_VERIFY: "1"
71+
INTENTPROOF_CONFORMANCE_JSON: "1"
72+
run: bash scripts/spec-conformance.sh
73+
74+
- name: Upload conformance report artifact
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: conformance-report-python
78+
path: intentproof-spec/conformance-report.json
79+
if-no-files-found: error
80+
81+
spec-golden-parity:
82+
runs-on: ubuntu-latest
83+
timeout-minutes: 10
84+
steps:
85+
- uses: actions/checkout@v6
86+
87+
- uses: actions/checkout@v6
88+
with:
89+
repository: IntentProof/intentproof-spec
90+
path: intentproof-spec
91+
92+
- uses: ./.github/actions/setup-python-pip
93+
with:
94+
python-version: "3.13"
95+
check-latest: false
96+
97+
- name: Install project (dev extras)
98+
run: pip install -e ".[dev]"
99+
100+
- name: Golden execution_event JSONL parity (schema + semantics)
101+
env:
102+
INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec
103+
run: pytest tests/unit/test_spec_golden_conformance.py -v
30104

31105
audit:
32106
runs-on: ubuntu-latest
@@ -71,6 +145,11 @@ jobs:
71145
steps:
72146
- uses: actions/checkout@v6
73147

148+
- uses: actions/checkout@v6
149+
with:
150+
repository: IntentProof/intentproof-spec
151+
path: intentproof-spec
152+
74153
- uses: ./.github/actions/setup-python-pip
75154
with:
76155
python-version: ${{ matrix.python-version }}
@@ -80,4 +159,6 @@ jobs:
80159
run: pip install "tox>=4"
81160

82161
- name: Run tox
162+
env:
163+
INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec
83164
run: tox run -e ${{ matrix.tox-env }}

.github/workflows/release.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,78 @@ on:
1515
tags:
1616
- "v*"
1717

18+
permissions:
19+
contents: read
20+
checks: read
21+
1822
jobs:
23+
preflight:
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 10
26+
steps:
27+
- name: Verify required CI checks passed on release SHA
28+
uses: actions/github-script@v8
29+
with:
30+
script: |
31+
const requiredExact = ["no-handwritten-model-types", "hardening", "intentproof-spec", "spec-golden-parity"];
32+
const requiredPrefixes = ["tox (static", "tox (cov"];
33+
const { owner, repo } = context.repo;
34+
const ref = context.sha;
35+
const runs = await github.paginate(github.rest.checks.listForRef, {
36+
owner,
37+
repo,
38+
ref,
39+
per_page: 100,
40+
});
41+
const names = runs.map((r) => `${r.name}:${r.conclusion ?? r.status}`);
42+
core.info(`check-runs on ${ref}: ${names.join(", ")}`);
43+
for (const name of requiredExact) {
44+
const matches = runs.filter((r) => r.name === name);
45+
if (matches.length === 0) {
46+
core.setFailed(`Missing required check-run: ${name}`);
47+
continue;
48+
}
49+
const ok = matches.some((r) => r.conclusion === "success");
50+
if (!ok) {
51+
core.setFailed(`Required check-run not successful: ${name}`);
52+
}
53+
}
54+
for (const prefix of requiredPrefixes) {
55+
const matches = runs.filter((r) => r.name.startsWith(prefix));
56+
if (matches.length === 0) {
57+
core.setFailed(`Missing required check-run prefix: ${prefix}`);
58+
continue;
59+
}
60+
const ok = matches.some((r) => r.conclusion === "success");
61+
if (!ok) {
62+
core.setFailed(`Required check-run prefix not successful: ${prefix}`);
63+
}
64+
}
65+
1966
publish:
67+
needs: preflight
2068
runs-on: ubuntu-latest
2169
permissions:
2270
id-token: write
2371
contents: read
2472
steps:
2573
- uses: actions/checkout@v6
2674

75+
- uses: actions/checkout@v6
76+
with:
77+
repository: IntentProof/intentproof-spec
78+
path: intentproof-spec
79+
2780
- uses: ./.github/actions/setup-python-pip
2881
with:
2982
python-version: "3.x"
3083
check-latest: true
3184

85+
- name: No handwritten model/types outside generated schema package
86+
env:
87+
INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec
88+
run: bash scripts/check-no-handwritten-model-types.sh
89+
3290
- name: Verify tag matches pyproject.toml version
3391
env:
3492
TAG_NAME: ${{ github.ref_name }}

CHANGELOG.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
# Changelog
22

3-
Repository: [IntentProof Python SDK (`intentproof-sdk-python`)](https://github.com/intentproof/intentproof-sdk-python).
3+
Repository: [IntentProof Python SDK (`intentproof-sdk-python`)](https://github.com/IntentProof/intentproof-sdk-python).
44

55
All notable changes to this repository are documented here. **PyPI** releases use SemVer for the **`intentproof-sdk`** distribution (`version` in [`pyproject.toml`](pyproject.toml)); tag releases in Git to match published versions.
66

77
## Unreleased
88

9+
- Add spec-driven generated models package under `src/intentproof/generated` (`execution_event`, `intentproof_config`, `normative_schemas`, and package exports) and switch SDK type aliases/wire paths to these generated schema models.
10+
- Add schema validation helpers in `src/intentproof/schema_validate.py` for execution events, wrap options, and runtime config; export them from `intentproof.__init__`.
11+
- Add deterministic codegen pipeline (`scripts/generate_schema_models.py`) that writes embedded normative schemas plus `src/intentproof/generated/spec_fingerprint.json` (spec version + per-schema/aggregate SHA-256 digests).
12+
- Add generated artifact drift checks (`scripts/verify-generated-types.sh`) and bundled schema guard (`scripts/check-no-bundled-schema.sh`) for release hygiene.
13+
- Add explicit no-handwritten-model policy enforcement (`scripts/check-no-handwritten-model-types.sh`) by delegating to the shared `intentproof-spec` checker, with a dedicated CI check-run (`no-handwritten-model-types`), the same step in **`tox -e static`** and release publish (parity with Node/Java), and release preflight gating on that check.
14+
- Strengthen CI/release hardening: add `hardening` job, enforce required check-runs in release preflight, and upload canonical conformance report artifact (`conformance-report-python`).
15+
- Improve spec-conformance wrapper (`scripts/spec-conformance.sh`) with strict spec pin verification (`scripts/check-sdk-spec-pin.sh`), standardized report metadata (`INTENTPROOF_SDK_NAME`, `INTENTPROOF_SDK_LANGUAGE`, `INTENTPROOF_SDK_VERSION`), and `./intentproof-spec` fallback when the env var and sibling clone are absent.
16+
- Pin `datamodel-code-generator` in `pyproject.toml` for stable code generation and make generated headers deterministic (no timestamp/random temp filenames).
17+
- Add/expand tests for spec semantics and generated artifacts: `tests/spec_semantics.py`, `tests/unit/test_spec_golden_conformance.py`, `tests/unit/test_schema_validate.py`, `tests/unit/test_generated_fingerprint.py`, with related SDK/exporter/wire test updates.
18+
- Update docs (`README.md`) and local quality wiring (`tox.ini`) to reflect spec-first model generation, dedicated policy gating, and deduplicated CI enforcement.
19+
- CI: **`no-handwritten-model-types`** job only checks out the SDK + spec and runs the delegated script (no **`pip install`**); **push** triggers also include **`master`** (parity with Node/Java).
920
## 0.1.1 — 2026-05-04
1021

1122
- **Security:** upgrade **pip** to **≥26.1** after Python setup in the composite **`.github/actions/setup-python-pip`** action, and in the default **`[testenv]`** via **`commands_pre`**, addressing **CVE-2026-3219** and **CVE-2026-6357**.
12-
- **CI:** add **`intentproof-spec`** job—checkout [`intentproof-spec`](https://github.com/intentproof/intentproof-spec) and run **`scripts/run-conformance.sh`** (canonical Vitest conformance oracle).
23+
- **CI:** add **`intentproof-spec`** job—checkout [`intentproof-spec`](https://github.com/IntentProof/intentproof-spec) and run **`scripts/run-conformance.sh`** (canonical Vitest conformance oracle).
1324
- **Local spec checks:** add **`scripts/spec-conformance.sh`** and **`tox -e spec`** (sibling **`../intentproof-spec`** or **`INTENTPROOF_SPEC_ROOT`**).
14-
- **Docs:** add this **`CHANGELOG.md`** (aligned with the [spec repo changelog](https://github.com/intentproof/intentproof-spec/blob/main/CHANGELOG.md)); refresh **`README.md`**—positioning, pinned PyPI/GitHub install guidance, reorganized API reference tables, **`intentproof-spec`** section, vulnerability reporting link, and related edits.
25+
- **Docs:** add this **`CHANGELOG.md`** (aligned with the [spec repo changelog](https://github.com/IntentProof/intentproof-spec/blob/main/CHANGELOG.md)); refresh **`README.md`**—positioning, pinned PyPI/GitHub install guidance, reorganized API reference tables, **`intentproof-spec`** section, vulnerability reporting link, and related edits.
1526
- **Packaging metadata:** update **`description`** and **`keywords`** in **`pyproject.toml`**.
1627
- **Repo layout:** remove **`.gitlab-ci.yml`** (GitLab CI mirror).
1728

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,9 @@ Custom **`body`** serializers: if **`body(event)`** raises, **`HttpExporter`** n
342342

343343
Schemas, golden oracles, and the **Vitest conformance oracle** live in the **[IntentProof specification repository (`intentproof-spec`)](https://github.com/intentproof/intentproof-spec)**.
344344

345-
- **CI:** every push/PR runs `scripts/run-conformance.sh` from that repo (see `.github/workflows/ci.yml`).
345+
- **Version pin:** **`[tool.intentproof].spec-version`** in **`pyproject.toml`** matches **`spec.json`** in that repo; **`scripts/check-sdk-spec-pin.sh`** enforces it before conformance.
346+
347+
- **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`).
346348
- **Local:** clone `intentproof-spec` **next to** this repository (`../intentproof-spec`), then:
347349

348350
```bash
@@ -351,6 +353,14 @@ Schemas, golden oracles, and the **Vitest conformance oracle** live in the **[In
351353

352354
Or set `INTENTPROOF_SPEC_ROOT` and run `bash scripts/spec-conformance.sh`.
353355

356+
- **Generated fingerprint metadata:** model generation writes **`src/intentproof/generated/spec_fingerprint.json`** (spec version, generator version, per-schema SHA-256, aggregate hash). Validate/update generated artifacts with:
357+
358+
```bash
359+
bash scripts/verify-generated-types.sh
360+
```
361+
362+
- **No handwritten model types:** **`scripts/check-no-handwritten-model-types.sh`** delegates to the shared **`intentproof-spec`** checker. It runs in CI (dedicated **`no-handwritten-model-types`** job—SDK + spec checkout and **`python3`** only—and **`tox -e static`**), release publish, and release preflight required checks, and fails if schema model class definitions appear outside **`src/intentproof/generated`** or if the bridge aliases in **`src/intentproof/types.py`** stop mapping to generated models.
363+
354364
---
355365

356366
## Project development

pyproject.toml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ classifiers = [
2626
"Typing :: Typed",
2727
]
2828

29-
dependencies = []
29+
dependencies = [
30+
"jsonschema>=4.23",
31+
"pydantic>=2.10",
32+
]
3033

3134
[project.optional-dependencies]
3235
dev = [
33-
"build>=1.2",
34-
"coverage[toml]>=7.4",
36+
"build>=1.2",
37+
"coverage[toml]>=7.4",
38+
"datamodel-code-generator==0.56.1",
3539
"pip-audit>=2.7",
3640
"pytest>=8.0",
3741
"pytest-asyncio>=0.24",
@@ -40,6 +44,10 @@ dev = [
4044
"tox>=4",
4145
]
4246

47+
[tool.intentproof]
48+
# Must match https://github.com/intentproof/intentproof-spec/blob/main/spec.json — enforced by scripts/check-sdk-spec-pin.sh
49+
spec-version = "spec-v1.0.0"
50+
4351
[project.urls]
4452
Homepage = "https://github.com/intentproof/intentproof-sdk-python"
4553
Repository = "https://github.com/intentproof/intentproof-sdk-python"
@@ -84,6 +92,7 @@ exclude_lines = [
8492
target-version = "py311"
8593
line-length = 100
8694
src = ["src", "tests"]
95+
extend-exclude = ["src/intentproof/generated"]
8796

8897
[tool.ruff.format]
8998
quote-style = "double"

scripts/check-no-bundled-schema.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
# SDK repos must not ship duplicate JSON Schema artifacts; intentproof-spec is canonical.
3+
set -euo pipefail
4+
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
cd "$repo"
6+
7+
mapfile -t schema_paths < <(find . \
8+
\( -path ./.git -o -path ./.tox -o -path ./.venv -o -path ./dist -o -path ./build \) -prune \
9+
-o -name '*.schema.json' -print)
10+
11+
bad=0
12+
for p in "${schema_paths[@]}"; do
13+
case "$p" in
14+
./intentproof-spec/schema/*.schema.json) ;;
15+
*)
16+
if [[ $bad -eq 0 ]]; then
17+
echo "check-no-bundled-schema: only ./intentproof-spec/schema/*.schema.json is allowed." >&2
18+
fi
19+
echo "$p"
20+
bad=1
21+
;;
22+
esac
23+
done
24+
25+
if [[ $bad -ne 0 ]]; then
26+
exit 1
27+
fi
28+
29+
echo "OK: no bundled *.schema.json"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
# Wrapper: run shared no-handwritten-model/types policy from intentproof-spec.
3+
set -euo pipefail
4+
5+
sdk_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6+
spec_root="${INTENTPROOF_SPEC_ROOT:-}"
7+
8+
if [[ -z "${spec_root}" ]]; then
9+
sibling="${sdk_root}/../intentproof-spec"
10+
if [[ -f "${sibling}/spec.json" ]]; then
11+
spec_root="$(cd "${sibling}" && pwd)"
12+
fi
13+
fi
14+
15+
if [[ -z "${spec_root}" ]]; then
16+
in_repo="${sdk_root}/intentproof-spec"
17+
if [[ -f "${in_repo}/spec.json" ]]; then
18+
spec_root="$(cd "${in_repo}" && pwd)"
19+
fi
20+
fi
21+
22+
if [[ -z "${spec_root}" || ! -f "${spec_root}/scripts/check-sdk-no-handwritten-model-types.sh" ]]; then
23+
echo "check-no-handwritten-model-types: intentproof-spec checkout not found (set INTENTPROOF_SPEC_ROOT or clone ../intentproof-spec)" >&2
24+
exit 1
25+
fi
26+
27+
exec bash "${spec_root}/scripts/check-sdk-no-handwritten-model-types.sh" "${sdk_root}"

scripts/check-sdk-spec-pin.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
# Fail if this SDK's declared IntentProof spec version does not match spec.json from the checkout.
3+
# Usage: check-sdk-spec-pin.sh /absolute/or/relative/path/to/intentproof-spec
4+
set -euo pipefail
5+
6+
spec_root="$(cd "$1" && pwd)"
7+
sdk_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8+
9+
if [[ ! -f "${spec_root}/spec.json" ]]; then
10+
echo "check-sdk-spec-pin: not a spec checkout (missing spec.json): ${spec_root}" >&2
11+
exit 2
12+
fi
13+
14+
spec_version="$(python3 -c "import json, pathlib, sys; print(json.loads(pathlib.Path(sys.argv[1]).read_text())['version'])" "${spec_root}/spec.json")"
15+
16+
declared="$(cd "$sdk_root" && python3 -c "
17+
import tomllib
18+
from pathlib import Path
19+
p = Path('pyproject.toml')
20+
data = tomllib.loads(p.read_text(encoding='utf-8'))
21+
print(data['tool']['intentproof']['spec-version'])
22+
")"
23+
24+
if [[ "$declared" != "$spec_version" ]]; then
25+
echo "check-sdk-spec-pin: pyproject [tool.intentproof] spec-version=${declared} but spec.json version=${spec_version}" >&2
26+
exit 1
27+
fi
28+
29+
echo "SDK spec pin OK (${spec_version})"

0 commit comments

Comments
 (0)