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
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
networkidlesettle. --parallel N— fans fuzz cases across N concurrent worker sessions ("tabs"), each taggedtab-<i>. EveryFinding/EffectBundlecarries itstab_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-sidemaxlength/type="number"fighting the payload. Falls back to the UI path per-form whenever the baseline body isn't a simple, unambiguousapplication/x-www-form-urlencodedshape.--har DIR— records one.harper 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 noconsole.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 accidentalqai https://example.comnever 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.
License
MIT © 2026 Asmin963
Project details
Release history Release notifications | RSS feed
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab9b181c15ca07f4b1fbc482f258af3d78a65bf5bf95c46b1707e02a7884bd3b
|
|
| MD5 |
83b5c83fe8b49cc4f40bb47f772f8e75
|
|
| BLAKE2b-256 |
e4ea7a20304fc292d88556f38049e1211f44e525e3ce662aa74f0cf9618a0ef8
|
Provenance
The following attestation bundles were made for qai_web-0.2.1.tar.gz:
Publisher:
publish.yml on zlexdev/QAi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qai_web-0.2.1.tar.gz -
Subject digest:
ab9b181c15ca07f4b1fbc482f258af3d78a65bf5bf95c46b1707e02a7884bd3b - Sigstore transparency entry: 2213235902
- Sigstore integration time:
-
Permalink:
zlexdev/QAi@20cab7fbb2cf7841319b9c40d886b4d551bb62ae -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/zlexdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@20cab7fbb2cf7841319b9c40d886b4d551bb62ae -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2bbed5e5e8fb07ac71e5acb10dee9b72765d7830c1a31ba0b18ec8de7979af15
|
|
| MD5 |
7328df2563e2916eb9f8c3fb114e07b6
|
|
| BLAKE2b-256 |
55d7b826b0116f17658d8d70ae86ecb57dcff6b5c910d1d4f29b978ace1f1587
|
Provenance
The following attestation bundles were made for qai_web-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on zlexdev/QAi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qai_web-0.2.1-py3-none-any.whl -
Subject digest:
2bbed5e5e8fb07ac71e5acb10dee9b72765d7830c1a31ba0b18ec8de7979af15 - Sigstore transparency entry: 2213235989
- Sigstore integration time:
-
Permalink:
zlexdev/QAi@20cab7fbb2cf7841319b9c40d886b4d551bb62ae -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/zlexdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@20cab7fbb2cf7841319b9c40d886b4d551bb62ae -
Trigger Event:
push
-
Statement type: