Skip to main content
All notable changes to the QWED platform, listed by release. Jump to: v7.2.0 · v7.1.0 · v7.0.0

v7.2.0 — Precision advisory and security hardening batch

Released: September 7, 2026 · GitHub Release ↗ · GitHub PR #362
v7.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 with constraint_id: "precision.float-constants" listing the offending constants and suggesting decimal.Decimal or SymPy exact rationals where exact arithmetic matters.
  • /verify/math attaches the advisory to the result on all branches, before trust enforcement.
  • StatsVerifier.verify_stats carries 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)**2 legitimately 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/binomial calls, caret-chain fold bounds, and a nested-power base-magnitude budget. Expressions like 2**factorial(10000) or ((9**9)**9999)**9999 are 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/stats gains 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 finally block 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.json is capped at 2 MB both inside the container wrapper (streamed, aborts at cap) and at host read-back before parsing. observed_result and every VerificationLog.result site 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 BLOCKED results 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 return BLOCKED 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 /metrics and GET /metrics/prometheus exposed every organization’s request volumes, latencies, provider usage, and per-tenant breakdowns to any self-service signup. The access gate treated the organization-scoped owner/admin role as platform-wide authority, and since signup is the only user-creation path — hardcoding role="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. owner and admin remain 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

Deployments that scrape the Prometheus endpoint (/metrics/prometheus) without the operator allowlist and valid scraper credentials will start receiving 401/403 after upgrading. Create a dedicated operator account, mint it an API key, and add its user ID to QWED_METRICS_OPERATOR_USER_IDS before deploying. The deploy/prometheus.yml in the qwed-verification repository documents the scrape bootstrap steps. See Security Hardening.

QWED Verification — float precision advisory for math and stats verification

Released: September 3, 2026 · GitHub PR #348 · Closes issue #347
Math 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-constants advisory. When an expression or generated statistics code contains float or complex constants, the result carries an AdvisoryCheck in developer_fields.advisory_checks listing the offending constants and suggesting decimal.Decimal or 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. AdvisoryCheck enforces advisory_only=True at construction. The advisory cannot change the verification status or proof_ref. Unparsable input yields no advisory; parse failures belong to the security gates.
See Math engine — Float precision advisory and Stats engine — Float precision advisory.

QWED Verification — structural math parser and sandbox gate hardening (security release)

Released: September 3, 2026 · GitHub PR #344 · GitHub PR #346
Two 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_xor joined the default transformation pipeline, so x^2 parses as x**2 instead 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.parse in eval mode). 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, the exec*/spawn* families, fork) are matched on bare names and attribute targets, and package-internal re-export gadgets through pandas/numpy internals are caught.
  • sys read-only allowlist in the stats executor. Only named read-only interpreter metadata is accessible; frame introspection and every other sys member fail closed.

Behavior changes

Math expression inputs that previously parsed may now be rejected before evaluation:
  • Bare decimals are rejected — write 0.5 and 2.0, not .5 and 2..
  • Non-ASCII symbols are rejected — write alpha, not α.
  • String constants, attribute access, and comparisons inside math expressions are rejected.
  • Multi-statement or import-bearing expressions fail by construction.
Legitimate arithmetic, implicit multiplication (2x, sin x), and pandas/numpy public APIs in generated stats code (np.linalg.norm, pd.Timestamp.now) are unaffected.

What this means for you

A character denylist can never defend an eval 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 #345
API-key lookup digests move from PBKDF2 to HMAC-SHA256 keyed by a new required QWED_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

API keys issued before v7.2 stop working. There is no PBKDF2 fallback — a legacy fallback would re-introduce the denial-of-service the migration fixes. Each key must be re-issued once; the old raw key is never required:
Keys can also be rotated by key ID through /admin/keys/rotate using any already-working key. Newly issued and rotated keys are HMAC digests automatically.

What changed

  • HMAC-SHA256 API-key lookup. The previous PBKDF2 digest (100,000 iterations, ~67ms) ran on every request carrying an x-api-key header, 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 to QWED_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 get 429 Too Many Requests with a Retry-After header of at least 1 second. X-Forwarded-For is honored only when the direct peer is listed in QWED_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 add QWED_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 #65
The @qwed-ai/tax npm verifier now requires the structured boolean claimed_collects_tax claim 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 requiredTaxPreFlight.audit and NexusGuard.checkNexus verify the boolean claimed_collects_tax against 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 narrowedtax_decision: 'no_tax' maps to claimed_collects_tax: false only when the intent does not contain a claimed_collects_tax key. 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: false with a block-pending-configuration error instead of being verified as no-nexus.
  • Input hardening — nexus validation runs whenever sales_data is present (including falsy or explicitly undefined values). Non-string state, 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 omitting sales_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 #62
Economic nexus verification now fails closed unless the caller provides an explicit claimed_collects_tax boolean. Free-form tax_decision / llm_decision text 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-only claimed_collects_tax: bool parameter. 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) return verified=False with an invalid-claim error.
  • llm_decision deprecated — The parameter is retained for positional compatibility but is never parsed. Passing it has no effect on the verdict.
  • TaxPreFlight economic_nexus intents — The required claim field changed from tax_decision (string) to claimed_collects_tax (boolean). Intents that still send tax_decision are blocked as incomplete claims before NexusGuard runs.

Breaking changes

