Skip to content

Commit ac78567

Browse files
author
Nathan Gillett
committed
feat(spec): enforce generated models and release hardening
Adopt intentproof-spec-generated schema models and validators. Add deterministic generation/fingerprint drift checks and a dedicated CI/release gate that blocks handwritten schema model types.
1 parent e33c538 commit ac78567

30 files changed

Lines changed: 1357 additions & 95 deletions

.github/workflows/ci.yml

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,98 @@ 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: ./.github/actions/setup-python-pip
20+
with:
21+
python-version: "3.13"
22+
check-latest: false
23+
24+
- name: Install project (dev extras)
25+
run: pip install -e ".[dev]"
26+
27+
- name: Enforce no handwritten schema model/types policy
28+
run: bash scripts/check-no-handwritten-model-types.sh
29+
30+
hardening:
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 10
33+
steps:
34+
- uses: actions/checkout@v6
35+
36+
- uses: actions/checkout@v6
37+
with:
38+
repository: IntentProof/intentproof-spec
39+
path: intentproof-spec
40+
41+
- uses: actions/setup-node@v6
42+
with:
43+
node-version-file: intentproof-spec/.nvmrc
44+
cache: npm
45+
cache-dependency-path: intentproof-spec/package-lock.json
46+
47+
- name: SDK hardening contract audit (spec-only + drift guards)
48+
run: bash intentproof-spec/scripts/check-sdk-hardening.sh "${{ github.workspace }}"
49+
1350
intentproof-spec:
1451
runs-on: ubuntu-latest
1552
timeout-minutes: 15
1653
steps:
17-
- uses: actions/checkout@v4
54+
- uses: actions/checkout@v6
55+
56+
- uses: actions/checkout@v6
1857
with:
19-
repository: intentproof/intentproof-spec
58+
repository: IntentProof/intentproof-spec
2059
path: intentproof-spec
2160

22-
- uses: actions/setup-node@v4
61+
- uses: actions/setup-node@v6
2362
with:
2463
node-version-file: intentproof-spec/.nvmrc
2564
cache: npm
2665
cache-dependency-path: intentproof-spec/package-lock.json
2766

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

31106
audit:
32107
runs-on: ubuntu-latest
@@ -71,6 +146,11 @@ jobs:
71146
steps:
72147
- uses: actions/checkout@v6
73148

149+
- uses: actions/checkout@v6
150+
with:
151+
repository: IntentProof/intentproof-spec
152+
path: intentproof-spec
153+
74154
- uses: ./.github/actions/setup-python-pip
75155
with:
76156
python-version: ${{ matrix.python-version }}
@@ -80,4 +160,6 @@ jobs:
80160
run: pip install "tox>=4"
81161

82162
- name: Run tox
163+
env:
164+
INTENTPROOF_SPEC_ROOT: ${{ github.workspace }}/intentproof-spec
83165
run: tox run -e ${{ matrix.tox-env }}

.github/workflows/release.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,56 @@ 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

CHANGELOG.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
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`) with a dedicated CI check-run (`no-handwritten-model-types`) 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`) plus standardized report metadata (`INTENTPROOF_SDK_NAME`, `INTENTPROOF_SDK_LANGUAGE`, `INTENTPROOF_SDK_VERSION`).
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.
919
## 0.1.1 — 2026-05-04
1020

