scpe-protocol 0.1.2__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. scpe_protocol-0.2.1/PKG-INFO +308 -0
  2. scpe_protocol-0.2.1/README.md +280 -0
  3. scpe_protocol-0.2.1/pyproject.toml +56 -0
  4. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/reference/level1_lint.py +1 -1
  5. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/reference/producer.py +107 -12
  6. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/reference/standalone/verify_envelope.py +70 -5
  7. scpe_protocol-0.2.1/scpe/__init__.py +16 -0
  8. scpe_protocol-0.2.1/scpe/cli.py +262 -0
  9. scpe_protocol-0.2.1/scpe/diffinfo.py +65 -0
  10. scpe_protocol-0.2.1/scpe/results.py +224 -0
  11. scpe_protocol-0.2.1/scpe/seal.py +305 -0
  12. scpe_protocol-0.2.1/scpe/testrun.py +188 -0
  13. scpe_protocol-0.2.1/scpe/verify.py +143 -0
  14. scpe_protocol-0.2.1/scpe_protocol.egg-info/PKG-INFO +308 -0
  15. scpe_protocol-0.2.1/scpe_protocol.egg-info/SOURCES.txt +53 -0
  16. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/scpe_protocol.egg-info/entry_points.txt +0 -1
  17. scpe_protocol-0.2.1/scpe_protocol.egg-info/requires.txt +4 -0
  18. scpe_protocol-0.2.1/tests/test_action_expressions.py +69 -0
  19. scpe_protocol-0.2.1/tests/test_action_yaml.py +561 -0
  20. scpe_protocol-0.2.1/tests/test_diff_resolution.py +124 -0
  21. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_differential_verifiers.py +54 -15
  22. scpe_protocol-0.2.1/tests/test_e2e_local.py +210 -0
  23. scpe_protocol-0.2.1/tests/test_empty_keys_anchor_parity.py +88 -0
  24. scpe_protocol-0.2.1/tests/test_entry_points.py +124 -0
  25. scpe_protocol-0.2.1/tests/test_gate_requires_forge_anchor.py +55 -0
  26. scpe_protocol-0.2.1/tests/test_key_source_anchor.py +178 -0
  27. scpe_protocol-0.2.1/tests/test_level2_implies_level1.py +112 -0
  28. scpe_protocol-0.2.1/tests/test_no_duplicate_verifier.py +110 -0
  29. scpe_protocol-0.2.1/tests/test_package_is_stdlib_only.py +83 -0
  30. scpe_protocol-0.2.1/tests/test_results_contract.py +135 -0
  31. scpe_protocol-0.2.1/tests/test_retired_envelope_format.py +237 -0
  32. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_seal.py +50 -0
  33. scpe_protocol-0.2.1/tests/test_seal_render.py +143 -0
  34. scpe_protocol-0.2.1/tests/test_seal_results.py +138 -0
  35. scpe_protocol-0.2.1/tests/test_testrun.py +136 -0
  36. scpe_protocol-0.2.1/tests/test_verify_cli_parity.py +71 -0
  37. scpe_protocol-0.1.2/PKG-INFO +0 -221
  38. scpe_protocol-0.1.2/README.md +0 -190
  39. scpe_protocol-0.1.2/pyproject.toml +0 -42
  40. scpe_protocol-0.1.2/scpe/__init__.py +0 -2
  41. scpe_protocol-0.1.2/scpe/analyze.py +0 -67
  42. scpe_protocol-0.1.2/scpe/attestation.py +0 -361
  43. scpe_protocol-0.1.2/scpe/backends.py +0 -93
  44. scpe_protocol-0.1.2/scpe/changes.py +0 -90
  45. scpe_protocol-0.1.2/scpe/checks.py +0 -96
  46. scpe_protocol-0.1.2/scpe/cli.py +0 -1020
  47. scpe_protocol-0.1.2/scpe/contribute.py +0 -142
  48. scpe_protocol-0.1.2/scpe/envelope.py +0 -211
  49. scpe_protocol-0.1.2/scpe/handshake.py +0 -229
  50. scpe_protocol-0.1.2/scpe/identity.py +0 -245
  51. scpe_protocol-0.1.2/scpe/inspect.py +0 -73
  52. scpe_protocol-0.1.2/scpe/label.py +0 -153
  53. scpe_protocol-0.1.2/scpe/mcp_server.py +0 -334
  54. scpe_protocol-0.1.2/scpe/prompting.py +0 -33
  55. scpe_protocol-0.1.2/scpe/repo_snapshot.py +0 -74
  56. scpe_protocol-0.1.2/scpe/sandbox.py +0 -196
  57. scpe_protocol-0.1.2/scpe/scrub.py +0 -48
  58. scpe_protocol-0.1.2/scpe/seal.py +0 -117
  59. scpe_protocol-0.1.2/scpe/signing.py +0 -55
  60. scpe_protocol-0.1.2/scpe/workspace.py +0 -189
  61. scpe_protocol-0.1.2/scpe_protocol.egg-info/PKG-INFO +0 -221
  62. scpe_protocol-0.1.2/scpe_protocol.egg-info/SOURCES.txt +0 -77
  63. scpe_protocol-0.1.2/scpe_protocol.egg-info/requires.txt +0 -8
  64. scpe_protocol-0.1.2/tests/test_action_yaml.py +0 -429
  65. scpe_protocol-0.1.2/tests/test_analyze.py +0 -76
  66. scpe_protocol-0.1.2/tests/test_attestation.py +0 -336
  67. scpe_protocol-0.1.2/tests/test_backends.py +0 -55
  68. scpe_protocol-0.1.2/tests/test_checks.py +0 -137
  69. scpe_protocol-0.1.2/tests/test_cli.py +0 -266
  70. scpe_protocol-0.1.2/tests/test_cli_attest.py +0 -230
  71. scpe_protocol-0.1.2/tests/test_cli_commands.py +0 -209
  72. scpe_protocol-0.1.2/tests/test_cli_seal.py +0 -133
  73. scpe_protocol-0.1.2/tests/test_cli_submit.py +0 -102
  74. scpe_protocol-0.1.2/tests/test_contribute.py +0 -61
  75. scpe_protocol-0.1.2/tests/test_e2e_local.py +0 -243
  76. scpe_protocol-0.1.2/tests/test_e2e_mock.py +0 -57
  77. scpe_protocol-0.1.2/tests/test_envelope.py +0 -171
  78. scpe_protocol-0.1.2/tests/test_envelope_identity.py +0 -82
  79. scpe_protocol-0.1.2/tests/test_handshake.py +0 -254
  80. scpe_protocol-0.1.2/tests/test_identity.py +0 -115
  81. scpe_protocol-0.1.2/tests/test_inspect.py +0 -75
  82. scpe_protocol-0.1.2/tests/test_label.py +0 -55
  83. scpe_protocol-0.1.2/tests/test_mcp.py +0 -327
  84. scpe_protocol-0.1.2/tests/test_prompting.py +0 -179
  85. scpe_protocol-0.1.2/tests/test_repo_snapshot.py +0 -68
  86. scpe_protocol-0.1.2/tests/test_sandbox.py +0 -121
  87. scpe_protocol-0.1.2/tests/test_scrub.py +0 -82
  88. scpe_protocol-0.1.2/tests/test_signing.py +0 -25
  89. scpe_protocol-0.1.2/tests/test_verify_identity.py +0 -44
  90. scpe_protocol-0.1.2/tests/test_workspace.py +0 -102
  91. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/LICENSE +0 -0
  92. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/LICENSE-SPEC +0 -0
  93. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/reference/__init__.py +0 -0
  94. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/reference/disclosure.py +0 -0
  95. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/reference/standalone/__init__.py +0 -0
  96. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/scpe/optin.py +0 -0
  97. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/scpe_protocol.egg-info/dependency_links.txt +0 -0
  98. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/scpe_protocol.egg-info/top_level.txt +0 -0
  99. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/setup.cfg +0 -0
  100. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_disclosure.py +0 -0
  101. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_level1_lint.py +0 -0
  102. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_optin.py +0 -0
  103. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_producer_cli.py +0 -0
  104. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_producer_roundtrip.py +0 -0
  105. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_producer_submit.py +0 -0
  106. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_profiles_demo.py +0 -0
  107. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_spec_vectors.py +0 -0
  108. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_ssrf_fetch.py +0 -0
  109. {scpe_protocol-0.1.2 → scpe_protocol-0.2.1}/tests/test_stress_100prs.py +0 -0
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: scpe-protocol
3
+ Version: 0.2.1
4
+ Summary: SCPE: an open protocol for verifiable provenance of any digital artifact — who signed a contribution or file, what they declared about AI use, and that nothing was tampered with, verified offline with no server and no new accounts. Ships the single-file reference verifier, its producer, and a CLI (`scpe`) that seals a pull request from it.
5
+ Author: Augusto Bastos
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/augbastos/scpe
8
+ Project-URL: Repository, https://github.com/augbastos/scpe
9
+ Project-URL: Documentation, https://github.com/augbastos/scpe/blob/main/spec/SPEC.md
10
+ Keywords: github,contributions,open-source,supply-chain,provenance,attestation,code-signing,ssh,sshsig,verification,ci
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Version Control :: Git
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Environment :: Console
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ License-File: LICENSE-SPEC
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8; extra == "dev"
26
+ Requires-Dist: pyyaml>=6; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ <p align="center">
30
+ <img src="docs/assets/scpe-logo.svg" alt="SCPE" width="104" height="104">
31
+ </p>
32
+
33
+ <h1 align="center">SCPE</h1>
34
+
35
+ <p align="center">
36
+ <b>Know who signed a contribution — and which AI they said they used.</b><br>
37
+ A single envelope. Multiple specifications.<br>
38
+ <i>Merge code, not claims.</i>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <a href="https://github.com/augbastos/scpe/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/augbastos/scpe/actions/workflows/ci.yml/badge.svg"></a>
43
+ <img alt="3 impls" src="https://img.shields.io/badge/verifiers-python%20%2B%20go%20%2B%20rust-41c257?style=flat-square&labelColor=0b0b0c">
44
+ <img alt="spec" src="https://img.shields.io/badge/spec-scpe%2F0.1-41c257?style=flat-square&labelColor=0b0b0c">
45
+ <img alt="python" src="https://img.shields.io/badge/python-3.11%2B-41c257?style=flat-square&labelColor=0b0b0c">
46
+ <img alt="license" src="https://img.shields.io/badge/code-Apache--2.0-41c257?style=flat-square&labelColor=0b0b0c">
47
+ <img alt="status" src="https://img.shields.io/badge/v0.2.1-early-d29922?style=flat-square&labelColor=0b0b0c">
48
+ </p>
49
+
50
+ <p align="center">
51
+ <a href="https://augbastos.github.io/scpe/"><b>augbastos.github.io/scpe</b></a> — what it is, in one page
52
+ </p>
53
+
54
+ An open protocol for two questions a pull request cannot answer today: **who signed this
55
+ change**, and **what did they declare about AI use** — with proof that the diff is
56
+ byte-for-byte what they signed. No SCPE server, no new accounts, no new keys: the contributor
57
+ signs with the SSH key already on their git host, and the owner re-derives everything with
58
+ `ssh-keygen` and `git`.
59
+
60
+ The AI-use disclosure is **signed**, not typed into a form. That is the whole difference. A
61
+ signature does not make a claim true — it makes it *attributable* and *tamper-evident*: bound
62
+ to one identity and to this exact diff, so it cannot be edited afterwards or quietly attached
63
+ to different code. Whether someone told the truth about their tools stays a human judgement;
64
+ SCPE makes sure the claim, the author, and the change cannot be separated.
65
+
66
+ When a PR arrives from someone you don't know — a person, or increasingly an AI agent — trust
67
+ today rests on a username, the platform, and reading the diff by eye. SCPE replaces the first
68
+ two with something the owner can check locally, and leaves the third where it belongs. There
69
+ is no SCPE server, so there is nothing to trust and nothing to shut down.
70
+
71
+ **SCPE standardizes evidence, not content.** It never says an artifact is good, true, or safe —
72
+ it standardizes how verifiable evidence (who produced it, that it's untampered, and any signed
73
+ attestations) travels with a hashed artifact and is checked offline.
74
+
75
+ **One core, many specifications.** *SCPE Core* — the envelope, identity, and verification — is
76
+ shared by every domain. Each *SCPE Specification* adds only its domain's conventions on top; the
77
+ `profile` label is surfaced but never changes the verify decision.
78
+
79
+ | Specification | For | Seals | Example |
80
+ |---|---|---|---|
81
+ | **SCPE-C** | Code | a diff (`code-change`) | a pull request |
82
+ | **SCPE-I** | Images | the file's bytes (`artifact`) | `.png`, `.jpg` |
83
+ | **SCPE-V** | Video | the file's bytes | `.mp4`, `.mov` |
84
+ | **SCPE-A** | Audio | the file's bytes | `.wav`, `.mp3` |
85
+ | **SCPE-M** | Models | the file's bytes | `.safetensors`, `.gguf` |
86
+ | **SCPE-DATA** | Datasets | the file's bytes | `.csv`, `.parquet` |
87
+ | **SCPE-D** | Documents | the file's bytes | `.pdf` |
88
+ | **SCPE-AR** | Any artifact | the file's bytes | any file |
89
+
90
+ Identity is a `(provider, subject)` pair checked against keys from a fixed host table (`github`,
91
+ `gitlab`, `codeberg`) or from a keys file — one the verifier's owner supplied, or one bundled
92
+ inside the submission. The manifest never carries a hostname, so a contribution can't steer the
93
+ verifier at an attacker's host; it *can* enclose its own keys, which is why the verifier reports
94
+ the anchor that answered as `key_source`.
95
+
96
+ **Open, and meant to stay that way.** SCPE is — and will always be — open source: the
97
+ specification and every reference implementation are free to read, implement, and fork. The
98
+ goal is a single, open, *universal* standard for artifact provenance, and a standard only
99
+ becomes universal if anyone can implement it without asking permission. So being open isn't a
100
+ license choice here; it's the whole point.
101
+
102
+ > **Attribution tells you *what*. Provenance proves *who* — and that nothing was tampered with.**
103
+
104
+ ## What SCPE is — and isn't
105
+
106
+ | **SCPE is** | **SCPE is not** |
107
+ |---|---|
108
+ | a minimal, transportable, offline-verifiable **evidence format** for a contribution or artifact — *who* produced it, proof it's *untampered*, and any *signed attestations* — with one core and thin per-domain profiles. | a code reviewer, a malware scanner, an artifact registry, a CI/CD security system, a compliance framework, or a hosted service. It doesn't judge whether the artifact is *good* or *safe* — only that the evidence checks out. |
109
+
110
+ For how SCPE relates to code review, build provenance, and attribution records, see
111
+ [docs/comparison.md](docs/comparison.md).
112
+
113
+ ## What ships in this repo
114
+
115
+ A protocol, and the smallest set of things needed to prove it is one. Nothing here writes,
116
+ reviews, or generates code — SCPE never looks at what a change *does*, only at who signed it and
117
+ whether it still matches.
118
+
119
+ | | What it is |
120
+ |---|---|
121
+ | [`spec/`](spec/) | The normative protocol: [SPEC.md](spec/SPEC.md), the threat model, the manifest schema, and 18 test vectors that are the conformance contract. |
122
+ | [`reference/standalone/verify_envelope.py`](reference/standalone/verify_envelope.py) | **The verifier.** One stdlib-only file that imports nothing else in this repo. |
123
+ | [`reference/producer.py`](reference/producer.py) | The producer (`scpe-envelope pack` / `pack-artifact` / `attest` / `verify` / `submit`) — signs an envelope with a key the contributor already owns. |
124
+ | [`impl/go/`](impl/go/), [`impl/rust/`](impl/rust/) | Two independent ports of the verifier, held to the same verdict by a differential test. |
125
+ | [`action.yml`](action.yml) | The maintainer-side GitHub Action. It runs the verifier above, on the same format, out of its own checkout. |
126
+ | [`scpe/`](scpe/) | The `scpe-protocol` package: a stdlib-only CLI over that same verifier, plus the seal the Action renders and the opt-in badge. |
127
+
128
+ The package is a *distribution* of the protocol, not a second implementation of it: `scpe verify`
129
+ is a passthrough whose JSON and exit code are byte-identical to running the single file directly.
130
+ There is one envelope format, one verification algorithm, and one verdict — everything above is a
131
+ different way of reaching the same one.
132
+
133
+ ## The assurance ladder
134
+
135
+ Adopt at the level that fits your project, and upgrade later without changing the format.
136
+
137
+ | Level | What the repo requires | Contributor cost |
138
+ |---|---|---|
139
+ | **L1 — Disclosure** | An AI-use disclosure is present (an `Assisted-by:` trailer or a PR-template checkbox). | Zero — it's the policy you may already have written, now enforced. |
140
+ | **L2 — Signed envelope** | A valid signed SCPE envelope: verifiable identity + an untampered diff. | One command to sign. |
141
+ | **L3 — Countersignature** *(roadmap)* | A third party (a reviewer, or the agent platform) co-signs. | — |
142
+
143
+ Higher levels include the lower ones. Most projects that already require AI disclosure need
144
+ **L1** today; the signature is the *mechanism*, the policy is the *product* — the same shape
145
+ SLSA uses to sell levels. See [docs/LEVELS.md](docs/LEVELS.md).
146
+
147
+ ## How it works
148
+
149
+ 1. **Contributor** (human or agent) packs the change into a signed *envelope*: a manifest
150
+ (target repo, base commit, a SHA-256 of the exact diff, an AI-use disclosure, and an
151
+ optional attribution record) signed with the SSH key already on their GitHub profile
152
+ (`ssh-keygen -Y sign -n scpe/0.1`). No new account.
153
+ 2. It travels inside a **normal pull request** — the diff in the branch, the ~1–2 KB signed
154
+ attestation embedded in the PR body. Merging leaves the repo history clean.
155
+ 3. The **owner's side** re-derives everything itself, with no SCPE server involved: the diff's
156
+ SHA-256 is recomputed from the PR and compared, and a seal is posted (or, in require mode, an
157
+ unverifiable PR is rejected). There is **one verifier, not two** — the Action runs the same
158
+ single-file verifier described below, out of its own checkout at the tag you pinned, so there
159
+ is no second implementation to drift. What can differ between runs is only which key anchor
160
+ answers, and the result always names it:
161
+ - **Through the Action**, a contribution cannot substitute the keys that judge it. The
162
+ transport carries `manifest.json` and `manifest.sig` and nothing else (SPEC §9), so there is
163
+ no enclosed key set to find and the keys come from `github.com/<login>.keys`, live, at
164
+ `key_source: forge`. A maintainer running air-gapped can hand it a keys file of their own
165
+ instead — that reports `flag`, and the seal says so, because "the keys this repo supplied"
166
+ is a different claim from "the keys GitHub serves for that account".
167
+ - **Run directly**, the same file is the general tool, auditable in ten minutes: it resolves a
168
+ keys file you pass it first, then any `keys` file bundled in the input, and only if neither
169
+ answers, one HTTPS GET to the contributor's host. It reports which one answered as
170
+ `key_source`, so an offline conformance run is never mistaken for a forge check.
171
+
172
+ ## What `verified` proves — and what it doesn't
173
+
174
+ **What a `verified` result means depends on where the verifier got the keys** — which it reports
175
+ as `key_source`. At `forge` it means exactly: *a key the contributor's git host publishes for
176
+ this account signed exactly this change and this disclosure, and the diff you're looking at
177
+ matches, byte-for-byte after normalizing line endings, what they signed.* At `flag` it means the
178
+ same, with the verifier owner's own key set standing in for the host. At `bundled` — keys
179
+ carried inside the submission, chosen by whoever sent it — it means only that these exact bytes
180
+ were signed by a key that arrived with them, and nothing about the named account. **A consumer
181
+ that needs forge-backed identity MUST require `key_source == "forge"`**, or supply the key set
182
+ itself and require `"flag"`.
183
+
184
+ Under every anchor it does **not** prove the code is safe or good (SCPE is not review), that the
185
+ disclosure is honest (a signature proves *who claimed*, not that the claim is true), or anything
186
+ if the key set that answered is compromised or attacker-chosen — that key set is the trust root.
187
+ Read [spec/THREAT_MODEL.md](spec/THREAT_MODEL.md) before relying on it: §2.1 lays out the three
188
+ anchors and what a verdict is worth under each.
189
+
190
+ ## For maintainers — turn it on
191
+
192
+ Add a workflow that verifies every PR and posts a seal. Set `require` to gate merges.
193
+
194
+ ```yaml
195
+ # .github/workflows/scpe.yml — the step. Copy BOTH files from docs/workflows/ for the
196
+ # fork-safe version: scpe.yml (verify) and its companion scpe-seal.yml (post the seal).
197
+ - uses: augbastos/scpe@v0.2.1
198
+ with:
199
+ level: "1" # 1 = disclosure lint · 2 = signed envelope required
200
+ require: "true" # fail the check on anything not verifiable
201
+ ```
202
+
203
+ Pin the exact tag while the protocol is pre-1.0. Every tag here is an **immutable alias** — it
204
+ points at one commit and is never moved, so a fix arrives as a new tag you adopt by editing the
205
+ pin, not as a silent change under the one you already wrote. `v0.2.1` is that in practice: `v0.2`
206
+ shipped a seal banner that read `VERIFIED` on unattested PRs and a workflow template GitHub
207
+ refuses to register, and both were fixed by publishing a new tag rather than by moving the old
208
+ one. The `v0.2` line is also the first in which the Action verifies the `scpe/0.1` envelope
209
+ itself; `v0.1.x` verified a different, now-removed format, so upgrading from it is a behaviour
210
+ change and not a patch — [docs/MIGRATION.md](docs/MIGRATION.md) has the steps.
211
+
212
+ The Action uses a fork-safe two-job split, and the two jobs live in **two files**: the untrusted
213
+ job in `scpe.yml` (which runs contributor code) holds no secrets, and only the trusted follow-up
214
+ job in `scpe-seal.yml` posts the comment. Two files is a GitHub constraint, not a preference — a
215
+ workflow that names itself in its own `workflow_run` trigger fails to register at all. Neither
216
+ level installs anything in the
217
+ runner — both run stdlib-only Python straight out of the Action's own checkout, so the bytes that
218
+ decide a merge are the bytes of the tag you pinned, not whatever a package index serves that day.
219
+ Check out with `fetch-depth: 0`: level 2 recomputes the diff as `git diff <base>...<head>`, and the
220
+ default shallow checkout has no base commit to compare against.
221
+
222
+ The seal it posts carries more than the verdict — a risk band, a file/line count, an optional test
223
+ run. **Those are the Action's own reporting layer, not part of the protocol**: no status, no
224
+ `verified`, and nothing in `spec/` depends on them. The verdict is the verifier's; the rest is a
225
+ report.
226
+
227
+ ## Verify anything yourself
228
+
229
+ The reference verifier is one stdlib-only file — read it top to bottom and you know exactly what
230
+ a seal means:
231
+
232
+ ```bash
233
+ python reference/standalone/verify_envelope.py <envelope.zip> --keys <login.keys>
234
+ # → [OK] verified (attestations: none) [keys: flag]
235
+ # (or a precise reject status: tampered, signature-invalid, …)
236
+ ```
237
+
238
+ `[keys: …]` names the anchor that answered — `flag` here, because `--keys` supplied the key set.
239
+ Without that flag, a `keys` file sitting beside `manifest.json` in the input answers as
240
+ `bundled`, and only if neither is present does the verifier fetch from the contributor's host as
241
+ `forge`.
242
+
243
+ The 18 normative [test vectors](spec/test-vectors) are the conformance contract for **status**:
244
+ an implementation that produces their expected statuses conforms to the spec's status behaviour.
245
+ They don't pin every normative requirement — no vector carries an expected `key_source`, so
246
+ passing all eighteen does not by itself show that the `key_source` MUST is honoured; that one is
247
+ checked by inspection. Every vector ships its own `keys` file so the suite runs offline, and no
248
+ vector reaches the `forge` anchor.
249
+
250
+ **Cost**
251
+
252
+ | | Measured |
253
+ |---|---|
254
+ | PR-body attestation (manifest + sig, base64) | 1.1–1.5 KB |
255
+ | Standalone envelope (3-file / 27-line PR, zipped) | ~1.5 KB |
256
+ | Verify wall-time | ~210 ms cold CLI · ~39 ms in a warm process |
257
+
258
+ An `artifact` subject adds its payload size on top of ~800 B fixed overhead. Order-of-magnitude,
259
+ single machine — not a formal benchmark suite.
260
+
261
+ ## Where it sits
262
+
263
+ - **Not code review.** Copilot / CodeRabbit judge whether the code is good. SCPE proves *who*
264
+ and *integrity*.
265
+ - **Complements attribution, doesn't compete.** [Agent Trace](https://github.com/cursor/agent-trace)
266
+ and [git-ai](https://github.com/git-ai-project/git-ai) *record* who/what wrote which lines,
267
+ self-reported; SCPE carries that record inside the signed manifest, making it verifiable.
268
+ - **A different layer from build provenance.** Sigstore / SLSA / in-toto attest *artifacts and
269
+ builds*; SCPE attests a *contribution*, at the pull-request boundary.
270
+ - **Direct prior art:** `patatt` + `b4` ([kernel.org](https://github.com/mricon/patatt)) have run
271
+ this exact pattern — self-sign a patch with a key the platform publishes, verify independently,
272
+ no CA, no server — on the Linux kernel's mailing list for years. SCPE applies the same shape to
273
+ the GitHub pull-request boundary.
274
+
275
+ ## Status
276
+
277
+ **v0.2.1 — early.** This is a specification plus a reference implementation (a single-file
278
+ verifier, a producer, and a maintainer-side Action). The full test suite — including a 100-PR
279
+ stress proof and a local end-to-end — runs on every push; the CI badge above is its live
280
+ result. Two more independent verifiers, in Go and Rust, reach the same verdict as the Python
281
+ reference on every one of the 18 normative vectors, and a differential test runs mutated
282
+ manifests through all three and confirms they never disagree. That three-way agreement covers
283
+ the path those vectors exercise — a directory input checked offline against a supplied keys
284
+ file, the `flag` anchor; no vector exercises the network fetch. The Rust port goes no further: it has no network key fetch (`--keys` is required) and
285
+ does not parse the zip-envelope or in-PR-body attestation input shapes that Python and Go both
286
+ accept. There is **no external adoption yet**. It is not a hosted service and never will be.
287
+
288
+ **`v0.1.x` is legacy: its signed-envelope path verifies a format that is not in this spec.** The
289
+ Action at those tags checked a second envelope format that shipped inside the package and was
290
+ deleted in `0.2.0`. A workflow still pinned there does not fail — it keeps posting seals for a
291
+ format nothing here produces, and once `0.2.0` reaches PyPI it breaks instead, because that
292
+ Action resolved its verifier from the package index at run time rather than from its own
293
+ checkout. [CHANGELOG.md](CHANGELOG.md) has what moved; [docs/MIGRATION.md](docs/MIGRATION.md)
294
+ has what to do about it.
295
+
296
+ ## Docs
297
+
298
+ - [spec/SPEC.md](spec/SPEC.md) — the protocol (`scpe/0.1`)
299
+ - [spec/THREAT_MODEL.md](spec/THREAT_MODEL.md) — what it does and does not defend against
300
+ - [spec/FAQ.md](spec/FAQ.md) — why SSH, why the PR body, relation to Agent Trace / Sigstore / patatt
301
+ - [docs/LEVELS.md](docs/LEVELS.md) — the L1 / L2 / L3 assurance ladder
302
+ - [CHANGELOG.md](CHANGELOG.md) — what changed per release, and which version axis it moved
303
+ - [docs/MIGRATION.md](docs/MIGRATION.md) — upgrading a `v0.1.x` pin, and why it doesn't warn you
304
+
305
+ ## License
306
+
307
+ Code is [Apache-2.0](LICENSE); the specification (everything under `spec/`) is
308
+ [CC-BY-4.0](LICENSE-SPEC). © 2026 Augusto Bastos.
@@ -0,0 +1,280 @@
1
+ <p align="center">
2
+ <img src="docs/assets/scpe-logo.svg" alt="SCPE" width="104" height="104">
3
+ </p>
4
+
5
+ <h1 align="center">SCPE</h1>
6
+
7
+ <p align="center">
8
+ <b>Know who signed a contribution — and which AI they said they used.</b><br>
9
+ A single envelope. Multiple specifications.<br>
10
+ <i>Merge code, not claims.</i>
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://github.com/augbastos/scpe/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/augbastos/scpe/actions/workflows/ci.yml/badge.svg"></a>
15
+ <img alt="3 impls" src="https://img.shields.io/badge/verifiers-python%20%2B%20go%20%2B%20rust-41c257?style=flat-square&labelColor=0b0b0c">
16
+ <img alt="spec" src="https://img.shields.io/badge/spec-scpe%2F0.1-41c257?style=flat-square&labelColor=0b0b0c">
17
+ <img alt="python" src="https://img.shields.io/badge/python-3.11%2B-41c257?style=flat-square&labelColor=0b0b0c">
18
+ <img alt="license" src="https://img.shields.io/badge/code-Apache--2.0-41c257?style=flat-square&labelColor=0b0b0c">
19
+ <img alt="status" src="https://img.shields.io/badge/v0.2.1-early-d29922?style=flat-square&labelColor=0b0b0c">
20
+ </p>
21
+
22
+ <p align="center">
23
+ <a href="https://augbastos.github.io/scpe/"><b>augbastos.github.io/scpe</b></a> — what it is, in one page
24
+ </p>
25
+
26
+ An open protocol for two questions a pull request cannot answer today: **who signed this
27
+ change**, and **what did they declare about AI use** — with proof that the diff is
28
+ byte-for-byte what they signed. No SCPE server, no new accounts, no new keys: the contributor
29
+ signs with the SSH key already on their git host, and the owner re-derives everything with
30
+ `ssh-keygen` and `git`.
31
+
32
+ The AI-use disclosure is **signed**, not typed into a form. That is the whole difference. A
33
+ signature does not make a claim true — it makes it *attributable* and *tamper-evident*: bound
34
+ to one identity and to this exact diff, so it cannot be edited afterwards or quietly attached
35
+ to different code. Whether someone told the truth about their tools stays a human judgement;
36
+ SCPE makes sure the claim, the author, and the change cannot be separated.
37
+
38
+ When a PR arrives from someone you don't know — a person, or increasingly an AI agent — trust
39
+ today rests on a username, the platform, and reading the diff by eye. SCPE replaces the first
40
+ two with something the owner can check locally, and leaves the third where it belongs. There
41
+ is no SCPE server, so there is nothing to trust and nothing to shut down.
42
+
43
+ **SCPE standardizes evidence, not content.** It never says an artifact is good, true, or safe —
44
+ it standardizes how verifiable evidence (who produced it, that it's untampered, and any signed
45
+ attestations) travels with a hashed artifact and is checked offline.
46
+
47
+ **One core, many specifications.** *SCPE Core* — the envelope, identity, and verification — is
48
+ shared by every domain. Each *SCPE Specification* adds only its domain's conventions on top; the
49
+ `profile` label is surfaced but never changes the verify decision.
50
+
51
+ | Specification | For | Seals | Example |
52
+ |---|---|---|---|
53
+ | **SCPE-C** | Code | a diff (`code-change`) | a pull request |
54
+ | **SCPE-I** | Images | the file's bytes (`artifact`) | `.png`, `.jpg` |
55
+ | **SCPE-V** | Video | the file's bytes | `.mp4`, `.mov` |
56
+ | **SCPE-A** | Audio | the file's bytes | `.wav`, `.mp3` |
57
+ | **SCPE-M** | Models | the file's bytes | `.safetensors`, `.gguf` |
58
+ | **SCPE-DATA** | Datasets | the file's bytes | `.csv`, `.parquet` |
59
+ | **SCPE-D** | Documents | the file's bytes | `.pdf` |
60
+ | **SCPE-AR** | Any artifact | the file's bytes | any file |
61
+
62
+ Identity is a `(provider, subject)` pair checked against keys from a fixed host table (`github`,
63
+ `gitlab`, `codeberg`) or from a keys file — one the verifier's owner supplied, or one bundled
64
+ inside the submission. The manifest never carries a hostname, so a contribution can't steer the
65
+ verifier at an attacker's host; it *can* enclose its own keys, which is why the verifier reports
66
+ the anchor that answered as `key_source`.
67
+
68
+ **Open, and meant to stay that way.** SCPE is — and will always be — open source: the
69
+ specification and every reference implementation are free to read, implement, and fork. The
70
+ goal is a single, open, *universal* standard for artifact provenance, and a standard only
71
+ becomes universal if anyone can implement it without asking permission. So being open isn't a
72
+ license choice here; it's the whole point.
73
+
74
+ > **Attribution tells you *what*. Provenance proves *who* — and that nothing was tampered with.**
75
+
76
+ ## What SCPE is — and isn't
77
+
78
+ | **SCPE is** | **SCPE is not** |
79
+ |---|---|
80
+ | a minimal, transportable, offline-verifiable **evidence format** for a contribution or artifact — *who* produced it, proof it's *untampered*, and any *signed attestations* — with one core and thin per-domain profiles. | a code reviewer, a malware scanner, an artifact registry, a CI/CD security system, a compliance framework, or a hosted service. It doesn't judge whether the artifact is *good* or *safe* — only that the evidence checks out. |
81
+
82
+ For how SCPE relates to code review, build provenance, and attribution records, see
83
+ [docs/comparison.md](docs/comparison.md).
84
+
85
+ ## What ships in this repo
86
+
87
+ A protocol, and the smallest set of things needed to prove it is one. Nothing here writes,
88
+ reviews, or generates code — SCPE never looks at what a change *does*, only at who signed it and
89
+ whether it still matches.
90
+
91
+ | | What it is |
92
+ |---|---|
93
+ | [`spec/`](spec/) | The normative protocol: [SPEC.md](spec/SPEC.md), the threat model, the manifest schema, and 18 test vectors that are the conformance contract. |
94
+ | [`reference/standalone/verify_envelope.py`](reference/standalone/verify_envelope.py) | **The verifier.** One stdlib-only file that imports nothing else in this repo. |
95
+ | [`reference/producer.py`](reference/producer.py) | The producer (`scpe-envelope pack` / `pack-artifact` / `attest` / `verify` / `submit`) — signs an envelope with a key the contributor already owns. |
96
+ | [`impl/go/`](impl/go/), [`impl/rust/`](impl/rust/) | Two independent ports of the verifier, held to the same verdict by a differential test. |
97
+ | [`action.yml`](action.yml) | The maintainer-side GitHub Action. It runs the verifier above, on the same format, out of its own checkout. |
98
+ | [`scpe/`](scpe/) | The `scpe-protocol` package: a stdlib-only CLI over that same verifier, plus the seal the Action renders and the opt-in badge. |
99
+
100
+ The package is a *distribution* of the protocol, not a second implementation of it: `scpe verify`
101
+ is a passthrough whose JSON and exit code are byte-identical to running the single file directly.
102
+ There is one envelope format, one verification algorithm, and one verdict — everything above is a
103
+ different way of reaching the same one.
104
+
105
+ ## The assurance ladder
106
+
107
+ Adopt at the level that fits your project, and upgrade later without changing the format.
108
+
109
+ | Level | What the repo requires | Contributor cost |
110
+ |---|---|---|
111
+ | **L1 — Disclosure** | An AI-use disclosure is present (an `Assisted-by:` trailer or a PR-template checkbox). | Zero — it's the policy you may already have written, now enforced. |
112
+ | **L2 — Signed envelope** | A valid signed SCPE envelope: verifiable identity + an untampered diff. | One command to sign. |
113
+ | **L3 — Countersignature** *(roadmap)* | A third party (a reviewer, or the agent platform) co-signs. | — |
114
+
115
+ Higher levels include the lower ones. Most projects that already require AI disclosure need
116
+ **L1** today; the signature is the *mechanism*, the policy is the *product* — the same shape
117
+ SLSA uses to sell levels. See [docs/LEVELS.md](docs/LEVELS.md).
118
+
119
+ ## How it works
120
+
121
+ 1. **Contributor** (human or agent) packs the change into a signed *envelope*: a manifest
122
+ (target repo, base commit, a SHA-256 of the exact diff, an AI-use disclosure, and an
123
+ optional attribution record) signed with the SSH key already on their GitHub profile
124
+ (`ssh-keygen -Y sign -n scpe/0.1`). No new account.
125
+ 2. It travels inside a **normal pull request** — the diff in the branch, the ~1–2 KB signed
126
+ attestation embedded in the PR body. Merging leaves the repo history clean.
127
+ 3. The **owner's side** re-derives everything itself, with no SCPE server involved: the diff's
128
+ SHA-256 is recomputed from the PR and compared, and a seal is posted (or, in require mode, an
129
+ unverifiable PR is rejected). There is **one verifier, not two** — the Action runs the same
130
+ single-file verifier described below, out of its own checkout at the tag you pinned, so there
131
+ is no second implementation to drift. What can differ between runs is only which key anchor
132
+ answers, and the result always names it:
133
+ - **Through the Action**, a contribution cannot substitute the keys that judge it. The
134
+ transport carries `manifest.json` and `manifest.sig` and nothing else (SPEC §9), so there is
135
+ no enclosed key set to find and the keys come from `github.com/<login>.keys`, live, at
136
+ `key_source: forge`. A maintainer running air-gapped can hand it a keys file of their own
137
+ instead — that reports `flag`, and the seal says so, because "the keys this repo supplied"
138
+ is a different claim from "the keys GitHub serves for that account".
139
+ - **Run directly**, the same file is the general tool, auditable in ten minutes: it resolves a
140
+ keys file you pass it first, then any `keys` file bundled in the input, and only if neither
141
+ answers, one HTTPS GET to the contributor's host. It reports which one answered as
142
+ `key_source`, so an offline conformance run is never mistaken for a forge check.
143
+
144
+ ## What `verified` proves — and what it doesn't
145
+
146
+ **What a `verified` result means depends on where the verifier got the keys** — which it reports
147
+ as `key_source`. At `forge` it means exactly: *a key the contributor's git host publishes for
148
+ this account signed exactly this change and this disclosure, and the diff you're looking at
149
+ matches, byte-for-byte after normalizing line endings, what they signed.* At `flag` it means the
150
+ same, with the verifier owner's own key set standing in for the host. At `bundled` — keys
151
+ carried inside the submission, chosen by whoever sent it — it means only that these exact bytes
152
+ were signed by a key that arrived with them, and nothing about the named account. **A consumer
153
+ that needs forge-backed identity MUST require `key_source == "forge"`**, or supply the key set
154
+ itself and require `"flag"`.
155
+
156
+ Under every anchor it does **not** prove the code is safe or good (SCPE is not review), that the
157
+ disclosure is honest (a signature proves *who claimed*, not that the claim is true), or anything
158
+ if the key set that answered is compromised or attacker-chosen — that key set is the trust root.
159
+ Read [spec/THREAT_MODEL.md](spec/THREAT_MODEL.md) before relying on it: §2.1 lays out the three
160
+ anchors and what a verdict is worth under each.
161
+
162
+ ## For maintainers — turn it on
163
+
164
+ Add a workflow that verifies every PR and posts a seal. Set `require` to gate merges.
165
+
166
+ ```yaml
167
+ # .github/workflows/scpe.yml — the step. Copy BOTH files from docs/workflows/ for the
168
+ # fork-safe version: scpe.yml (verify) and its companion scpe-seal.yml (post the seal).
169
+ - uses: augbastos/scpe@v0.2.1
170
+ with:
171
+ level: "1" # 1 = disclosure lint · 2 = signed envelope required
172
+ require: "true" # fail the check on anything not verifiable
173
+ ```
174
+
175
+ Pin the exact tag while the protocol is pre-1.0. Every tag here is an **immutable alias** — it
176
+ points at one commit and is never moved, so a fix arrives as a new tag you adopt by editing the
177
+ pin, not as a silent change under the one you already wrote. `v0.2.1` is that in practice: `v0.2`
178
+ shipped a seal banner that read `VERIFIED` on unattested PRs and a workflow template GitHub
179
+ refuses to register, and both were fixed by publishing a new tag rather than by moving the old
180
+ one. The `v0.2` line is also the first in which the Action verifies the `scpe/0.1` envelope
181
+ itself; `v0.1.x` verified a different, now-removed format, so upgrading from it is a behaviour
182
+ change and not a patch — [docs/MIGRATION.md](docs/MIGRATION.md) has the steps.
183
+
184
+ The Action uses a fork-safe two-job split, and the two jobs live in **two files**: the untrusted
185
+ job in `scpe.yml` (which runs contributor code) holds no secrets, and only the trusted follow-up
186
+ job in `scpe-seal.yml` posts the comment. Two files is a GitHub constraint, not a preference — a
187
+ workflow that names itself in its own `workflow_run` trigger fails to register at all. Neither
188
+ level installs anything in the
189
+ runner — both run stdlib-only Python straight out of the Action's own checkout, so the bytes that
190
+ decide a merge are the bytes of the tag you pinned, not whatever a package index serves that day.
191
+ Check out with `fetch-depth: 0`: level 2 recomputes the diff as `git diff <base>...<head>`, and the
192
+ default shallow checkout has no base commit to compare against.
193
+
194
+ The seal it posts carries more than the verdict — a risk band, a file/line count, an optional test
195
+ run. **Those are the Action's own reporting layer, not part of the protocol**: no status, no
196
+ `verified`, and nothing in `spec/` depends on them. The verdict is the verifier's; the rest is a
197
+ report.
198
+
199
+ ## Verify anything yourself
200
+
201
+ The reference verifier is one stdlib-only file — read it top to bottom and you know exactly what
202
+ a seal means:
203
+
204
+ ```bash
205
+ python reference/standalone/verify_envelope.py <envelope.zip> --keys <login.keys>
206
+ # → [OK] verified (attestations: none) [keys: flag]
207
+ # (or a precise reject status: tampered, signature-invalid, …)
208
+ ```
209
+
210
+ `[keys: …]` names the anchor that answered — `flag` here, because `--keys` supplied the key set.
211
+ Without that flag, a `keys` file sitting beside `manifest.json` in the input answers as
212
+ `bundled`, and only if neither is present does the verifier fetch from the contributor's host as
213
+ `forge`.
214
+
215
+ The 18 normative [test vectors](spec/test-vectors) are the conformance contract for **status**:
216
+ an implementation that produces their expected statuses conforms to the spec's status behaviour.
217
+ They don't pin every normative requirement — no vector carries an expected `key_source`, so
218
+ passing all eighteen does not by itself show that the `key_source` MUST is honoured; that one is
219
+ checked by inspection. Every vector ships its own `keys` file so the suite runs offline, and no
220
+ vector reaches the `forge` anchor.
221
+
222
+ **Cost**
223
+
224
+ | | Measured |
225
+ |---|---|
226
+ | PR-body attestation (manifest + sig, base64) | 1.1–1.5 KB |
227
+ | Standalone envelope (3-file / 27-line PR, zipped) | ~1.5 KB |
228
+ | Verify wall-time | ~210 ms cold CLI · ~39 ms in a warm process |
229
+
230
+ An `artifact` subject adds its payload size on top of ~800 B fixed overhead. Order-of-magnitude,
231
+ single machine — not a formal benchmark suite.
232
+
233
+ ## Where it sits
234
+
235
+ - **Not code review.** Copilot / CodeRabbit judge whether the code is good. SCPE proves *who*
236
+ and *integrity*.
237
+ - **Complements attribution, doesn't compete.** [Agent Trace](https://github.com/cursor/agent-trace)
238
+ and [git-ai](https://github.com/git-ai-project/git-ai) *record* who/what wrote which lines,
239
+ self-reported; SCPE carries that record inside the signed manifest, making it verifiable.
240
+ - **A different layer from build provenance.** Sigstore / SLSA / in-toto attest *artifacts and
241
+ builds*; SCPE attests a *contribution*, at the pull-request boundary.
242
+ - **Direct prior art:** `patatt` + `b4` ([kernel.org](https://github.com/mricon/patatt)) have run
243
+ this exact pattern — self-sign a patch with a key the platform publishes, verify independently,
244
+ no CA, no server — on the Linux kernel's mailing list for years. SCPE applies the same shape to
245
+ the GitHub pull-request boundary.
246
+
247
+ ## Status
248
+
249
+ **v0.2.1 — early.** This is a specification plus a reference implementation (a single-file
250
+ verifier, a producer, and a maintainer-side Action). The full test suite — including a 100-PR
251
+ stress proof and a local end-to-end — runs on every push; the CI badge above is its live
252
+ result. Two more independent verifiers, in Go and Rust, reach the same verdict as the Python
253
+ reference on every one of the 18 normative vectors, and a differential test runs mutated
254
+ manifests through all three and confirms they never disagree. That three-way agreement covers
255
+ the path those vectors exercise — a directory input checked offline against a supplied keys
256
+ file, the `flag` anchor; no vector exercises the network fetch. The Rust port goes no further: it has no network key fetch (`--keys` is required) and
257
+ does not parse the zip-envelope or in-PR-body attestation input shapes that Python and Go both
258
+ accept. There is **no external adoption yet**. It is not a hosted service and never will be.
259
+
260
+ **`v0.1.x` is legacy: its signed-envelope path verifies a format that is not in this spec.** The
261
+ Action at those tags checked a second envelope format that shipped inside the package and was
262
+ deleted in `0.2.0`. A workflow still pinned there does not fail — it keeps posting seals for a
263
+ format nothing here produces, and once `0.2.0` reaches PyPI it breaks instead, because that
264
+ Action resolved its verifier from the package index at run time rather than from its own
265
+ checkout. [CHANGELOG.md](CHANGELOG.md) has what moved; [docs/MIGRATION.md](docs/MIGRATION.md)
266
+ has what to do about it.
267
+
268
+ ## Docs
269
+
270
+ - [spec/SPEC.md](spec/SPEC.md) — the protocol (`scpe/0.1`)
271
+ - [spec/THREAT_MODEL.md](spec/THREAT_MODEL.md) — what it does and does not defend against
272
+ - [spec/FAQ.md](spec/FAQ.md) — why SSH, why the PR body, relation to Agent Trace / Sigstore / patatt
273
+ - [docs/LEVELS.md](docs/LEVELS.md) — the L1 / L2 / L3 assurance ladder
274
+ - [CHANGELOG.md](CHANGELOG.md) — what changed per release, and which version axis it moved
275
+ - [docs/MIGRATION.md](docs/MIGRATION.md) — upgrading a `v0.1.x` pin, and why it doesn't warn you
276
+
277
+ ## License
278
+
279
+ Code is [Apache-2.0](LICENSE); the specification (everything under `spec/`) is
280
+ [CC-BY-4.0](LICENSE-SPEC). © 2026 Augusto Bastos.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ # >=77 is the floor that reads `license` below as a PEP 639 SPDX expression rather than as
3
+ # legacy free text. Older setuptools still builds this file, it just records the license the
4
+ # old way — pinning the floor keeps the published metadata the same wherever it is built.
5
+ requires = ["setuptools>=77"]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ name = "scpe-protocol"
10
+ version = "0.2.1"
11
+ description = "SCPE: an open protocol for verifiable provenance of any digital artifact — who signed a contribution or file, what they declared about AI use, and that nothing was tampered with, verified offline with no server and no new accounts. Ships the single-file reference verifier, its producer, and a CLI (`scpe`) that seals a pull request from it."
12
+ readme = "README.md"
13
+ requires-python = ">=3.11"
14
+ # A bare SPDX expression (PEP 639), not `{ text = ... }` and not a `License ::` classifier:
15
+ # setuptools deprecates both older spellings, and a build that prints deprecation warnings
16
+ # is a build whose real warnings stop being read.
17
+ license = "Apache-2.0"
18
+ authors = [{ name = "Augusto Bastos" }]
19
+ keywords = ["github", "contributions", "open-source", "supply-chain", "provenance", "attestation", "code-signing", "ssh", "sshsig", "verification", "ci"]
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Software Development :: Version Control :: Git",
26
+ "Topic :: Software Development :: Quality Assurance",
27
+ "Topic :: Security",
28
+ "Topic :: Security :: Cryptography",
29
+ "Environment :: Console",
30
+ ]
31
+ # Deliberately empty, and enforced by a test. Stdlib only is not minimalism for its own
32
+ # sake: it is what lets a CI job run this package straight from a pinned checkout
33
+ # (`PYTHONPATH=<action_path> python3 -m scpe.cli seal ...`) with no install step, so the
34
+ # bytes that verify a pull request are the bytes of the tag the caller pinned rather than
35
+ # whatever a package index happens to serve at that moment. Signatures are checked by
36
+ # `ssh-keygen -Y verify` (OpenSSH >= 8.2), not by a Python crypto library.
37
+ dependencies = []
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/augbastos/scpe"
41
+ Repository = "https://github.com/augbastos/scpe"
42
+ Documentation = "https://github.com/augbastos/scpe/blob/main/spec/SPEC.md"
43
+
44
+ [project.optional-dependencies]
45
+ dev = ["pytest>=8", "pyyaml>=6"]
46
+
47
+ [project.scripts]
48
+ scpe = "scpe.cli:main"
49
+ scpe-envelope = "reference.producer:main"
50
+
51
+ # `reference*` is packaged alongside `scpe*` on purpose: reference/standalone/verify_envelope.py
52
+ # is where every verdict is produced, and scpe/verify.py imports it rather than carrying a
53
+ # second copy. Shipping one and not the other would fork the conformance surface that the
54
+ # eighteen normative test vectors and the Go and Rust ports are measured against.
55
+ [tool.setuptools.packages.find]
56
+ include = ["scpe*", "reference*"]