Callers that previously passed a free-form decision string — check_nexus_liability(state, sales, count, llm_decision="No tax needed") or a TaxPreFlight intent with "tax_decision": "No tax needed" — now receive a fail-closed result instead of a verified verdict. Translate the AI’s decision into an explicit boolean before calling:

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.
Breaking behavior change. verify() with zero guards now returns verified=False with the guard result message “No guards configured”, and blocked=True in strict mode. Previously it returned verified=True because no guard failed. If you use ResponseVerifier() or VerifiedOpenAI without configuring guards, those calls now block: set default_guards (Python) / defaultGuards (TypeScript) or pass guards per call.

What changed

  • Zero-guard verify fails closed. Absence of verification is not success. A verify() call with no guards returns a ResponseVerifier guard result with severity="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_use blocks and matches type values case-insensitively, so a Tool_Use block is policed instead of ignored. OpenAI function wrappers 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.content shape, so PII, injection, and harmful patterns inside nested structures are detected.

What this means for you

If your agent relied on verify() 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 explicit claimed_collects_tax boolean. Free-form tax_decision / llm_decision text 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-only claimed_collects_tax: bool parameter. 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) return verified=False with an invalid-claim error.
  • llm_decision deprecated — The parameter is retained for positional compatibility but is never parsed. Passing it has no effect on the verdict.
  • TaxPreFlight economic_nexus intents — The required claim field changed from tax_decision (string) to claimed_collects_tax (boolean). Intents that still send tax_decision are blocked as incomplete claims before NexusGuard runs.

Breaking changes

Callers that previously passed a free-form decision string — check_nexus_liability(state, sales, count, llm_decision="No tax needed") or a TaxPreFlight intent with "tax_decision": "No tax needed" — now receive a fail-closed result instead of a verified verdict. Translate the AI’s decision into an explicit boolean before calling:

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 in qwed-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 2x or sin 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, and 2(x+1) still parse; the guard blocks 2x.__class__.
  • Regex denylist retained as defense in depth.

What this means for you

Upgrade to qwed-mcp 0.2.2 or later:
No API changes. Legitimate mathematical expressions parse exactly as before. Expressions containing attribute access, subscripts, string literals, or other non-arithmetic syntax now fail with a 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, 2026
Every qwed-infra guard now emits a portable Verification Context v1.0 document via to_verification_context(), and ADMIT decisions now require a cryptographically valid ES256 attestation bound to the exact claim and evidence.

