Skip to main content

Autonomous web-form fuzzer that finds a bug and points at the line of code where it lives — self-hosted, no AI/paid APIs.

Project description

QAi

Autonomous QA fuzzer that finds broken forms & APIs and points at the exact line of code where the bug lives

License Python 3.12+ MCP server REST API Playwright

QAi is an autonomous QA fuzzer/validator for web forms and APIs — not a penetration-testing tool. Give it a URL (+ optionally a repo, an OpenAPI/GraphQL spec, or a login), it fuzzes every field/operation with valid and malicious input, catches whatever breaks (server errors, console errors, a DOM error banner a network-only check would miss), and correlates every finding straight back to file:line — self-hosted, no manual test writing, no AI/paid APIs required to run a scan. A few optional check plugins (IDOR, auth-bypass, security headers) add security-adjacent coverage on top of the same fuzz engine — a bonus, not the core of what qai does.

Full documentation · AI-agent docs

URL → page model / API spec → fuzz matrix + active checks → capture effects
(network+console+DOM) → oracle (5xx/console-error/IDOR/auth-bypass) → correlate
to file:line → report (JSON / HTML / MCP)

Install

uv tool install qai-web       # or: pip install qai-web
qai --help

The distribution is qai-web (the name qai was taken on PyPI). Everything else is still qai — the command, the import package, the MCP server:

from qai.engine.runner import run_scan

Playwright needs its browser once per machine: playwright install chromium.

Quickstart from source

uv sync --extra dev --extra demo
uv run playwright install chromium

Try it against the bundled demo target — a tiny FastAPI app with one signup form whose name field has an unguarded server-side length check that a fuzzer's overflow payload reliably trips:

uv run uvicorn qai.demo_target.app:app --port 8000 &
uv run qai http://127.0.0.1:8000 --repo qai/demo_target --html report.html

Open report.html — a self-contained dark-themed report with a clickable qai/demo_target/app.py:LINE next to the finding. See docs/USAGE.md for every CLI flag, library call, and MCP tool signature.

Why qai

qai's core loop is QA validation, not security testing: the oracle's question is "does this input crash or break the app", the same thing a hand-written end-to-end test suite checks, just generated and run automatically instead of typed out by hand. The IDOR/auth-bypass/security-header checks are optional plugins layered on the same fuzz engine — useful, but a bonus on top of the bug-finding core, not what qai primarily is.

qai Playwright/Cypress (hand-written E2E) Burp Suite / ZAP
Finds bugs without writing test cases yes — fuzz-generated no — you write every case no — not its job
Bug → exact file:line in your repo yes no no
Native MCP server (agent-drivable) yes no no
OpenAPI/GraphQL spec-driven fuzzing yes no yes
Optional security checks (IDOR, auth-bypass) yes — opt-in plugins no yes — that's the whole point
Self-hosted, no AI/paid API calls yes yes yes
Free & open source yes yes ZAP: yes / Burp: no

The file:line correlation and MCP-native design are the two things neither a hand-written test suite nor a general pentest scanner does: an agent (Claude Code, Cursor, …) can run a scan and open the exact file the bug lives in, in the same session.

Contents

Stability, parallel tabs, direct-request fuzzing, HAR

uv run qai http://127.0.0.1:8000 --repo qai/demo_target \
  --parallel 5 --direct --har ./har --i-own-this-target
  • Cloudflare-aware navigation — waits out an automatic JS challenge (never solves or bypasses an interactive CAPTCHA; if one appears, it logs a warning and proceeds honestly rather than faking success) plus a best-effort networkidle settle.
  • --parallel N — fans fuzz cases across N concurrent worker sessions ("tabs"), each tagged tab-<i>. Every Finding/EffectBundle carries its tab_id, so console logs and requests from the same tab can be stitched back into one stack later.
  • --direct — after one baseline (valid) UI submission per form, remaining fuzz cases fire straight over HTTP via the same session's cookies (context.request), skipping the DOM entirely. Faster, and immune to client-side maxlength/ type="number" fighting the payload. Falls back to the UI path per-form whenever the baseline body isn't a simple, unambiguous application/x-www-form-urlencoded shape.
  • --har DIR — records one .har per tab for offline network inspection.
  • DOM error oracle — catches errors a backend reports via 200 OK + a visible banner ([role="alert"], .error, [aria-invalid="true"]) with no 5xx and no console.error, which a pure network/console oracle would miss.
  • --i-own-this-target — required to actually submit anything against a non-local host. Without it, a non-local URL is only page-modeled (fields inventoried, nothing filled or sent) — an accidental qai https://example.com never fuzzes a site you don't control. Localhost/127.0.0.1 targets are exempt (the normal dev loop above).