1121
- **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).
22+
- **CI:** add **`intentproof-spec`** job—checkout [`intentproof-spec`](https://github.com/IntentProof/intentproof-spec) and run **`scripts/run-conformance.sh`** (canonical Vitest conformance oracle).
1323
- **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.
24+
- **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.
1525
- **Packaging metadata:** update **`description`** and **`keywords`** in **`pyproject.toml`**.
1626
- **Repo layout:** remove **`.gitlab-ci.yml`** (GitLab CI mirror).
1727

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`** (wired into dedicated CI job **`no-handwritten-model-types`** and release preflight required checks) 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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
if [[ $(find . \( -path ./.git -o -path './.tox' -o -path './.venv' -o -path './dist' -o -path './build' \) -prune -false -o -name '*.schema.json' -print | wc -l) -gt 0 ]]; then
8+
echo "check-no-bundled-schema: *.schema.json files are forbidden in this SDK repo (use intentproof-spec checkout)." >&2
9+
find . \( -path ./.git -o -path ./.tox -o -path ./.venv -o -path ./dist -o -path ./build \) -prune -o -name '*.schema.json' -print
10+
exit 1
11+
fi
12+
13+
echo "OK: no bundled *.schema.json"
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env bash
2+
# Enforce that schema model/types are generated from intentproof-spec only.
3+
set -euo pipefail
4+
5+
sdk_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6+
cd "${sdk_root}"
7+
8+
python3 - <<'PY'
9+
from __future__ import annotations
10+
11+
import ast
12+
import json
13+
from pathlib import Path
14+
15+
repo = Path.cwd()
16+
generated = repo / "src" / "intentproof" / "generated"
17+
types_py = repo / "src" / "intentproof" / "types.py"
18+
fingerprint = generated / "spec_fingerprint.json"
19+
20+
required_generated_files = {
21+
generated / "__init__.py",
22+
generated / "execution_event.py",
23+
generated / "intentproof_config.py",
24+
generated / "normative_schemas.py",
25+
fingerprint,
26+
}
27+
missing = [str(p.relative_to(repo)) for p in sorted(required_generated_files) if not p.is_file()]
28+
if missing:
29+
raise SystemExit(
30+
"check-no-handwritten-model-types: missing required generated artifacts:\n"
31+
+ "\n".join(f" - {m}" for m in missing)
32+
)
33+
34+
fp = json.loads(fingerprint.read_text(encoding="utf-8"))
35+
if not isinstance(fp.get("specVersion"), str) or not fp["specVersion"].strip():
36+
raise SystemExit("check-no-handwritten-model-types: generated/spec_fingerprint.json missing specVersion")
37+
if not isinstance(fp.get("files"), dict) or not fp["files"]:
38+
raise SystemExit("check-no-handwritten-model-types: generated/spec_fingerprint.json missing per-schema hashes")
39+
40+
schema_model_names = {
41+
"ExecutionError",
42+
"IntentProofExecutionEventV1",
43+
"IntentProofRuntimeConfigV1",
44+
"JsonValue",
45+
"Status",
46+
"WrapOptionsV1",
47+
}
48+
49+
offenders: list[str] = []
50+
for py in (repo / "src").rglob("*.py"):
51+
if py.is_relative_to(generated):
52+
continue
53+
tree = ast.parse(py.read_text(encoding="utf-8"), filename=str(py))
54+
for node in ast.walk(tree):
55+
if isinstance(node, ast.ClassDef) and node.name in schema_model_names:
56+
offenders.append(f"{py.relative_to(repo)}:{node.lineno}:{node.name}")
57+
if offenders:
58+
raise SystemExit(
59+
"check-no-handwritten-model-types: schema model class definitions must live only in "
60+
"src/intentproof/generated:\n"
61+
+ "\n".join(f" - {line}" for line in offenders)
62+
)
63+
64+
tree = ast.parse(types_py.read_text(encoding="utf-8"), filename=str(types_py))
65+
imports: dict[str, tuple[str, str]] = {}
66+
for node in tree.body:
67+
if isinstance(node, ast.ImportFrom) and node.module in {
68+
"intentproof.generated.execution_event",
69+
"intentproof.generated.intentproof_config",
70+
}:
71+
for alias in node.names:
72+
imports[alias.asname or alias.name] = (node.module, alias.name)
73+
74+
required_import_aliases = {
75+
"ExecutionError": ("intentproof.generated.execution_event", "ExecutionError"),
76+
"IntentProofExecutionEventV1": ("intentproof.generated.execution_event", "IntentProofExecutionEventV1"),
77+
"JsonValue": ("intentproof.generated.execution_event", "JsonValue"),
78+
"Status": ("intentproof.generated.execution_event", "Status"),
79+
"IntentProofRuntimeConfigV1": ("intentproof.generated.intentproof_config", "IntentProofRuntimeConfigV1"),
80+
"IntentProofWrapOptionsV1": ("intentproof.generated.intentproof_config", "WrapOptionsV1"),
81+
}
82+
for alias, expected in required_import_aliases.items():
83+
if imports.get(alias) != expected:
84+
raise SystemExit(
85+
"check-no-handwritten-model-types: src/intentproof/types.py must import "
86+
f"{alias} from generated models ({expected[0]}::{expected[1]})"
87+
)
88+
89+
assignments: dict[str, str] = {}
90+
for node in tree.body:
91+
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
92+
target = node.targets[0].id
93+
if isinstance(node.value, ast.Name):
94+
assignments[target] = node.value.id
95+
96+
required_aliases = {
97+
"ExecutionEvent": "IntentProofExecutionEventV1",
98+
"ExecutionErrorSnapshot": "ExecutionError",
99+
}
100+
for alias, expected in required_aliases.items():
101+
if assignments.get(alias) != expected:
102+
raise SystemExit(
103+
"check-no-handwritten-model-types: src/intentproof/types.py must alias "
104+
f"{alias} = {expected}"
105+
)
106+
107+
print("OK: no handwritten schema model/types; bridge aliases map to generated modules")
108+
PY

0 commit comments

Comments
 (0)