Added

  • to_verification_context() on all four guardsIamGuard (#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 an ADMIT/DENY admission 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-None fail-closed AttestationResult contract, 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 a VERIFIED diagnostic’s own evidence commitment.

Breaking changes

Signature migration (pre-1.0 API). Guard adapters no longer accept pre-computed result objects. to_verification_context() takes raw verification inputs and runs the guard’s own deterministic solver internally. A result-accepting signature is forgeable, so it was removed before release. Pass raw inputs: NetworkGuard.to_verification_context(resources, source, destination, port, ...), IamGuard.to_verification_context(policy, action, resource, context=None, ...), CostGuard.to_verification_context(resources, budget_monthly, ...), ArtifactBoundaryGuard.to_verification_context(package_dir, ...).
  • Admission semanticsVERIFIED results 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 produce BLOCKED. A missing token demotes VERIFIED to UNVERIFIABLE/DENY. Callers previously passing placeholder tokens must mint via create_verification_attestation() or mint_diagnostic_attestation().
  • New runtime dependenciespyjwt and cryptography for 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/DENY documents instead of exceptions or guessed approval.
See the guards reference and usage examples for the updated contracts.

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_ref evidence commitment (PR #302). See the Verification Context specification.
  • VerificationContextDocument model + schema validation — typed VerificationContext, VerificationContextDocument, Verdict, Admission, and nested layer types with fail-closed invariants enforced at construction: VERIFIED requires a sha256:<64-hex> proof_ref; UNVERIFIABLE/BLOCKED require proof_ref: null with admission DENY (PR #308).
  • Public proof_ref generation and resolutioncompute_document_proof_ref() and resolve_document_proof_ref() derive and verify the content-bound SHA-256 commitment over the canonical document. Resolution fails closed: a proof_ref that cannot be resolved confers no authority (PR #309).

Surface exposure

  • API endpointsPOST /verification-context/from-diagnostic, /validate, and /resolve convert DiagnosticResult records into VC documents, validate documents against the schema, and resolve proof_ref commitments (PR #311).
  • CLI commands — the qwed context group: validate, resolve, and from-diagnostic (PR #311).
  • SDK re-exports — all VC v1.0 types and helpers importable directly from qwed_sdk, plus create_verification_context_from_diagnostic(), validate_verification_context(), and resolve_verification_context() on both sync and async clients. See the Python SDK (PRs #311, #315).
  • to_verification_context() on all 13 verifiers — complete engine coverage (Math, Logic, Symbolic, SQL, Code, Schema, Fact, Image, Graph, Reasoning, Stats, Consensus, and the secure code executor) maps each engine’s DiagnosticResult to a VC document (PRs #310, #316).
  • Docker action VC outputs — the GitHub Action emits verdict, admission, proof_ref, and verification_context outputs in every mode (PR #313).

Fail-closed behavior

  • A VERIFIED diagnostic without a valid attestation token is demoted to UNVERIFIABLE when converted to a VC document — attestation is enforced through the same trust boundary as /verify/*.
  • Malformed diagnostic payloads convert to BLOCKED documents instead of crashing, so the audit trail records the failure.
  • resolve returns true only for a schema-valid VERIFIED document whose stored proof_ref matches 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 #300
v7.0.0 completes the engine migration to the unified DiagnosticResult contract (META #216). SchemaVerifier, SQLVerifier, CodeVerifier, SecureCodeExecutor, and StatsVerifier — plus the fact and image batch entry points — now all return DiagnosticResult (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 separate AdmissionDecision answers “should this be allowed at this boundary?”.
Two breaking wire changes. POST /verify/code now returns status: "VERIFIED" for proven-unsafe code, with admission driven by the new admission field. POST /verify/stats now returns status: "UNVERIFIABLE" on successful execution instead of the legacy SUCCESS shape — execution is not verification. Migration examples below.

Breaking: POST /verify/codeVERIFIED 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:
After (v7.0.0) — unsafe code:
Migrating: consumers that branched on 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:
After (v7.0.0) — successful execution:
Migrating: read the computed value from 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.

SchemaVerifierDiagnosticResult (#294)

  • verify() and verify_ucp_transaction() return DiagnosticResult. A schema violation is VERIFIED (as-invalid) with developer_fields.is_valid: false; BLOCKED (schema_verifier.parse_error / schema_verifier.validation_error) is reserved for schemas that cannot be parsed or validated.
  • proof_ref is computed deterministically from canonical JSON of the schema + instance evidence; unsupported values and cyclic structures fail closed to BLOCKED.
  • Recursive schema meta-validation: malformed keyword shapes (non-dict properties, invalid required entries, invalid or non-finite numeric constraints, negative size constraints) return BLOCKED instead of being silently treated as empty. Oversized integer bounds (e.g. 10**1000) no longer raise OverflowError.
  • UCP hardening: money arithmetic uses Decimal quantized to the currency precision (no more 0.01 float tolerance), tax is selected by key presence so tax: 0 is honored, verdict fields (transaction_type, currency, schema_verifier.ucp_* constraint ids) are complete on every path, and string/None amounts no longer raise.
  • agent_message is sanitized — no rule IDs, issue types, or schema internals leak into agent-facing output.
See the updated Schema verifier page.

SQLVerifierDiagnosticResult (#295)

  • verify_sql() returns DiagnosticResult. A safe query is VERIFIED (sql_verifier.sql_valid, is_valid: true, proof_ref from the AST). A proven-malicious query is VERIFIED-as-malicious (sql_verifier.malicious, is_valid: false, malicious_classification: true) — proving malice is a successful proof, so it retains its proof_ref.
  • BLOCKED is 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, and sql_verifier.execution_error.
  • POST /verify/sql attaches the admission field (ADMIT / BLOCKED) alongside the verdict.
See the updated SQL engine page.

CodeVerifier & SecureCodeExecutorDiagnosticResult (#296)

  • verify_code(), verify_python_deep(), and verify_batch() return DiagnosticResult with the truth-vs-admission semantics described above.
  • verify_batch() returns per-item verdicts plus a summary (safe / unsafe / blocked counts, total_critical) and an overall is_valid that is true only 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 with CONSTRAINT_DANGEROUS_PATTERN. The scan is AST-aware, so dangerous keywords appearing only in comments, docstrings, or string literals do not cause false denials.
  • ConsensusVerifier and StatsVerifier code stages now require is_verified and developer_fields.is_valid is True, so consensus results can no longer admit unsafe code.
See the updated Code engine page.

StatsVerifierDiagnosticResult (#297)

  • verify_stats() returns DiagnosticResult with the execution-is-not-verification semantics described above.
  • The API boundary is a thin pass-through: POST /verify/stats forwards the engine’s DiagnosticResult through enforce_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 mutable developer_fields.is_valid metadata is true.
  • A non-serializable sandbox result (e.g. a DataFrame) is coerced to a JSON-safe value before entering developer_fields, so a legitimate UNVERIFIABLE verdict is not silently downgraded to BLOCKED.
See the updated Stats engine page.

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_ref binds 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 on admission / 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 #291
v6.0.0 closes the Trust Boundary Completion epic (Issue #263, 12/12 sub-issues). Every verification pathway now returns a unified DiagnosticResult and routes through enforce_trust_decision. The trust boundary is no longer advisory: the control plane requires and verifies attestation before admitting VERIFIED results, and VERIFIED is a protocol guarantee backed by a deterministic proof_ref — never by execution, agreement, confidence, or provenance.
Breaking change. /verify/* API responses now use the unified DiagnosticResult schema (status / agent_message / developer_fields / proof_ref). Consumers that parsed the previous ad-hoc dict responses must migrate to the unified 3-layer contract. The high-level SDK clients (qwed, qwed_sdk, @qwed-ai/sdk, and the qwed Rust crate) already surface DiagnosticResult at v6.0.0 and require no code change beyond the version bump.

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 as VERIFIED. 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 return DiagnosticResult — unified response contract across every verification surface (PR #276)
  • Control plane enforces mandatory attestationrequire_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/batch results carry DiagnosticResult, proof_ref, and attestation, and pass through enforce_trust_decision (PR #282). See POST /verify/batch.
  • Attestation scope alignment — attestations bind to the translated expression, not the natural-language query, so query_hash binds 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.
  • ConsensusResult uses the DiagnosticStatus enum with proof_ref and verified_evidence (PR #280). See Consensus engine.
  • FactVerifier heuristic SUPPORTED verdict → UNVERIFIABLE with advisory_checks (PR #283).
  • Consensus code execution advisory-only — VERIFIEDUNVERIFIABLE (PR #281).
  • Consensus stats advisory-only — never VERIFIED (PR #277).
  • LogicVerifier migrated to DiagnosticResult (PR #262 — details in the entry below).
  • AgentStateGuard proof_ref is now a real sha256 of committed bytes, not a static sentence (PR #284). See Agent state guard.

Engineering and security hardening

  • TOCTOU closure in enforce_trust_decisiondeveloper_fields snapshotted via recursive rebuild (no deepcopy alias window); fail-closed snapshot (PR #290)
  • Attestation signature verified before claim decode — silent generic error for every failure mode (PR #287)
  • Tenant-isolated verification cacheVerificationCache keys namespaced by normalized tenant_id (PR #286)
  • Unicode normalization in AgentStateGuard canonicalization — NFC collisions rejected (PR #288)
  • Mandatory proof artifact for VERIFIED attestations 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 returns DiagnosticResult. 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
LogicVerifier is the second engine — after FactVerifier — to conform to the unified 3-layer DiagnosticResult contract introduced in v5.2.0. All nine public methods now return a DiagnosticResult with .status, .developer_fields, .is_verified, .agent_message, and .proof_ref. The legacy LogicResult dataclass has been removed. Two fail-closed input-strictness fixes ship with it.
Breaking change for direct LogicVerifier callers. The LogicResult dataclass is gone. Code that reads result.status == "SAT", result.model, result.error, result.proof_summary, or result.explanation must migrate to result.is_verified and result.developer_fields. The high-level SDK client (client.verify_logic()) and the /verify/logic API endpoint response shape are unchanged.

What changed

  • All nine methods return DiagnosticResult. verify_logic, verify_with_quantifiers, verify_bitvector, verify_array, prove_theorem, check_implication, check_equivalence, verify_optimization, and check_vacuity all now return a DiagnosticResult with the three diagnostic layers populated.
  • Per-method SAT/UNSAT disambiguation. Z3’s sat/unsat outcome is mapped to a DiagnosticStatus per method (for example, verify_logic satVERIFIED, but prove_theorem satBLOCKED with a counterexample; prove_theorem unsatVERIFIED because the theorem was proved by contradiction). The raw solver verdict is preserved on developer_fields["deterministic_verdict"].
  • symbol_table on every VERIFIED result. 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_ref from the Z3 assertion stack. Every VERIFIED result carries a deterministic sha256:... proof_ref computed from the solver’s assertions — the authority bit that downstream gates use for admissibility.
  • Empty variables dict now fails closed. The _infer_variables type-guessing heuristic has been removed. Calls with an empty variables dict return BLOCKED with constraint_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 return BLOCKED with constraint_id = "dsl_compiler.type_validation".
  • agent_message is sanitized. No raw Z3 output leaks into the agent-facing layer. Structured diagnostic details live under developer_fields.

Before and after

Reading a satisfiability result
Proving a theorem
Empty variables 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 response status and trust_boundary.overall_status. Both fields are set from the enforced decision after attestation is issued, never from the raw verifier verdict.
  • A VERIFIED math result is only surfaced once create_verification_attestation() returns ISSUED. Any attestation signing failure downgrades the response to BLOCKED — the error code is echoed under trust_boundary.attestation_error and no token is returned.
  • trust_boundary.attestation_policy is now always "mandatory". The previous advisory mode has been removed.
  • The attestation qwed.query_hash binds to the translated expression the engine actually evaluated, not to the natural-language query. The natural-language query is still returned in response.user_query for display, and trust_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. Hashing user_query will 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 as BLOCKED with attestation_error, not as a raw VERIFIED verdict.
  • See Trust boundary and POST /verify/natural_language for the full response contract, and Cryptographic attestations for the AttestationResult fail-closed contract.

v5.3.0 — SymbolicVerifier: DiagnosticResult reference implementation

Released: July 25, 2026 · GitHub Release · Minor · PR #244
QWED-Verification v5.3.0 makes SymbolicVerifier the first fully DiagnosticResult-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.
Breaking change for the Code engine. All six SymbolicVerifier public methods now return a DiagnosticResult instead of a dict. Callers that read result["status"], result.verified, result["issues"], result["complexity"], result["loop_depth"], or result["recursive"] must migrate. See the Code engine migration guide for the field-by-field mapping.

What changed

  • Six methods migrated to DiagnosticResultverify_code, verify_function_contract, verify_safety_properties, verify_bounded, analyze_complexity, and get_verification_budget now return the unified 3-layer diagnostic type. Every result carries status (DiagnosticStatus), agent_message (Layer 1), developer_fields (Layer 2), and proof_ref (Layer 3, always None for this engine).
  • verification_mode on every resultdeveloper_fields["verification_mode"] is "symbolic" for standard runs and "bounded_symbolic" for verify_bounded(), so callers can distinguish the two verification modes without inspecting the call site.
  • VERIFIED is intentionally never emitted — CrossHair’s search is timeout-bounded, not a completeness proof, so a clean run maps to UNVERIFIABLE with constraint_id = "symbolic_verifier.no_counterexample_found". The DiagnosticResult contract structurally requires a proof_ref for VERIFIED, and this engine has no proof artifact to bind. Downstream policy gates must reject symbolic-engine output for control flow.
  • verify_code() no longer accepts check_assertions — the parameter was never wired up. The current signature is verify_code(self, code: str) -> DiagnosticResult.
  • Advisory checks throughoutverify_safety_properties, analyze_complexity, and get_verification_budget attach structured AdvisoryCheck entries to developer_fields["advisory_checks"]. Advisory checks never influence the verdict.

Before and after

Before
After

Field cheat sheet

Version propagation

Included PRs

  • #212 — feat: migrate SymbolicVerifier to DiagnosticResult (Phase 1)
  • #220 — fix: remove unused check_assertions parameter from verify_code
  • #239 — feat: add verification_mode to SymbolicVerifier DiagnosticResults
  • #240 — feat: migrate verify_bounded to return DiagnosticResult
  • #241 — feat: migrate get_verification_budget to return DiagnosticResult
  • #242 — feat: migrate analyze_complexity to return DiagnosticResult
  • #243 — feat: migrate verify_safety_properties to return DiagnosticResult
  • #244 — release: v5.3.0 — SymbolicVerifier: DiagnosticResult reference implementation

What this means for you

If your integration reads any dict-shape field from a SymbolicVerifier 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 signed FORWARDED verdict for empty or malformed FINANCIAL_TRANSACTION and LOGIC_ASSERTION payloads, and no longer silently forwards GENERAL / DATA_QUERY messages. Both cases now return verdict.status = "unverifiable" with attestation_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. A FINANCIAL_TRANSACTION missing data, line_items, or claimed_total, or with non-numeric amounts, now returns status=unverifiable from finance_guard. A LOGIC_ASSERTION with a missing, non-list, or empty assertions array, or malformed entries, returns status=unverifiable from logic_guard. Earlier releases collapsed these cases to verified=True + a signed FORWARDED verdict.
  • GENERAL and DATA_QUERY are no longer silently forwarded. The passthrough branch now returns an unverifiable verdict with engine_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 Bypass verification stage. The pipeline has four stages, not five. config.trusted_agents is pre-added to the TrustBoundary allowlist 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.
  • CodeGuard is 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 is BLOCKED if either layer triggers, or HEURISTIC_PASS if both are clean — not a proof of safety.
  • JWT attestation expiry is 300 seconds (5 minutes), not 24 hours. The default validity_seconds on A2ACryptoService is 300 — one A2A hop lifetime. The Crypto attestations page and its signing example were updated.

New verdict-status contract

Behavior change. Downstream code that assumed every VerificationVerdict carries an attestation_jwt will see None on UNVERIFIABLE verdicts and must handle it. Callers that treated a signed FORWARDED verdict as proof that finance/logic content had been checked should also branch on verdict.status — an empty payload no longer produces a signed FORWARDED, and a GENERAL/DATA_QUERY message no longer produces one at all. Note that VerdictStatus.ERROR exists in the public enum but is not emitted by intercept() — internal exceptions surface as BLOCKED (or FORWARDED when block_on_error=False), never as an error verdict.

What this means for you

If you integrate QWED-A2A: audit any caller that reads attestation_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 #290
Two 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 detached DiagnosticResult snapshot, closing a TOCTOU window where a concurrent caller could mutate developer_fields between 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-side attestation.rejected audit 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_fields is 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’s DiagnosticResult can neither skew the decision nor leak into the returned result.
  • Fail-closed snapshot failure. When developer_fields contains an unsupported value type (custom class instance, generator, open file handle) or an object that resists copying, enforce_trust_decision() returns a BLOCKED result with constraint_id="trust_gate.diagnostic_snapshot_failed". Only the exception type is logged — never the exception message, which could embed caller data.
  • Runtime dependency. cryptography is now a runtime dependency (previously an optional extra), so ES256 signature verification runs by default.

Before and after

Rejection error — before
Rejection error — after
Trust-boundary snapshot — before
Trust-boundary snapshot — after

What this means for you

Callers of verify_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 #248
Two related enforcement-boundary changes ship together. Issuance side, the crypto layer now refuses to sign a VERIFIED verdict that has no proof artifact. Consumption side, a new enforce_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 with verified=True (or status="VERIFIED") and an empty or missing proof_data now returns AttestationResult(status=BLOCKED, token=None, error_code="VERIFIED_WITHOUT_PROOF", error=...). The crypto layer will not sign a VERIFIED result that has no evidence to hash into the token’s proof_hash claim. UNVERIFIABLE and BLOCKED verdicts are unaffected.
  • New enforce_trust_decision() trust-boundary gate. Exported from qwed_new.core, this is the single consumption-side entry point every release gate must route through. It takes the engine’s DiagnosticResult plus the attestation token, verifies the JWT (signature, expiry, trusted issuer), and blocks the decision when the token’s qwed.result.status, qwed.query_hash, or qwed.proof_hash claims do not match the result. UNVERIFIABLE/BLOCKED results pass through unchanged.
  • require_attestation policy toggle. enforce_trust_decision(result, require_attestation=True, ...) (default) is the mandatory policy — VERIFIED without a valid token is downgraded to BLOCKED. require_attestation=False is the advisory policy for staged rollouts: a missing token still passes as VERIFIED, but a present-and-invalid token still blocks. Both modes emit structured trust_gate.blocked audit 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
Consumption — release gate on a VERIFIED result

What this means for you

If you call create_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, an AttestationContext describing the current request, and rejects the token if the sender, receiver, payload hash, or session claim does not match.
Breaking change. The single-argument form verify_attestation(token) has been removed — there is no context-free overload. Every call site must construct an AttestationContext and pass it as the second positional argument, or the call raises TypeError. See the migration example under “What this means for you” below for the exact before/after.

What changed

  • New AttestationContext dataclass in qwed_a2a.security.crypto with fields sender_agent_id: str, receiver_agent_id: str, payload: Any, and optional session_id: str | None. The payload is hashed internally with the same deterministic method as sign_verdict(), so callers never handle raw hashes.
  • New context-binding verification step — sender, receiver, payload hash, and (when supplied) session_id are compared against the token’s qwed_a2a claims and sub claim. 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 jti replay check. This is deliberate: an out-of-context token no longer pollutes the jti registry, 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 of verify_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:
See Verifying an attestation for the full field-by-field reference, the updated ordered verification steps, and the complete list of rejection messages.

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 #219
Three core-verifier fixes tighten the math engine’s fail-closed contract. verify_statistics(statistic="mode"), verify_matrix_operation(operation="eigenvalues"), and verify_irr() no longer produce VERIFIED when the underlying claim is ambiguous, under-specified, or numerically unproven. Callers see BLOCKED or CORRECTION_NEEDED with structured diagnostics instead of a best-effort answer.
Behavior change. Inputs that previously received VERIFIED may now receive BLOCKED or CORRECTION_NEEDED — treat both as unverified. If your code branched on result.verified or result.status == "VERIFIED" alone, it will continue to work; if you consumed calculated/calculated_irr/calculated_eigenvalues from a BLOCKED result as a fallback, those fields are not present on BLOCKED responses. They remain available on CORRECTION_NEEDED responses for diagnostic use. Consume the new structured fields (ambiguous_modes, calculated_count/claimed_count, converged, iterations_used) for richer diagnostics.

What changed

  • verify_statistics(statistic="mode") requires a unique mode. When two or more values tie for the maximum frequency, the engine returns BLOCKED with an ambiguous_modes list instead of heuristically picking one. Only a unique mode can produce VERIFIED.
  • verify_matrix_operation(operation="eigenvalues") requires cardinality match. The claimed eigenvalue list length must equal the calculated count (counting algebraic multiplicity). Mismatched lengths return CORRECTION_NEEDED with calculated_count/claimed_count. Previously the value comparison used zip, which silently truncated to the shorter list.
  • verify_irr() requires proof of Newton-Raphson convergence. BLOCKED is 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 include converged: true and iterations_used.

Before and after

Ambiguous mode
Incomplete eigenvalue claim
IRR with multiple sign changes

What 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 #40
qwed-finance v2.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 so QWED Finance Guard reports a single consistent version across PyPI, npm, and SARIF output in GitHub Advanced Security.

What changed

  • qwed-finance Python package is now v2.1.0 on PyPI (qwed_finance.__version__ == "2.1.0").
  • @qwed-ai/finance npm package version bumped to 2.1.0 to match.
  • GitHub Action auto-syncs its reported version from the installed package — the QWED Finance Guard name in workflow logs and the version field on SARIF uploads to the GitHub Security tab now both read 2.1.0 instead of a hardcoded v2.0.
  • Quickstart qwed-verify.yml workflow 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:
If you run QWED Finance in CI, update the action reference so SARIF findings and workflow-run names line up with the current package:
This is a version-sync patch for the existing v2.1.0 release (May 2026). It contains no new API or guard behavior changes beyond those already shipped in v2.1.0 (Decimal migration, fail-closed enforcement, rate parsing fix). For the original breaking changes, see the v2.1.0 release notes. See GitHub Action (CI/CD) for the updated workflow example.

QWED-UCP — v0.3.0 released

Released: July 20, 2026 · GitHub PR #38
qwed-ucp v0.3.0 is now the current release. This version ships the typed TrustStatus enum on every verification result, alongside the fail-closed middleware and internal-error handling delivered over the v0.2.x series.

What changed

  • qwed-ucp Python package is now v0.3.0 on PyPI.
  • Express middleware qwed-ucp-middleware package version bumped to match.
  • TrustStatus enum 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:
If you audit checkouts in CI, update the action reference:
Existing code that branches on 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 #36
When a guard raised an unexpected exception, the Express middleware previously logged the error and called next() — letting an unverified checkout through to the downstream handler. That defeated the trust boundary. The catch block now short-circuits with HTTP 500, X-QWED-Verified: false, and code: "INTERNAL_VERIFICATION_ERROR" so an internal crash can no longer be mistaken for a passing verification.

What changed

  • Express middleware catch block is now fail-closed. On any exception raised during verifyCheckoutLocally(), the middleware returns:
    The underlying exception is logged server-side via console.error but is not included in the response body — raw stack traces, file paths, and internal messages stay out of client-visible output.
  • X-QWED-Verified: false is set on the 500 response so upstream policy layers can treat it the same as a 422 failure.
  • npm package fix. qwed-ucp-middleware.js is now included in the published files array. Previous versions installed via npm were missing the entrypoint that index.js required at runtime.
Behavior change. Any client code that treated an Express middleware error as a soft pass (e.g. retrying without checking the status, or relying on next() being called) will now see a 500. Treat 500 INTERNAL_VERIFICATION_ERROR the same as 422 VERIFICATION_FAILED — the request has not been verified and must not be settled.

What this means for you

Existing integrations that already branched on X-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 single verified: bool that collapsed “proof disproved,” “proof could not be established,” “input outside supported semantics,” and “verifier engine crashed” into the same False bucket. Every result dataclass now also carries a typed status: TrustStatus field so downstream policy code can make trust-aware decisions without parsing error strings.

What changed

  • New TrustStatus enum exported from qwed_ucp with seven states: VERIFIED, FAILED, UNVERIFIABLE, UNSUPPORTED, PARTIAL, ENGINE_ERROR, and QUARANTINED (reserved).
  • status field on every resultGuardResult, UCPVerificationResult, and each per-guard result type (MoneyGuardResult, StateGuardResult, SchemaGuardResult, LineItemsGuardResult, DiscountGuardResult, CurrencyGuardResult, RefundGuardResult, TipGuardResult, FeeGuardResult, AttestationResult).
  • UCPVerifier.verify_checkout() now surfaces ENGINE_ERROR when any guard raises an exception, instead of silently collapsing to verified=False.
  • verified: bool is preserved as a backward-compatible derived field: result.verified is True only when result.status == TrustStatus.VERIFIED. Existing constructors that pass verified=True/verified=False continue to produce the corresponding VERIFIED/FAILED status.

What this means for you

Existing code that reads result.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 #32
The 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-sessions route that defeated the trust boundary — an attacker could send {}’s worth of nothing and skip verification. Both middlewares now reject those requests with HTTP 422, X-QWED-Verified: false, and code: "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"
  • Express: all three cases return "Empty or non-JSON request body: cannot verify unparseable payload".
  • Non-protected methods and paths (for example, GET /health or a POST to a route outside verify_paths) still pass through untouched.
This is a fail-closed behavior change. Any client that was previously reaching a /checkout-sessions, /checkout, /cart, or /payment handler with a missing, malformed, or non-object JSON body will now receive 422 UNPARSEABLE_REQUEST instead. Send checkout payloads as application/json with a JSON object at the top level, or narrow verify_paths if a route should not be treated as a checkout endpoint.

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 #29
A2ACryptoService no longer generates an ephemeral ECDSA P-256 key per process. The signing key is now loaded from the QWED_A2A_SIGNING_KEY_PEM environment variable (or the pem_key constructor argument) so attestation JWTs issued before a restart remain verifiable afterwards. A new /.well-known/jwks.json endpoint publishes the current public key for external consumers.

What changed

  • Persistent signing keyA2ACryptoService reads an unencrypted PKCS#8 P-256 PEM from QWED_A2A_SIGNING_KEY_PEM on first use. The derived key_id is a SHA-256 fingerprint of the public key, so it stays stable as long as the PEM does.
  • Fail-closed on missing keysign_verdict, verify_attestation, get_public_key_jwk, and A2AVerificationInterceptor.intercept() all raise RuntimeError when the PEM is missing, malformed, or uses a curve other than SECP256R1. The FastAPI gateway surfaces this as HTTP 503 Signing key unavailable.
  • JWKS endpoint — A new wellknown_router exposes GET /.well-known/jwks.json for 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

A2ACryptoService() and A2AVerificationInterceptor() no longer produce a working signer without configuration. Every deployment must now set QWED_A2A_SIGNING_KEY_PEM (and continue to set QWED_A2A_DEPLOYMENT_ID) before the service starts. Generate a key with:
Load the PEM into QWED_A2A_SIGNING_KEY_PEM from your secrets manager, and make sure every replica in a logical deployment uses the same PEM so their kids match and tokens cross-verify.

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, or BLOCKED. No HEURISTIC or AMBIGUOUS middle 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 on UNVERIFIABLE and BLOCKED.
  • First migrationsTDSGuard, InputCreditGuard, and GSTGuard (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 #44
Three input-strictness fixes ship together. CryptoTaxGuard.verify_flat_tax_rate now compares quantized paise values exactly instead of within a 0.1 tolerance, ValuationGuard and RemittanceGuard reject 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 — The Decimal("0.1") tolerance was removed. Both the computed expected_tax and the caller’s claimed_tax are now quantized to two decimal places using ROUND_HALF_UP and compared with an exact ==. A 1-paise (0.01) deviation correctly returns verified=False.
  • ValuationGuard.verify_conversion — Added explicit range checks: discount must be in [0, 1), and cap, next_round_price, and investment must all be strictly positive. DivisionByZero is now caught alongside InvalidOperation so a degenerate cap = 0 or discount = 1 no longer crashes — it fails closed with a structured {"verified": False, "error": "..."} response. The previous behavior allowed a discount > 1 to produce a negative share count.
  • RemittanceGuard.verify_lrs_limit — After numeric parsing, amount_usd and financial_year_usage are 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 modelsAddress, WorkArrangement, WorkerClassificationParams, ContractorPayment, TaxEntry, DeductionEntry, PayrollEntry, and VerificationResult are now configured with model_config = ConfigDict(extra="forbid"). Any payload with an unexpected key raises a Pydantic ValidationError at the boundary. QWEDTaxMiddleware already surfaces this as status: "BLOCKED" with risk: "INVALID_PAYLOAD".

Breaking changes

Callers that previously relied on the 0.1-rupee tolerance in CryptoTaxGuard.verify_flat_tax_rate will now receive verified=False for claims that differ from vda_income * 0.30 by 1 paise or more. Round your claimed tax to two decimal places with ROUND_HALF_UP before calling the guard.
Payloads sent through QWEDTaxMiddleware (or constructed directly with the QWED-Tax input models) that include keys outside the declared schema now raise ValidationError. Strip unknown fields from AI-generated output — including typos and speculative overrides like "override_verification": true — before invoking the middleware.
ValuationGuard.verify_conversion no longer returns a result for cap <= 0, next_round_price <= 0, investment <= 0, discount < 0, or discount >= 1. These inputs now return {"verified": False, "error": "..."} instead of crashing or producing nonsensical share counts.

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 #43
Two fail-closed fixes ship together. The Gusto interceptor middleware no longer overstates a gross-to-net arithmetic pass as full tax verification, and ReciprocityGuard no longer returns verified=True for 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 now status: "ARITHMETIC_VERIFIED" with execution_permitted: false. The middleware only verifies gross-to-net math; classification, withholding legality, reciprocity, and filing checks are still required before execution. The response includes checks_run (what was verified) and checks_not_run (what is still required) so callers can decide what to run next.
  • TaxPreFlight report — Every report now includes a checks_not_run list covering both unselected guards and known gaps (checks not yet implemented for the action). For example, action="hire" reports payroll_arithmetic, withholding_legality, reciprocity, and filing_obligations as known gaps.
  • ReciprocityGuard — The Z3 solver was removed (the prior expression was tautologically satisfiable and ignored the same_state parameter). 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 new verify_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

The middleware no longer returns status: "VERIFIED" or execution_permitted: true. Any caller that gated execution on decision["status"] == "VERIFIED" or decision["execution_permitted"] being truthy will now always block. Update your integration to handle ARITHMETIC_VERIFIED and run the checks listed in checks_not_run (worker classification, withholding legality, reciprocity, filing) before forwarding to Gusto/Avalara.
ReciprocityGuard no longer returns verified=True for state pairs without a reciprocity agreement. Callers that previously relied on a Z3-backed “always sat” result for, e.g., NJ → NY will now correctly receive verified=False. Route these to your withholding logic for the work state or to human review.

What this means for you

If your agent forwards payroll payloads to Gusto/Avalara based on a VERIFIED 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=False with an “ambiguous classification” error. The guard only returns CONTRACTOR when 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=False error naming the unknown value, instead of being coerced to OTHER or INDIVIDUAL and potentially suppressing a statutory RCM obligation. The verifier also gained an optional claim parameter — when you pass a claimed_is_rcm value, the guard compares it to the computed result and only returns verified=True on an exact match. Calls without the claim get a computed_only=True flag so calculation and verification are no longer conflated.
  • CryptoTaxGuard — Zero VDA income now verifies a claimed tax of zero, instead of returning verified=True regardless of claim. Negative VDA income (a loss) returns verified=False with a message directing the caller to use verify_set_off for loss treatment — the method does not internally invoke verify_set_off, so callers must handle the verified=False branch explicitly.

What this means for you

If your agent relied on any of these guards returning verified=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. TaxPreFlight now 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=None with a manual-determination flag, instead of defaulting to “no filing required.”
  • InputCreditGuard (GST)verified=True remains the legal default (ITC is allowed unless specifically blocked), but unknown categories now carry an explicit unverified_category=True flag in the result and an audit_trace entry of category_match: "default_allow" so downstream consumers can distinguish known-eligible from default-allowed.

What this means for you

If your agent currently relies on a verified=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, 2026
qwed-infra now 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.
No runtime behavior changes. Affects contributors and anyone consuming the repository’s CI.

QWED-MCP — boundary-check parity with QWED-Infra

Released: June 18, 2026
The qwed-mcp boundary-check tool now catches the same import-alias, module-alias, wildcard-import, eval/exec alias, and shell=True patterns that qwed-infra does, and fails closed when the scan root is missing.

What this means for you

If you run qwed-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, 2026
Aligns qwed-open-responses with the rest of the QWED package versions and patches a transitive qs CVE flagged by Dependabot.
No API changes. Upgrade to pick up the dependency fix.

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 statusVERIFIED / UNVERIFIABLE / BLOCKED only. No HEURISTIC or AMBIGUOUS proliferation; richer distinctions live in developer_fields.constraint_id
  • proof_ref is the authority bit — present = admissible for control flow, None = reject. No separate authoritative boolean needed (resolves #190 design debate)
  • VERIFIED requires proof — structurally enforced in __post_init__; “VERIFIED without proof” is impossible to construct
  • Frozen dataclassesDiagnosticResult and AdvisoryCheck are frozen=True; post-construction mutation blocked
  • Advisory checks never influence verdictsAdvisoryCheck.advisory_only=True enforced via __post_init__
  • compute_proof_ref() — deterministic sha256 hashing of JSON-serialized evidence
  • from_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. Existing VerificationResult 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
See the Verification Diagnostics guide for full API documentation.
Older entries: Changelog Archive