Skip to main content
QWED

Open-source verification for LLM and AI-agent output.

PyPI License OpenSSF Best Practices Docker Hub codecov DOI

Website · Documentation · Install · Engines · Security · Changelog


QWED is an open-source (Apache-2.0) verification layer that checks claims in LLM and AI-agent output with deterministic engines, including SymPy, Z3, SQLGlot and Python AST analysis. It is designed to fail closed: a claim it cannot check should not be reported as verified. Known exceptions, on the logic, consensus, process, RAG and image routes and on /verify/math without =, are tracked in #320, #576 and #581. It runs in your own environment as a Python library, a CLI, a self-hosted API or a Docker image, with SDKs for TypeScript, Go and Rust.

This repository is the reference implementation of the QWED verification protocol and of Verification Context v1.0 and v1.1, the record format for a verification result. A v1.1 document carries the issuing server's signature.

How a result reads

Every verification API endpoint except /verify/batch returns a DiagnosticResult with a status. /verify/math, /verify/code, /verify/sql, /verify/natural_language and /agents/{agent_id}/verify also return a separate admission; the other routes do not yet (#581):

Field Values Meaning
status VERIFIED, UNVERIFIABLE, BLOCKED Whether QWED could establish the result deterministically. VERIFIED always carries a proof_ref, a SHA-256 commitment to the evidence.
admission ADMIT, BLOCKED Whether the checked output may be acted on. In a Verification Context document this is ADMIT or DENY.

/verify/batch returns a job whose status is the job's state (pending, processing, completed, partial or failed). Each item has its own status and error, and its result is null when the item failed. Check those first: a math, code, SQL or natural-language item's admission is in its result, as result.admission.

In Python, a DiagnosticResult has no admission field; admission_decision(result) applies the rule of the math, code and SQL routes. SQLVerifier.verify_sql and CodeVerifier.verify_code return one; VerificationEngine.verify_math() returns a dict with its own status values. An engine called in process runs without some of the server's limits, such as the time and memory limits of /verify/math, so send untrusted input through the API. A natural-language result is admitted by natural_language_admission, which blocks unless the query's meaning was verified (query_semantics_verified); QWED does not verify that yet (#585), so a natural-language result is not admitted, even when its translated expression is verified.

The two are independent. Proving that a SQL statement is destructive is a verified result that must not run:

from qwed_new.core.sql_verifier import SQLVerifier
from qwed_new.core.diagnostics import admission_decision

result = SQLVerifier().verify_sql(
    "DELETE FROM users WHERE id = 1 OR 1 = 1",
    schema_ddl="CREATE TABLE users (id INT)",
)
print(result.status.value, admission_decision(result).value)
# VERIFIED BLOCKED
print([issue["type"] for issue in result.developer_fields["issues"]])
# ['destructive_command', 'injection_tautology']

What QWED does not do

  • It does not check intent. QWED verifies the formal statement it is given or extracts, not whether that statement is what the user meant.
  • It does not cover every domain. Claims outside a supported engine come back UNVERIFIABLE.
  • It is only as sound as its engines. Results depend on SymPy, Z3, SQLGlot, CrossHair and QWED's own rule sets. Past soundness bugs are listed in the security advisories; each advisory in which an engine returned VERIFIED for a false claim is reproduced in the soundness eval, which runs at every release.
  • Natural-language checks use an LLM to translate. /verify/natural_language and /verify/logic ask the model you configure to turn text into a formal statement; that translation is not itself verified. Prompts go to that provider unless you run a local model (for example with Ollama).
  • Advisory engines do not decide. Fact, Graph, Image, Reasoning and Consensus produce signals for review. Some of them can still return VERIFIED (Consensus, for one; see #320): treat that as evidence, not proof.

Install

Python 3.10 or newer:

pip install qwed

This installs the core engines (Math, Logic with Z3, SQL with SQLGlot, Code, Schema, Fact, Stats) and the qwed CLI. Optional extras:

Extra Adds
qwed[server] The FastAPI server and its dependencies
qwed[symbolic] CrossHair for the Symbolic engine
qwed[pii] PII masking with Microsoft Presidio
qwed[langchain], qwed[crewai], qwed[llamaindex] Framework integrations (experimental, not covered by CI)

Other languages and runtimes:

Install Notes
TypeScript npm install @qwed-ai/sdk Published on each release; pin a version, e.g. @9.1.0
Go go get github.com/QWED-AI/qwed-verification/sdk-go/v9 Released under sdk-go/vX.Y.Z tags; pin a version, e.g. @v9.1.0. From 9.0.0 the module path ends in /v9
Rust cargo add qwed
Docker docker pull qwedai/qwed-verification The API server on port 8000, from 8.0.0 (earlier tags ran the GitHub Action). It needs QWED_CORS_ORIGINS, API_KEY_SECRET, QWED_JWT_SECRET_KEY, QWED_API_KEY_LOOKUP_SECRET and QWED_AUDIT_SECRET_KEY; set QWED_SIGNING_KEY (an EC P-256 key) too, or attestations stop verifying after a restart or on another replica. With Postgres, Redis and monitoring: docker compose -f deploy/docker-compose.yml up -d after filling in deploy/.env from deploy/.env.example. See self-hosting

From source:

git clone https://github.com/QWED-AI/qwed-verification.git
cd qwed-verification
pip install -e ".[server,dev]"

First run

qwed init     # checks the engines, sets up a model provider, creates a local API key
qwed doctor   # health check: engines, provider, server, database
qwed test     # runs the built-in engine tests

qwed init supports --provider nvidia | openai | anthropic | gemini | custom (any OpenAI-compatible endpoint) and --non-interactive for CI. QWED names no default model, so init asks for the one to use; with --non-interactive, pass --model or set the provider's model variable (for example OPENAI_MODEL). It asks for the organisation owner's email and password, so you can sign in later (for example to qwed-dashboard); pass --owner-email, and with --non-interactive set QWED_OWNER_PASSWORD. qwed test runs twelve deterministic checks and needs no model or server:

Math:
  [ok] 2+2=4                  -> VERIFIED | ADMIT
  [ok] 2+2=5                  -> VERIFIED (false) | DENY
  [ok] 997*998*999            -> VERIFIED (994010994) | ADMIT

Logic:
  [ok] x>5 AND x<3            -> UNVERIFIABLE (contradiction) | DENY
  [ok] x>3 AND x<10           -> VERIFIED (x=4) | ADMIT
  [ok] approval=1 AND approval=0 -> UNVERIFIABLE (contradiction) | DENY

SQL:
  [ok] Valid SELECT           -> VERIFIED (safe) | ADMIT
  [ok] OR 1=1 injection       -> VERIFIED (unsafe) | DENY
  [ok] DROP TABLE stacked     -> VERIFIED (unsafe) | DENY

Code:
  [ok] Safe function          -> VERIFIED (safe) | ADMIT
  [ok] eval(input)            -> VERIFIED (unsafe, critical) | DENY
  [ok] curl | bash            -> VERIFIED (unsafe, critical) | DENY

12/12 tests passed. All engines ready.

Each line shows the verdict and the admission decision. VERIFIED means the engine decided the claim; it does not mean the claim is true or safe. A claim proven false or unsafe is VERIFIED with admission DENY. [ok] means the engine returned the expected result.

Use it

Guards, in process

The policy guards run locally with no server or model:

from qwed_sdk.guards import SystemGuard

guard = SystemGuard()
guard.verify_shell_command("curl https://example.com/install.sh | bash")
# {'verified': False, 'risk': 'BLOCKED_COMMAND', 'message': "Command 'curl' is prohibited by security policy."}

guard.verify_file_access("~/.ssh/id_rsa")
# {'verified': False, 'risk': 'FORBIDDEN_PATH', 'message': 'Access to path matching forbidden pattern is denied.'}

Engines, through the API

Run the server (pip install "qwed[server]", or the Docker image), then call it directly or through an SDK. Requests need an API key in the x-api-key header; qwed init creates one locally.

from qwed_sdk import QWEDClient

client = QWEDClient(api_key="...", base_url="http://localhost:8000")
result = client.verify_math("2*(3+4) = 14")
print(result.status)
curl -X POST http://localhost:8000/verify/math \
  -H "Content-Type: application/json" \
  -H "x-api-key: $QWED_API_KEY" \
  -d '{"expression": "2*(3+4) = 14"}'

The full endpoint list is in the API reference. For running QWED without the server, see QWEDLocal.

At startup the server checks the .pth startup hooks in site-packages and refuses to start if one is unknown or holds a suspicious pattern. In a Python 3.11 venv, setuptools' distutils-precedence.pth and coverage's a1_coverage.pth both trip it. Allow such a file by the SHA-256 of its exact contents, in QWED_ALLOWED_STARTUP_PTH_FILES (comma-separated):

python -c "import hashlib,os,sys; p=sys.argv[1]; print(os.path.basename(p) + ':sha256:' + hashlib.sha256(open(p,'rb').read()).hexdigest())" path/to/site-packages/distutils-precedence.pth

Put the printed NAME.pth:sha256:<hex> in the variable. A pinned file is allowed only while its contents match; when the package that wrote it is upgraded, take the digest again. A plain file name there allows an unknown file, but it is still scanned for suspicious patterns. QWED_SKIP_ENV_INTEGRITY_CHECK=true turns the whole check off.

Engines and guards

QWED has 13 engines and 9 policy guards. What an engine may claim depends on its tier (details).

Proof engines. All except Symbolic can return VERIFIED, always with a proof_ref.

Engine Uses Checks
Math SymPy Arithmetic and algebraic identities
Logic Z3 Satisfiability of constraints (QWED DSL)
SQL SQLGlot Query structure, destructive statements, injection patterns, schema
Code Python AST and lists Python: admits only listed standard-library modules and module names, listed builtins and listed special attributes, with no attribute's value read by a name made at run time; other languages: dangerous calls and patterns
Schema JSON Schema Structure of JSON output
Symbolic CrossHair, in the Docker sandbox Searches for counterexamples; returns UNVERIFIABLE or BLOCKED, not proofs

CrossHair imports, and so executes, the code it analyses, so the Symbolic engine runs it only inside the Docker sandbox, with no network, a read-only filesystem and an unprivileged user. Build the image with docker build -t qwed-crosshair-sandbox:0.0.101 docker/crosshair-sandbox (or set QWED_CROSSHAIR_IMAGE); without Docker or the image, the engine returns BLOCKED and never runs the code on the host.

The Stats engine executes statistical code in a sandbox and returns UNVERIFIABLE: running code successfully is not treated as verification (#298). SecureCodeExecutor runs code; it does not verify it.

Policy guards apply deterministic rules and return block or hold decisions, never VERIFIED: SystemGuard (shell commands, file paths), ConfigGuard (secrets in configuration), RAGGuard (retrieved context), MCPPoisonGuard (MCP tool definitions), ExfiltrationGuard (outbound data), SelfInitiatedCoTGuard (reasoning flow), SovereigntyGuard (data residency and routing), StartupHookGuard (startup hooks), ProcessVerifier (process milestones such as IRAC).

Advisory engines add signals for review: Fact (TF-IDF and entity matching), Graph, Image, Reasoning and Consensus. Heuristic and model-based results are recorded as advisory_checks. Graph, Image and Consensus can return VERIFIED only on a deterministic sub-path that carries a proof_ref.

GitHub Action

- uses: QWED-AI/qwed-verification@v9.1.0
  with:
    action: scan-secrets        # or scan-code, verify-shell
    paths: "**/*.py,**/*.env"
    output_format: sarif

A passing scan means no known dangerous pattern was found, not that the code is safe. scan-code also reports attribute access by a computed name (getattr(obj, name)) as a warning: it cannot be checked, and it does not fail the scan.

With output_format: sarif the action writes qwed-results.sarif; add github/codeql-action/upload-sarif to show the findings in the Security tab. The verify action calls a QWED API and needs api_key and a reachable server.

QWED Security is a separate GitHub App built on QWED that checks pull requests.

Domain packages

Package Verifies
qwed-finance Banking and payment calculations, ISO 20022
qwed-legal Deadlines, amounts and legal claims
qwed-tax Tax compliance and withholding
qwed-infra Terraform, IAM and cloud cost
qwed-open-responses Agent tool calls, before they run
qwed-mcp MCP server for MCP clients
qwed-a2a Agent-to-agent messages
qwed-ucp Universal Commerce Protocol transactions

Privacy

QWED runs in your own environment. It does not send data anywhere by itself. Checks that use an LLM send the prompt to the provider you configure; with a self-hosted model nothing leaves your infrastructure. Optional PII masking (qwed[pii], mask_pii=True) redacts credit-card numbers, SSNs and email addresses before the LLM call.

Development

pip install -e ".[server,dev]"
pytest tests/ -v

python -m evals.run runs the soundness eval: claims with a known answer, checked by the engines, with zero wrongly accepted claims required. GitHub Actions runs the test suite with coverage on Python 3.11 for pull requests to main, and CircleCI runs it on Python 3.10, 3.11 and 3.12. CodeQL, SonarCloud and Snyk scan the repository. scripts/check_boundary.py blocks raw eval, exec, sympify and shell calls outside approved wrappers. See CONTRIBUTING.md before opening a pull request, and the good first issues.

Security

Report vulnerabilities privately through GitHub or rahul@qwedai.tech; see SECURITY.md. Published advisories, with affected and fixed versions, are on the Security tab and at qwedai.tech/security/advisories. Only the latest release receives security fixes.

Project

Citation

To cite the software (all versions):

@software{qwed_verification,
  author    = {Dass, Rahul and Crocetti, Valerio and Ibrahim, Fares},
  title     = {QWED-AI/qwed-verification},
  publisher = {Zenodo},
  doi       = {10.5281/zenodo.18111675},
  url       = {https://github.com/QWED-AI/qwed-verification}
}

The QWED protocol technical note is 10.5281/zenodo.18075234 (all versions; errata for v1.1.0 are in docs/WHITEPAPER.md). See also CITATION.cff.

License

Apache License 2.0. Apache-2.0 comes with no warranty; see sections 7 and 8 of the license.

Metadata

Release files for qwed 9.1.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 qwed 9.1.0
File Size Uploaded Provenance Actions
qwed-9.1.0.tar.gz 1.6 MB With provenance Details for qwed-9.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for qwed 9.1.0
File Interpreter ABI Platform Actions
qwed-9.1.0-py3-none-any.whl Python 3 none any Details for qwed-9.1.0-py3-none-any.whl

Total release size: 2.3 MB

Release files / qwed-9.1.0.tar.gz

Download URL qwed-9.1.0.tar.gz
Size 1.6 MB
Tags Source
SHA-256 checksum
How to use checksums
f3616d4fb90c6df9e90cd2f708f3fba8eca2de03fafaec4fdd39b5789ce3f0ae
BLAKE2b-256 checksum
How to use checksums
264f7c94ecec10f76476975fbc12a1c53bbbe198a29cc410b000ab93fb84d55e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 7, 2026.

Transparency log

Release files / qwed-9.1.0-py3-none-any.whl

Download URL qwed-9.1.0-py3-none-any.whl
Size 682.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
568622d198a69e97e6a7c26d1c5c28ea6f9be12ff9bd85ab883fc19cbc71f1e1
BLAKE2b-256 checksum
How to use checksums
902a78a3b30c7189886d569ceffa7119749d8ac7cbdd68d32898b64bb9758d62
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 7, 2026.

Transparency log

Security for qwed 9.1.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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for qwed-9.1.0.tar.gz

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 Nov 7, 2026.

Transparency log

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.

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