Skip to main content

verifiable-gates

CI PyPI Python Code: Apache-2.0 Rules: CC BY 4.0 DOI 10.5281/zenodo.22103110

Thai: README.th.md.

A registry of CI gates for projects built with or without AI coding agents. Every gate carries evidence of having gone red on a real defect, and the same rules ship as an agent skill, so the agent works under the constraints CI will later enforce. Of the 92 rules, nine have a checker in this package; the other 83 are written for an agent to read.

What this is not. Not a linter and not a SAST replacement. It decides configuration and process posture — pinning, CSP, ADR bookkeeping, supply chain — not code correctness. A green run means the nine checks it can decide found nothing; it says nothing about the 83 it cannot.

Quickstart

pip install verifiable-gates
cd your-project
python -m verifiable_gates.install .     # writes tools/, scaffold.json, gates.yaml, a starting workflow
python3 tools/gates_doctor.py            # runs the checkers the project now holds

What the two commands printed, in an empty git repository (re-run 2026-09-06, v0.5.0+4; the install line's absolute path is shortened to <your-project>):

$ python -m verifiable_gates.install .
installed into <your-project> — 9 gates (9 scan) · check with: python3 tools/gates_doctor.py
for the instruction file your agents read (AGENTS.md, CLAUDE.md), add one line: `run python3 tools/gates_doctor.py --rules before editing`
this bundle also carries the working: 13 practices, each with the lesson behind it and the pull requests it held on — off here; read `python3 tools/gates_doctor.py --working`, turn on with `install <dest> --working`
$ python3 tools/gates_doctor.py
[   NA] actions-sha-pinned — only the bundle's own starting workflow, untouched — nothing of yours to read
[   NA] adr-index-complete — no docs/adr — this rule reads the .md records and the README.md index under docs/adr (scaffold.json adr_path)
[   NA] ci-tools-hash-pinned — only the bundle's own starting workflow, untouched — nothing of yours to read
[   NA] csp-no-inline — no app/templates — this rule reads .html, .htm, .jinja, .jinja2 and .j2 templates under app/templates (scaffold.json templates_path)
[   NA] delete-means-soft-delete — no app — this rule reads Python modules under app (scaffold.json src_path) — session.delete calls outside the purge_paths
[ pass] gates-registry-total
[   NA] image-digest-pinned — no Dockerfile — this rule reads the FROM lines of the root Dockerfile (scaffold.json dockerfiles), .github/dependabot.yml for a docker ecosystem, and — when the project named no Dockerfile — every Dockerfile* in the tree that git does not ignore
[   NA] logic-knows-no-http — no app/services — this rule reads Python modules under app/services (scaffold.json services_path) — their imports, for request-side symbols
[   NA] no-debug-entrypoint — no entrypoint — this rule reads the Python entrypoints run.py, wsgi.py, app.py and main.py (scaffold.json entrypoints), as an AST

waiting on this project's own tests: 0 gates
[exit 0]

On a fresh install the only pass is the registry index, which has to be true about itself. Everything else is NA until the project has something to check. Exit 0 here means nothing was measured, not that the project passed.

Add one workflow with a floating tag and an unpinned install, and the same command answers with two findings and exits 1 (the full transcript, with the third finding the new job itself causes, is in docs/output-semantics.md):

[found] actions-sha-pinned — Every action is pinned to a commit SHA with the version in a comment
  born from: Tags move, commits do not — upload-artifact once sat on @v4 at a single call site for ten days with CI green throughout.
actions-sha-pinned: .github/workflows/lint.yml: actions/checkout@v4
…
[found] ci-tools-hash-pinned — Tools CI installs for itself are pinned by hash, on both the Python and the Node side
  born from: An unpinned install command takes whatever is newest at the second the job runs, and it runs with our workflow's privileges · pinning one package at a time pins only that package while the rest of the tree still floats.
ci-tools-hash-pinned: .github/workflows/lint.yml: pip install ruff
…
** scans found problems in 3 gates: actions-sha-pinned, ci-tools-hash-pinned, gates-registry-total
[exit 1]

Reading the output

Verdict Means Does not mean
pass The checker looked and the rule holds. Any rule without a checker holds.
[found] The checker looked and the rule is broken; the doctor exits 1. The build is unsafe in ways this checker does not read.
NA Nothing of the kind this checker reads is here; it says what it looked for. The rule passed.
[error] The checker could not answer: crash, timeout, undecodable or oversize file, unreadable directory, malformed scaffold.json. Its stderr is passed through; the doctor exits 1. "Looked and found nothing." It is red without a verdict.
[waived] The checker found it and a waiver in scaffold.json covers it — gate, reason, until, decided_by, optional scope; the run exits 0 for it and prints what was excused. The finding is gone. It is counted on every run, an expired waiver is a finding, and a waiver missing a field excuses nothing.

Two consequences to know before trusting a green:

  • A project where every rule is NA exits 0. That is an unmeasured project, not a passed one (DECISIONS.md doctor-all-na-exits-zero).
  • A path that scaffold.json names and the project does not have is a finding, not NA: a broken configuration is a defect, not an absence.
  • Saying "not yet" goes through the record, not around it. There is no # noqa here and no warn-only mode; a finding a project will not fix this week gets a waiver in scaffold.json (waivers: [{gate, reason, until, decided_by, scope?}]), which the doctor prints on every run, turns red when it expires, and refuses when a field is missing. The SARIF keeps the result, with the reason as its suppression.

The full taxonomy — every NA and [error] case, the --installed record, --rules off an edited bundle, the SARIF mapping — is in docs/output-semantics.md.

Three ways to run it

Entry point Runs Config Default
GitHub Action on push / pull request uses: sayam/verifiable-gates@<commit-sha> # vX.Y.Z · optional with: sarif: gates.sarif (action.yml) —
pre-commit before each commit repo: https://github.com/sayam/verifiable-gates · hook gates-doctor, or one hook per rule id (.pre-commit-hooks.yaml) —
Claude Code edit hook after every Edit / Write plugin installed (below) · "env": {"VERIFIABLE_GATES_AT_EDIT": "1"} in .claude/settings.json (hooks/hooks.json) off; reports, never refuses

What it costs. The doctor is about 0.4 s of interpreter starts — nine scans, nine processes — plus roughly 1.6 ms per Python file under src_path: 0.5 s on this repository (98 modules), 2.5 s at 907, and 14.5 s at 7 256 (measured 2026-09-05, one laptop; the numbers and the method are in .local/ of that day's audit round). Most of it is one scan, delete-means-soft-delete, which reads every module. Every pre-commit hook here is always_run, so each commit re-reads the whole tree however small the change — on a large tree prefer the action, or the one hook whose rule you care about. There is no timing gate: a threshold on wall clock is a ratchet against the runner's hardware, not against the code (DECISIONS.md the-cost-is-stated-and-not-gated).

All three run tools/ as the project has it, and none carries a copy of the checkers. Moving the SHA, the rev or the plugin version changes nothing about what the project is held to (DECISIONS.md ci-runs-the-bundle-the-project-installed).

The action is listed on the GitHub Marketplace as verifiable-gates — pin the SHA, not the tag the listing offers. The edit hook hands a finding back to the agent while it still holds the file, and refuses nothing (DECISIONS.md the-edit-hook-reports-and-does-not-refuse).

What the nine checkers decide

Each of the nine stdlib-only checkers is one Python file under tools/checks/, run by the doctor or on its own; nothing the bundle installs reaches off this machine (its one socket asks whether a service is listening on 127.0.0.1). python3 tools/gates_doctor.py --rules prints this list off the installed bundle, with each rule's incident.

Rule id What it catches What it reads
gates-registry-total A CI job with no row in the gate index, a row nothing can fail, a test file no gate claims gates.yaml, every workflow under .github/workflows, the test files under tests_path
actions-sha-pinned A uses: on a floating tag, or on a SHA with no version comment beside it uses: steps of workflows and composite actions under .github
ci-tools-hash-pinned A tool CI installs for itself without hashes or a lock pip, pipx, uv, poetry, pdm, pipenv, npm, npx, yarn, pnpm and python -m build lines in workflows, the scripts they run, the root Dockerfile
image-digest-pinned A base image not pinned to a manifest-index digest, or pinned with nobody to move it FROM lines of the root Dockerfile, .github/dependabot.yml
csp-no-inline Inline script, style or handler in a template .html, .htm, .jinja, .jinja2, .j2 under the templates path
no-debug-entrypoint An entrypoint that can open a debug console, including one whose debug= falls back to a truthy default run.py, wsgi.py, app.py, main.py, as an AST
logic-knows-no-http A service module importing from the request side Python modules under the services path, their imports
delete-means-soft-delete A session.delete outside the one purge path (layer business) Python modules under the source path
adr-index-complete An ADR missing from the index, a repeated or skipped number, a supersession recorded one way .md records and the README.md index under the ADR path

The paths are the defaults scaffold.json carries; the project moves them there.

The 92 rules

rules.yaml — 92 rules, each carrying the incident that produced it (born_from), because a rule with no origin is a rule nobody knows when to remove. They are rendered into an agent skill in the layout of the Agent Skills specification: skills/verifiable-gates/SKILL.md is the front page, and the full entries sit beside it in references/.

Two pipes this repository does not own install the skill without cloning:

Pipe Command What lands
Skills CLI npx skills add sayam/verifiable-gates lands the four files under skills/verifiable-gates/ and nothing else; the pipe sends the repository and skill identifiers as telemetry, off with DISABLE_TELEMETRY=1
Claude Code claude plugin marketplace add sayam/verifiable-gates, then claude plugin install verifiable-gates@verifiable-gates the whole repository, as a plugin

The nine rules with a checker (script: in rules.yaml) are the ones the doctor and the installer decide, and nothing else. The other 83 are the rule sheets an agent is held to by reading, and the Enforced in the reference line on each says how one project turned it into a test. What each pipe sends and fetches, and why there is no registry of this project's own, is in docs/history.md and DECISIONS.md distribution-is-two-pipes-nobody-here-owns.

A rule that turns out to be wrong is withdrawn in place, never deleted: retracted: keeps it in the catalogue and on its sheet with the date and the reason, marked and out of every count, so a reader who followed it can find out that they should stop. Nothing is withdrawn today.

A rule may also say where it sits in a vocabulary somebody else already speaks: maps_to: names items of OpenSSF Scorecard, SLSA v1.0 and NIST SSDF — 35 of them name where they sit, and the rest deliberately name nothing, because no item of those three covers them. A mapping says the rule would satisfy or contribute to that item, never that the two are equal; the item names are a closed set read off the publications, so a misspelling is refused rather than published as a map to nothing.

rules.yaml and the sheets come with the checkout, not with the wheel. The package is the machinery that reads the catalogue:

import verifiable_gates

catalogue = verifiable_gates.rules.load("rules.yaml")
# `package_dir` is where a rule's `script:` is looked for; without it, only the
# shape of the path is checked — a checker that is not there would go unnoticed.
for problem in verifiable_gates.rules.problems(catalogue, package_dir="src/verifiable_gates"):
    print(problem)

Design constraints

The two schemas — rules.py for this catalogue, registry.py for a project's gates.yaml — encode five rules that came from real traps, not from theory:

  • a gate whose layer is internal cannot be portable — a rule tied to one project's architecture, exported as universal, is an overclaim; that hold is in the gate schema, and the rule schema refuses internal outright, since a rule in this catalogue is published whole (portable on a rule is refused as a gate's field);
  • a key neither schema knows is refused, not skipped — a misspelt born_frm is a rule with no origin that looks like one with;
  • anything exported must name the trap that created it (born_from), because a rule with no origin is a rule nobody knows when to remove;
  • proved_by entries must say what they caught and when — a gate nobody has seen go red is indistinguishable from a gate that checks nothing;
  • the vocabularies for kind, severity, layer, and pillar are closed — and a severity is what something reads: blocking is every gate on a run (a scan is red whatever the row says), watched is a watched_by promise the red-streak census measures, and there is no third word, because a label no run reads is a softness nobody delivers.

A rule and its enforcement live in separate files, because they have separate lifetimes: rules.yaml is what this project publishes; gates.yaml is what this project is itself held to. What was deliberately not done, each with the condition that would expire it, is DECISIONS.md.

Provenance and status

Archived at doi:10.5281/zenodo.22103110, which resolves to the latest version; each release also gets a DOI of its own. The tag evidence-freeze-1 is the state the measurements were taken on and v0.1.0 (2026-08-28) the state the package first shipped in — different commits on purpose. The extraction from the reference implementation, sayam/flask-todolist, is complete; the stage table, the census and what stayed behind are in docs/history.md. Consume a pinned submodule or a versioned dependency, never main.

None of this has to be taken on trust. docs/auditing.md is the hour: the eleven rules this repository states about itself, the command that decides each one, and — said plainly — what no command here can answer.

Who maintains this

One maintainer, and no promise about how long. What survives if that stops — your CI, your alerts' ids, and a fork that is complete — is in GOVERNANCE.md, together with how a rule is withdrawn and how you would tell whether anybody is still here.

Licence

  • Code: Apache-2.0. Contributors sign CLA.md — one line in the pull request; you keep your copyright.
  • Rules and documentation: CC BY 4.0.

The application this was extracted from stays AGPL-3.0-or-later. The two differ on purpose: a CI tool is not a network service, and a rule meant to be adopted inside an organisation's internal handbook must not require share-alike.


README.th.md · docs/ · CONTRIBUTING.md · SECURITY.md · CHANGELOG.md · the experiment

Metadata

Release files for verifiable-gates 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for verifiable-gates 0.7.0
File Size Uploaded
verifiable_gates-0.7.0.tar.gz 544.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for verifiable-gates 0.7.0
File Interpreter ABI Platform
verifiable_gates-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 848.4 kB

Release files / verifiable_gates-0.7.0.tar.gz

Download URL verifiable_gates-0.7.0.tar.gz
Size 544.9 kB
Tags Source
SHA-256 checksum
How to use checksums
b3f121b867ab67c9542fb2a1a1fb055a652576d9abc01ee291c224067e3a9b4e
BLAKE2b-256 checksum
How to use checksums
0e88fdd90082999b73d63bb04b7f1bce1d506e6d6cb1eb86e70eacbb9d0515e2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release files / verifiable_gates-0.7.0-py3-none-any.whl

Download URL verifiable_gates-0.7.0-py3-none-any.whl
Size 303.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cba215628e8d56f43f594c84db3e47340eac9e40cbbb7e709300277d98563bf4
BLAKE2b-256 checksum
How to use checksums
df103292c4d1f566443b08ac121d71b71d87f2d39d0f724444a291e6545382ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.0

2 release files

0.8.0

2 release files

This release

0.7.0 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page