Use as a library

from qai.engine.runner import run_scan

report = await run_scan("http://127.0.0.1:8000", repo_path="qai/demo_target")
for finding in report.findings:
    print(finding.severity, finding.detail, finding.source_location)

Deploy — install as a plugin for AI agents (MCP)

qai ships an MCP (Model Context Protocol) server, so any MCP-speaking agent — Claude Code, Claude Desktop, Cursor, a custom agent harness — can call it as a tool instead of you running the CLI by hand. Nine tools are exposed (see docs/USAGE.md for full signatures):

Tool What it does
qa_scan Fuzz one page's forms, return a JSON RunReport. Optional plugins=[...] runs check plugins (e.g. security_headers, idor, auth_bypass) alongside the fuzz oracle; login_macro= replays a saved login first.
qa_scan_html Same as qa_scan, plus writes a self-contained dark-themed HTML report.
qa_crawl BFS-discover same-origin pages from a root URL, fuzz every form found.
qa_api_scan Parse an OpenAPI/GraphQL spec and fuzz every operation straight over HTTP — same RunReport shape, no DOM needed.
qa_login_record Record a login once (fills the form, reads back cookies/bearer token) and return a reusable LoginMacro.
qa_pipeline_start Open a resumable, externally-driven pipeline (recon + check stages) — one live browser session across calls. own_target=True is required for an ACTIVE check stage (e.g. idor) to actually run.
qa_pipeline_step Run or skip exactly one stage; optionally inject an AgentDirective first.
qa_pipeline_report Project the session's current findings as a RunReport, without tearing it down.
qa_pipeline_abort Free the live browser and delete the session.

Active checks (idor, auth_bypass) fire real extra requests and are gated by the same own_target/safe-mode rule as fuzzing itself — omit own_target=True and they're always SKIPPED, never run, against a target you haven't confirmed you own.

1. Install

git clone https://github.com/zlexdev/QAi.git
cd QAi
uv sync --extra mcp
uv run playwright install chromium

There's no PyPI package yet — install from a local clone (above) or straight from git:

uv tool install "qai[mcp] @ git+https://github.com/zlexdev/QAi.git"
uv tool run playwright install chromium   # once, after install

uv tool install puts qai and qai-mcp on your PATH globally, isolated in their own venv — no need to activate anything before pointing an agent at them.

2. Register it with an agent

Claude Code (this CLI) — one command, from the repo root:

claude mcp add qai -- uv run --directory /absolute/path/to/QAi qai-mcp
# or, if installed with `uv tool install`:
claude mcp add qai -- qai-mcp

Use --scope user to make it available in every project, not just this one:

claude mcp add qai --scope user -- qai-mcp

Claude Desktop — add to claude_desktop_config.json (%APPDATA%\Claude\claude_desktop_config.json on Windows, ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "qai": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/QAi", "qai-mcp"]
    }
  }
}

Any other MCP client — the server speaks stdio, so the shape is always {"command": ..., "args": [...]}; point it at qai-mcp (if uv tool installed) or uv run --directory <repo> qai-mcp (running from a clone). No network port, no auth token — it's a local subprocess the agent's own harness spawns and owns.

3. Safety when handing an agent this tool

own_target=True gates real submissions exactly like --i-own-this-target does on the CLI — an agent can call qa_scan/qa_crawl against any URL and safely get a read-only page model back; it must explicitly pass own_target=True to actually fuzz a non-local host. Don't grant own_target=True by default in an agent's system prompt/tool config unless every target it might be pointed at is one you own — see Safety below.

4. Examples

These are real tool calls against the bundled demo target (uv run uvicorn qai.demo_target.app:app --port 8000) — the JSON is abbreviated for readability but the shapes and values are unmodified from an actual run.

Plain fuzz scan — no plugins, matches CLI-only behaviour byte-for-byte except the always-present plugin_findings: []:

// call: qa_scan(url="http://127.0.0.1:8000", repo_path="qai/demo_target")
{
  "run_id": "5c77053f29de",
  "forms_scanned": 1,
  "cases_executed": 71,
  "findings": [
    {
      "severity": "high",
      "kind": "server_error",
      "detail": "POST http://127.0.0.1:8000/signup -> 500 on intent=overflow value='AAA...'",
      "source_location": { "file": "app.py", "line": 40, "symbol": "signup" }
    },
    { "severity": "medium", "kind": "console_error", "detail": "Failed to load resource: ... 500 ..." }
  ],
  "plugin_findings": []
}

Scan + check plugin — same call, plugins=["security_headers"] added, runs the fuzz oracle and the header check in one pass:

// call: qa_scan(url="http://127.0.0.1:8000", repo_path="qai/demo_target",
//               plugins=["security_headers"])
{
  "run_id": "e635378e46d6",
  "forms_scanned": 1,
  "cases_executed": 71,
  "findings": [ /* same 500-on-overflow finding as above */ ],
  "plugin_findings": [
    { "plugin": "security_headers", "category": "missing_csp", "severity": "low",
      "title": "Missing content-security-policy response header" },
    { "plugin": "security_headers", "category": "missing_x_frame_options", "severity": "low",
      "title": "Missing x-frame-options response header" },
    { "plugin": "security_headers", "category": "missing_hsts", "severity": "low",
      "title": "Missing strict-transport-security response header" },
    { "plugin": "security_headers", "category": "missing_x_content_type_options", "severity": "low",
      "title": "Missing x-content-type-options response header" }
  ]
}

Screenshot the page qai scanned — real output from an actual run:

// call: qa_scan(url="http://127.0.0.1:8000", repo_path="qai/demo_target", screenshot=true)
{
  "run_id": "849c2cf13a41",
  "forms_scanned": 1,
  "findings": [ /* 2 findings, same overflow bug */ ],
  "screenshot_path": "qai-reports/screenshots/849c2cf13a41.png"
}
// -> Read that path directly (Claude Code, or any agent that can open images)

Screenshot every page of a full crawl — one PNG per visited page, form-less pages included:

// call: qa_crawl(url="http://127.0.0.1:8000", screenshot=true, screenshot_dir="shots")
{
  "run_id": "849c2cf13a41",
  "screenshots": [
    { "url": "http://127.0.0.1:8000/",      "path": "shots/849c2cf13a41/001-root.png" },
    { "url": "http://127.0.0.1:8000/login", "path": "shots/849c2cf13a41/002-login.png" }
  ]
}
// -> ordinal-prefixed, so the files sort in visit order

Active checks — IDOR + auth-bypass replay, gated by own_target (never fire without it, see Safety):

// call: qa_scan(url="http://127.0.0.1:8000", repo_path="qai/demo_target",
//               plugins=["idor", "auth_bypass"], own_target=true)
{
  "plugin_findings": [
    { "plugin": "idor", "category": "idor_candidate", "severity": "medium",
      "title": "GET /api/items/{id} returned 200 for an id never linked from the page" },
    { "plugin": "auth_bypass", "category": "auth_bypass", "severity": "high",
      "title": "GET /api/admin/stats returned 200 with no auth header at all" }
  ]
}

Spec-driven API scan — fuzz an OpenAPI/GraphQL spec straight over HTTP, no DOM:

// call: qa_api_scan(spec={"kind": "openapi", "raw": "<contents of openapi.json>"},
//                    base_url="http://127.0.0.1:8000", own_target=true)
{ "run_id": "...", "forms_scanned": 2, "findings": [ /* per-operation findings */ ] }

Record a login once, replay it on every future scan:

// call: qa_login_record(login_url="http://127.0.0.1:8000/login",
//                        username="admin", password="secret")
{
  "macro": { "login_url": "...", "username_selector": "#username", "...": "..." },
  "auth_result": { "authenticated": true, "cookies": [ { "name": "session", "...": "..." } ] }
}
// save `macro` to a file, then: qa_scan(url="...", login_macro=<saved macro>)

Authenticated scan — cookies injected before any navigation:

