prooft
Proof marks for client builds. A gun barrel gets a proof mark once it survives a test firing. prooft does the same for commits: run your lint and tests locally, stamp the passing commit, and CI and the merge queue skip the jobs that stamp already proves.
$ prooft run lint -- soldr lint # runs it; on success stamps HEAD
attested lint on 3f2a9c1e0b7d
$ git push # CI reads the stamp and skips ci.yml:lint
No server, no signing keys, no artifacts. The proof lives in the commit
message as one Ci-Attestation: trailer per gate, the format defined by
ci.yml GATE-010.
prooft's trailers are byte-compatible with ci-lint's.
The rules
Six rules. Everything else is detail.
- A gate is a check on a platform:
<ecosystem>/<platform>/<check>, e.g.rust/x86_64-unknown-linux-gnu/clippy. A lane is the local command that proves one or more gates. - A proof is bound to what the lane read. By default that is the exact
tree and parents: change one byte, or rebase, and it no longer counts
(
wrong-tree,wrong-parents). A lane that declares itsinputsis bound to those files instead, so it survives a rebase that left them alone. Edit the JSON and the stamp breaks (bad-stamp). - Omission means "not run". A gate without a trailer runs in CI. There is no way to say "skipped".
- A job skips only when every gate it covers is proven and fresh (default 24 h per lane), and the head is trusted.
- The base decides. CI reads the policy from the PR base. A PR that
touches the policy, workflows or
ci.toml, a fork, or a title tag ([ci-full]) runs everything remotely. - Releases trust nothing. A release recomputes every gate.
Rule 2 makes forgery visible rather than impossible: a trailer is an
honesty contract, not a signature. A forged pass can skip a PR job, but
the push to main runs the jobs on the merged commit, so a lie turns into
a visible red main, never a silent green.
The merge queue
A queue commit is brand new, so its own trailers can never match. prooft credits the PR's proofs when the queue commit is a single commit on the base and either
- its tree is identical to the attested PR head (the queue would test the proven bytes), or
- the lane's declared inputs are unchanged at the queue commit:
mainmoved, but not under anything the lane reads.
Otherwise the queue runs the job. prooft's own main merges through a
single-PR merge queue whose verify job runs prooft queue from the base.
The policy: ci-attestations.yml
version: 1
lanes:
lint:
max-age-hours: 24 # optional; 24 is the default
inputs: ["src/**", pyproject.toml] # optional: key the proof to these files
gates:
python/all/fmt: {lane: lint}
python/all/types: {lane: lint}
python/linux-x64/test: {lane: tests}
jobs: # <workflow file>:<job id> -> gates
ci.yml:lint: [python/all/fmt, python/all/types]
ci.yml:test-linux-x64: [python/linux-x64/test]
It is a restricted YAML subset (no anchors, aliases, tags or block scalars)
parsed by the standard library alone, so every host reads it the same way.
fidelity: emulation (Wine, Darling) may prove unit, never test.
Command line
prooft run LANE -- CMD... run CMD on a clean HEAD; on success stamp LANE's gates
prooft verify [REV] every gate's state on REV
prooft plan --base REV which jobs a PR run may skip (--github-output, --json)
prooft queue --base REV --pr-head REV [--queue-head REV]
prooft lanes lanes and the gates each proves
prooft soldr [-- CMD] soldr lint (porcelain) -> per-target Rust gates
prooft emit [REV] [-o FILE] REV's valid attestations as a JSON bundle
prooft apply FILE|- stamp HEAD with the bundle entries valid for HEAD
prooft hook install [--force] pre-push hook: warn (shadow) or refuse (enforce) an unattested head
run refuses a dirty tree, and refuses if the command changes tracked
files or moves HEAD. Re-running a lane replaces only that lane's trailers.
Shadow or enforce
Top-level mode: in the policy decides what the pre-push hook does with an
unattested head. shadow (the default) warns and pushes: CI simply runs
the jobs. enforce refuses the push. The strictest mode between the pushed
commit and the remote tip (or the default branch, for a new branch) wins,
so a branch cannot flip itself back to shadow to escape.
Rust: soldr
$ prooft soldr # SOLDR_PORCELAIN=1 soldr lint rust --check
attested rust/all/fmt
attested rust/x86_64-unknown-linux-gnu/clippy
attested rust/x86_64-pc-windows-msvc/clippy
attested rust/x86_64-unknown-linux-gnu/dylint
soldr's one-line porcelain report lists its steps; each passed step maps to
a gate (fmt -> rust/all/fmt, clippy --target T -> rust/T/clippy,
bare clippy/dylint -> the host triple). Only gates the policy declares
are stamped. A failing step stamps nothing for itself, so CI runs it.
Python API
import prooft
repo = prooft.open(".")
repo.lane("lint").run(["ruff", "check", "."]) # run a lane, stamp its gates
repo.soldr() # soldr lint -> per-target Rust gates
repo.verify().ok("python/all/fmt") # True
plan = repo.plan(base="origin/main", title="Add x")
plan.skipped # ("ci.yml:lint",)
plan.outputs() # {"skip_lint": "true", ...}
repo.queue(pr_head="pr", queue_head="HEAD", base="origin/main").skipped
Lower level: prooft.Attestation, prooft.Trailers, prooft.Policy,
prooft.verify(commit, policy), prooft.decide(policy, verdict).
Proofs from elsewhere
A build farm that cannot amend your commit can still prove it:
farm$ prooft run tests -- ./ci.py test && prooft emit -o proof.json
you$ prooft apply proof.json # stamps only entries valid for YOUR HEAD
apply judges the bundle exactly as if its trailers were already on HEAD:
same tree and parents, or (for a lane with inputs) unchanged inputs.
Ledgers: hosted proofs by input key
A trailer proves a lane on one commit. A ledger proves it for an input key, wherever that key turns up again: a merge-queue entry, a rebased head, or a PR whose author never ran the lane.
prooft queue --pr-head P --queue-head Q --base B --ledger github \
--ledger-name 'lane-proof-v1-{lane}-{key}' # GitHub artifacts, by name
prooft plan --base B --ledger .prooft/ledger # a directory of JSON files
The rules are the same as for trailers:
- only lanes with
inputshave a key; - the entry must be fresh (within the lane's
max-age-hours); - an untrusted head (fork, title tag, trust-surface edit) skips nothing.
A GitHub artifact also counts only if it is unexpired and was uploaded by
the CI workflow (.github/workflows/ci.yml by default) on a pull_request
or merge_group run from a branch of this repository. Any other workflow
(a claim bot, a dispatch) can upload an artifact with the same name, so it
proves nothing.
A repository that keys its lanes its own way passes the keys in:
--lane-key rust-test=$(uv run ci/lane_proof.py key --lane rust-test --base B).
prooft then judges only trust and freshness.
Simulating GitHub
prooft.sim.github.FakeGitHub is a bare origin plus pull requests, CI
runs and a merge queue, all offline. tests/test_simulation.py drives it
through every rule above: unattested, partial, stale, tampered, rebased,
fork, title tag, policy edit, forged pass, queue credit and queue rerun.
Development
./install # uv sync + dev tools
./test # pytest
./lint # ruff format --check (black style), ruff check (isort, ...), basedpyright
./lint --fix
Status
Alpha (0.2). prooft's own PRs skip fast and test-linux-x64 when their trailers prove them.
The merge queue credits every entry of a multi-PR group whose lane inputs are unchanged, and it reads hosted proofs
(--ledger) under the same trust rules. The real-world case is obs-rust/obs-studio (docs/CASE-OBS-RUST.md,
obs-rust/obs-studio#4460). Next: a ci-lint adapter. See
docs/ROADMAP.md.
License: BSD-3-Clause.
Metadata
Release files for prooft 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| prooft-0.2.0.tar.gz | 31.4 kB | Without provenance | Details for prooft-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| prooft-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for prooft-0.2.0-py3-none-any.whl |
Total release size: 68.2 kB
Release files / prooft-0.2.0.tar.gz
| Download URL | prooft-0.2.0.tar.gz |
|---|---|
| Size | 31.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9f79c650b928e6b70caebc97f35149378a29a9c73d4210b71e86cd19d65b0ae8
|
|
BLAKE2b-256 checksum How to use checksums |
791d9ed0df1989cdd55aaa68b7a963fd23bbce223d17e7c5c00fc7fa5373cebb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"26.05","id":"yarara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / prooft-0.2.0-py3-none-any.whl
| Download URL | prooft-0.2.0-py3-none-any.whl |
|---|---|
| Size | 36.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4571ead6be9e2c770c051245cd021cd9738091c65bc158e63bace0c791e1d733
|
|
BLAKE2b-256 checksum How to use checksums |
96a20a0fdadb623eff993e09de28f4e066d1b25e4a174aa7f157d3c101fa4bdb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"26.05","id":"yarara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for prooft 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.