Formal verification pipeline for AI-generated Python code — 5-stage proof from syntax to Dafny, with CVE scanning, property-based testing, and CEGIS retry loop.
Project description
fastmcp 2.14.5 — OAuth authorization codes can be redirected to attacker-controlled URLs.
fnmatch("https://evil.com/cb?legit.example.com/anything", "https://*.example.com/*")returnsTrue.OAuthProxyProvider(allowed_client_redirect_uris=None)allows every redirect URI — the docs say localhost-only. JWT expiry check:if exp and exp < time.time()—exp=0is the Unix epoch and passes because0is falsy in Python.Both confirmed in one script. Full findings →
Install
pip install nightjar-verify
nightjar init mymodule
nightjar verify --spec .card/mymodule.card.md
Python 3.11+. Dafny 4.x is optional — without it, Nightjar falls back to CrossHair and Hypothesis and still gives you a confidence score.
What it found
48 confirmed bugs across 20 codebases. Every finding runs in one script.
fastmcp 2.14.5 — JWT tokens with exp=0 and exp=None accepted as valid
fastmcp/server/auth/jwt_issuer.py:215
if exp and exp < time.time(): # exp=None → False. exp=0 → False.
raise JoseError("expired")
# A token from 1970 or with no expiry passes without error
Any bearer token with a missing or zero expiry claim is silently accepted. Details →
litellm 1.82.6 — Budget windows never reset on long-running servers
litellm/budget_manager.py:81
def create_budget(
total_budget: float,
user: str,
duration: Optional[...] = None,
created_at: float = time.time(), # evaluated once at import, not at call time
):
On any server running longer than the budget window, every new budget is immediately treated as expired. Daily limits stop working. Details →
python-jose 3.5.0 — algorithms=None accepts any signing algorithm
jose/jws.py
if algorithms is not None and alg not in algorithms: # None skips the check entirely
raise JWSError("The specified alg value is not allowed")
Related to CVE-2024-33663. Passing algorithms=None decodes tokens signed with any algorithm, including unexpected ones. Details →
minbpe — train('a', 258) crashes with ValueError
minbpe/basic.py:35 — a crash in Andrej Karpathy's BPE tokenizer reference implementation
pair = max(stats, key=stats.get) # ValueError: max() iterable argument is empty
# Fix is one line:
if not stats:
break
Short text, repetitive input, or any vocab_size that requests more merges than the text can produce — all crash. Details →
MiroFish — Hardcoded secret key and RCE-enabled debug mode in default config
backend/app/config.py:24-25
SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key') # publicly known
DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true' # Werkzeug PIN bypass
Any deployment without a .env file runs with a known session signing key and Flask's interactive debugger enabled. Details →
open-swe — Safety-net middleware silently skips PR recovery on tool failure
agent/middleware/open_pr.py:87
if "success" in pr_payload: # True when success=False too — key always present
return None # abandons recovery regardless of outcome
When commit_and_open_pr fails, the middleware that should retry does nothing. The agent ends without a PR and without an error. Details →
Clean results — what disciplined code looks like
Not every repo has bugs. These passed with no violations:
| Package | Functions scanned | Result |
|---|---|---|
datasette 0.65.2 |
1,129 | Clean — layered SQL injection defense, parameterized queries throughout |
sqlite-utils 3.39 |
~237 | Clean — consistent identifier escaping, no raw string interpolation |
rich 14.3.3 |
~705 | Clean — markup escape works correctly, all edge cases handled |
hypothesis 6.151.9 |
— | Clean — no invariant violations found |
Nightjar finds the gap between what code claims and what it does. These repos have a small gap.
Why not just...
| Tool | What it catches | What it misses |
|---|---|---|
| mypy | Type errors | Logic bugs, edge cases, invariant violations |
| bandit | Known vulnerability patterns | Novel logic flaws, spec violations |
| pytest | What you write tests for | What you forget to test |
| Nightjar | Mathematical proof from specs | Requires writing specs |
Nightjar does not replace any of these. It checks whether the code satisfies the properties you wrote in its spec, for all inputs — not just the inputs you thought of.
How it works
You write a .card.md spec. An LLM generates the implementation. Nightjar runs five stages cheapest-first and short-circuits on the first failure.
graph LR
A["Stage 0<br/>Preflight"] --> B["Stage 1<br/>Deps"]
B --> C["Stage 2<br/>Schema"]
C --> D["Stage 2.5<br/>Negation Proof"]
D --> E["Stage 3<br/>Property Tests"]
E --> F["Stage 4<br/>Formal Proof"]
F -->|"Pass ✓"| G["Verified"]
F -->|"Fail ✗"| H["CEGIS Retry"]
H --> C
style A fill:#1a1409,color:#D4920A,stroke:#D4920A
style B fill:#1a1409,color:#D4920A,stroke:#D4920A
style C fill:#1a1409,color:#D4920A,stroke:#D4920A
style D fill:#1a1409,color:#D4920A,stroke:#D4920A
style E fill:#1a1409,color:#D4920A,stroke:#D4920A
style F fill:#1a1409,color:#F5B93A,stroke:#F5B93A
style G fill:#1a1409,color:#FFD060,stroke:#FFD060
style H fill:#1a1409,color:#C84B2F,stroke:#C84B2F
When Dafny fails, the CEGIS loop extracts the concrete counterexample and puts it in the next prompt. Simple functions skip Dafny and route to CrossHair (about 70% faster) — routing is automatic based on cyclomatic complexity.
Verified by Nightjar
This repo runs nightjar verify on its own pipeline code. The verification pipeline has a spec in .card/. If Nightjar's own code violates a property, Nightjar's own CI fails. The CI badge above shows the last passing run.
nightjar badge # prints the shields.io URL for your last verification run
Sponsors
No sponsors yet. If Nightjar saves your team time, consider sponsoring development. Every sponsor gets listed here and a direct line for support.
Links
- Architecture — how the pipeline works internally
- References — papers the algorithms come from (CEGIS, Daikon, CrossHair)
- LLM docs — structured project description for LLM consumption
- Contributing · Security
- Commercial license for teams that can't work with AGPL: $2,400/yr (teams) · $12,000/yr (enterprise). Contact: nightjar-license@proton.me
How Nightjar Compares
| Feature | Nightjar | Semgrep | CrossHair | Bandit | mypy |
|---|---|---|---|---|---|
| Formal proofs (Dafny) | ✓ | ✗ | ✗ | ✗ | ✗ |
| Symbolic execution | ✓ | ✗ | ✓ | ✗ | ✗ |
| Property-based testing | ✓ | ✗ | ✓ | ✗ | ✗ |
| Zero-config scanning | ✓ | ✗ | ✗ | ✓ | ✓ |
| AI-native spec format | ✓ | ✗ | ✗ | ✗ | ✗ |
| CEGIS retry loop | ✓ | ✗ | ✗ | ✗ | ✗ |
| CVE-level bug finding | ✓ | ✓ | ✗ | Partial | ✗ |
FAQ
Can Nightjar verify async/await code? Yes. The PBT and schema stages handle async functions. Formal proof via Dafny is limited to pure function equivalents.
What happens without Dafny installed? Nightjar falls back to CrossHair symbolic execution and Hypothesis property testing. You still get 4 of 5 stages.
How long does verification take?
Simple functions: 2-5 seconds. Complex modules with formal proof: 30-120 seconds. Use --fast to skip Dafny.
Does Nightjar work with monorepos?
Yes. Point nightjar verify --spec .card/module.card.md at any module.
Is the AGPL license a problem for commercial use? Commercial licenses are available ($2,400/yr teams, $12,000/yr enterprise). See nightjarcode.dev/pricing.
Verified Clean Codebases
These popular packages pass all 5 Nightjar stages with zero violations:
- datasette — 1,129 functions scanned
- rich — ~705 functions scanned
- hypothesis — formally verified
- sqlite-utils — ~237 functions scanned
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nightjar_verify-0.1.0.tar.gz.
File metadata
- Download URL: nightjar_verify-0.1.0.tar.gz
- Upload date:
- Size: 267.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57c5e43dc9f8b996781499fd876bad98c9c8ce472918483b103311aefa2dfcfd
|
|
| MD5 |
021deac6178c84a07696cc98df96be35
|
|
| BLAKE2b-256 |
97e4111807b53867c4668dea2e158e0d13244831d964f6e2c1320c193305c00d
|
File details
Details for the file nightjar_verify-0.1.0-py3-none-any.whl.
File metadata
- Download URL: nightjar_verify-0.1.0-py3-none-any.whl
- Upload date:
- Size: 303.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93f5eebdc237967b7bdcfc7945f05029273f9a7b2c8c68a29fc8bdce49a5efa2
|
|
| MD5 |
ff73c542c535806b8f2a88805eeca425
|
|
| BLAKE2b-256 |
8cea948b70d3d2d49f34f8cf290e56720d55fbddcbbe7da2fe9e3fce97655656
|