// call: qa_scan(url="https://app.internal/dashboard", repo_path="/repos/app",
//               cookies=[{"name": "session", "value": "abc123", "domain": "app.internal"}],
//               own_target=true)

HTML report for a human reader:

// call: qa_scan_html(url="http://127.0.0.1:8000", repo_path="qai/demo_target",
//                     out_path="report.html")
{ "run_id": "...", "ok": false, "findings_count": 2, "html_report": "C:\\...\\report.html" }

Crawl mode — BFS-discover pages, fuzz every form found:

// call: qa_crawl(url="http://127.0.0.1:8000", repo_path="qai/demo_target",
//                 max_depth=2, max_actions=50)
{ "run_id": "...", "states_visited": 3, "pages": [ /* one RunReport per page with a form */ ] }

Driven pipeline — step-by-step, inspecting output and injecting context between stages (this is the sequence a Claude Code session actually ran):

// 1) qa_pipeline_start(url="http://127.0.0.1:8000", repo_path="qai/demo_target")
{ "session_id": "a4b10b47...", "cursor": 0, "steps": [
  { "name": "recon", "status": "pending" }, { "name": "security_headers", "status": "pending" }
]}

// 2) qa_pipeline_step(session_id="a4b10b47...")            -> runs "recon"
{ "cursor": 1, "steps": [{ "name": "recon", "status": "done" }, { "name": "security_headers", "status": "pending" }] }

// 3) qa_pipeline_step(session_id="a4b10b47...", inject={"notes": "focus on headers"})
{ "cursor": 2, "steps": [{ "status": "done" }, { "status": "done" }],
  "last_step_output": [ /* the same 4 security_headers findings as above */ ] }

// 4) qa_pipeline_report(session_id="a4b10b47...")          -> RunReport-shaped projection
{ "plugin_findings": [ /* 4 findings */ ] }

// 5) qa_pipeline_abort(session_id="a4b10b47...")           -> frees the browser
{ "ok": true }

Recon-only pipeline (skip the check stage entirely):

// call: qa_pipeline_start(url="http://127.0.0.1:8000", plugins=[])
// -> steps: [{"name": "recon", "status": "pending"}]   (no security_headers stage)

Skipping a stage mid-pipeline instead of running it:

// call: qa_pipeline_step(session_id="a4b10b47...", skip=true)
// -> cursor advances, that stage's status becomes "skipped", no side effects run

Deploy — remote FastAPI service

For driving qai over the network instead of a local MCP subprocess — put it on a server and hit it with curl/any HTTP client. Same underlying engine as the MCP tools above (run_scan, run_crawl, run_api_scan, record_login, the pipeline session store); this is just a second transport, not a second engine.

Run it locally

uv sync --extra api
export QAI_API_KEY=$(openssl rand -hex 32)
uv run qai-api          # listens on 0.0.0.0:8000
curl -X POST http://127.0.0.1:8000/v1/scan \
  -H "X-API-Key: $QAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"url": "http://127.0.0.1:8000", "repo_path": "qai/demo_target"}'
# -> {"job_id": "...", "status": "pending"}
curl http://127.0.0.1:8000/v1/jobs/<job_id> -H "X-API-Key: $QAI_API_KEY"
# -> {"job_id": "...", "status": "done", "result": { ...RunReport... }}

Scans/crawls/API-scans are long-running, so every submit endpoint returns 202 + a job_id immediately; poll GET /v1/jobs/{id} for the result. Pipeline sessions (/v1/pipeline/*) mirror the MCP qa_pipeline_* tools one-to-one and don't need polling — each call returns the current state directly.

Endpoint Mirrors MCP tool
POST /v1/scan qa_scan
POST /v1/crawl qa_crawl
POST /v1/api-scan qa_api_scan
POST /v1/login-record qa_login_record
GET /v1/jobs/{id} — poll result of any of the above
POST /v1/pipeline/start qa_pipeline_start
POST /v1/pipeline/{id}/step qa_pipeline_step
GET /v1/pipeline/{id}/report qa_pipeline_report
DELETE /v1/pipeline/{id} qa_pipeline_abort
GET /healthz — unauthenticated liveness check

Every route except /healthz requires an X-API-Key header matching QAI_API_KEY (constant-time compared). Run single-worker (qai-api always does) — job/session state is in-process, so a multi-worker run would silently split jobs across processes.

Interactive API reference (built from the live OpenAPI schema, Scalar): open http://127.0.0.1:8000/scalar (or https://<your-domain>/scalar once deployed).

REST examples

Same demo target as the MCP examples above (uv run uvicorn qai.demo_target.app:app --port 8000); $KEY is $QAI_API_KEY.

Crawl — BFS-discover pages, fuzz every form found:

curl -X POST http://127.0.0.1:8000/v1/crawl -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"url": "http://127.0.0.1:8000", "repo_path": "qai/demo_target", "max_depth": 2, "max_actions": 50}'
# -> {"job_id": "...", "status": "pending"}  — poll GET /v1/jobs/{id} for the CrawlReport

Spec-driven API scan — fuzz an OpenAPI/GraphQL spec straight over HTTP, no DOM:

curl -X POST http://127.0.0.1:8000/v1/api-scan -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d "{\"spec\": $(cat qai/demo_target/openapi.json | jq -Rs .), \"base_url\": \"http://127.0.0.1:8000\", \"own_target\": true}"

Record a login once, replay it on every future scan:

curl -X POST http://127.0.0.1:8000/v1/login-record -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"login_url": "http://127.0.0.1:8000/login", "username": "admin", "password": "secret"}'
# -> {"job_id": "...", "status": "pending"}
curl http://127.0.0.1:8000/v1/jobs/<job_id> -H "X-API-Key: $KEY"
# -> result: {"macro": {...LoginMacro...}, "auth_result": {"authenticated": true, "cookies": [...]}}
# save `macro` from the result, then pass it as "login_macro" on a later /v1/scan or /v1/crawl call

Driven pipeline — step-by-step, inspecting output and injecting context between stages:

SID=$(curl -s -X POST http://127.0.0.1:8000/v1/pipeline/start -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"url": "http://127.0.0.1:8000", "repo_path": "qai/demo_target"}' | jq -r .session_id)

curl -X POST http://127.0.0.1:8000/v1/pipeline/$SID/step -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{}'
# -> runs "recon"; steps[0].status becomes "done"

curl -X POST http://127.0.0.1:8000/v1/pipeline/$SID/step -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"inject": {"notes": "focus on headers"}}'
# -> runs "security_headers"; last_step_output carries its findings

curl http://127.0.0.1:8000/v1/pipeline/$SID/report -H "X-API-Key: $KEY"
# -> RunReport-shaped projection of the session so far, doesn't tear it down

curl -X DELETE http://127.0.0.1:8000/v1/pipeline/$SID -H "X-API-Key: $KEY"
# -> {"ok": true}, frees the browser and deletes the session

Deploy to a server with a domain + TLS

git clone https://github.com/zlexdev/QAi.git && cd QAi
bash scripts/install.sh

Prompts once for a domain (must already point an A/AAAA record at the host) and generates/caches an API key in ~/.qai.conf; from there it's idempotent — re-run after a git pull to pick up an update. It installs system deps, syncs the api extra, provisions a qai-api systemd service, and fronts it with nginx + a Let's Encrypt cert via certbot. bash scripts/install.sh --dry-run prints every step without touching the host.

On a shared host, cap the service's memory so one heavy scan (Chromium) can't starve sibling processes: QAI_MEMORY_MAX=512M bash scripts/install.sh (default 512M, cached in ~/.qai.conf like the domain/port/API key — the systemd unit gets MemoryMax/MemorySwapMax=0, so a scan gets OOM-killed by its own cgroup instead of taking the host down).

Full install-phase breakdown, config reference, operating/update commands, memory-cap sizing guidance, and TLS/DNS troubleshooting: docs/DEPLOY.md.

How it works

Layer What it does Built on
engine/capture.py Playwright + CDP: network, console, navigation → EffectBundle Playwright
engine/modeler.py Inventories form fields + types from the live DOM one page.evaluate (self-contained, no Node runtime)
engine/fuzzer/ FieldFuzzStrategy registry (one per field kind) + DataGenerator stdlib
engine/analyzer.py Oracle: valid input must not 5xx/console-error; malicious/overflow must reject gracefully stdlib
engine/correlator.py Captured request → FastAPI route table (ast) → optional cx call-graph enrichment stdlib ast, optional codeanalyzer
engine/direct_executor.py Learns a form's request shape once, replays fuzz cases straight over HTTP Playwright APIRequestContext
engine/reporter.py rich terminal table + self-contained HTML report rich

Everything crosses layer boundaries as frozen Pydantic DTOs (qai/engine/contracts.py) — no raw dicts between layers.

engine/fuzzer/strategies.py's pattern-violation case (FuzzIntent.SCHEMA_VIOLATION — find a value that provably fails a field's JSON-schema pattern, then assert the API rejects it) ports the "negative testing" idea from Schemathesis (MIT) — same concept (prove a value violates the schema, expect a graceful rejection), implemented here with a stdlib-only re-verified heuristic instead of Schemathesis's Hypothesis-driven schema negation.

Safety

Only run against staging, or your own targets — the fuzzer submits forms with malicious/overflow payloads and, with plugins=["idor","auth_bypass"], fires real mutated replay requests. Links/buttons matching a destructive keyword are never clicked during a crawl unless explicitly allow-listed (--allow-destructive / allow_destructive=[...]) — see qai.engine.risk for the heuristic.

Non-local targets require --i-own-this-target (CLI) / own_target=True (MCP) before anything is filled, submitted, or actively replayed — otherwise qai only inventories the page's fields and active checks are always SKIPPED. qai never attempts to solve or bypass a CAPTCHA/anti-bot challenge; it waits out an automatic Cloudflare JS check and otherwise proceeds honestly (an unresolved interactive challenge just means an honestly-empty result, not a crash or a bypass).

See also

  • docs/USAGE.md — full CLI/library/MCP reference, troubleshooting, safety model
  • docs/DEPLOY.md — remote FastAPI service deploy guide (install phases, config reference, shared-host memory sizing, TLS troubleshooting)
  • docs/PLUGINS.md — writing a check plugin (passive or active)
  • docs/REPORTS.md — report formats (JSON/HTML/Markdown) and auto-save
  • docs/for_ai/ — condensed package map for an AI coding agent working in this repo

Contributing

git clone https://github.com/zlexdev/QAi.git && cd QAi
uv sync --extra dev --extra demo --extra api
uv run playwright install chromium
uv run pytest
uv run ruff check . && uv run mypy qai

PRs go against master; CI expectation is ruff + mypy (strict) + pytest all green. New engine-level types go in qai/engine/contracts.py as frozen Pydantic models — no raw dict/tuple crossing a layer boundary. Sync the relevant _MODULE_AUTO.md after touching a package (see docs/for_ai/ for the doc map).

Community

Use issues for bugs and feature requests.

Asmin963

License

MIT © 2026 Asmin963

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

qai_web-0.2.1.tar.gz (221.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qai_web-0.2.1-py3-none-any.whl (126.9 kB view details)

Uploaded Python 3

File details

Details for the file qai_web-0.2.1.tar.gz.

File metadata

  • Download URL: qai_web-0.2.1.tar.gz
  • Upload date:
  • Size: 221.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for qai_web-0.2.1.tar.gz
Algorithm Hash digest
SHA256 ab9b181c15ca07f4b1fbc482f258af3d78a65bf5bf95c46b1707e02a7884bd3b
MD5 83b5c83fe8b49cc4f40bb47f772f8e75
BLAKE2b-256 e4ea7a20304fc292d88556f38049e1211f44e525e3ce662aa74f0cf9618a0ef8

See more details on using hashes here.

Provenance

The following attestation bundles were made for qai_web-0.2.1.tar.gz:

Publisher: publish.yml on zlexdev/QAi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qai_web-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: qai_web-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 126.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for qai_web-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2bbed5e5e8fb07ac71e5acb10dee9b72765d7830c1a31ba0b18ec8de7979af15
MD5 7328df2563e2916eb9f8c3fb114e07b6
BLAKE2b-256 55d7b826b0116f17658d8d70ae86ecb57dcff6b5c910d1d4f29b978ace1f1587

See more details on using hashes here.

Provenance

The following attestation bundles were made for qai_web-0.2.1-py3-none-any.whl:

Publisher: publish.yml on zlexdev/QAi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page