verify_fact fails closed offline · Request caps and sandbox limits · v7.2.3 · QWED-A2A runtime hardening · QWED-Tax v0.2.1 · QWED-Legal v0.5.1 · v7.2.2 · Open Responses v0.6.1 · QWED-Finance v3.0.1 · QWED-Legal date and input bounds · Open Responses v0.6.0 · QWED-Legal fail-closed hardening · qwed init server identity handshake · Leaked key auto-revocation · API-key format spec · Signed secret-scanning webhook · v7.2.1 · v7.2.0 · v7.1.0 · v7.0.0
QWED Verification — tenant-scoped admin endpoints and working RBAC
Released: October 15, 2026 · GitHub PR #450 · Closes issue #222
Security fix. Admin endpoints now authorize by the API key owner’s role and act only on the caller’s own organization. Previously, role checks never ran, so every admin endpoint returned 401 Unauthorized. The endpoints also accepted organization, log, and key IDs from the caller without an ownership check, which would have allowed cross-tenant access once role checks worked.
What changed
- Working role checks. QWED authenticates the API key, then checks the owning user’s role. The key must be live (not revoked or expired), and the owning user and organization must be active, with the user in the key’s organization. Otherwise QWED returns
403 Forbidden. - Tenant scoping with
403.GET /admin/compliance/export/csv,GET /admin/compliance/report/soc2/{org_id}, andGET /admin/security/threats/{org_id}return403 Forbiddenfor another organization’s ID. - Tenant scoping with
404.GET /admin/compliance/verify/{log_id}andPOST /admin/keys/rotatereturn404 Not Foundfor an ID in another organization, without revealing that it exists. - Key rotation rules. Members can rotate only their own keys. Owners and admins can rotate other users’ keys only when the key owner doesn’t hold a higher role. Revoked or inactive keys return
404 Not Found. The new secret is returned only for a key in the caller’s organization.
QWED Verification — verify_fact fails closed offline and Money rejects non-finite amounts
Released: October 14, 2026 · GitHub PR #449 · GitHub PR #451
QWEDClient.verify_factno longer reports a claim as verified when the QWED service can’t be reached.Moneyrejects NaN and Infinity amounts, andverify_invoice_totalreports invalid inputs as a failed check instead of raising.
What changed
verify_factfails closed when the service is unreachable (#449). On a connection failure or a connect or read timeout, the Python SDK returnsstatus="UNVERIFIABLE"andis_verified=Falsewith a message explaining that remote verification was unavailable. Previously, a connection failure returnedVERIFIEDafter only the local PII scan, and a timeout raised an exception. Seeverify_fact.Moneyrejects non-finite amounts (#451).Money("NaN"),Money("Infinity"), andMoney(float("inf"))raiseValueError.verify_invoice_totalnever raises on invalid input (#451). Invalid amounts, a negative or non-finitetolerance, or a non-Decimaltolerancereturn(False, reason)instead of raising. Currency mismatches still return(False, "Currency mismatch: ...").
QWED Verification — request body caps, timeouts, and sandbox concurrency limits
Released: October 14, 2026 · GitHub PR #447 · GitHub PR #448Availability hardening for self-hosted servers. Oversized bodies, slow providers, unbounded batch jobs, and concurrent sandbox containers can no longer stall or exhaust the API. Verdicts and fail-closed behavior are unchanged.
What changed
- Body cap on every
/verifyroute. JSON routes accept up to 1,000,000 bytes./verify/statsand/verify/imagekeep their larger upload caps. Oversized bodies return413before processing. See Request limits. - Blocking work runs off the event loop.
/verify/coderuns underQWED_CODE_VERIFY_TIMEOUT(default 10 seconds) and returns503on timeout. Provider calls fornatural_language,logic, andfactrun underQWED_PROVIDER_TIMEOUT(default 30 seconds) and fail closed on timeout. - Bounded batch job store.
/verify/batchkeeps at mostQWED_BATCH_MAX_JOBSjobs (default 1000) and evicts finished jobs first. When every stored job is active, new submissions return503. Items that exceedQWED_BATCH_ITEM_TIMEOUTare marked failed. - Sandbox concurrency limit. Each server process runs at most
QWED_SANDBOX_MAX_CONCURRENTsandbox containers (default 4). A request that can’t get a slot withinQWED_SANDBOX_ACQUIRE_TIMEOUT(default 10 seconds) is rejected withSandbox capacity reached — retry shortly.
v7.2.3 — Code, secret, and SQL verification security release
Released: October 13, 2026 · GitHub Release ↗ · GitHub PR #441 · GitHub PR #444v7.2.3 is a fail-closed security release covering three advisories published on October 13, 2026: GHSA-6rxr-gq43-694g, GHSA-64xf-wffc-63fm, and GHSA-q5g6-6cw2-cjm3. They affect all versions up to 7.2.2. Upgrading is recommended. No breaking wire changes.
Security fixes
- Code verification never admits shell or process execution (GHSA-6rxr-gq43-694g).
os.system,os.popen, theos.exec*andos.spawn*families,os.posix_spawn*,os.startfile,subprocess.*,pty.spawn, andasyncio.create_subprocess_*are now blockingprocess_executionfindings. This includes calls through aliased imports such asimport subprocess as sp,from os import system as run_it, andfrom os import *. Before, they were warnings and the code was admitted.verify_code,verify_python_deep, andPOST /verify/codeare covered. See Code engine. - Project-scoped OpenAI keys are detected (GHSA-64xf-wffc-63fm).
ConfigGuardand the code verifier detectsk-proj-,sk-svcacct-, andsk-admin-keys as well as legacy keys. See ConfigGuard. - SQL in executable comments and tables outside the schema are rejected (GHSA-q5g6-6cw2-cjm3). Executable comments (
/*! ... */,/*!NNNNN ... */,/*M! ... */) are expanded before analysis, so the SQL inside them is checked. Withschema_ddl, a query on an undeclared table isBLOCKEDwithsql_verifier.schema_violationinstead of being admitted with a warning. CTE names are not treated as tables. See SQL engine.
Soundness eval
From v7.2.3 on, each GitHub release includes a soundness eval report (eval-results.json). The eval runs claims with a known answer through the Math, Logic, SQL, Code, and Schema engines, including reproductions of advisories where an engine returned a false VERIFIED. The v7.2.3 report covers 76 claims with 0 false accepts.
Version bumps
Upgrading
Pinqwed>=7.2.3 on PyPI, @qwed-ai/sdk@^7.2.3 on npm, and qwed = "7.2.3" on crates.io, then redeploy. If your generated code runs shell commands, or your SQL queries reference tables outside the schema_ddl you pass, expect those to be blocked after the upgrade.
QWED-A2A — request size limits, engine budget, and code guard hardening
Released: October 12, 2026 · GitHub PR #164 · GitHub PR #165 · GitHub PR #166 · GitHub PR #169
Four fail-closed fixes to the A2A runtime. Oversized requests are rejected before they reach an engine. Engines run off the event loop under a wall-clock budget. The code guard sees through aliases and re-bindings. TrustBoundary is safe to share across threads.
What changed
max_payload_size_bytesis enforced. It was declared but never read.POST /a2a/interceptreturns413for bodies over the limit plus a 64 KiB envelope allowance, before buffering. An invalidContent-Lengthreturns400.intercept()returnsblockedwith enginesize_limitfor oversized payloads, which also covers library callers. The code guard blocks code over 256 KiB before parsing and returnsunverifiablefor non-string code. Keep a reverse-proxy body limit too.- Engine execution budget. New
engine_timeout_secondsconfig (float, default5.0, greater than 0 and at most 60). Engines run on a dedicated pool (4 running, 32 waiting), so health, metrics, and JWKS stay responsive. A timeout returnserrorwith engineengine_timeout. Overload returnserrorwith engineengine_busy. Call the newA2AVerificationInterceptor.close()from your shutdown hook. A run that already started cannot be killed, and long calls that hold the GIL can still delay the loop. - Code guard alias detection. The AST layer resolves import aliases, alias chains, re-bound functions, and annotated, walrus, and tuple-unpacking bindings. It also blocks
posix,nt,builtins, and__builtins__receivers,osexec/spawn/posix_spawn/startfile, star imports from dangerous modules, andgetattron a dangerous module. Known gaps are documented. A clean scan stays a non-admissibleheuristic_pass. - Thread-safe
TrustBoundary. Every public method runs under a re-entrant lock. Rate limits hold under concurrency, and eviction no longer races. Trust scopes are snapshotted as frozensets at grant time.
QWED-Tax v0.2.1 — classification claim vocabulary and strict facts
Released: October 10, 2026 inqwed-tax 0.2.1 (PyPI and npm @qwed-ai/tax) · GitHub PR #81 · Changelog
qwed-taxv0.2.1 is a security patch. Upgrading is recommended for all users ofNexusGuard(Python and npm) andClassificationGuard. The release also ships the structured nexus claim described in the August 31 and September 1 entries.
What changed
- Closed claim vocabulary.
ClassificationGuard.verify_classification_claimmaps the claim through a fixed set of values instead of substring matching.W2,W-2, andEMPLOYEEmap to employee.1099,1099-NEC,CONTRACTOR, andINDEPENDENT CONTRACTORmap to contractor. Matching ignores case, spaces, hyphens, and underscores. Any other text fails closed withINVALID_CLAIM. - Strict worker facts.
provides_tools,reimburses_expenses, andindefinite_relationshipmust all be present booleans. Otherwise the result fails closed withINVALID_FACTS. Strings such as"no"are rejected. TaxPreFlighthireintents. The same vocabulary and fact rules apply toworker_typeandworker_facts.- npm
NexusGuardfails closed. The@qwed-ai/taxNexusGuardblocks states that are not in its threshold table and rejects invalid sales data, including non-object, non-finite, and negative values. The transaction-count prong applies only to states that have one. - npm classification unchanged. The npm SDK still compares
worker_typeexactly. Pass"W2"or"1099".
Migration
Translate the AI’s classification into"W2" or "1099" and pass each worker fact as a real boolean — map known values explicitly ("yes"/"true" → True, "no"/"false" → False) and reject anything else. Do not wrap a string in bool(): bool("no") is True, which would recreate the bug this release fixes. Nexus callers that relied on llm_decision strings must pass claimed_collects_tax as a boolean. See ClassificationGuard and NexusGuard.
QWED-Legal v0.5.1 — DeadlineGuard signing-anchor allowlist
Released: October 9, 2026 inqwed-legal 0.5.1 (PyPI and npm @qwed-ai/legal) · GitHub PR #102 · GitHub PR #103
Security patch.DeadlineGuardnow computes a deadline fromsigning_dateonly for terms measured forward from signing. Upgrade if you useDeadlineGuardthrough the Python API, the npm SDK, or the GitHub Action.
What changed
- Signing-anchor allowlist.
DeadlineGuardcomputes fromsigning_dateonly when the term is bare-relative ("30 days") or explicitly anchored to signing or execution ("30 days from signing","15 days after execution"). Every other term fails closed withUNVERIFIABLEand anUNSUPPORTEDtrace step. - Directional terms fail closed. Terms using
before,prior to,ahead of, orprecedingwere previously computed forward from signing, which could certify the opposite direction. - Any non-signing event anchor fails closed. Anchors outside the earlier event list, such as
"after closing","after completion", or"within 30 days of request", are now rejected. The open-ended event-noun list is replaced by an allowlist of signing-anchored phrasings. - Check runs before date arithmetic. Rejected business-day terms never consult the holiday calendar.
- npm engine version gate.
DeadlineVerifierin@qwed-ai/legalrejects its promise when the importable Pythonqwed-legalis older than 0.5.1. An npm-only upgrade cannot silently keep the old deadline engine. Upgrade both packages:
v7.2.2 — Verification soundness and safety hardening
Released: October 9, 2026 · GitHub Release ↗ · GitHub PR #434 · Advisories GHSA-4v5r-g7f4-vvgc, GHSA-mfh5-3c8f-975p, GHSA-j622-qffp-rc27, GHSA-mxwv-x5qm-mrmfv7.2.2 is a fail-closed security release covering four coordinated advisories. They affect all versions up tov7.2.1. No new capability and no breaking wire changes. Upgrade is recommended for any deployment onv6.0.0throughv7.2.1.
Security fixes
- Local SDK math verification. The SymPy expression validator in
QWEDLocalnow rejects string literals nested inside container arguments. This closes an allow-list bypass that could route untrusted model output to SymPy’s string-evaluation path. - Logic verification soundness. The Z3 constraint evaluator no longer accepts Python boolean operators (
and,or,not) or chained comparisons (a < b < c) on symbolic objects. The DSL compiler now rejects string literals instead of materializing them as variables. Both cases could previously yield aVERIFIEDverdict that did not follow from the input. - Large-number math verification.
verify_mathnow compares results at a precision scaled to the operands. Values beyond 15 significant digits are no longer rounded before the comparison, so a wrong claim on a large number can no longer pass asVERIFIED. - Computational cost bounds. The math parser applies its compute-cost gates to implicit-multiplication input (
2x,3(x+1)). The logic and DSL engines bound statically known integer powers. Short requests can no longer exhaust a worker.
Version bumps
Upgrading
Patch release with no code changes required for valid inputs. Pin the new version and redeploy:and/or/not, chained comparisons on symbolic variables, or string literals in DSL expressions now fail closed instead of returning VERIFIED. Rewrite Z3 constraints with And(...), Or(...), Not(...) and separate comparisons, and write DSL logic with the AND/OR/NOT operators.
QWED Open Responses v0.6.1 — ToolGuard verifies client action items
Released: October 9, 2026 inqwed-open-responses 0.6.1 (PyPI and npm) · GHSA-xhq6-w3f2-m5w6 · PR #71
ToolGuard now checks Responses API items that ask your client to run shell commands, apply patches, or control a computer. Before v0.6.1, these items read as tool-free content and passed. Upgrade from v0.6.0.
What changed
- Client action items are tool calls.
ToolGuardnormalizeslocal_shell_call,shell_call,apply_patch_call,computer_call,custom_tool_call, andmcp_approval_requestitems into tool calls. Blocklist, allowlist, dangerous-pattern, and validator checks apply. This covers direct items,output[]of a response object,tool_calls[]entries, and the streaming middleware. - Canonical tool names. Action items are checked as
local_shell,shell,apply_patch, andcomputer_use. Custom tool and MCP approval items use their ownname.local_shellargv lists are also scanned as a joined command line. - Unknown executable items fail closed. Any other
*_callor*_requestitem in a streamed item,output_itemevent, oroutput[]of a response object is blocked. Provider-hosted items (web_search_call,file_search_call,code_interpreter_call,image_generation_call,mcp_call) keep their existing behavior. - Ambiguous action items fail closed. An action item that also carries a
tool_call,function_call,function, ortool_callsenvelope is blocked. So is a named item whosetool_nameortoolNamedisagrees withname.
What this means for you
- If your agent runs local shell, patch, or computer-use tools, configure
ToolGuardwithuse_default_blocklist=Falseand anallowed_toolsallowlist. - If you rely on the default blocklist for shells, re-add those names through
blocked_toolswhen you opt out.
QWED-Finance v3.0.1 — ISO 20022, sanctions, and QueryGuard security fixes
Released: October 9, 2026 inqwed-finance 3.0.1 (PyPI, npm @qwed-ai/finance, and GitHub Action QWED-AI/qwed-finance@v3.0.1) · Changelog
qwed-finance v3.0.1 is the current release. It fixes three security advisories. Upgrade from v2.1.0 or v3.0.0.
What changed
- ISO 20022 business rules read the parsed document (GHSA-mrrj-6m2q-jch9).
CrossGuard.check_business_rules,verify_iso20022_with_rules, andUCPIntegration.verify_iso20022_paymenttake amounts and currencies from the parsed XML tree instead of regex over raw text. Amounts with child nodes, exponents, or grouping separators fail closed. - Sanctions screening resists in-word perturbations (GHSA-mv2c-jwm9-pfrq). Diacritics and look-alike letters are folded, and separator-free names are compared, so
E.V.I.L. CORPorÉVIL CORPno longer clears anEVIL CORPentry. - QueryGuard rejects MySQL executable comments (GHSA-q8r4-6gpp-5fx2). Queries containing
/*! ... */or/*M! ... */are unsafe in every QueryGuard check and inCrossGuard.verify_query_with_pii_protection.
Included from v3.0.0 (September 27, 2026)
- Breaking (npm): the
@qwed-ai/financebridge sends JSON over stdin with runtime validation. Malformed inputs are rejected instead of falling back to defaults. VerificationReceipt.get_signature(key): keyed HMAC-SHA256 over all 15 receipt fields, with no default key. The unkeyedget_signature()was removed.- ISO currency and amount business limits in receipts and UCP.
UCPIntegration.verify_iso20022_paymenttakeskyc_verifiedand requires it to approve. - Fail-closed AML, sanctions, amount, and message-validation fixes.
QWED-Legal — fail-closed date handling and input bounds
Released: October 7, 2026 inqwed-legal 0.5.0 · GitHub PR #89 · GitHub PR #90 · GitHub PR #91 · GitHub PR #92 · GitHub PR #93
DeadlineGuardandStatuteOfLimitationsGuardno longer guess what a date means. Order-ambiguous dates, partial dates, and event-anchored terms now returnUNVERIFIABLEinstead of a confident verdict.ClauseGuardandContradictionGuardgain input bounds.
What changed
- Order-ambiguous numeric dates fail closed (#90).
DeadlineGuardandStatuteOfLimitationsGuard, including computation-only mode, returnUNVERIFIABLEwith anAMBIGUITY_NOTEDtrace step for dates such as03/04/2026that read differently month-first and day-first. - Partial dates fail closed (#91). Inputs such as
"March 2024","Friday", or"23:00"would be completed from the system clock, so both guards reject them. In both cases, parsed datetimes in the result areNoneand the raw strings stay in the trace.DeadlineGuard.calculate_business_days_between()raisesValueErrorfor these dates. - Event-anchored terms fail closed (#92).
DeadlineGuardreturnsUNVERIFIABLEfor terms that start the clock at receipt, notice, delivery, occurrence, demand, invoice, breach, termination, service of process, or payment. It no longer computes those deadlines from the signing date. - Deadline comparison by calendar date (#92). Claimed and computed deadlines are compared by date, not timestamp. Timezone-aware claims are converted to the computed deadline’s timezone. Mixed timezone-aware and naive inputs return
UNVERIFIABLE.tolerance_daysstays in whole days. - ClauseGuard input bounds (#89).
check_consistency()returnsinvalid_inputfor more than 200 clauses or any clause over 4,096 characters. It reports at most 100 conflict pairs and adds a truncation notice when it stops. - Digit-run bound (#89). Digit runs longer than 18 characters fail closed in
DeadlineGuardterms,ClauseGuardday counts, andContradictionGuardoperands. - Faster US citation patterns (#89).
CitationGuardUS patterns no longer slow down on adversarial input. - Added — npm SDK (#93).
JurisdictionVerifier.verifyChoiceOfLaw()in@qwed-ai/legalaccepts an optional fourthcontractTypeargument and passes it to the guard ascontract_type.JurisdictionResultadds optionalcontractTypeandcontractClassification(goods,declared_non_goods, orunclassified) fields. OmittingcontractTypekeeps the existing partial-coverage warning. The 0.5.0 npm package is published as@qwed-ai/legal.
What this means for you
- Normalize dates to ISO
YYYY-MM-DDbefore calling the deadline and statute guards. - For event-anchored deadlines, pass the event date as
signing_dateand drop the anchor from the term. The result reports that date assigning_date, so keep the real signing date separately. - Treat
invalid_inputfromcheck_consistency()on oversized clause lists as a rejection, not a pass.
QWED Open Responses v0.6.0 — fail-closed hardening
Released: October 2, 2026 inqwed-open-responses 0.6.0 · PR #48 · PR #49 · PR #50 · PR #51 · PR #52 · PR #55 · PR #57 · PR #58 · PR #59
SchemaGuard now rejects undeclared root fields by default, verification is resource-bounded, and ToolGuard, the streaming middleware, and the Express middleware close several bypasses. The Python and npm packages both move to 0.6.0.
What changed
- SchemaGuard cross-language parity. JSON Schema
formatvalidation foremail,uuid,date-time,uri, andipv4now matches across Python and npm. - Resource-bounded verification. JSON responses over 100,000 characters or 100 nesting levels, and responses with oversized integers, return a failed verdict instead of raising an exception. SafetyGuard scanning fails closed above 10,000 nodes, 100,000 characters, or 12 nesting levels. Email PII detection uses a linear-time scan.
- SchemaGuard reports the first error only. Failed results carry one error, with the total error count reported as unknown (
total_errors=None,errors_truncated=True). - ToolGuard raw argument scanning. Dangerous patterns run on the raw parsed argument strings, including nested values and JSON-encoded calls, so custom patterns match across tabs and newlines. Cyclic or too-deep arguments still fail closed.
- ToolGuard name and command normalization. Blocked tool names are normalized for case, surrounding whitespace, dashes, underscores, and camelCase. The default blocklist adds
run_command,execute_command,command_line, andterminal. Command checks catch comment-separated keywords, spaced dots, and reorderedrmflags, including inside JSON- and base64-encoded arguments. Allowlist entries still require the exact configured separators. Custom validator keys match case and whitespace but don’t merge distinct names. - Streaming tool-call verification.
OpenResponsesMiddleware.verify_streamverifies each tool item in its original shape before yielding it. Tool-call types are normalized for case and whitespace. Unknown, ambiguous, or nested tool-shaped items are blocked.tool_resultandfunction_call_outputitems pass through when their correlation metadata is valid. ArgumentGuard and TaxGuard checks cover batched and nested calls. - SafetyGuard scans field names and quoted credentials. Object field names are now checked for PII and prompt injection, not only values. Quoted JSON credentials such as
"api_key": "sk-..."are blocked. Documented placeholders stay exempt. - npm SafetyGuard configuration parity. The TypeScript
SafetyGuardacceptscustomPatternsandpiiAllowList. Each matched custom pattern blocks and reports its owncustom_patternfinding. Invalid patterns throw at construction, and a pattern that exceeds its 25 ms execution limit fails the check.piiAllowListaccepts camelCase and snake_case names, such asipAddressandip_address. - ToolGuard envelope hardening. Envelopes that carry conflicting tool identities at the root, in a nested call, or in a
functionwrapper are blocked. Configured ToolGuards now checkstructured_outputresponses too. Nested scans that reach the depth limit block instead of reporting “No tool calls”. - Verification context propagation. A
contextwith usage caps now flows throughverify_tool_call,verify_structured_output, and the OpenAI, streaming, LangChain, and LlamaIndex integrations. The OpenAI wrapper takes it asverification_contextand doesn’t send it to OpenAI. The streaming middleware carries runningtotal_costandtotal_tokensforward, so later items can hit SafetyGuard budget limits. - Express middleware fails closed on unparsed bodies. The npm
verifyRequestBodymiddleware returns HTTP 422 when a request declares a body but has no parsed body, even withblockOnFailure: false. Bodyless requests and parsed empty JSON continue.
What this means for you
- Review SchemaGuard schemas for responses that carry extra root fields, and declare them or opt out.
- If you use the Express middleware, register a body parser such as
express.json()before it. - If ToolGuard now blocks your structured outputs or wrapped tool calls, give each call a single tool identity.
- To enforce budgets across a stream or agent run, pass usage totals in the verification context.
QWED-Legal — fail-closed hardening for guards, diagnostics, and GitHub Action
Released: October 7, 2026 inqwed-legal 0.5.0 (merged September 29, 2026) · GitHub PR #83 · GitHub PR #84 · GitHub PR #85 · GitHub PR #87 · GitHub PR #88
Five QWED-Legal paths that could report success over empty, blank, or unclassified input now fail closed. The QWED Legal GitHub Action now passes only on deterministic proofs.
What changed
LiabilityGuard.verify_tiered_liability: empty tier lists.tiers=[]returnsUNVERIFIABLEwithtotal_computed=Noneand noproof_ref, whetherclaimed_totalis zero or not.ProvenanceGuard: strict required fields.content_hash,model_id, andgeneration_timestampmust be non-empty strings.None,0,False,[], and whitespace now giveINCOMPLETE_PROVENANCE. Checks that cannot run are recorded ashash_integrity_skipped,timestamp_valid_skipped, andmodel_allowed_skippedinstead ofCONTENT_TAMPEREDorUNAUTHORIZED_MODEL.JurisdictionGuard: blank party entries.check_convention_applicability()returnsUNVERIFIABLEfor blank entries such as[""]. Genuine negatives such as["XX"]stay deterministic.JurisdictionGuard: three-waycontract_typeclassification for CISG. Goods contracts (separator and case variants accepted, bare"sale"excluded) get the CISG warning. Declared non-goods contracts such as"services"get no CISG warning. Missing, blank, or unknown values get a partial-coverage warning and are not verified. The US and non-US party pair no longer triggers the CISG warning by itself. The classification is recorded in trace inputs.- Diagnostics: no
proof_refwithout a deterministic step.LegalDiagnosticResult.verified()raises unless the evidence trace has aDETERMINISTICstep. Traceless or transport-stripped"consistent"verdicts and emptyclaim_textsare demoted toUNVERIFIABLE. Blank messages fall back to defaults instead of crashing.
QWED Verification — qwed init server identity handshake
Released: September 23, 2026 · GitHub PR #388 · Closes issue #376
qwed initnow proves that the local server holds this project’s secrets before it bootstraps an API key. Previously, a healthy/healthonly proved that something answered on the port. A foreign process or a stale server running older secret values could receive the bootstrap and issue a key that stops working after a restart.
What changed
- New
GET /health/identityendpoint. Takes a caller-suppliednonce(1–128 characters) and returns HMAC-SHA256 proofs over it underQWED_JWT_SECRET_KEYandQWED_API_KEY_LOOKUP_SECRET. Returns400for a missing or oversized nonce and503when server secrets aren’t configured. The secrets never leave the server. SeeGET /health/identity. - Nonce-bound, replay-safe handshake.
qwed initgenerates a fresh nonce on every run and compares the proofs against locally computed values in constant time. A captured response is useless for any later handshake. - Runs on reused and freshly spawned servers. A reused server can be foreign or stale even when secrets were retained. A fresh spawn can lose a port race after the health poll.
- Fails closed. A mismatch, a
404from an older server, any other non-200status, a malformed response, an unreachable server, or missing local secrets stopsqwed initwith exit status1before any key is issued. Each message tells you to restart the local server or re-runqwed init.
qwed init and troubleshooting.
QWED Verification — leaked API key auto-revocation and false-positive feedback
Released: September 22, 2026 · GitHub PR #380 · GitHub PR #386 · Closes issue #368 and issue #369
Verified reports from POST /webhooks/secret-scanning now trigger enforcement. QWED automatically revokes any matching active API key, records a security event, and emails the key owner. An opt-in reporter can send true-positive and false-positive labels back to the scanning service.
What changed
- Automatic revocation. A verified match against an active key sets
is_active=Falseandrevoked_at, and writes aSECRET_LEAK_AUTO_REVOKEDsecurity event. The event records only thekey_preview. The plaintext token is never persisted, logged, or emailed. - Owner notification. QWED emails the key’s user, falling back to the organization’s first active owner or admin. SMTP phases time out after 10 seconds. Revocation is committed first and is never rolled back if notification fails.
- Idempotent handling. Already-revoked keys and unknown tokens are not acted on. Redelivered or concurrent reports revoke and notify exactly once. Registered scanner patterns cover
qwed_live_keys only, soqwed_test_keys are never reported. - False-positive feedback reporter (opt-in). Set
QWED_LEAK_FEEDBACK_ENABLED=trueandQWED_LEAK_FEEDBACK_URLto POST one label per match after the revocation attempt. Items carry SHA-256 token hashes by default.QWED_LEAK_FEEDBACK_SEND_RAW=trueis a separate explicit opt-in to send plaintext tokens, and only works with an HTTPS URL.
QWED Verification — canonical API-key format spec
Released: September 22, 2026 · GitHub PR #387 · Closes issue #370QWED now publishes a canonical, machine-readable specification of its API-key formats, with lookaround-anchored scanning regexes and synthetic test vectors. Use it to build secret scanners, leak detectors, and SDK-side format validators.
What changed
- v2 checksummed format documented. Current keys match
qwed_live_[A-Za-z0-9]{36}: 30 random Base62 characters followed by a 6-character Base62 CRC32 checksum that you can validate offline. - Legacy v1 still accepted. Keys matching
qwed_live_[A-Za-z0-9_-]{43}keep working and are never force-rotated. qwed_test_exclusion. Scanners must exclude test keys from high-confidence production-secret matching.- Lookaround-anchored regexes. Published patterns use charset-complement lookarounds instead of
\b, because a v1 body can end in-. RE2-compatible delimiter-class alternatives are included. - Masking guidance. Masked previews show the prefix, asterisks, and only the last 4 characters.
- Machine-readable spec and test vectors.
spec/api-key-format.jsonandspec/api-key-test-vectors.jsonin theqwed-verificationrepository.
QWED Verification — signed secret-scanning webhook endpoint
Released: September 20, 2026 · GitHub PR #374 · Closes issue #367
New POST /webhooks/secret-scanning endpoint receives signed reports of leaked QWED API keys from GitHub secret scanning and compatible partner scanners. The endpoint is unauthenticated at the transport layer by design — external scanners have no QWED credentials — and the ECDSA-NIST-P256V1-SHA256 signature over the raw request body is the only credential. Verification runs before any JSON parsing, logging, or database work, and every failure mode fails closed to a distinct status code.
What changed
- New public endpoint.
POST /webhooks/secret-scanningaccepts a JSON array of leaked-token matches, verifies theGithub-Public-Key-IdentifierandGithub-Public-Key-Signatureheaders against GitHub’s published signing keys, and hands verified batches to the revocation sink in the background. SeePOST /webhooks/secret-scanningfor the full request/response contract. - Raw-body signature verification. The signature is verified over the exact bytes received; the body is never re-serialized. Nothing in the payload is parsed or logged until the signature is proven genuine.
- P-256 pinning at key-load. Signing keys are pinned to
secp256r1when the PEM is loaded, so a future keys-list entry on another curve is dropped at load rather than silently trusted at verify time. - 6-hour key cache with rotation retry. Signing keys are cached in-process for 6 hours with an
ETag-conditional refetch. On an unknownkey_identifier, QWED refetches the list exactly once (throttled to one forced refetch per 60 s after a success, 10 s after a failure) and retries. A dead or throttled key service fails closed to503. - Timing-safe verdict comparison. Success and failure branches run through
hmac.compare_digeston a fixed-shape verdict, and every failure returns the same generic detail so the failing step is not observable. - Fail-closed status separation.
401for missing headers,403for a proven-bad signature against a fresh anchor,503for any state where verification was impossible (signing-key service down, anchor unusable, rotation retry throttled).413for over-cap bodies (1 MiB) or batches (500 matches);400for a malformed envelope. Tokens never appear in logs. - Optional keys-fetch token.
GITHUB_KEYS_TOKEN(orGITHUB_TOKENas a fallback) can be set to raise the outbound rate limit on the keys request. Neither token grants any authority over the incoming webhook — only the signature does.
QWED Verification — qwed init provisions QWED_API_KEY_LOOKUP_SECRET
Released: September 21, 2026 · GitHub PR #375 · Closes issue #372
Freshqwed initruns no longer crash the child server at boot on the fail-closedQWED_API_KEY_LOOKUP_SECRETimport check. The CLI now generates the lookup secret alongsideQWED_JWT_SECRET_KEY, writes it to.env, and threads it into the local server it starts.
What changed
- Auto-provisioned on first run.
qwed initgenerates a URL-safe randomQWED_API_KEY_LOOKUP_SECRET(at least 48 bytes) whenever one is not already set, persists it to.envwith the same0600permissions as other credentials, and exports it into the child server’s environment. - Retained on re-runs, never rotated. If
.env, the process environment, or the project-root.envalready holds a value,qwed initreuses it verbatim. Rotating this secret invalidates every stored API-key lookup digest, so the CLI will not do it for you. - Refuses to persist an unbootable value. If the existing
QWED_API_KEY_LOOKUP_SECRETequalsQWED_JWT_SECRET_KEY,qwed initexits with an error instead of writing a value the server would refuse to start with. Remove the offending line (a fresh distinct value is generated) or set a distinct value manually. - Stale-secret guard around the local server. If
qwed initregenerates a missing secret but a QWED server is already running with older values, the CLI stops before bootstrapping a new API key — issuing one against a server that cannot verify it would produce a key that dies on the next restart.
qwed init and API key storage and migration for the full contract.
QWED-A2A — /a2a/intercept requires per-agent API keys (breaking)
Released: September 9, 2026 · GitHub PR #106
The FastAPI gateway’sPOST /a2a/interceptendpoint previously trusted the body-declaredsender_agent_id— any caller could impersonate any agent for trust evaluation, rate limits, and attestation. The endpoint now authenticates every request with a per-agent API key in theX-API-Keyheader, validated against theQWED_A2A_API_KEYSenvironment variable. The authenticated identity overrides the body’ssender_agent_ideverywhere it matters. The deadAgentMessage.signaturefield, which was never verified, is removed.
What changed
- Per-agent API-key authentication —
QWED_A2A_API_KEYSholds a JSON object mapping each key to its agent ID, e.g.{"key-abc123": "procurement-agent"}. Missing, unknown, or unconfigured keys return401. Keys are compared in constant time. - Authenticated identity wins — trust-boundary evaluation, token-bucket rate limiting, telemetry, and the attestation JWT all bind to the key’s agent ID. If the body’s
sender_agent_iddiffers, the gateway logs a warning and overrides it. Direct library callers ofintercept()get no override and own identity binding themselves. - No-restart key rotation — the key map is read fresh on every request. Update the environment variable and old keys stop matching immediately.
- Agent-ID normalization — map entries are trimmed and validated against the
AgentMessagecontract (max 256 chars, no control characters); invalid entries are dropped and their keys denied. AgentMessage.signatureremoved — the optional field was documented as “JWT signature for tamper detection” but was never read by any verification path. Attestation integrity comes from the verdict JWT, not a caller-supplied field.
Migration
Before (now returns401):
signature in the message body, remove the field. See Gateway authentication for the full key format and rotation guidance.
QWED-A2A — cross-deployment trusted issuers and jti replay lifecycle (security release)
Released: September 9, 2026 · GitHub PR #108 · GitHub PR #109 · Closes issue #84 and issue #85Two fixes toA2ACryptoServiceattestation verification. Peer deployments can now be registered as trusted issuers, so verifying another deployment’s attestations no longer tempts operators into sharing one private key across deployments. And the jti replay registry now separates issuance from consumption: an issuer can verify its own token once, duplicatetrace_ids are rejected at signing, and the consumption registry is injectable for cross-worker replay protection.
Cross-deployment verification (PR #108)
- Trusted issuers.
verify_attestationaccepts an optionaltrusted_issuersmapping of issuer IDs to{"deployment_id": ..., "jwks": ...}, with theQWED_A2A_TRUSTED_ISSUERSenvironment variable as fallback. Register a peer by copying its/.well-known/jwks.jsonentry verbatim. The env var is re-read on every verification, so key rotation needs no restart. - Fail-closed on every mismatch. Unknown issuers,
kidmismatches, anddeployment_idmismatches all deny. Routing never trusts unverified claims: header and payload are read only after a candidate key verifies the signature, and one public key registered under two issuer identities keeps only the first identity. - Verifier-only nodes. A node with no local signing key can verify peer tokens against trusted issuers. Only signing, JWKS publishing, and the interceptor still require
QWED_A2A_SIGNING_KEY_PEM. With neither a local key nor trusted issuers, verification returnsNo verification keys availableinstead of raising. - Peer jti namespacing. Replay tracking for peer tokens is namespaced per issuer, so two deployments reusing the same trace ID never shadow each other.
jti registry lifecycle (PR #109)
- Issuance/consumption split.
sign_verdictrecords thetrace_idin a separate per-instance issuance record instead of the consumption registry, so an issuer can verify its own token once — the second presentation is a replay. - Duplicate trace IDs rejected at signing. Reusing a
trace_idwithin its validity window raisesValueErroratsign_verdictinstead of minting two tokens under onejti. - Injectable consumption registry.
A2ACryptoService(jti_registry=...)accepts a shared registry (Redis, database) for cross-worker replay protection. The default remains process-local: two workers in two processes share nothing. Injected registries must provide atomic insert-if-absent, retention covering token lifetime, and a numericttl_secondsat leastvalidity_seconds— anything less raisesValueErrorat construction.
Upgrading
See Crypto attestations for configuration and the full replay-scope documentation.v7.2.1 — Math answer comparison security patch
Released: September 14, 2026 · GitHub Release ↗ · GitHub PR #365 · Advisory GHSA-xmm6-8r3x-j567v7.2.1 is a fail-closed security patch. The math answer comparison path previously fed the untrusted model answer to rawsympy.sympify, which is an eval sink that can execute dunder-traversal gadgets smuggled in the answer turn. The answer is now parsed only through the hardenedsafe_parse_expr, and unparseable input fails closed. No new capability and no breaking wire changes. Upgrade is recommended for any deployment onv6.0.0throughv7.2.0.
Security fix
- Hardened answer parsing.
QWEDLocal._math_answers_matchno longer callssympy.sympifyon the model’s answer. It now parses throughsafe_parse_expr, which applies a character allow-list, an AST allow-list, strips builtins, and enforces the compute-cost bounds introduced in v7.2.0. Unparseable answers and a missing parser both returnFalse, so the comparison never treats an attacker-controlled string as “equal” by accident. - Release boundary gate covers the SDK.
scripts/check_boundary.pynow scansqwed_sdk/and forbids rawsympifyoutside approved wrappers, so the eval sink cannot reappear in a future refactor.
Version bumps
Upgrading
Patch release with no code changes required. Pinqwed>=7.2.1 on PyPI, @qwed-ai/sdk@^7.2.1 on npm, and qwed = "7.2.1" on crates.io, then redeploy. Wire formats and public APIs are unchanged.
v7.2.0 — Precision advisory and security hardening batch
Released: September 7, 2026 · GitHub Release ↗ · GitHub PR #362v7.2.0 unifies the precision advisory capability and the security hardening batch across all SDKs. One additive capability — an advisory flag for binary floating-point constants in math and stats verification — plus fail-closed fixes: bounded engine-call waits, sandbox containment, and event-loop offload. The release is a semver minor with no breaking wire changes.
Precision advisory (new capability)
Math and stats verification inputs may contain binary floating-point constants (0.1 + 0.2, 1e9) whose evaluation can be inexact relative to decimal arithmetic. v7.2.0 surfaces that signal as a DiagnosticResult advisory without ever affecting the verdict (PR #348):
AdvisoryCheck.float_precision()parses the verification source and, when float or complex constants are present, returns an advisory withconstraint_id: "precision.float-constants"listing the offending constants and suggestingdecimal.Decimalor SymPy exact rationals where exact arithmetic matters./verify/mathattaches the advisory to the result on all branches, before trust enforcement.StatsVerifier.verify_statscarries the advisory on completed analyses when the generated code uses floats — the expected shape for numpy/pandas code, so it flags without disrupting.- Advisory, never a gate. Execution-safety gates decide what may run, not what is exact. Documented inputs like
1000 * (1 + 0.05)**2legitimately contain floats and still verify. Unparsable input returns no advisory; parse failures belong to the security gates.
Engine-call bounds (PR #354)
Every engine-call wait is now bounded so a single request cannot stall or exhaust the service:- SymPy compute-cost gate. The safe expression parser adds a computational-cost layer: integer-literal cap (10^300), static exponent bound (10^4), exact-expansion bounds on
factorial/binomialcalls, caret-chain fold bounds, and a nested-power base-magnitude budget. Expressions like2**factorial(10000)or((9**9)**9999)**9999are rejected before they can reach SymPy’s eager exact expansion. All cost comparisons use exact arithmetic, never binary floats. - Provider HTTP timeouts. All six LLM provider clients are pinned to a 30-second timeout with zero SDK-level retries. The previous SDK defaults allowed roughly 30 minutes per stalled call. Retry policy belongs to the caller.
- Stats upload cap.
/verify/statsgains a byte-counting body-limit middleware with a 30-second read deadline and an in-flight concurrency cap, a 10 MB file cap with header preflight, and an early abort at 1 million cells.
Sandbox containment (PR #351)
The Docker code-execution sandbox can no longer leak host resources (CWE-400/401):- Log rotation and process caps. Sandbox containers run with json-file log rotation (10 MB, single file) and
pids_limit=128. - Guaranteed container removal. Containers are force-removed in a
finallyblock covering the whole post-creation lifecycle, with create-then-start ordering so start failures cannot leak, one retry for transient daemon races, and a warn-only fallback so cleanup failure never discards a valid verification result. - Result size caps.
result.jsonis capped at 2 MB both inside the container wrapper (streamed, aborts at cap) and at host read-back before parsing.observed_resultand everyVerificationLog.resultsite are bounded by a shared pre-encode traversal with per-string caps, an aggregate budget, and cycle markers, and remain valid JSON for the audit integrity verifier.
Event-loop offload (PR #352)
/verify/consensus and /verify/stats declared async def but ran synchronous verification chains inline on the event loop, so a single low-rate tenant could stall the whole service:
- Consensus now awaits an async path with a per-call executor sized to the engine list, one aggregate deadline, and per-engine timeouts. Timed-out, errored, or circuit-open engines degrade to partial
BLOCKEDresults with correct circuit-breaker recording instead of hanging the request. - Stats offloads CSV parsing and the verification chain to a worker thread, and every Docker daemon call is bounded at 30 seconds.
Version bumps
Included PRs
The v7.2.0 release also rolls up the security fixes released individually since v7.1.0. See the entries below for the metrics operator allowlist, the math parser and sandbox gate hardening, and the API-key lookup migration.
Upgrading
No action required for the changes in this entry: the precision advisory is additive, and the hardening fixes restore intended behavior with no wire changes. Requests that previously stalled or over-consumed resources — unbounded exponent expressions, oversized stats uploads, oversized sandbox results — now returnBLOCKED or 413-class rejections instead. If you upgrade from v7.1.0 directly, also review the API-key migration notes in the September 2 entry.
QWED Verification — all-tenant metrics restricted to explicit platform operators (security release)
Released: September 4, 2026 · GitHub PR #349 · Closes issue #337 (CWE-284, CVSS 7.1)GET /metricsandGET /metrics/prometheusexposed every organization’s request volumes, latencies, provider usage, and per-tenant breakdowns to any self-service signup. The access gate treated the organization-scopedowner/adminrole as platform-wide authority, and since signup is the only user-creation path — hardcodingrole="owner"for every new account — the gate degenerated to “possess any account,” one anonymous request away. Access is now an explicit operator allowlist, fail-closed when unset.
What changed
- Operator allowlist. All-tenant metrics require the caller’s user ID to be listed in
QWED_METRICS_OPERATOR_USER_IDS(comma-separated). The list is read per request, so granting or rotating operators takes effect without a restart. - Org roles are no longer platform authority.
ownerandadminremain meaningful inside their own organization; they confer no cross-tenant access anywhere. - API-key path hardened. A key resolves through its owning user, who must be on the allowlist. Expired (
expires_at) or revoked (revoked_at) keys are no longer usable credentials — but they also no longer preempt a valid operator JWT presented in the same request. Key-only callers without a valid credential are still denied. - Fail-closed by default. With the variable unset, both endpoints deny every caller. Per-tenant metrics (
GET /metrics/{organization_id}) are unchanged.
Behavior changes
QWED Verification — float precision advisory for math and stats verification
Released: September 3, 2026 · GitHub PR #348 · Closes issue #347Math and stats inputs may contain binary floating-point constants (0.1 + 0.2,1e9) whose evaluation can be inexact relative to decimal arithmetic. A new advisory surfaces that signal without gating anything: it is a precision advisory, never a rejection, because execution-safety gates decide what may run, not what is exact.
What changed
precision.float-constantsadvisory. When an expression or generated statistics code contains float or complex constants, the result carries anAdvisoryCheckindeveloper_fields.advisory_checkslisting the offending constants and suggestingdecimal.Decimalor exact SymPy rationals./verify/math. The advisory is attached on all result branches. The submitted expression is checked lexically, so equations (0.1 + 0.2 = 0.3,0.5x = 0.5x) and collapsing forms retain their float literals even when symbolic simplification would eliminate them. Scientific notation and complex literals (1.0j) are flagged too.- Stats verification. The completed-analysis result carries the advisory when the generated code uses floats — the expected shape for numpy/pandas code, so it flags without disrupting.
- Structurally non-verdict-affecting.
AdvisoryCheckenforcesadvisory_only=Trueat construction. The advisory cannot change the verificationstatusorproof_ref. Unparsable input yields no advisory; parse failures belong to the security gates.
QWED Verification — structural math parser and sandbox gate hardening (security release)
Released: September 3, 2026 · GitHub PR #344 · GitHub PR #346Two security fixes close sandbox-escape bypasses in math expression parsing and sandboxed code execution. The math parser now validates expressions structurally (NFKC normalization, ASCII charset gate, AST node allowlist) instead of relying on a denylist, and the consensus and stats execution paths gained a single-expression structural gate plus a module-indirection blocklist.
Math expression parser (PR #344)
The safe expression parser applies layered structural validation before any SymPy evaluation:- NFKC normalization first. CPython normalizes identifiers at compile time, so Unicode look-alike codepoints (mathematical bold letters, fullwidth forms) could previously slip past string filters. All checks now see exactly what the compiler sees.
- ASCII charset allowlist. Only letters, digits, underscore, whitespace, and
+ - * / ( ) . , ^ %are accepted. A.is legal only as a decimal point with digits on both sides, which makes attribute access structurally impossible on the implicit-multiplication path. - AST node-type allowlist. Python-parseable input may contain only arithmetic operators, function calls, names, and numeric constants. Attribute access, subscripts, string constants, lambdas, comprehensions, and comparisons are rejected before evaluation.
- Caret exponentiation fixed.
convert_xorjoined the default transformation pipeline, sox^2parses asx**2instead of failing at evaluation time. - The regex denylist is retained as defense in depth.
Execution gates (PR #346)
- Single-expression gate on translated math. A translated math expression must parse as exactly one Python expression (
ast.parseinevalmode). Multi-statement, import-bearing, and multi-line-smuggled input fails by construction. Expressions over 500 characters are rejected before parsing. - Module-indirection blocklist. The pre-execution AST check on sandboxed code now inspects every dotted segment of imports and attribute chains. Blocked module roots were extended (
posix,nt,importlib,ctypes,builtins), OS-primitive call names (system,popen,import_module, theexec*/spawn*families,fork) are matched on bare names and attribute targets, and package-internal re-export gadgets through pandas/numpy internals are caught. sysread-only allowlist in the stats executor. Only named read-only interpreter metadata is accessible; frame introspection and every othersysmember fail closed.
Behavior changes
What this means for you
A character denylist can never defend aneval sink — these releases replace pattern-matching known attacks with structural guarantees that reject entire vulnerability classes by construction. See Math engine — Expression input rules, Consensus engine — Secure execution gates, and Stats engine — Pre-execution security validation.
QWED Verification — API-key lookup migration and per-IP auth throttling
Released: September 2, 2026 · GitHub PR #345API-key lookup digests move from PBKDF2 to HMAC-SHA256 keyed by a new requiredQWED_API_KEY_LOOKUP_SECRET, removing ~67ms of CPU cost from every unauthenticated request. Anonymous/auth/*routes gain a per-IP rate limit, and password hashing no longer blocks the event loop.
Breaking changes
What changed
- HMAC-SHA256 API-key lookup. The previous PBKDF2 digest (100,000 iterations, ~67ms) ran on every request carrying an
x-api-keyheader, valid or not, letting ~15 garbage requests per second saturate the service. API keys are high-entropy random tokens, so the KDF cost bought no brute-force resistance. The replacement keyed MAC costs microseconds. - Dedicated lookup secret (required). Digests are keyed by
QWED_API_KEY_LOOKUP_SECRET. Self-hosted servers fail closed at startup if it is missing or equal toQWED_JWT_SECRET_KEY— reusing the JWT secret would silently break every API-key lookup on the next JWT-secret rotation. Set it before issuing v7.2 keys; changing it later requires a one-time re-issue. - Per-IP rate limit on
/auth/*. Anonymous auth routes (signup, signin) are throttled per client IP:QWED_RATE_LIMIT_PER_IP, default 10 requests per minute. Over-limit requests get429 Too Many Requestswith aRetry-Afterheader of at least 1 second.X-Forwarded-Foris honored only when the direct peer is listed inQWED_AUTH_TRUSTED_PROXIES(comma-separated CIDRs, default empty), and only the rightmost hop is used, so clients cannot choose their own bucket. The IP table is hard-bounded against spoofed-address floods. - Password hashing off the event loop. Signup and signin bcrypt calls run in a worker thread instead of blocking the server. Signin also performs a dummy verify on unknown emails, so response timing no longer reveals which addresses are registered.
- Atomic signup. The organization and user rows commit in one transaction; a failure mid-signup rolls back both instead of stranding an orphaned organization.
What this means for you
Re-issue any API key created before v7.2. Self-hosted operators must addQWED_API_KEY_LOOKUP_SECRET to their environment (distinct from QWED_JWT_SECRET_KEY) and, if running behind a proxy, set QWED_AUTH_TRUSTED_PROXIES so per-IP throttling keys on real client addresses. See Authentication for the migration path and Rate limits for the throttle contract.
QWED-Tax — npm verifier enforces structured nexus claims fail-closed
Released: September 1, 2026 · GitHub PR #65The@qwed-ai/taxnpm verifier now requires the structured booleanclaimed_collects_taxclaim for economic-nexus checks, matching the Python guard’s contract. Unknown states, non-boolean claims, and malformed sales facts fail closed instead of passing as no-nexus.
What changed
- Structured claim required —
TaxPreFlight.auditandNexusGuard.checkNexusverify the booleanclaimed_collects_taxagainst the independently computed nexus. Non-boolean values (including strings like"false") are rejected with"Computed nexus liability only. Provide claimed_collects_tax as a boolean for deterministic verification.". - Legacy fallback narrowed —
tax_decision: 'no_tax'maps toclaimed_collects_tax: falseonly when the intent does not contain aclaimed_collects_taxkey. A present-but-invalid structured claim is blocked even when the legacy string is also set. - Unknown states fail closed — states not in the npm threshold table (currently NY, CA, TX, FL) return
verified: falsewith a block-pending-configuration error instead of being verified as no-nexus. - Input hardening — nexus validation runs whenever
sales_datais present (including falsy or explicitlyundefinedvalues). Non-stringstate, non-finite amounts (NaN,Infinity), and negative sales or transaction counts are rejected before threshold math.
What this means for you
If your frontend or Node integration relied on unknown states passing, on truthy strings as claims, or on omittingsales_data fields to skip the nexus check, those intents now block. Pass claimed_collects_tax as an explicit boolean and treat unknown-state blocks as hold-and-escalate signals. See Economic nexus in the TypeScript SDK for the full contract.
QWED-Tax — explicit boolean claim required for economic nexus verification
Released: August 31, 2026 · GitHub PR #62Economic nexus verification now fails closed unless the caller provides an explicitclaimed_collects_taxboolean. Free-formtax_decision/llm_decisiontext is no longer interpreted as a verification claim — free-form model output cannot be a verification substrate.
What changed
NexusGuard.check_nexus_liability— Gains a keyword-onlyclaimed_collects_tax: boolparameter. The guard computes the state’s threshold independently and verifies the boolean against the computed nexus. When the parameter is omitted, the guard returns{"verified": False, "computed_only": True, "has_nexus": <bool>, "error": "Computed nexus liability only. Provide claimed_collects_tax as a boolean for deterministic verification."}. Non-boolean values (including truthy strings) returnverified=Falsewith an invalid-claim error.llm_decisiondeprecated — The parameter is retained for positional compatibility but is never parsed. Passing it has no effect on the verdict.TaxPreFlighteconomic_nexusintents — The required claim field changed fromtax_decision(string) toclaimed_collects_tax(boolean). Intents that still sendtax_decisionare blocked as incomplete claims beforeNexusGuardruns.
Breaking changes
What this means for you
An LLM can no longer “verify” its own nexus decision by phrasing it in text — the claim must be a machine-checkable boolean that the guard compares against the deterministically computed threshold. See NexusGuard for the full parameter contract and TaxPreFlight integration for the intent migration.QWED Open Responses — fail-closed verification and expanded guard coverage
Released: August 30, 2026 · GitHub PR #33
verify() now fails closed when no guards are configured, ToolGuard recognizes and polices more tool-call envelope shapes, and SafetyGuard scans nested content. All three changes ship in both the Python and TypeScript packages.
What changed
- Zero-guard verify fails closed. Absence of verification is not success. A
verify()call with no guards returns aResponseVerifierguard result withseverity="error"and the block reason “No guards configured — fail-closed (zero-guard verify).” - ToolGuard envelope coverage. ToolGuard now extracts tool calls from Anthropic
content[].type=tool_useblocks and matchestypevalues case-insensitively, so aTool_Useblock is policed instead of ignored. OpenAIfunctionwrappers and JSON-encoded argument strings are normalized before blocklist and dangerous-pattern checks. - ToolGuard fail-closed rejections. Tool-like content in an unrecognized shape, non-object entries in
tool_calls/choices/content, hybrid envelopes that mix a direct tool call with a sibling collection, nameless tool calls, and argument payloads over 10,000 characters or 128 nesting levels are all blocked instead of passing silently. - SafetyGuard recursive extraction. SafetyGuard scans string content nested up to 12 levels deep, including the canonical
choices[].message.contentshape, so PII, injection, and harmful patterns inside nested structures are detected.
What this means for you
If your agent relied onverify() passing with no guards, or on responses in unrecognized tool-call shapes passing as “no tool calls”, those calls now block. Configure the guards you need and emit tool calls in a recognized envelope.
See the guards reference and troubleshooting for the updated contracts.
Released: August 31, 2026 · GitHub PR #62
Economic nexus verification now fails closed unless the caller provides an explicitclaimed_collects_taxboolean. Free-formtax_decision/llm_decisiontext is no longer interpreted as a verification claim — free-form model output cannot be a verification substrate.
What changed
NexusGuard.check_nexus_liability— Gains a keyword-onlyclaimed_collects_tax: boolparameter. The guard computes the state’s threshold independently and verifies the boolean against the computed nexus. When the parameter is omitted, the guard returns{"verified": False, "computed_only": True, "has_nexus": <bool>, "error": "Computed nexus liability only. Provide claimed_collects_tax as a boolean for deterministic verification."}. Non-boolean values (including truthy strings) returnverified=Falsewith an invalid-claim error.llm_decisiondeprecated — The parameter is retained for positional compatibility but is never parsed. Passing it has no effect on the verdict.TaxPreFlighteconomic_nexusintents — The required claim field changed fromtax_decision(string) toclaimed_collects_tax(boolean). Intents that still sendtax_decisionare blocked as incomplete claims beforeNexusGuardruns.
Breaking changes
What this means for you
An LLM can no longer “verify” its own nexus decision by phrasing it in text — the claim must be a machine-checkable boolean that the guard compares against the deterministically computed threshold. See NexusGuard for the full parameter contract and TaxPreFlight integration for the intent migration.QWED-MCP v0.2.2 — hardened math expression sandbox (security release)
Released: August 27, 2026 · GitHub PR #48 · GHSA-2p69-jpm6-jrxh ↗
qwed-mcp v0.2.2 fixes GHSA-2p69-jpm6-jrxh (CWE-94), a residual sandbox-escape bypass in the math expression parser. The previous regex denylist could be evaded through unlisted dunder names, string-literal splitting, and NFKC-equivalent Unicode identifiers. The parser now validates expressions structurally before any evaluation.
What changed
The math expression sandbox inqwed-mcp now applies layered validation before an expression reaches SymPy’s parse_expr:
- AST node allowlist. Only arithmetic node types survive (binary and unary operations, calls, names, numeric constants). The allowlist rejects attribute access, subscripts, lambdas, and comprehensions before parsing. Every sandbox-escape traversal needs an attribute or subscript node; legitimate math never does.
- String and bytes literals rejected. The validator fails expressions containing string or bytes constants with
SafeParserError, closing the concatenation-gadget class (for example'__glo'+'bals'+'__'). - NFKC normalization before validation. CPython NFKC-normalizes identifiers at compile time, so Unicode look-alike characters (mathematical bold letters, fullwidth forms) could previously bypass the ASCII denylist. The sandbox now normalizes input first, so the checks see exactly what the compiler sees.
- Implicit-multiplication charset guard. Inputs that are not valid Python (such as
2xorsin x) cannot pass AST checks, so the guard restricts their character set instead: it bans quotes, brackets, and separators, and allows.only as a decimal point.2x,sin x,x^2,2.5x, and2(x+1)still parse; the guard blocks2x.__class__. - Regex denylist retained as defense in depth.
What this means for you
Upgrade toqwed-mcp 0.2.2 or later:
SafeParserError instead of reaching the evaluator.
See the MCP tools reference for the current tool surface.
QWED-Infra v0.3.0 — Verification Context v1.0 and attestation trust boundary
Released: August 24, 2026Everyqwed-infraguard now emits a portable Verification Context v1.0 document viato_verification_context(), andADMITdecisions now require a cryptographically valid ES256 attestation bound to the exact claim and evidence.
Added
to_verification_context()on all four guards — IamGuard (#45), NetworkGuard (#46), CostGuard (#48), and ArtifactBoundaryGuard (#50) each produce a schema-valid, tamper-evident VC document with claim, verifier identity,sha256-bound evidence, and anADMIT/DENYadmission decision, built on the shared bridge module from PR #44.- Attestation trust boundary (PR #54) — ES256 (ECDSA P-256) JWT attestation service with a never-
Nonefail-closedAttestationResultcontract, revocation registry, and a single consumption-side gate validating signature, issuer, expiry, and revocation plus claim bindings (status match,query_hash == sha256(formal_statement),proof_hash == proof_ref). mint_diagnostic_attestation()— issues a token bound to aVERIFIEDdiagnostic’s own evidence commitment.
Breaking changes
- Admission semantics —
VERIFIEDresults admit only with a cryptographically valid attestation bound to the exact claim and evidence. Arbitrary non-empty attestation strings are rejected as forged tokens and produceBLOCKED. A missing token demotesVERIFIEDtoUNVERIFIABLE/DENY. Callers previously passing placeholder tokens must mint viacreate_verification_attestation()ormint_diagnostic_attestation(). - New runtime dependencies —
pyjwtandcryptographyfor attestation signing and validation.
Fixed
- Fail-closed on malformed inputs at every VC boundary (PR #51 and follow-ups) — undecimal budgets, non-string build backends, malformed topology, policy, or package inputs, symlink escapes and loops, and wheel entries outside the scanned boundary all map to
BLOCKED/DENYdocuments instead of exceptions or guessed approval.
v7.1.0 — Verification Context v1.0 Rollout
Released: August 16, 2026 · GitHub Release ↗ · GitHub PR #317
v7.1.0 ships Verification Context v1.0 end-to-end: a formal specification with a machine-readable JSON Schema, a typed document model with fail-closed invariants, public proof_ref generation and resolution, and exposure across the API, Python SDK, CLI, verifiers, and the Docker GitHub Action. The release is additive — a semver minor with no breaking wire changes.
Spec and core model
- Verification Context v1.0 specification — the atomic JSON record of a verification: verified object, four context layers (interpretation / proof / evidence / decision), verdict, and admission, with canonical RFC 8785 encoding for the
proof_refevidence commitment (PR #302). See the Verification Context specification. VerificationContextDocumentmodel + schema validation — typedVerificationContext,VerificationContextDocument,Verdict,Admission, and nested layer types with fail-closed invariants enforced at construction:VERIFIEDrequires asha256:<64-hex>proof_ref;UNVERIFIABLE/BLOCKEDrequireproof_ref: nullwith admissionDENY(PR #308).- Public
proof_refgeneration and resolution —compute_document_proof_ref()andresolve_document_proof_ref()derive and verify the content-bound SHA-256 commitment over the canonical document. Resolution fails closed: aproof_refthat cannot be resolved confers no authority (PR #309).
Surface exposure
- API endpoints —
POST /verification-context/from-diagnostic,/validate, and/resolveconvertDiagnosticResultrecords into VC documents, validate documents against the schema, and resolveproof_refcommitments (PR #311). - CLI commands — the
qwed contextgroup:validate,resolve, andfrom-diagnostic(PR #311). - SDK re-exports — all VC v1.0 types and helpers importable directly from
qwed_sdk, pluscreate_verification_context_from_diagnostic(),validate_verification_context(), andresolve_verification_context()on both sync and async clients. See the Python SDK (PRs #311, #315). to_verification_context()across the verifier classes — implemented on the Code, Consensus, DSL Logic, Fact, Graph Fact, Image, Logic, Reasoning, Schema, SQL, Stats, and Symbolic verifiers plus theBatchFactVerifierandMultiVLMVerifierhelpers, each mapping aDiagnosticResultto a VC document. Math and the secure code executor have no such method; their results convert throughPOST /verification-context/from-diagnostic(PRs #310, #316).- Docker action VC outputs — the GitHub Action emits
verdict,admission,proof_ref, andverification_contextoutputs in every mode (PR #313).
Fail-closed behavior
- A
VERIFIEDdiagnostic without an attestation token fails closed toBLOCKED(trust_gate.mandatory_attestation_missing) when converted to a VC document — attestation is enforced through the same trust boundary as/verify/*. - Malformed diagnostic payloads convert to
BLOCKEDdocuments instead of crashing, so the audit trail records the failure. resolvereturnstrueonly for a schema-validVERIFIEDdocument whose storedproof_refmatches the re-derived commitment.
Version bumps
Included PRs
Upgrading
No action required. Every change is additive: existing/verify/* responses, SDK methods, CLI commands, and action outputs are unchanged. Adopt Verification Context by calling the new endpoints, importing the new types from qwed_sdk, or reading the new action outputs.
v7.0.0 — Full DiagnosticResult engine conformance
Released: August 8, 2026 · GitHub Release ↗ · GitHub PR #300v7.0.0 completes the engine migration to the unifiedDiagnosticResultcontract (META #216).SchemaVerifier,SQLVerifier,CodeVerifier,SecureCodeExecutor, andStatsVerifier— plus the fact and image batch entry points — now all returnDiagnosticResult(status/agent_message/developer_fields/proof_ref). The release also completes the truth-vs-admission separation: verification answers “is this claim provably true?” while a separateAdmissionDecisionanswers “should this be allowed at this boundary?”.
Breaking: POST /verify/code — VERIFIED is truth, admission is policy
Proving a snippet is unsafe is a successful proof. Unsafe code is therefore VERIFIED (previously BLOCKED), with developer_fields.is_valid: false, a bound proof_ref, and a non-null critical_count. BLOCKED is reserved for cases where verification itself failed (empty code, non-string language, internal errors), and blocked results carry no proof_ref. The response attaches an explicit admission field (ADMIT / BLOCKED) so authority-only consumers reading status == "VERIFIED" cannot admit unsafe code.
Before (v6.x) — unsafe code:
status == "BLOCKED" or status == "VERIFIED" for safety gating must switch to the admission field or developer_fields.is_valid. status == "VERIFIED" alone must never be treated as “safe to execute”.
Breaking: POST /verify/stats — execution success is never VERIFIED
A run that executes cleanly in the Docker sandbox and returns an observed statistic is UNVERIFIABLE (stats_verifier.claim_not_verified) — the engine has no deterministic claim-proof, so it cannot attest the original natural-language claim. Execution evidence (observed_result, generated_code, columns, a deterministic dataset_sha256, sandbox type, timing, and security checks) is retained in developer_fields for audit. BLOCKED is reserved for failure states: stats_verifier.validation_error, stats_verifier.execution_failure, and stats_verifier.runtime_unavailable.
Before (v6.x) — successful execution:
developer_fields.observed_result instead of result, and treat the verdict as advisory — a legitimate execution is not a proven claim. compute_statistics() and get_sandbox_info() are utilities, not verification boundaries, and keep their existing dict return shape.
SchemaVerifier → DiagnosticResult (#294)
verify()andverify_ucp_transaction()returnDiagnosticResult. A schema violation isVERIFIED(as-invalid) withdeveloper_fields.is_valid: false;BLOCKED(schema_verifier.parse_error/schema_verifier.validation_error) is reserved for schemas that cannot be parsed or validated.proof_refis computed deterministically from canonical JSON of the schema + instance evidence; unsupported values and cyclic structures fail closed toBLOCKED.- Recursive schema meta-validation: malformed keyword shapes (non-dict
properties, invalidrequiredentries, invalid or non-finite numeric constraints, negative size constraints) returnBLOCKEDinstead of being silently treated as empty. Oversized integer bounds (e.g.10**1000) no longer raiseOverflowError. - UCP hardening: money arithmetic uses
Decimalquantized to the currency precision (no more0.01float tolerance),taxis selected by key presence sotax: 0is honored, verdict fields (transaction_type,currency,schema_verifier.ucp_*constraint ids) are complete on every path, and string/Noneamounts no longer raise. agent_messageis sanitized — no rule IDs, issue types, or schema internals leak into agent-facing output.
SQLVerifier → DiagnosticResult (#295)
verify_sql()returnsDiagnosticResult. A safe query isVERIFIED(sql_verifier.sql_valid,is_valid: true,proof_reffrom the AST). A proven-malicious query isVERIFIED-as-malicious (sql_verifier.malicious,is_valid: false,malicious_classification: true) — proving malice is a successful proof, so it retains itsproof_ref.BLOCKEDis reserved for incomplete or failed analysis:sql_verifier.parse_error,sql_verifier.schema_parse_error(takes precedence over malice detection — no authoritative proof from incomplete analysis),sql_verifier.complexity_limit_exceeded, andsql_verifier.execution_error.POST /verify/sqlattaches theadmissionfield (ADMIT/BLOCKED) alongside the verdict.
CodeVerifier & SecureCodeExecutor → DiagnosticResult (#296)
verify_code(),verify_python_deep(), andverify_batch()returnDiagnosticResultwith the truth-vs-admission semantics described above.verify_batch()returns per-itemverdictsplus asummary(safe/unsafe/blockedcounts,total_critical) and an overallis_validthat istrueonly when all snippets are safe — the batch is otherwise non-admissible.SecureCodeExecutor.execute()no longer executes code wholesale on the verifier verdict: an unconditional OWASP LLM06 dangerous-pattern gate blocks execution withCONSTRAINT_DANGEROUS_PATTERN. The scan is AST-aware, so dangerous keywords appearing only in comments, docstrings, or string literals do not cause false denials.ConsensusVerifierandStatsVerifiercode stages now requireis_verifiedanddeveloper_fields.is_valid is True, so consensus results can no longer admit unsafe code.
StatsVerifier → DiagnosticResult (#297)
verify_stats()returnsDiagnosticResultwith the execution-is-not-verification semantics described above.- The API boundary is a thin pass-through:
POST /verify/statsforwards the engine’sDiagnosticResultthroughenforce_trust_decision()unchanged. - Logging is fail-closed and claim-aware: a non-authoritative result (
BLOCKED/UNVERIFIABLE,proof_ref: null) can never be persisted as verified, even if mutabledeveloper_fields.is_validmetadata istrue. - A non-serializable sandbox result (e.g. a DataFrame) is coerced to a JSON-safe value before entering
developer_fields, so a legitimateUNVERIFIABLEverdict is not silently downgraded toBLOCKED.
Fact & image batch verification fail closed (#297)
BatchFactVerifier.verify_batch() and ImageVerifier.verify_batch() — the last two public engine entry points returning ad-hoc dicts — now return a single DiagnosticResult with per-claim verdicts in developer_fields.results and a summary:
- The batch is authoritative (
VERIFIED+proof_ref) only when every claim is deterministically verified. - Any refuted or blocked claim fails the whole batch closed (
fact_verifier.batch_blocked/image_verifier.batch_blocked). - An empty batch is
BLOCKED(*.empty_batch). - The batch
proof_refbinds full claim digests and the shared input (image digest for image batches, context digest for fact batches), never truncated display text. - Aggregation is shared via
diagnostics.aggregate_batch_diagnostic()so the fail-closed logic cannot drift between engines.
Version bumps
Included PRs
Upgrading
If you only consume the high-level SDK clients and gate onadmission / developer_fields.is_valid, bump the version and you are done. If your code branches on status from POST /verify/code for safety gating, or parses the legacy SUCCESS / ERROR shape from POST /verify/stats, apply the migrations shown above before upgrading.
v6.0.0 — Trust Boundary Completion
Released: August 2, 2026 · GitHub Release ↗ · GitHub PR #291v6.0.0 closes the Trust Boundary Completion epic (Issue #263, 12/12 sub-issues). Every verification pathway now returns a unifiedDiagnosticResultand routes throughenforce_trust_decision. The trust boundary is no longer advisory: the control plane requires and verifies attestation before admittingVERIFIEDresults, andVERIFIEDis a protocol guarantee backed by a deterministicproof_ref— never by execution, agreement, confidence, or provenance.
Architecture: observation vs. admission
The API is the observation surface — an honest witness that returns what verification found. The control plane is the admission authority — the judge that decides what is admitted asVERIFIED. QWED_RULES.md now codifies this separation (#13 Separation of Responsibilities, #14 Verification Semantics, #15 Truth Before Policy; rules #7/#8 updated for admission-boundary and deterministic-proof semantics).
- All
/verify/*endpoints returnDiagnosticResult— unified response contract across every verification surface (PR #276) - Control plane enforces mandatory attestation —
require_attestation=True, attestation issued and verified at the admission boundary, and the enforced status drives the HTTP response status (PR #278). See Cryptographic attestations. - Batch math routes through the trust boundary —
/verify/batchresults carryDiagnosticResult,proof_ref, and attestation, and pass throughenforce_trust_decision(PR #282). SeePOST /verify/batch. - Attestation scope alignment — attestations bind to the translated expression, not the natural-language query, so
query_hashbinds to what was actually verified (PR #285).
VERIFIED is a protocol guarantee
No engine emits VERIFIED without a deterministic proof_ref. Heuristic and advisory analysis now reports UNVERIFIABLE with structured advisory_checks instead of masquerading as verified.
ConsensusResultuses theDiagnosticStatusenum withproof_refandverified_evidence(PR #280). See Consensus engine.FactVerifierheuristic SUPPORTED verdict →UNVERIFIABLEwithadvisory_checks(PR #283).- Consensus code execution advisory-only —
VERIFIED→UNVERIFIABLE(PR #281). - Consensus stats advisory-only — never
VERIFIED(PR #277). LogicVerifiermigrated toDiagnosticResult(PR #262 — details in the entry below).AgentStateGuardproof_refis now a realsha256of committed bytes, not a static sentence (PR #284). See Agent state guard.
Engineering and security hardening
- TOCTOU closure in
enforce_trust_decision—developer_fieldssnapshotted via recursive rebuild (nodeepcopyalias window); fail-closed snapshot (PR #290) - Attestation signature verified before claim decode — silent generic error for every failure mode (PR #287)
- Tenant-isolated verification cache —
VerificationCachekeys namespaced by normalizedtenant_id(PR #286) - Unicode normalization in
AgentStateGuardcanonicalization — NFC collisions rejected (PR #288) - Mandatory proof artifact for
VERIFIEDattestations at both issuance and consumption (PR #248) - Credential / JWT / dockerignore security alerts resolved (PR #249)
- Math whitelist injection bypass removed (PR #251)
- Engine classification docs — Proof / Policy Enforcement / Advisory (PR #247)
Version bumps
Every SDK, container, and manifest ships at 6.0.0:Included PRs
The following 21 PRs shipped in the v6.0.0 release:Upgrading
If you are on v5.3.x and only consume the high-level SDK clients or attestations, bump the version and you are done — every engine already returnsDiagnosticResult. If your code parses raw HTTP responses from /verify/*, migrate to the 3-layer DiagnosticResult shape before upgrading. Direct callers of LogicVerifier should also review the LogicVerifier migration notes below.
v6.0.0 — LogicVerifier conforms to DiagnosticResult
Released: August 2, 2026 · GitHub PR #262 · part of the v6.0.0 release
LogicVerifieris the second engine — afterFactVerifier— to conform to the unified 3-layerDiagnosticResultcontract introduced in v5.2.0. All nine public methods now return aDiagnosticResultwith.status,.developer_fields,.is_verified,.agent_message, and.proof_ref. The legacyLogicResultdataclass has been removed. Two fail-closed input-strictness fixes ship with it.
What changed
- All nine methods return
DiagnosticResult.verify_logic,verify_with_quantifiers,verify_bitvector,verify_array,prove_theorem,check_implication,check_equivalence,verify_optimization, andcheck_vacuityall now return aDiagnosticResultwith the three diagnostic layers populated. - Per-method SAT/UNSAT disambiguation. Z3’s
sat/unsatoutcome is mapped to aDiagnosticStatusper method (for example,verify_logicsat→VERIFIED, butprove_theoremsat→BLOCKEDwith a counterexample;prove_theoremunsat→VERIFIEDbecause the theorem was proved by contradiction). The raw solver verdict is preserved ondeveloper_fields["deterministic_verdict"]. symbol_tableon everyVERIFIEDresult.developer_fields["symbol_table"]is a sorted list of{"name": ..., "type": ...}entries for every declared variable, so audit logs capture exactly what was proven.proof_reffrom the Z3 assertion stack. EveryVERIFIEDresult carries a deterministicsha256:...proof_refcomputed from the solver’s assertions — the authority bit that downstream gates use for admissibility.- Empty
variablesdict now fails closed. The_infer_variablestype-guessing heuristic has been removed. Calls with an emptyvariablesdict returnBLOCKEDwithconstraint_id = "logic_verifier.explicit_declarations_required". Declare every variable explicitly. - Malformed
BitVec[N]declarations now fail closed. Type strings such as"BitVec","BitVec[]", or"BitVec[abc]"no longer silently default to 32 bits. They returnBLOCKEDwithconstraint_id = "dsl_compiler.type_validation". agent_messageis sanitized. No raw Z3 output leaks into the agent-facing layer. Structured diagnostic details live underdeveloper_fields.
Before and after
Reading a satisfiability resultvariables dict
What this means for you
Callers that already treat non-VERIFIED results as unverified continue to work — they just need to migrate the field names (result.is_verified in place of result.status == "SAT", result.developer_fields["model"] in place of result.model). Callers that relied on implicit variable inference or on "BitVec" defaulting to 32-bit must now declare every variable explicitly. See LogicVerifier returns DiagnosticResult for the full status matrix, constraint_id list, and migration snippets, and the Verification Diagnostics guide for the 3-layer model.
QWED Control Plane — mandatory attestation admission on /verify/math, translated-expression attestation scope
Released: July 31, 2026 · GitHub PR #278 · GitHub PR #285
The control plane now treats attestation as an admission gate on the math verification path — the final response status is set by the enforcement step after signing, not by the raw verifier verdict. In the same release, the attestation qwed.query_hash now binds to the translated deterministic expression, not to the user’s natural-language query.
What changed
enforce_trust_decision(..., require_attestation=True)is now the single source of truth for the math responsestatusandtrust_boundary.overall_status. Both fields are set from the enforced decision after attestation is issued, never from the raw verifier verdict.- A
VERIFIEDmath result is only surfaced oncecreate_verification_attestation()returnsISSUED. Any attestation signing failure downgrades the response toBLOCKED— the error code is echoed undertrust_boundary.attestation_errorand no token is returned. trust_boundary.attestation_policyis now always"mandatory". The previous advisory mode has been removed.- The attestation
qwed.query_hashbinds to the translated expression the engine actually evaluated, not to the natural-language query. The natural-language query is still returned inresponse.user_queryfor display, andtrust_boundary.verification_scope = "translated_expression_only"continues to disclose the narrowed scope.
What this means for you
- Downstream consumers verifying a QWED attestation must hash the translated expression when re-checking
qwed.query_hash. Hashinguser_querywill not match — that is intentional, because QWED does not attest to the LLM translation step. - If your integration branched on
trust_boundary.overall_status, it now reflects the post-attestation decision. Signing outages surface asBLOCKEDwithattestation_error, not as a rawVERIFIEDverdict. - See Trust boundary and
POST /verify/natural_languagefor the full response contract, and Cryptographic attestations for theAttestationResultfail-closed contract.
v5.3.0 — SymbolicVerifier: DiagnosticResult reference implementation
Released: July 25, 2026 · GitHub Release · Minor · PR #244
QWED-Verification v5.3.0 makesSymbolicVerifierthe first fullyDiagnosticResult-conformant engine. The unified 3-layer diagnostic model introduced in v5.2.0 is no longer aspirational — the Code engine is the reference implementation the remaining engines will migrate to.
What changed
- Six methods migrated to
DiagnosticResult—verify_code,verify_function_contract,verify_safety_properties,verify_bounded,analyze_complexity, andget_verification_budgetnow return the unified 3-layer diagnostic type. Every result carriesstatus(DiagnosticStatus),agent_message(Layer 1),developer_fields(Layer 2), andproof_ref(Layer 3, alwaysNonefor this engine). verification_modeon every result —developer_fields["verification_mode"]is"symbolic"for standard runs and"bounded_symbolic"forverify_bounded(), so callers can distinguish the two verification modes without inspecting the call site.VERIFIEDis intentionally never emitted — CrossHair’s search is timeout-bounded, not a completeness proof, so a clean run maps toUNVERIFIABLEwithconstraint_id = "symbolic_verifier.no_counterexample_found". TheDiagnosticResultcontract structurally requires aproof_refforVERIFIED, and this engine has no proof artifact to bind. Downstream policy gates must reject symbolic-engine output for control flow.verify_code()no longer acceptscheck_assertions— the parameter was never wired up. The current signature isverify_code(self, code: str) -> DiagnosticResult.- Advisory checks throughout —
verify_safety_properties,analyze_complexity, andget_verification_budgetattach structuredAdvisoryCheckentries todeveloper_fields["advisory_checks"]. Advisory checks never influence the verdict.
Before and after
BeforeField cheat sheet
Version propagation
Included PRs
- #212 — feat: migrate SymbolicVerifier to DiagnosticResult (Phase 1)
- #220 — fix: remove unused
check_assertionsparameter fromverify_code - #239 — feat: add
verification_modeto SymbolicVerifier DiagnosticResults - #240 — feat: migrate
verify_boundedto returnDiagnosticResult - #241 — feat: migrate
get_verification_budgetto returnDiagnosticResult - #242 — feat: migrate
analyze_complexityto returnDiagnosticResult - #243 — feat: migrate
verify_safety_propertiesto returnDiagnosticResult - #244 — release: v5.3.0 — SymbolicVerifier:
DiagnosticResultreference implementation
What this means for you
If your integration reads any dict-shape field from aSymbolicVerifier result, update it before upgrading — the return type has changed and the old keys are gone. The Code engine reference documents the new contract, and Symbolic execution limits has been rewritten around DiagnosticResult. Callers that gated control flow on is_verified for symbolic-engine output must move to result.proof_ref (the authority bit) instead — this engine intentionally never sets it, so symbolic results must not admit downstream execution.
QWED-A2A — UNVERIFIABLE verdicts carry no JWT, and five README behaviors realigned with the code
Released: July 27, 2026 · GitHub PR #40 · GitHub PR #57
The interceptor no longer produces a signedFORWARDEDverdict for empty or malformedFINANCIAL_TRANSACTIONandLOGIC_ASSERTIONpayloads, and no longer silently forwardsGENERAL/DATA_QUERYmessages. Both cases now returnverdict.status = "unverifiable"withattestation_jwt = None— signing a token for content that was never verified would be a false cryptographic claim. The A2A docs are updated to match the shipping behavior on five points that previously overstated what the interceptor does.
What changed
- Empty/malformed finance and logic payloads →
UNVERIFIABLE, no JWT. AFINANCIAL_TRANSACTIONmissingdata,line_items, orclaimed_total, or with non-numeric amounts, now returnsstatus=unverifiablefromfinance_guard. ALOGIC_ASSERTIONwith a missing, non-list, or emptyassertionsarray, or malformed entries, returnsstatus=unverifiablefromlogic_guard. Earlier releases collapsed these cases toverified=True+ a signedFORWARDEDverdict. GENERALandDATA_QUERYare no longer silently forwarded. The passthrough branch now returns anunverifiableverdict withengine_used="passthrough",attestation_jwt=None, and reason"No verification engine available for this payload type". Callers must decide their own downstream policy for unverified traffic.- No
Bypassverification stage. The pipeline has four stages, not five.config.trusted_agentsis pre-added to theTrustBoundaryallowlist at interceptor construction time — trusted agents still flow through engine routing and receive a normal verdict. The Verification interceptor and Architecture pages have been corrected. CodeGuardis AST-first, regex-second. Code payloads are first parsed into an AST and inspected for direct dangerous constructs (eval(,exec(,subprocess.run(,import subprocess, etc.). Only if the AST layer is clean does a regex heuristic scan run to catch obfuscation patterns (getattr(__builtins__, ...), base64-encoded exec, dynamic__import__). The verdict isBLOCKEDif either layer triggers, orHEURISTIC_PASSif both are clean — not a proof of safety.- JWT attestation expiry is 300 seconds (5 minutes), not 24 hours. The default
validity_secondsonA2ACryptoServiceis300— one A2A hop lifetime. The Crypto attestations page and its signing example were updated.
New verdict-status contract
What this means for you
If you integrate QWED-A2A: audit any caller that readsattestation_jwt or trusts status == "forwarded" without also checking engine_used. Legitimate agent traffic on unsupported payload types is not blocked — it is marked unverifiable so your policy layer can decide whether to route it. See the updated Verification interceptor and Quick start pages for the full verdict contract and example payload shapes.
QWED verification — signature-first attestation validation, uniform rejection error, and TOCTOU-safe trust-boundary snapshot
Released: August 1, 2026 · PR #287 · PR #290Two follow-ups harden the attestation trust boundary introduced last week.verify_attestation()now verifies the JWT signature before applying any issuer authorization or revocation checks, and every rejection path returns the same generic"Invalid token"error so callers cannot enumerate trusted issuers, probe token expiry, or confirm revocation state through response text.enforce_trust_decision()now returns a fully detachedDiagnosticResultsnapshot, closing a TOCTOU window where a concurrent caller could mutatedeveloper_fieldsbetween the validation read and the admission decision.
What changed
- Signature-first verification order.
verify_attestation()cryptographically verifies the JWT (signature + expiration + required claims) before it applies the trusted-issuer authorization check or the revocation check. An unknown-issuer token that is not correctly signed is rejected on signature grounds and the trust-list check never runs. - Uniform
"Invalid token"rejection. Every failure mode — expired, revoked, untrusted issuer, unsupported external issuer, malformed, oversized, malformed base64 — returns the same(is_valid=False, claims=None, error="Invalid token")triple. Detailed reasons are still recorded in the server-sideattestation.rejectedaudit log; only the caller-facing error is generic. This closes the trusted-issuer enumeration side channel. - Detached-result snapshot in
enforce_trust_decision(). Before validation,developer_fieldsis recursively rebuilt into an isolated snapshot that admits only immutable scalars,AdvisoryCheck, and JSON-safe containers. The value you validate is the value you return, so a concurrent mutation of the caller’sDiagnosticResultcan neither skew the decision nor leak into the returned result. - Fail-closed snapshot failure. When
developer_fieldscontains an unsupported value type (custom class instance, generator, open file handle) or an object that resists copying,enforce_trust_decision()returns aBLOCKEDresult withconstraint_id="trust_gate.diagnostic_snapshot_failed". Only the exception type is logged — never the exception message, which could embed caller data. - Runtime dependency.
cryptographyis now a runtime dependency (previously an optional extra), so ES256 signature verification runs by default.
Before and after
Rejection error — beforeWhat this means for you
Callers ofverify_attestation() must not branch on the error string any more — treat any is_valid=False result as a hard block and rely on the server-side audit log for the actual reason. Callers of enforce_trust_decision() should keep developer_fields values to JSON-safe types (strings, numbers, booleans, None, lists, dicts) plus AdvisoryCheck; anything else will be rejected by the snapshotter and blocked at the trust boundary with trust_gate.diagnostic_snapshot_failed. See Uniform rejection error and Detached result snapshot for the full behavior.
QWED verification — mandatory proof artifact for VERIFIED attestations and a single trust-boundary entry point
Released: July 28, 2026 · PR #248Two related enforcement-boundary changes ship together. Issuance side, the crypto layer now refuses to sign aVERIFIEDverdict that has no proof artifact. Consumption side, a newenforce_trust_decision()function is the single trust-boundary entry point that release gates route through — it verifies the attestation JWT, binds its claims to the result (status, query hash, proof hash), and fails closed on any mismatch.
What changed
create_verification_attestation()blocks VERIFIED without proof. Calling withverified=True(orstatus="VERIFIED") and an empty or missingproof_datanow returnsAttestationResult(status=BLOCKED, token=None, error_code="VERIFIED_WITHOUT_PROOF", error=...). The crypto layer will not sign aVERIFIEDresult that has no evidence to hash into the token’sproof_hashclaim.UNVERIFIABLEandBLOCKEDverdicts are unaffected.- New
enforce_trust_decision()trust-boundary gate. Exported fromqwed_new.core, this is the single consumption-side entry point every release gate must route through. It takes the engine’sDiagnosticResultplus the attestation token, verifies the JWT (signature, expiry, trusted issuer), and blocks the decision when the token’sqwed.result.status,qwed.query_hash, orqwed.proof_hashclaims do not match the result.UNVERIFIABLE/BLOCKEDresults pass through unchanged. require_attestationpolicy toggle.enforce_trust_decision(result, require_attestation=True, ...)(default) is the mandatory policy —VERIFIEDwithout a valid token is downgraded toBLOCKED.require_attestation=Falseis the advisory policy for staged rollouts: a missing token still passes asVERIFIED, but a present-and-invalid token still blocks. Both modes emit structuredtrust_gate.blockedaudit logs on rejection.- Control plane integration. The math control plane now routes every verification through
enforce_trust_decision()and records the outcome and policy on the trust boundary (trust_enforced,attestation_policy) so operators can watch the mandatory rollout from telemetry before flipping the switch.
Before and after
Issuance —VERIFIED without a proof artifact
What this means for you
If you callcreate_verification_attestation() directly, always pass proof_data (typically json.dumps(evidence, sort_keys=True)) when signing a VERIFIED verdict, and check result.is_issued before treating the token as valid. If you consume verification results in a release gate, route them through enforce_trust_decision() — start in advisory mode (require_attestation=False) while your engines are being migrated to emit proof artifacts, watch the trust_boundary.trust_enforced telemetry, then flip to mandatory once every VERIFIED path is signing with proof. See Fail-closed contract and Enforce the trust boundary for the full parameter reference and fail-closed matrix.
QWED-A2A — verify_attestation() now requires an AttestationContext (breaking)
Released: July 26, 2026 · GitHub PR #41
A2ACryptoService.verify_attestation()used to accept a token on its own — a cryptographically valid signature was treated as sufficient proof. A detached attestation could therefore be lifted from one exchange and replayed against a different sender, receiver, or payload without detection. The method now requires a second argument, anAttestationContextdescribing the current request, and rejects the token if the sender, receiver, payload hash, or session claim does not match.
What changed
- New
AttestationContextdataclass inqwed_a2a.security.cryptowith fieldssender_agent_id: str,receiver_agent_id: str,payload: Any, and optionalsession_id: str | None. The payload is hashed internally with the same deterministic method assign_verdict(), so callers never handle raw hashes. - New context-binding verification step — sender, receiver, payload hash, and (when supplied)
session_idare compared against the token’sqwed_a2aclaims andsubclaim. Any mismatch returns a structured rejection:"Attestation sender mismatch: expected=..., got=...","Attestation receiver mismatch: ...","Attestation payload hash mismatch — detached attestation rejected", or"Attestation session mismatch: ...". - Verification order updated. Context binding now runs after the deployment-context check but before the
jtireplay check. This is deliberate: an out-of-context token no longer pollutes thejtiregistry, so a legitimate future presentation of the same token in its correct context is not locked out. - Signing is unchanged.
sign_verdict()still takes the same arguments — only the verify side gained a parameter.
What this means for you
Every caller ofverify_attestation() must be updated. Build an AttestationContext from the same identifiers and payload the receiving side is about to act on, then pass it as the second argument:
QWED Math Engine — fail-closed on mode ambiguity, eigenvalue cardinality, and IRR convergence
Released: July 24, 2026 · Jump to details · PR #217 · PR #218 · PR #219Three core-verifier fixes tighten the math engine’s fail-closed contract.verify_statistics(statistic="mode"),verify_matrix_operation(operation="eigenvalues"), andverify_irr()no longer produceVERIFIEDwhen the underlying claim is ambiguous, under-specified, or numerically unproven. Callers seeBLOCKEDorCORRECTION_NEEDEDwith structured diagnostics instead of a best-effort answer.
What changed
verify_statistics(statistic="mode")requires a unique mode. When two or more values tie for the maximum frequency, the engine returnsBLOCKEDwith anambiguous_modeslist instead of heuristically picking one. Only a unique mode can produceVERIFIED.verify_matrix_operation(operation="eigenvalues")requires cardinality match. The claimed eigenvalue list length must equal the calculated count (counting algebraic multiplicity). Mismatched lengths returnCORRECTION_NEEDEDwithcalculated_count/claimed_count. Previously the value comparison usedzip, which silently truncated to the shorter list.verify_irr()requires proof of Newton-Raphson convergence.BLOCKEDis now returned when cash flows have more than one sign change (multi-root ambiguity per Descartes’ rule), zero sign changes (no real IRR), all-zero cash flows (IRR undefined), the Newton derivative stalls at zero, or the method fails to converge within 100 iterations. Successful results includeconverged: trueanditerations_used.
Before and after
Ambiguous modeWhat this means for you
Existing code that already treated non-VERIFIED statuses as unverified continues to work. New policy code should read the structured diagnostic fields on BLOCKED/CORRECTION_NEEDED results — ambiguous_modes, calculated_count/claimed_count, and converged/iterations_used — to explain why a claim was rejected and to satisfy audit requirements. See Math engine — Fail-closed semantics for the full state tables and response schemas.
QWED-Finance — v2.1.0 released
Released: July 22, 2026 · GitHub PR #40qwed-financev2.1.0 is now the current release. This is a version-sync release: the Python package, npm wrapper, GitHub Action, and quickstart workflow reference are all realigned soQWED Finance Guardreports a single consistent version across PyPI, npm, and SARIF output in GitHub Advanced Security.
What changed
qwed-financePython package is now v2.1.0 on PyPI (qwed_finance.__version__ == "2.1.0").@qwed-ai/financenpm package version bumped to 2.1.0 to match.- GitHub Action auto-syncs its reported version from the installed package — the
QWED Finance Guardname in workflow logs and theversionfield on SARIF uploads to the GitHub Security tab now both read2.1.0instead of a hardcodedv2.0. - Quickstart
qwed-verify.ymlworkflow is re-pinned from the stale v1.1.4 SHA to the v2.1.0 SHA (QWED-AI/qwed-finance@19ce969f21d1fc2019da4d89fff23bc108e15a98 # v2.1.0).
What this means for you
Upgrade your dependency to pick up the current release:QWED-UCP — v0.3.0 released
Released: July 20, 2026 · GitHub PR #38qwed-ucpv0.3.0 is now the current release. This version ships the typedTrustStatusenum on every verification result, alongside the fail-closed middleware and internal-error handling delivered over the v0.2.x series.
What changed
qwed-ucpPython package is now v0.3.0 on PyPI.- Express middleware
qwed-ucp-middlewarepackage version bumped to match. TrustStatusenum is confirmed as available from v0.3.0 onward — see Trust status for the full state table and usage.- GitHub Action should now be pinned to
QWED-AI/qwed-ucp@v0.3.0.
What this means for you
Upgrade your dependency to pick up the current release:result.verified continues to work unchanged. New code should branch on result.status (a TrustStatus) to distinguish FAILED from ENGINE_ERROR, UNVERIFIABLE, and other non-VERIFIED verdicts.
QWED-UCP — Express middleware fails closed on internal verification errors
Released: July 18, 2026 · GitHub PR #36When a guard raised an unexpected exception, the Express middleware previously logged the error and callednext()— letting an unverified checkout through to the downstream handler. That defeated the trust boundary. Thecatchblock now short-circuits withHTTP 500,X-QWED-Verified: false, andcode: "INTERNAL_VERIFICATION_ERROR"so an internal crash can no longer be mistaken for a passing verification.
What changed
- Express middleware
catchblock is now fail-closed. On any exception raised duringverifyCheckoutLocally(), the middleware returns:The underlying exception is logged server-side viaconsole.errorbut is not included in the response body — raw stack traces, file paths, and internal messages stay out of client-visible output. X-QWED-Verified: falseis set on the 500 response so upstream policy layers can treat it the same as a 422 failure.- npm package fix.
qwed-ucp-middleware.jsis now included in the publishedfilesarray. Previous versions installed via npm were missing the entrypoint thatindex.jsrequired at runtime.
What this means for you
Existing integrations that already branched onX-QWED-Verified or on the response status keep working — they just start seeing a new 500 path that was previously invisible. New integrations should treat both 4xx and 5xx responses from the middleware as an unverified request. See Express.js middleware — Fail-closed on internal verification errors for the full response contract.
QWED-UCP — typed TrustStatus enum on every verification result
Released: July 15, 2026 · GitHub PR #33
Verification results previously exposed a singleverified: boolthat collapsed “proof disproved,” “proof could not be established,” “input outside supported semantics,” and “verifier engine crashed” into the sameFalsebucket. Every result dataclass now also carries a typedstatus: TrustStatusfield so downstream policy code can make trust-aware decisions without parsing error strings.
What changed
- New
TrustStatusenum exported fromqwed_ucpwith seven states:VERIFIED,FAILED,UNVERIFIABLE,UNSUPPORTED,PARTIAL,ENGINE_ERROR, andQUARANTINED(reserved). statusfield on every result —GuardResult,UCPVerificationResult, and each per-guard result type (MoneyGuardResult,StateGuardResult,SchemaGuardResult,LineItemsGuardResult,DiscountGuardResult,CurrencyGuardResult,RefundGuardResult,TipGuardResult,FeeGuardResult,AttestationResult).UCPVerifier.verify_checkout()now surfacesENGINE_ERRORwhen any guard raises an exception, instead of silently collapsing toverified=False.verified: boolis preserved as a backward-compatible derived field:result.verifiedisTrueonly whenresult.status == TrustStatus.VERIFIED. Existing constructors that passverified=True/verified=Falsecontinue to produce the correspondingVERIFIED/FAILEDstatus.
What this means for you
Existing code that readsresult.verified or constructs results with verified=... keeps working unchanged. New policy code should branch on result.status to distinguish a disproved proof from a crashed verifier — see Trust status for the full state table and an example.
QWED-UCP — middleware fails closed on empty and non-JSON request bodies
Released: July 14, 2026 · GitHub PR #32The FastAPI and Express middleware previously forwarded requests with an empty body, malformed JSON, or a non-object JSON payload to the downstream handler without running any guards. On a/checkout-sessionsroute that defeated the trust boundary — an attacker could send{}’s worth of nothing and skip verification. Both middlewares now reject those requests withHTTP 422,X-QWED-Verified: false, andcode: "UNPARSEABLE_REQUEST"before the handler runs.
What changed
- FastAPI: returns a distinct message per case:
- Empty body →
"Empty request body: cannot verify empty payload" - Malformed or non-UTF-8 body →
"Malformed request body: expected JSON" - Top-level non-object JSON →
"Invalid request body: expected JSON object"
- Empty body →
- Express: all three cases return
"Empty or non-JSON request body: cannot verify unparseable payload". - Non-protected methods and paths (for example,
GET /healthor aPOSTto a route outsideverify_paths) still pass through untouched.
What this means for you
If you deploy QWED-UCP as middleware in front of a UCP merchant server, unparseable requests can no longer bypass the guards. See FastAPI middleware, Express.js middleware, and the troubleshooting entry for the exact response shape and how to configure protected paths.QWED-A2A — persistent signing key and JWKS discovery endpoint
Released: July 10, 2026 · GitHub PR #29A2ACryptoServiceno longer generates an ephemeral ECDSA P-256 key per process. The signing key is now loaded from theQWED_A2A_SIGNING_KEY_PEMenvironment variable (or thepem_keyconstructor argument) so attestation JWTs issued before a restart remain verifiable afterwards. A new/.well-known/jwks.jsonendpoint publishes the current public key for external consumers.
What changed
- Persistent signing key —
A2ACryptoServicereads an unencrypted PKCS#8 P-256 PEM fromQWED_A2A_SIGNING_KEY_PEMon first use. The derivedkey_idis a SHA-256 fingerprint of the public key, so it stays stable as long as the PEM does. - Fail-closed on missing key —
sign_verdict,verify_attestation,get_public_key_jwk, andA2AVerificationInterceptor.intercept()all raiseRuntimeErrorwhen the PEM is missing, malformed, or uses a curve other thanSECP256R1. The FastAPI gateway surfaces this asHTTP 503 Signing key unavailable. - JWKS endpoint — A new
wellknown_routerexposesGET /.well-known/jwks.jsonfor downstream services and auditors to fetch the current public key without an out-of-band exchange. get_public_key_jwk()— Returns the current public key as a JWK (kty,crv,x,y,kid,use,alg).
Breaking changes
What this means for you
If you deploy the QWED-A2A gateway, you now get audit continuity across restarts and rolling deployments — a JWT signed by an earlier process is still verifiable by the next one, provided the PEM is unchanged. External services can also verify attestations by fetching/.well-known/jwks.json directly. See Crypto attestations and Deployment for the full setup, JWKS shape, and key rotation guidance.
QWED-Tax — structured 3-layer diagnostics for TDS, ITC, and GST-RCM
Released: June 21, 2026 · GitHub PR #45
QWED-Tax adopts the same 3-layer DiagnosticResult contract introduced in QWED-Verification v5.2.0 — as an independent model, no cross-package dependency. The first three guards (TDS, Input Tax Credit, GST reverse-charge) now expose a structured diagnostic in addition to their existing dict response.
What’s new
- Tri-state status — Every diagnostic resolves to
VERIFIED,UNVERIFIABLE, orBLOCKED. NoHEURISTICorAMBIGUOUSmiddle ground. - Three disclosure layers — A short agent-safe summary (no statute IDs or detection logic), a structured developer evidence block (constraint ID, statute, jurisdiction, audit trace, deduction, net payable), and an optional proof reference.
- Proof reference is the authority bit — A deterministic sha256 hash of the audit trace, present only when a result is
VERIFIED. Absent onUNVERIFIABLEandBLOCKED. - First migrations —
TDSGuard,InputCreditGuard, andGSTGuard(reverse-charge) each expose the new diagnostic format alongside their existing return shape.
Compatibility
Additive release. The legacy dict API on every guard is unchanged — the diagnostic format is opt-in. Existing integrations continue to work without modification. The remaining nine QWED-Tax guards (CapitalGains, Classification, Speculation, Setoff, Crypto, Valuation, Remittance, PoEM, Withholding) will migrate in follow-up releases.What this means for you
If you’ve already adopted the QWED-Verification diagnostic contract, the same response shape now applies to TDS, ITC, and GST-RCM checks — including the proof-reference authority bit you can gate execution on. See the Verification Diagnostics guide for the response shape and the tax guards reference for guard-by-guard coverage.QWED-Tax — exact paise comparison, edge-case input rejection, and strict payload schemas
Released: June 21, 2026 · GitHub PR #44Three input-strictness fixes ship together.CryptoTaxGuard.verify_flat_tax_ratenow compares quantized paise values exactly instead of within a 0.1 tolerance,ValuationGuardandRemittanceGuardreject the edge-case numeric inputs that previously slipped through, and every QWED-Tax Pydantic input model now forbids unexpected fields.
What changed
CryptoTaxGuard.verify_flat_tax_rate— TheDecimal("0.1")tolerance was removed. Both the computedexpected_taxand the caller’sclaimed_taxare now quantized to two decimal places usingROUND_HALF_UPand compared with an exact==. A 1-paise (0.01) deviation correctly returnsverified=False.ValuationGuard.verify_conversion— Added explicit range checks:discountmust be in[0, 1), andcap,next_round_price, andinvestmentmust all be strictly positive.DivisionByZerois now caught alongsideInvalidOperationso a degeneratecap = 0ordiscount = 1no longer crashes — it fails closed with a structured{"verified": False, "error": "..."}response. The previous behavior allowed adiscount > 1to produce a negative share count.RemittanceGuard.verify_lrs_limit— After numeric parsing,amount_usdandfinancial_year_usageare now checked for negativity. Negative inputs return{"verified": False, "error": "BLOCKED: ..."}instead of being summed into the limit check, where a negative usage could mask a transaction that exceeds the $250,000 LRS cap.- Input models —
Address,WorkArrangement,WorkerClassificationParams,ContractorPayment,TaxEntry,DeductionEntry,PayrollEntry, andVerificationResultare now configured withmodel_config = ConfigDict(extra="forbid"). Any payload with an unexpected key raises a PydanticValidationErrorat the boundary.QWEDTaxMiddlewarealready surfaces this asstatus: "BLOCKED"withrisk: "INVALID_PAYLOAD".
Breaking changes
What this means for you
If your agent forwards Indian VDA tax claims, startup conversion math, or LRS remittance requests through QWED-Tax, audit the calling code for: (1) tax claims that aren’t pre-quantized to two decimal places, (2) discount/cap/investment inputs that can legitimately be zero or out of range, (3) negative remittance amounts being passed defensively, and (4) AI payloads that include fields not declared on the QWED-Tax input models. See the CryptoTaxGuard, ValuationGuard, RemittanceGuard, and middleware integration guide for the updated contracts.Audit reference
QWED-Tax — middleware never returns full “verified” and ReciprocityGuard no longer always-passes
Released: June 21, 2026 · GitHub PR #43Two fail-closed fixes ship together. The Gusto interceptor middleware no longer overstates a gross-to-net arithmetic pass as full tax verification, andReciprocityGuardno longer returnsverified=Truefor arrangements with no reciprocity agreement.
ARITHMETIC_VERIFIED is a pre-conformance middleware-layer status, not part of the DiagnosticResult tri-state vocabulary (VERIFIED / UNVERIFIABLE / BLOCKED) introduced in v5.2.0. The middleware will migrate to DiagnosticResult when engine-conformance work lands. Until then, treat ARITHMETIC_VERIFIED as a distinct middleware signal — it is not equivalent to VERIFIED.What changed
QWEDTaxMiddleware.process_ai_payroll_request— The success response is nowstatus: "ARITHMETIC_VERIFIED"withexecution_permitted: false. The middleware only verifies gross-to-net math; classification, withholding legality, reciprocity, and filing checks are still required before execution. The response includeschecks_run(what was verified) andchecks_not_run(what is still required) so callers can decide what to run next.TaxPreFlightreport — Every report now includes achecks_not_runlist covering both unselected guards and known gaps (checks not yet implemented for the action). For example,action="hire"reportspayroll_arithmetic,withholding_legality,reciprocity, andfiling_obligationsas known gaps.ReciprocityGuard— The Z3 solver was removed (the prior expression was tautologically satisfiable and ignored thesame_stateparameter). The guard is now a deterministic lookup against the reciprocity-pair table with explicit fail-closed paths: same state → verified, known pair → verified, no agreement →verified=False, unknown state →verified=False. A newverify_reciprocity(residence_state, work_state, same_state=None)method takes string inputs;determine_withholding_state(arrangement)is kept for backwards compatibility.
The reciprocity-pair table covers only the pairs modeled within the
State enum’s 8-state scope (NJ, PA, MD, VA). Pennsylvania has additional reciprocity agreements (with IN, MI, OH, VA, WV, WI) that are not modeled here because those states are not in the enum. VA-PA in particular is a known gap — callers will receive verified=False for that pair until the enum and table are extended.Breaking changes
What this means for you
If your agent forwards payroll payloads to Gusto/Avalara based on aVERIFIED status from the middleware, those calls will start blocking until you run the remaining guards (ClassificationGuard, WithholdingGuard, ReciprocityGuard.verify_reciprocity, Form1099Guard) yourself. See the tax integration guide for the updated response shape and the ReciprocityGuard reference for the new lookup contract.
Audit reference
QWED-Tax — fail-closed on ambiguous classification and unverified claims
Released: June 21, 2026 · GitHub PR #42
Six more QWED-Tax guards now refuse to sign off when they can’t independently prove a result. Ambiguous worker classifications, unparseable trade dates, unknown set-off heads, and reverse-charge inputs the guard doesn’t recognize all return verified=False with a structured error instead of a quiet pass.
Bug fixes
- CapitalGainsGuard — Unparseable acquisition or disposal dates and unknown asset types now block instead of being coerced into a sentinel value that flowed through to
verified=True. SLAB-rate verification can no longer succeed without an income bracket — slab rates can’t be proven from the claim alone. - ClassificationGuard — Mixed employee/contractor signals now return
verified=Falsewith an “ambiguous classification” error. The guard only returnsCONTRACTORwhen no employee indicators are present. - SpeculationGuard — Set-off verification now requires a known income source (
intraday,f&o,futures,options,delivery,business,capital_gains). Unrecognized sources are blocked instead of silently treated as non-speculative. - InterHeadAdjustmentGuard — Set-off eligibility now runs against an explicit allowlist of heads. Salary loss set-off is added to the prohibition matrix, and unknown heads are blocked rather than defaulting to allow.
- GSTGuard (RCM) — Unrecognized service or entity types in reverse-charge checks now surface as a
verified=Falseerror naming the unknown value, instead of being coerced toOTHERorINDIVIDUALand potentially suppressing a statutory RCM obligation. The verifier also gained an optional claim parameter — when you pass aclaimed_is_rcmvalue, the guard compares it to the computed result and only returnsverified=Trueon an exact match. Calls without the claim get acomputed_only=Trueflag so calculation and verification are no longer conflated. - CryptoTaxGuard — Zero VDA income now verifies a claimed tax of zero, instead of returning
verified=Trueregardless of claim. Negative VDA income (a loss) returnsverified=Falsewith a message directing the caller to useverify_set_offfor loss treatment — the method does not internally invokeverify_set_off, so callers must handle theverified=Falsebranch explicitly.
What this means for you
If your agent relied on any of these guards returningverified=True for inputs they don’t model — ambiguous worker status, unknown set-off heads, unrecognized RCM service types, or capital-gains transactions with malformed dates — those calls now block. Surface the error to a human reviewer or extend the guard’s configured rules before re-running.
See the QWED-Tax guards reference for the updated contracts on each guard.
QWED-Tax — fail-closed on unknown tax rules
Released: June 19, 2026
Tax guards no longer silently pass when they encounter a service, asset, jurisdiction, or payment type they don’t model. Six guards now return verified=False with a structured error instead of an unsafe default.
What changed
- TDSGuard — unrecognized payment categories no longer return “verified, zero deduction.” This closes a path where an agent could classify a payment into an unknown bucket and have it execute with no withholding.
TaxPreFlightnow blocks these payments. - CapitalGainsGuard — unknown asset class or holding term fails closed instead of returning “no hard constraint.”
- NexusGuard — states not in the configured risk list now require manual review instead of being treated as low-risk.
- AddressGuard — unknown state codes fail closed with “manual review required” instead of “assumed valid.”
- Form1099Guard (US) — unmodeled payment types return
filing_required=Nonewith a manual-determination flag, instead of defaulting to “no filing required.” - InputCreditGuard (GST) —
verified=Trueremains the legal default (ITC is allowed unless specifically blocked), but unknown categories now carry an explicitunverified_category=Trueflag in the result and anaudit_traceentry ofcategory_match: "default_allow"so downstream consumers can distinguish known-eligible from default-allowed.
What this means for you
If your agent currently relies on averified=True response for inputs the guards don’t model, those calls will start blocking. Add explicit rules for the categories you care about, or route unverified results to human review.
See the tax guards reference and tax integration guide for the updated contracts.
QWED-Infra — ecosystem policy framework adopted
Released: June 17, 2026No runtime behavior changes. Affects contributors and anyone consuming the repository’s CI.qwed-infranow ships with the shared QWED governance baseline:QWED_RULES.md, contributor guidance, PR template, CodeRabbit config, and a boundary-check workflow that is consistent with the other QWED repositories.
QWED-MCP — boundary-check parity with QWED-Infra
Released: June 18, 2026Theqwed-mcpboundary-check tool now catches the same import-alias, module-alias, wildcard-import,eval/execalias, andshell=Truepatterns thatqwed-infradoes, and fails closed when the scan root is missing.
What this means for you
If you runqwed-mcp boundary checks in CI, expect to catch additional bypass patterns that previously slipped through. Existing passing scans should continue to pass; previously hidden findings may now surface as failures.
See the MCP tools reference for the current rule set.
QWED Open Responses v0.3.0 — version sync and dependency fix
Released: June 12, 2026AlignsNo API changes. Upgrade to pick up the dependency fix.qwed-open-responseswith the rest of the QWED package versions and patches a transitiveqsCVE flagged by Dependabot.
v5.2.0 — Structured Verification Diagnostics
Released: June 19, 2026 · GitHub Release · Minor
Introduces the unified 3-layer DiagnosticResult model — the diagnostic contract that all QWED verification engines will conform to. This is an additive release: no existing engine return types are changed. Engine conformance is tracked in blocked issues (#129, #130, #131, #133, #134, #162, #163, #164, #190, #205).
New: DiagnosticResult model
Three disclosure layers:
- Layer 1 — Agent-Safe:
agent_message: str— agent/model-facing summary, no internals leaked - Layer 2 — Developer:
developer_fields: dict— structured evidence (constraint_id,advisory_checks,methods_used, evidence) - Layer 3 — Proof:
proof_ref: Optional[str]— sha256 hash of retained proof artifact; the authority bit
Key design
- Tri-state status —
VERIFIED/UNVERIFIABLE/BLOCKEDonly. NoHEURISTICorAMBIGUOUSproliferation; richer distinctions live indeveloper_fields.constraint_id proof_refis the authority bit — present = admissible for control flow, None = reject. No separateauthoritativeboolean needed (resolves #190 design debate)- VERIFIED requires proof — structurally enforced in
__post_init__; “VERIFIED without proof” is impossible to construct - Frozen dataclasses —
DiagnosticResultandAdvisoryCheckarefrozen=True; post-construction mutation blocked - Advisory checks never influence verdicts —
AdvisoryCheck.advisory_only=Trueenforced via__post_init__ compute_proof_ref()— deterministic sha256 hashing of JSON-serialized evidencefrom_legacy_dict()— migration helper for ad-hoc engine dicts (fail-closed states only; raises for legacy VERIFIED)
Version propagation
Tests
83 new tests covering: status taxonomy, all 3 layers, authority contract, fail-closed enforcement, advisory checks, proof hashing determinism, serialization round-trip, legacy migration, frozen dataclass immutability, and realistic scenarios drawn from the 10 blocked issues.Compatibility
Additive release. No breaking changes. ExistingVerificationResult dataclasses and ad-hoc engine dicts continue to work. DiagnosticResult is opt-in — engines migrate incrementally.
Included PRs
- #206 — feat(diagnostics): unified 3-layer DiagnosticResult model (#204)
- #207 — release: v5.2.0 version propagation
Older entries: Changelog Archive