> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog Archive

> Archived release notes for older QWED Protocol updates: past guard fail-closed hardening, breaking changes, and SDK migrations across QWED engines.

Older changelog entries. See [Changelog](/changelog) for recent releases.

***

## QWED-Tax — fail-closed on unknown or unmodeled tax rules

**Released: June 19, 2026** · [GitHub PR #41](https://github.com/QWED-AI/qwed-tax/pull/41)

> Closes a fail-open gap across six QWED-Tax guards. Inputs that hit an unmodeled rule path (unknown service type, state, asset class, payment type, address jurisdiction, or ITC category) no longer return a quiet `verified=True`. They now return `verified=False` with a structured error, so an upstream LLM cannot route a payment through a category the guard has not been configured to evaluate.

### What changed

* **`TDSGuard.calculate_deduction`** — Unknown `service_type` returns `{"verified": False, "error": "No TDS rule configured for service type '<TYPE>'. Cannot verify — block pending rule configuration."}`. Previously returned `{"verified": True, "deduction": "0"}`, which `TaxPreFlight._check_invoice_tds` accepted as a clean payment with zero withholding.
* **`CapitalGainsGuard.verify_tax_rate`** — Unknown `(asset_type, term)` pair returns `{"verified": False, "error": "No statutory rate configured for <asset>_<term>. Cannot verify claimed rate."}`. The "no hard constraint, assume verified" branch has been removed.
* **`NexusGuard.check_nexus_liability`** — Unconfigured `state` returns `{"verified": False, "error": "State <CODE> not in configured nexus threshold table. Cannot verify nexus liability — block pending rule configuration."}` instead of `verified=True` "not in high-risk list".
* **`AddressGuard.verify_address`** — Unmodeled state returns `{"verified": False, "message": "State <CODE> not in validation database. Address cannot be auto-verified — manual review required."}` instead of `verified=True` "assumed valid".
* **`Form1099Guard.verify_filing_requirement`** — Unmodeled `payment_type` returns `{"filing_required": "UNVERIFIABLE", "form": None, "reason": "No filing rule configured for payment type '<TYPE>'. Cannot verify filing requirement — manual determination required."}`. Previously returned `filing_required=False`.
* **`InputCreditGuard`** — ITC under GST is "allowed unless specifically blocked", so the default verdict for unknown categories is still `verified=True`. The response now also carries `"unverified_category": True` and `audit_trace.inputs.category_match: "default_allow"` so consumers can distinguish a known-eligible category from a default-allow fallback.

### Why this matters

`TDSGuard` was the live exploitation path. An AI agent that classified a vendor payment under an unrecognized service category produced `verified=True, deduction="0"`, which `TaxPreFlight._check_invoice_tds` treated as "no block needed" — the payment executed with zero withholding. All six guards above now refuse to sign off on rule paths they cannot independently evaluate.

### Behavior comparison

| Guard               | Trigger                      | Before                                   | After                                                             |
| ------------------- | ---------------------------- | ---------------------------------------- | ----------------------------------------------------------------- |
| `TDSGuard`          | Unknown `service_type`       | `verified=True`, `deduction="0"`         | `verified=False` with error                                       |
| `CapitalGainsGuard` | Unknown `(asset_type, term)` | `verified=True`, "no hard constraint"    | `verified=False` with error                                       |
| `NexusGuard`        | Unknown `state`              | `verified=True`, "not in high-risk list" | `verified=False` with error                                       |
| `AddressGuard`      | Unknown `state`              | `verified=True`, "assumed valid"         | `verified=False`, "manual review required"                        |
| `Form1099Guard`     | Unknown `payment_type`       | `filing_required=False`                  | `filing_required="UNVERIFIABLE"`, "manual determination required" |
| `InputCreditGuard`  | Unknown category             | `verified=True` (silent)                 | `verified=True` + `unverified_category=True`                      |

### Breaking changes

<Warning>
  Any integration that branched only on `verified=True` from `TDSGuard`, `CapitalGainsGuard`, `NexusGuard`, or `AddressGuard` to mean "proceed" must now also handle a `verified=False` response with `error` for the unknown-rule path. The recommended action is to surface the error to a human reviewer rather than retry with a different input.
</Warning>

<Warning>
  `Form1099Guard.verify_filing_requirement` may now return `filing_required="UNVERIFIABLE"` (a string), not just `True` or `False`. Callers that did `if result["filing_required"]:` will incorrectly treat `"UNVERIFIABLE"` as truthy. Update consumers to branch on the three-state value explicitly.
</Warning>

### Migration

```python theme={null}
from qwed_tax.jurisdictions.us.form1099_guard import Form1099Guard

result = Form1099Guard().verify_filing_requirement(payment)

# Before
if result["filing_required"]:
    file_form(result["form"])

# After
filing = result["filing_required"]
if filing is True:
    file_form(result["form"])
elif filing == "UNVERIFIABLE":
    escalate_to_reviewer(payment, result["reason"])
# filing is False -> below threshold, nothing to file
```

See the [QWED-Tax guards reference](/tax/guards) for the updated fail-closed notes on each guard.

***

## QWED-Legal v0.4.0 — verification traces across all guards + fail-closed hardening

**Released: May 30, 2026**

> Every guard now returns an auditable `verification_trace` with explicit evidence types, and a batch of trust-boundary fixes stop guards from presenting heuristic or unsupported results as proof. Closes 8 hardening issues (#11, #12, #16, #17, #18, #19, #20, #21).

### Verification traces (new)

* **`verification_trace` on every guard** — an ordered list of `VerificationStep` records that make each decision auditable. Not a narrative explanation.
* **`evidence_type` taxonomy** — each step is `DETERMINISTIC`, `PARSED`, `INFERRED`, `HEURISTIC`, or `UNSUPPORTED`. `VerificationStep.is_proven()` returns `True` only for `DETERMINISTIC`.
* **`to_dict()` / `trace_to_dict()`** — JSON-safe serialization for audit logs. Non-serializable inputs are stringified (no silent data loss); each serialized step carries an explicit `is_proven` flag.

### Trust-boundary changes

* **`FairnessGuard` never returns `verified=True`** (#18) — fairness cannot be proven by counterfactual text substitution and string equality. A consistent outcome is `UNVERIFIABLE_FAIRNESS`; a differing outcome is a `HEURISTIC_BIAS_SIGNAL` for human review. Malformed swap input (empty, non-string values, case-colliding keys) fails closed.
* **`CitationGuard` separates format from authority** (#17) — `verified` is always `False`; a format match returns `status="unverifiable_authority"`. Format validity is never treated as proof a cited authority exists.
* **`IRACGuard` fail-closed reasoning boundary** (#12) — structure checks are `INFERRED` and the reasoning conclusion is `UNSUPPORTED`; a passing result is `unverifiable_reasoning`, never proof of correct legal reasoning.
* **`ClauseGuard` non-operative keyword handling** (#11) — heuristic conclusions are `INFERRED`; limited or ambiguous coverage fails closed instead of returning a misleading "consistent".
* **`StatuteOfLimitationsGuard` and `ContradictionGuard` reclassified as `MIXED`** — deterministic core (date arithmetic / Z3 SAT-UNSAT) over a parsed lookup; unmodeled inputs fail closed.

### Security hardening

* **`JurisdictionGuard` fail-closed on ambiguity and empty parties** (#16, #20) — unresolved warnings fail verification; empty `parties_countries` no longer returns a false `verified=True` (`all([])` fail-open fixed) in both `verify_choice_of_law` and `check_convention_applicability`.
* **`LiabilityGuard` exact-match caps** (#19) — cap, tiered, and indemnity checks require exact match after currency rounding; approximate tolerance is not accepted as proof.
* **`DeadlineGuard`** — business-day results fail closed when the requested holiday calendar cannot be built; on date parse failure, dates are `None` (not `datetime.min`).

### SDK changes (TypeScript `@qwed-ai/legal`)

* Every result interface now exposes `verification_trace`.
* Added `FairnessVerifier` reflecting the fail-closed contract; `verified` is hardcoded `false` at the boundary.
* `CitationResult` exposes `format_valid` / `status` / `verified: false`; `ClauseResult.conflicts` typed as `[number, number, string][]`.
* The fairness client factory is resolved via a validated `importlib` descriptor (no raw code execution).
* npm version reconciled from `1.0.0` to `0.4.0` to match the Python package.

### Breaking changes

* `FairnessGuard.verify_decision_fairness` no longer returns `verified=True`. Migration: treat the output as a signal requiring human review, consuming `status` / `risk` instead of `verified`.

***

## QWED-Verification — context-bound verification cache (replay prevention)

**Released: May 17, 2026**

> `VerificationCache` now keys every entry on both the normalized query **and** the trust-bound context: provider, model, policy version, tenant or session, and environment fingerprint. A mismatch on any context dimension is a deterministic cache miss. This prevents cross-context replay of `VERIFIED` results.

### What changed

* **New `CacheContext` datatype** — Required argument to every `get()` and `set()` call. Carries `provider`, `model`, and `policy_version` (required), plus optional `tenant_id` and `env_fingerprint`.
* **Composite cache key** — Each key combines `SHA-256(normalized_query)` with `SHA-256(canonical_context_json)`. Identical queries under different contexts never collide.
* **New `cache_v2` schema** — The on-disk SQLite table uses a composite `PRIMARY KEY (key, context_fingerprint)` and stores the fingerprint per entry. The cache re-validates the fingerprint on every read as defence-in-depth.
* **Legacy entries are never returned** — Pre-v2 cache rows have no context fingerprint. No code path reads them, and the cache treats them as `UNVERIFIABLE`.

### Breaking changes

* `VerificationCache.get(query)` and `VerificationCache.set(query, result)` now require a `CacheContext` argument. Calls without one raise `TypeError`.
* Results stored under one `(provider, model, policy_version, tenant_id)` tuple are not returned to a caller using a different tuple, even when the query string is identical.

### Example

```python theme={null}
from qwed_sdk.cache import CacheContext, VerificationCache

cache = VerificationCache()

ctx = CacheContext(
    provider="openai",
    model="gpt-4o",
    policy_version="v1",
    tenant_id="tenant-alpha",       # optional
    env_fingerprint="sha256-...",   # optional
)

# First call: miss, then populate
result = cache.get("2+2", ctx)            # None
cache.set("2+2", {"verified": True}, ctx)

# Same query + same context: hit
cache.get("2+2", ctx)                     # {"verified": True}

# Same query, different provider: deterministic miss (replay prevention)
ctx2 = CacheContext(provider="anthropic", model="claude-opus-4-5", policy_version="v1")
cache.get("2+2", ctx2)                    # None
```

See [Python SDK — Verification cache](/sdks/python#verification-cache) for the full API.

***

## QWED-Legal — `StatuteOfLimitationsGuard` fail-closed on unknown jurisdictions and claim types

**Released: May 10, 2026**

> Hardens `StatuteOfLimitationsGuard` so unknown jurisdictions and claim types return an `UNVERIFIABLE` result instead of fabricating a limitation period from a partial string match or a default fallback table.

### What changed

* **Exact jurisdiction match only** — Lookups uppercase and trim the input, then require an exact key in the rule table. Substring matches (for example, `"CALIF"` → `"CALIFORNIA"`, `"NEW"` → `"NEW YORK"`) are no longer accepted.
* **No default fallback for unknown jurisdictions** — Unsupported jurisdictions previously fell back to a generic `DEFAULT_LIMITATIONS` table. They now return a `StatuteResult` with `verified=False`, `jurisdiction_matched=False`, all date and period fields set to `None`, and a message that lists the supported jurisdictions.
* **No silent default for unknown claim types** — Unsupported claim types previously defaulted to a 3-year period. They now return `verified=False`, `claim_type_matched=False`, and a message that lists the supported claim types for the jurisdiction.
* **`get_limitation_period()` returns `Optional[float]`** — Returns `None` when the jurisdiction or claim type is unsupported, instead of always returning a number.
* **`compare_jurisdictions()` returns `Dict[str, Optional[float]]`** — Unsupported jurisdictions in the input list map to `None` in the result.
* **New `StatuteResult` fields** — `jurisdiction_matched: bool` and `claim_type_matched: bool` let callers distinguish "claim is time-barred" from "we cannot determine the limit".

### Breaking changes

* `StatuteResult.incident_date`, `filing_date`, `limitation_period_years`, `expiration_date`, and `days_remaining` are now `Optional` and are `None` when the input is unverifiable. Code that does arithmetic or comparisons on these fields must check `result.verified` (and the new `*_matched` flags) first.
* `get_limitation_period()` now returns `Optional[float]`. Code that previously assumed a numeric result must handle `None`.
* Callers that relied on partial jurisdiction matching (for example, passing `"Calif"` and expecting California rules) must update their inputs to a fully spelled, supported jurisdiction.

### Example

```python theme={null}
from qwed_legal import StatuteOfLimitationsGuard

guard = StatuteOfLimitationsGuard()

result = guard.verify(
    claim_type="breach_of_contract",
    jurisdiction="Atlantis",
    incident_date="2023-01-15",
    filing_date="2026-06-01",
)

print(result.verified)              # False
print(result.jurisdiction_matched)  # False
print(result.limitation_period_years)  # None
# result.message starts with "⚠️ UNVERIFIABLE: Jurisdiction 'Atlantis' ..."
```

See [Guards — StatuteOfLimitationsGuard](/legal/guards#6-statuteoflimitationsguard) and [Troubleshooting — StatuteOfLimitationsGuard issues](/legal/troubleshooting#statuteoflimitationsguard-issues) for migration guidance.

***

## QWED-Verification — audit logger fail-closed and per-organization chains

**Released: May 8, 2026** · [GitHub PR #179](https://github.com/QWED-AI/qwed-verification/pull/179)

> Hardens the cryptographic audit logger so signing-key, payload, and chain-continuity failures fail closed instead of silently writing or "verifying" a broken audit trail.

### What changed

* **Signing key required at startup** — `AuditLogger()` now raises `SecurityError` when `QWED_AUDIT_SECRET_KEY` is unset or empty. The previous insecure `dev_only_change_in_production` fallback was removed.
* **Per-organization hash chains** — Each organization now has its own isolated audit chain. Tampering or gaps in one tenant's chain cannot affect another tenant's verification.
* **Hash coverage extended to `raw_llm_output`** — New entries hash and verify the raw LLM output. Legacy entries created before this change are still accepted by the verifier via a backward-compatible canonical payload.
* **Stronger pre-append checks** — Before writing a new entry, the logger verifies the current persisted chain head and refuses to append on top of malformed payloads, missing hashes, or in-memory/persisted divergence. Appends are serialized with `BEGIN IMMEDIATE` on SQLite (or `SELECT … FOR UPDATE` on other backends).
* **Constant-time HMAC comparison** — Signature checks use `hmac.compare_digest` to prevent timing side channels.

### Action required

Set `QWED_AUDIT_SECRET_KEY` in every environment that runs the verification service (including CI and local development). The service will not start without it.

```bash theme={null}
export QWED_AUDIT_SECRET_KEY="your-secure-production-key-here"
```

See [Compliance — Audit trail setup](/advanced/compliance#3-audit-trail-setup) for the updated configuration steps and failure modes.

***

## QWED-Verification — fail-closed denial of unknown agent action types

**Released: May 6, 2026** · [GitHub PR #176](https://github.com/QWED-AI/qwed-verification/pull/176)

> Hardens `AgentService.verify_action` so action types with no registered engine or tool risk binding are denied instead of routed through a permissive default.

### What changed

* **`verify_action` denies unregistered action types** — Calls whose `action_type` is not in `ACTION_ENGINES` or `TOOL_RISK_LEVELS` now return `decision: "DENIED"` with error code `QWED-AGENT-ACTION-001`. The verification kernel no longer falls back to a `"security"` engine label for unknown actions.
* **Risk assessment is now deterministic** — `_assess_risk` raises `ValueError` if it is invoked for an unregistered `action_type`. Risk levels must come from explicit registration.
* **Step reservation released on action denial** — A `QWED-AGENT-ACTION-001` denial releases the in-flight step reservation, so the agent can retry the same `(conversation_id, step_number)` pair with a registered action type.

### Example

```python theme={null}
result = client.verify_action(
    agent_id="agent_abc123",
    action={"type": "transfer_funds_internal_v2", "query": "Move funds between ledgers"},
    context={"conversation_id": "conv_xyz", "step_number": 1},
)
# {
#   "decision": "DENIED",
#   "error": {
#     "code": "QWED-AGENT-ACTION-001",
#     "message": "Unknown action_type 'transfer_funds_internal_v2' cannot be verified without explicit registered semantics"
#   }
# }
```

### Why

Unknown actions are not verifiable actions. If QWED has no explicit semantics for an action type, it must not approve it or mark it as verified.

See [Agent verification — Unknown action types denied](/advanced/agent-verification#unknown-action-types-denied) and the [Agent spec error code table](/specs/agent#appendix-a-error-codes).

## QWED-Verification — fail-closed on unknown agent actions

**Released: May 6, 2026**

> The agent service now rejects verification requests whose `action_type` has no explicit registered semantics. Previously, an unrecognized action could pass through risk assessment and receive an `APPROVED` decision with a generic `"security"` engine fallback.

### Security

<Warning>
  **Unknown agent `action_type` values are rejected.** Agents that previously relied on the implicit `"security"` engine fallback for ad-hoc action types must now register them in `ACTION_ENGINES` (engine binding) or `TOOL_RISK_LEVELS` (tool-control binding). Unregistered actions return `QWED-AGENT-ACTION-001`.
</Warning>

### References

* [Fail-closed for unknown action types](/advanced/agent-verification#fail-closed-for-unknown-action-types)
* [Agent action registration errors](/api/errors#agent-action-registration-errors)

***

## QWED-Finance v2.1.0 — security audit hardening

**Released: May 2, 2026** · [GitHub PR #25](https://github.com/QWED-AI/qwed-finance/pull/25)

> Roll-up release that consolidates all P0/P1 findings from the deterministic security audit (Issue #16). Combines the four hardening PRs (#21–#24) into a single tagged release, bumps `pyproject.toml` from `2.0.1` to `2.1.0`, and promotes `mpmath` to an explicit runtime dependency.

### What's in this release

| PR                                                     | Change                                                                                                                         | Audit findings   |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ---------------- |
| [#21](https://github.com/QWED-AI/qwed-finance/pull/21) | Fail-closed enforcement in `OpenResponsesIntegration` + AML high-risk country list unified across all paths                    | S-01, S-02, S-06 |
| [#22](https://github.com/QWED-AI/qwed-finance/pull/22) | `BondGuard._parse_rate()` heuristic removed; returns `Decimal`                                                                 | C-04, H-01, S-05 |
| [#23](https://github.com/QWED-AI/qwed-finance/pull/23) | `float` → `Decimal`/`mpmath` migration for `BondGuard`, `DerivativesGuard`, `RiskGuard`                                        | C-01, C-02, C-03 |
| [#24](https://github.com/QWED-AI/qwed-finance/pull/24) | Integration-layer Black-Scholes unified with `DerivativesGuard`; `verify_compound_interest` fail-closed on unknown frequencies | N-01, N-04       |

### Breaking changes

<Warning>
  **Greeks are returned as Decimal-quantized strings, not `float`.** `verify_black_scholes` and the OpenResponses `price_option` tool both produce `result.greeks` with each Greek as a string (e.g. `"0.4502"`). Cast explicitly when you need a numeric value:

  ```python theme={null}
  from decimal import Decimal
  delta = Decimal(greeks["delta"])
  exposure = delta * Decimal(str(notional))
  ```
</Warning>

<Warning>
  **`BondGuard._parse_rate()` returns `Decimal` instead of `float`.** Downstream consumers that performed `float`-based arithmetic on the parsed rate must wrap inputs with `Decimal(str(value))` or migrate to `Decimal` math.
</Warning>

<Warning>
  **Unknown compounding frequencies now raise `ValueError`.** `FinanceVerifier.verify_compound_interest()` previously fell back silently to annual compounding for unrecognized values. It now raises with the accepted list (`annual`, `semi-annual`, `quarterly`, `monthly`, `daily`).
</Warning>

<Warning>
  **Unregistered tools in `OpenResponsesIntegration` are rejected.** Any tool call that does not match an explicitly registered tool is denied. Previously, unknown tools were auto-approved through a permissive default path.
</Warning>

### Dependency changes

* `mpmath>=1.3.0` is now a declared runtime dependency in `pyproject.toml`. It was already available transitively via `sympy`, so existing installs require no extra step.

### Test coverage

* 150 tests passing — zero regressions
* 23 new float-contamination tests (`TestBondGuardDecimal`, `TestDerivativesGuardMpmath`, `TestRiskGuardDecimal`, `TestNoFloatInOutput`)
* N-04 regression test asserting unknown compounding frequencies raise `ValueError`

### Upgrade

```bash theme={null}
pip install --upgrade qwed-finance==2.1.0
```

GitHub Action consumers should update the pinned ref:

```yaml theme={null}
- uses: QWED-AI/qwed-finance@v2.1.0
  with:
    test-script: tests/verify_agent.py
```

See the per-PR entries below for full implementation detail: [N-01/N-04 release blockers](#qwed-finance--release-blockers-n-01-and-n-04-resolved-no-go--go), [Decimal/mpmath migration](#qwed-finance--decimalmpmath-migration-for-bondguard-derivativesguard-and-riskguard), [OpenResponses fail-closed](#qwed-finance--openresponses-fail-closed-and-computed-status), and [rate parsing heuristic removed](#qwed-finance--rate-parsing-heuristic-removed).

***

## QWED-Finance — release blockers N-01 and N-04 resolved (NO-GO → GO)

**Released: May 2, 2026** · [GitHub PR #24](https://github.com/QWED-AI/qwed-finance/pull/24)

> Closes the two release-blocking findings from the post-fix deterministic audit. The integration layer now shares a single Black-Scholes implementation with `DerivativesGuard`, and `FinanceVerifier.verify_compound_interest` no longer silently defaults unknown compounding frequencies.

### What changed

* **`OpenResponsesIntegration._verify_option_price`** — Removed the duplicate IEEE-754 `math.log/exp/sqrt/erf` Black-Scholes routine. The `price_option` tool now delegates to `DerivativesGuard.verify_black_scholes()`, so OpenResponses-mediated pricing and direct guard pricing are bit-identical.
* **Tool result fields** — `price` and `delta` are now sourced from the guard's `mpmath`-backed, `Decimal`-quantized output instead of the integration's own float computation.
* **`FinanceVerifier.verify_compound_interest`** — Unknown values for `compounding` now raise `ValueError` listing the accepted frequencies (`annual`, `semi-annual`, `quarterly`, `monthly`, `daily`). Previously, unknown values silently fell back to annual compounding.
* **Test suite** — 150 tests pass (including a regression test that asserts unknown compounding frequencies raise `ValueError`); zero regressions.

### Breaking changes

<Warning>
  **`verify_compound_interest` no longer accepts arbitrary `compounding` strings.** Calls that previously relied on the silent annual-compounding fallback will now raise:

  ```python theme={null}
  # Before — silent fallback to annual
  verifier.verify_compound_interest(
      principal=10000, rate=0.05, periods=10,
      compounding="weekly",        # treated as "annual"
      llm_amount="$16,288.95",
  )

  # After — fail-closed
  verifier.verify_compound_interest(
      principal=10000, rate=0.05, periods=10,
      compounding="weekly",        # ValueError
      llm_amount="$16,288.95",
  )
  # ValueError: Unknown compounding frequency: 'weekly'.
  # Accepted values: annual, daily, monthly, quarterly, semi-annual
  ```

  Migrate by passing one of the supported frequencies, or pre-compute an equivalent annualized rate.
</Warning>

<Warning>
  **`price_option` now returns Greeks as Decimal-quantized strings**, matching `DerivativesGuard.verify_black_scholes`. If your downstream code expected `delta` as a `float`, cast explicitly:

  ```python theme={null}
  from decimal import Decimal
  delta = Decimal(result.result["delta"])
  ```
</Warning>

### Audit references

| Finding | Severity | Description                                                                                                           | Fix                                                                        |
| ------- | -------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| N-01    | HIGH     | Integration-layer Black-Scholes used `float` while `DerivativesGuard` used `mpmath`, producing path-dependent outputs | Delegate `_verify_option_price` to `DerivativesGuard.verify_black_scholes` |
| N-04    | MEDIUM   | `verify_compound_interest` silently defaulted unknown frequencies to annual (fail-open)                               | Raise `ValueError` listing accepted frequencies                            |

### Release-blocking checklist (post-fix)

* No fail-open execution paths
* No heuristic → verified transitions
* All financial math deterministic
* All parsing fail-closed
* Integration layer enforces verification
* Output status consistent and truthful
* No silent fallback logic

149 tests passed with zero regressions. Release status: **NO-GO → GO**.

See [Open Responses — Black-Scholes single source of truth](/finance/integrations/open-responses#black-scholes-single-source-of-truth) for the integration-layer change and [Derivatives Guard](/finance/guards#3-derivatives-guard-black-scholes) for the underlying mpmath implementation.

***

## QWED-Finance — decimal/mpmath migration for BondGuard, DerivativesGuard, and RiskGuard

**Released: May 2, 2026** · [GitHub PR #23](https://github.com/QWED-AI/qwed-finance/pull/23)

> Eliminates IEEE-754 `float` from every financial guard. `BondGuard`, `DerivativesGuard`, and `RiskGuard` now match the precision standard already used by `TradingGuard` and `FinanceVerifier`: exact arithmetic with `Decimal` and arbitrary-precision transcendentals with `mpmath`.

### What changed

* **BondGuard** — Newton-Raphson YTM solving, duration, and convexity now run in `Decimal` with `getcontext().prec = 50`. `tolerance_pct` is stored as `Decimal`; `verify_ytm` returns quantized percentage strings.
* **DerivativesGuard** — `mpmath` (30 dp) replaces `math.log/exp/sqrt/erf` in `verify_black_scholes`, `_norm_cdf`, `_norm_pdf`, and `verify_put_call_parity`. `verify_margin_call` compares `Decimal` values.
* **RiskGuard** — `Decimal.sqrt()` replaces `math.sqrt()` in `verify_var` and `verify_sortino_ratio`. `verify_beta` accumulates covariance in `Decimal` to eliminate catastrophic cancellation. `Z_SCORES` is now `Decimal`-typed.
* **Test coverage** — 23 new tests in `TestBondGuardDecimal`, `TestDerivativesGuardMpmath`, `TestRiskGuardDecimal`, and a cross-guard `TestNoFloatInOutput` IEEE-754 artifact scan.

### Breaking changes

<Warning>
  **Greeks are now `str` (Decimal-quantized), not `float`.** `verify_black_scholes` returns `result.greeks` with each Greek as a quantized string. Cast explicitly when arithmetic is needed:

  ```python theme={null}
  # Before
  delta = greeks["delta"]            # 0.4502 (float)
  exposure = delta * notional

  # After
  from decimal import Decimal
  delta = Decimal(greeks["delta"])   # "0.4502" → Decimal
  exposure = delta * Decimal(str(notional))
  ```

  `mpmath` is now a runtime dependency. It was already available transitively via `sympy`, so existing installs require no extra step.
</Warning>

### Audit references

| Finding | Description                                               | Fix                                         |
| ------- | --------------------------------------------------------- | ------------------------------------------- |
| C-01    | `BondGuard` float Newton-Raphson and accumulation         | Full `Decimal` arithmetic                   |
| C-02    | `DerivativesGuard` `math.*` transcendentals               | `mpmath` at 30 dp                           |
| C-03    | `RiskGuard` `math.sqrt` and float covariance cancellation | `Decimal.sqrt()` and `Decimal` accumulation |

See [Bond Guard](/finance/guards#8-bond-guard-yield-analytics), [Derivatives Guard](/finance/guards#3-derivatives-guard-black-scholes), and [Risk Guard](/finance/guards#10-risk-guard-portfolio-metrics) for the updated guard documentation.

**Jump to:** [QWED-Tax fail-closed unknown tax rules](#qwed-tax-—-fail-closed-on-unknown-or-unmodeled-tax-rules) · [QWED-Verification fail-closed unknown agent actions](#qwed-verification--fail-closed-unknown-agent-actions) · [QWED-Finance v2.1.0 — security audit hardening](#qwed-finance-v210--security-audit-hardening) · [QWED-Finance N-01/N-04 release blockers](#qwed-finance--release-blockers-n-01-and-n-04-resolved-no-go--go) · [QWED-Finance Decimal/mpmath migration](#qwed-finance--decimalmpmath-migration-for-bondguard-derivativesguard-and-riskguard) · [QWED-Verification SecureCodeExecutor fail-closed fallback](#qwed-verification--securecodeexecutor-fail-closed-when-codeverifier-is-unavailable) · [QWED-Finance security audit — OpenResponses fail-closed](#qwed-finance--openresponses-fail-closed-and-computed-status) · [QWED-Finance security audit — rate parsing](#qwed-finance--rate-parsing-heuristic-removed) · [QWED-Verification symbolic verifier fail-closed](#qwed-verification--symbolic-verifier-fail-closed-without-proof) · [QWED-Verification strict additionalProperties](#qwed-verification--strict-additionalproperties-fail-closed) · [QWED-Tax example redaction](#qwed-tax-—-redacted-preflight-block-details-in-examples) · [QWED-Tax decimal hardening](#qwed-tax-—-decimal-hardening-in-financial-guards) · [v5.1.0](#v5-1-0-—-agent-state-governance-and-fail-closed-hardening) · [v5.0.0](#v5-0-0-—-enforcement-boundary-hardening) · [v4.0.5](#v4-0-5-—-stats-exception-masking) · [v4.0.4](#v4-0-4-—-fail-closed-execution-enforcement) · [v4.0.3](#v4-0-3-—-server-enforced-agent-security) · [v4.0.2](#v4-0-2-—-security-cleanup) · [v4.0.1](#v4-0-1-—-sentinel-guard-sync) · [v4.0.0](#v4-0-0-—-sentinel-edition) · [v3.0.1](#v3-0-1-—-ironclad-update) · [v2.4.1](#v2-4-1-—-the-reasoning-engine)

***

## QWED-Verification — fail-closed unknown agent actions

**Released: May 6, 2026** · [GitHub PR #176](https://github.com/QWED-AI/qwed-verification/pull/176)

> Closes a fail-open gap in `AgentService.verify_action()`. Action types that are not bound to a verification engine or a governed tool are now denied with a dedicated error code instead of silently routing through a permissive default engine.

### What changed

* **`AgentService.verify_action`** — Action types not present in `ACTION_ENGINES` or `TOOL_RISK_LEVELS` now return `decision: "DENIED"` with `error.code: "QWED-AGENT-ACTION-001"`. The reserved conversation step is released so the agent can retry with a registered action type.
* **`AgentService._assess_risk`** — Raises `ValueError` for unregistered action types instead of falling back to `RiskLevel.MEDIUM`. This prevents the deterministic risk path from inferring a default for actions QWED has no registered semantics for.
* **Engine resolution** — New `_resolve_action_engine` helper distinguishes engine-backed actions (`math`, `logic`, `sql`, etc.) from tool-controlled actions, which now report `engine: "tool_control"` in the verification response. The previous `"security"` default for unknown actions has been removed.
* **`_run_verification_checks`** — When an action has no registered engine, an `action_registered` check is recorded as failed before any other check runs, making the denial reason visible in the `checks_failed` array.

### Behavior comparison

| Action type                                     | Before                                                                | After                                 |
| ----------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------- |
| Registered engine action (e.g. `verify_math`)   | `engine: "math"`, normal risk path                                    | Unchanged                             |
| Governed tool (e.g. `read_database`)            | `engine: "security"` (default)                                        | `engine: "tool_control"`              |
| Unregistered action (e.g. `do_arbitrary_thing`) | `engine: "security"`, MEDIUM risk, may APPROVE/PENDING by trust level | `DENIED` with `QWED-AGENT-ACTION-001` |

### Upgrade notes

<Warning>
  If your agents emit `action_type` values that are not in QWED's engine map or tool registry, they will now be denied. Audit production traffic for non-standard action types and either bind them to an engine or register them as governed tools with an explicit risk level before upgrading.
</Warning>

See [Agent Verification — Registered action enforcement](/advanced/agent-verification#registered-action-enforcement) for the full denial flow and migration guidance, and [API errors](/api/errors#agent-errors) for the new error code.

***

## QWED-Verification — SecureCodeExecutor fail-closed when CodeVerifier is unavailable

**Released: May 2, 2026**

> Closes a fail-open gap in `SecureCodeExecutor._is_safe_code()`. When the primary `CodeVerifier` cannot be imported **or raises at runtime**, execution is now blocked instead of falling back to a permissive keyword check that could authorize otherwise-unsafe code.

### What changed

* **`SecureCodeExecutor._is_safe_code`** — An `ImportError` on `CodeVerifier` no longer routes authorization to `_basic_safety_check()`. The method returns `(False, "CodeVerifier unavailable; cannot validate code safety.")` and execution does not reach `containers.run()`.
* **Runtime exceptions in `CodeVerifier.verify_code`** — Any unexpected exception raised by the verifier (for example, an internal engine failure) is now caught and routed to the same fail-closed denial. The exception message is sanitized before logging and is not returned to callers.
* **Shared denial helper** — Both branches go through `_build_fail_closed_safety_denial(code)`, producing the same deterministic `(False, "CodeVerifier unavailable; …")` result regardless of whether the verifier was missing or crashed mid-call.
* **Advisory-only heuristic** — The basic keyword scan is retained as advisory context. If it would have flagged the code, its reason is appended to the error message (`"Advisory-only fallback also flagged: <reason>"`), but it never authorizes execution on its own.
* **`execute()` surface** — Calls into `SecureCodeExecutor.execute(code, context)` now return `(success=False, error="Code safety validation failed: CodeVerifier unavailable; cannot validate code safety.", result=None)` whenever `CodeVerifier` is missing or fails, regardless of the input.

### Behavior comparison

| `CodeVerifier` state               | Code                         | Before                               | After                                                                                          |
| ---------------------------------- | ---------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------- |
| Available                          | Any                          | Verified by `CodeVerifier`           | Verified by `CodeVerifier` (unchanged)                                                         |
| `ImportError`                      | `print("hello")`             | Allowed via basic keyword check      | Blocked (`CodeVerifier unavailable`)                                                           |
| `ImportError`                      | `import os; os.system("ls")` | Blocked by basic keyword check       | Blocked (`CodeVerifier unavailable; Advisory-only fallback also flagged: dangerous operation`) |
| Runtime exception in `verify_code` | Any                          | Propagated as an unhandled exception | Blocked (`CodeVerifier unavailable`); error sanitized in logs                                  |

### Upgrade notes

<Warning>
  Self-hosted deployments that relied on the implicit basic-keyword fallback when `qwed_new.core.code_verifier` failed to import — or that swallowed verifier exceptions upstream — must now ensure `CodeVerifier` is installed, importable, and operational. While the verifier is unavailable or failing, both `/verify/stats` and `/verify/consensus` Python execution paths return a safety-check failure with no container run.

  If you see `Code safety validation failed: CodeVerifier unavailable` in logs, fix the underlying import error or runtime failure in `qwed_new.core.code_verifier` rather than disabling the safety gate.
</Warning>

See [Security hardening — Fail-closed code execution](/advanced/security-hardening#fail-closed-code-execution) for the updated fallback behavior.

***

## QWED-Finance — OpenResponses fail-closed and COMPUTED status

**Released: April 30, 2026** · [GitHub PR #21](https://github.com/QWED-AI/qwed-finance/pull/21)

> Closes the most critical finding from the QWED-Finance security audit: the OpenResponses integration no longer auto-approves tools without a verification function, and built-in tool calls no longer falsely claim `verified: true`.

### What changed

* **Fail-closed default** — Tools registered without a `verification_fn` now return `ToolCallStatus.REJECTED` instead of `ToolCallStatus.APPROVED`. A cryptographic receipt is generated for every rejection, ensuring compliance audit trail coverage.
* **`ToolCallStatus.COMPUTED`** — New enum member distinguishing "we computed this result deterministically" from "we verified an LLM claim." All four built-in tools (`calculate_npv`, `calculate_loan_payment`, `check_aml_compliance`, `price_option`) now return `COMPUTED`.
* **`format_for_responses_api`** — `COMPUTED` results output `"status": "computed_only"` and `"verified": false` in the streaming payload, preventing downstream agents from trusting unverified computations.
* **Error boundary** — `verification_fn` execution is wrapped in `try/except`. If a custom verifier raises, it returns `ToolCallStatus.ERROR` instead of crashing the agent loop.
* **AML country consolidation** — `_verify_aml` now delegates to `ComplianceGuard.high_risk_countries` (14 countries) instead of a hardcoded subset (5 countries).
* **Black-Scholes input guard** — `price_option` rejects zero/negative values for `spot_price`, `strike_price`, `time_to_expiry`, and `volatility` before computing.
* **Precision preservation** — NPV and loan outputs use `Decimal.quantize()` instead of `float()`, preventing IEEE 754 artifacts in monetary results.

### Breaking changes

<Warning>
  `ToolCallStatus.COMPUTED` is new. Consumers checking `status == APPROVED` must also handle `COMPUTED` as a valid non-error state.

  Tools registered without `verification_fn` now return `REJECTED`. Add a `verification_fn` to all registered tools.
</Warning>

### Audit references

| Finding | Description                                   | Fix                           |
| ------- | --------------------------------------------- | ----------------------------- |
| S-01    | `handle_tool_call()` fail-open default        | `APPROVED` → `REJECTED`       |
| S-02    | Built-in functions hardcoded `verified: True` | `APPROVED` → `COMPUTED`       |
| S-04    | `float(npv)` precision leak                   | `Decimal.quantize()`          |
| S-06    | AML 5 vs 14 country mismatch                  | Delegate to `ComplianceGuard` |

***

## QWED-Finance — rate parsing heuristic removed

**Released: April 30, 2026** · [GitHub PR #22](https://github.com/QWED-AI/qwed-finance/pull/22)

> Removes silent rate format guessing from `BondGuard._parse_rate()` and `FinanceVerifier.verify_irr()`. Both functions used different heuristics that produced 100x errors for the same input.

### What changed

* **`BondGuard._parse_rate()`** — Removed `val < 1` heuristic. Bare numbers without `%` are now treated as decimal fractions (e.g., `"1.5"` → `1.5`, not `0.015`).
* **`FinanceVerifier.verify_irr()`** — Removed `llm_rate > 1` heuristic. Uses same explicit logic: `%` suffix → divide by 100, otherwise use as-is.
* **`_solve_ytm` clamp** — Widened Newton-Raphson convergence clamp from `1.0` (100%) to `10.0` (1000%) to support distressed debt scenarios.

### Breaking changes

<Warning>
  Code that passes bare numbers ≥ 1 to `_parse_rate()` expecting auto-division will break. Use explicit `%` suffix: `"5.25%"` instead of `"5.25"`.
</Warning>

| Input      | Before     | After      |
| ---------- | ---------- | ---------- |
| `"5.25%"`  | `0.0525` ✅ | `0.0525` ✅ |
| `"0.0525"` | `0.0525` ✅ | `0.0525` ✅ |
| `"1.5"`    | `0.015` ❌  | `1.5` ✅    |

### Audit references

| Finding | Description                    | Fix                    |
| ------- | ------------------------------ | ---------------------- |
| C-04    | `_parse_rate` silent heuristic | Removed `val < 1`      |
| H-01    | `verify_irr` guessing          | Removed `llm_rate > 1` |
| S-05    | Cross-guard inconsistency      | Unified parsing logic  |

## QWED-Verification — symbolic verifier fail-closed without proof

**Released: April 30, 2026**

> Fix in `SymbolicVerifier.verify_code()` that closes a fail-open gap where absence of symbolic proof could be reported as a successful verification. The verifier now fails closed unless a real CrossHair proof was actually performed.

### What changed

* **Code with no functions** now returns `is_verified: false` and `status: "no_verifiable_functions"` instead of a pass-like `no_functions_to_check` result.
* **Untyped functions** are reported as `skipped` and `unverifiable`, never as verified. CrossHair requires type annotations.
* **Mixed typed/untyped code** stays unverifiable: a single skipped function is enough to set `is_verified: false` and `status: "unverifiable"`.
* **Aggregation now distinguishes** `functions_discovered`, `functions_checked`, `functions_verified`, `functions_skipped`, `functions_unverifiable`, and `counterexamples_found`.
* `is_verified: true` now requires at least one function actually checked, all checked functions proven, no skipped or unverifiable functions, and no counterexamples.
* The result dict now also exposes `is_safe` and `verified` as aliases for `is_verified`, and pre-verification exits (`crosshair_not_available`, `syntax_error`) return the same fail-closed shape with zeroed counters.

### Result shape

```json theme={null}
{
  "is_verified": false,
  "status": "unverifiable",
  "message": "Symbolic verification was incomplete; at least one function could not be proven.",
  "functions_discovered": 2,
  "functions_checked": 1,
  "functions_verified": 1,
  "functions_skipped": 1,
  "functions_unverifiable": 1,
  "counterexamples_found": 0,
  "issues": [
    {
      "type": "unverifiable",
      "function": "untyped_add",
      "description": "Function skipped: no type annotations for symbolic verification"
    }
  ]
}
```

See [Symbolic Limits — Fail-closed verification results](/advanced/symbolic-limits#fail-closed-verification-results) for full field documentation.

### Upgrade notes

<Warning>
  Callers that previously treated `status: "no_functions_to_check"` as success must now treat the new `status: "no_verifiable_functions"`, `"unverifiable"`, and `"verification_error"` values as failures. The `is_verified` boolean is the safest single indicator of a real proof.
  Callers that previously treated `is_verified: true` as proof of correctness without inspecting `functions_checked` may have been accepting unverified code. Re-run any cached verification results, and ensure downstream gates require `functions_checked > 0` in addition to `is_verified`.
</Warning>

***

## QWED-Verification — strict `additionalProperties` fail-closed

**Released: April 30, 2026**

> Patch in `SchemaVerifier` that closes a fail-open gap when `strict=True` and a schema sets `"additionalProperties": false`. Payloads with undeclared properties are now rejected as invalid instead of silently passing with an advisory warning.

### What changed

* **`SchemaVerifier._validate_object`** — Undeclared properties under `"additionalProperties": false` in strict mode are now recorded with `severity: "ERROR"` instead of `"WARNING"`. Because overall validity is computed from `ERROR`-severity issues, the payload now correctly returns `is_valid: false` and `status: "INVALID"`.
* Nested objects with `"additionalProperties": false` propagate the same fail-closed behavior at any depth (for example, `$.user.role`).
* Non-strict mode (`strict=False`) is unchanged: `additionalProperties: false` remains permissive and does not block validation.

### Issue shape

```json theme={null}
{
  "path": "$.role",
  "issue_type": "additional_property",
  "expected": "no additional properties",
  "actual": "role",
  "severity": "ERROR",
  "message": "Additional property 'role' not allowed"
}
```

See [Schema Verifier — `additionalProperties: false`](/engines/schema#additionalproperties-false--strict-fail-closed-validation) for the full strict and nested examples.

### Upgrade notes

<Warning>
  If you previously relied on strict-mode payloads passing despite undeclared properties, those payloads will now fail validation. Either remove the extra fields, declare them in `properties`, or call `verify_schema` with `strict=False` for advisory-only behavior.
</Warning>

***

## QWED-Tax — redacted preflight block details in examples

**Released: April 24, 2026**

> Example scripts bundled with `qwed-tax` no longer print raw `TaxPreFlight` block reasons. Demo output is reduced to a high-level allow/block outcome so that running the examples cannot incidentally leak field values from the sample intents.

* **`examples/verify_tax_expansion.py`** — Replaces direct prints of `report["blocks"]` with a `_print_outcome` helper that emits only the pass/fail verdict and a fixed `"Verification details intentionally redacted."` notice on blocks.
* **Guidance** — New [Redacting block reasons in untrusted contexts](/tax/integration#redacting-block-reasons-in-untrusted-contexts) section in the Tax integration reference documents the expected handling for `report["blocks"]` outside a trusted audit boundary.

No runtime behavior in `TaxPreFlight.audit_transaction` changes; the report shape (`allowed`, `action`, `blocks`, `checks_run`, `advisories`) is unchanged.

***

## QWED-Tax — Decimal hardening in financial guards

**Released: April 24, 2026**

> Patch release across the QWED-Tax cross-border and financial guards. Hardens decimal parsing, fails closed on malformed numeric inputs, and stabilizes JSON output so monetary and ratio fields survive round-trips through non-Python consumers without float drift.

### Fail-closed numeric parsing

All financial guards below now route numeric fields through a shared `parse_decimal_input` helper that rejects booleans, `NaN`, infinite values, and non-numeric strings before any math runs. Previously, a bare `Decimal(str(value))` call would either propagate a raw `InvalidOperation` or silently produce `NaN`/`Infinity` that corrupted downstream comparisons.

* **`DTAAGuard.verify_foreign_tax_credit`** — Rejects non-numeric, non-finite, boolean, and negative values for `foreign_income`, `foreign_tax_paid`, `home_tax_rate`, and `foreign_tax_limit_rate`. Returns `{"verified": false, "message": "<field> must be a ... numeric value.", "allowable_credit": "0", "excess_tax_lapsed": "0"}` on invalid input.
* **`TransferPricingGuard.verify_arms_length_price`** — Invalid `transaction_price`, `benchmark_price`, or `tolerance_percent` now return `{"verified": false, "risk": "INVALID_NUMERIC_INPUT", "safe_harbour_range": [], "potential_adjustment": "0"}` instead of raising.
* **`PoEMGuard.determine_residency`** — Malformed turnover/asset/payroll values, negative values, and impossible invariants (`assets_outside_india > assets_total`, etc.) now return `{"verified": false, "residency": "UNVERIFIABLE", "reason": "..."}`. Employee counts must be non-negative and `employees_outside_india` cannot exceed `employees_total`.

Additional guards hardened with the same numeric contract: `indirect_tax_guard`, `nexus_guard`, `related_party_guard`, `remittance_guard`, `speculation_guard`, `tds_guard`, and the US `withholding_guard`.

### Breaking changes

<Warning>
  Monetary and ratio fields are now returned as plain-text Decimal **strings**, not Python `float`s. If you previously read `result["allowable_credit"]` as a number, update your consumers to parse the string (for example, `Decimal(result["allowable_credit"])`).
</Warning>

* **`DTAAGuard`** — `allowable_credit` and `excess_tax_lapsed` changed from `float` to Decimal string (e.g., `"150.0"` instead of `150.0`).
* **`TransferPricingGuard`** — `safe_harbour_range` changed from `[float, float]` to `[str, str]`. `potential_adjustment` (renamed from the legacy `adjustment_required` on the verified path) is now a Decimal string. The `tolerance_percent` default is now `Decimal("3.0")` instead of `3.0`.
* **`PoEMGuard`** — `metrics.assets_outside_ratio`, `metrics.employees_outside_ratio`, and `metrics.payroll_outside_ratio` changed from `float` to Decimal string, quantized to four decimal places. The ABOI decision continues to use the unrounded ratios internally.

### PoEM validation flow

`PoEMGuard.determine_residency` was refactored to separate parsing, validation, ratio computation, and ratio quantization. The split is internal, but two observable effects follow:

* Validation errors are surfaced as structured `UNVERIFIABLE` responses rather than Python exceptions.
* Reported ratios are rounded for display only; the `is_aboi` decision is still computed from the raw ratios, so no boundary cases flip from `RESIDENT` to `NON-RESIDENT` purely from rounding.

### Example — migration

```python theme={null}
from decimal import Decimal
from qwed_tax.guards.dtaa_guard import DTAAGuard

guard = DTAAGuard()
result = guard.verify_foreign_tax_credit(
    foreign_income=1000,
    foreign_tax_paid=200,
    home_tax_rate=15.0,
)

# Before
# allowable_credit = result["allowable_credit"]            # 150.0 (float)

# After
allowable_credit = Decimal(result["allowable_credit"])     # Decimal("150.0")
```

See the [QWED-Tax guards reference](/tax/guards) for full parameter and return-shape details for each guard.

***

## v5.1.1 — Trust Boundary Hardening

**Released: May 22, 2026** · [GitHub Release](https://github.com/QWED-AI/qwed-verification/releases/tag/v5.1.1) · Patch

Packages the post-v5.1.0 trust-boundary and fail-closed corrections into a coherent release. Changes are correctness and security class — not cosmetic.

### Cache trust-context binding

**PRs #178, #192** · `qwed_sdk/cache.py`, `qwed_sdk/qwed_local.py`

Verification cache keys are now bound to the full trust context — provider, model, policy version, and session ID. A cache miss is forced when any of these change, even if the query string matches. Prevents cross-context replay in multi-tenant and multi-provider deployments.

### Attestation fail-closed hardening

**PRs #188, #194** · `src/qwed_new/core/attestation.py`

`create_verification_attestation()` now returns an explicit `AttestationResult` on all code paths. `AttestationStatus` is now an enum. `is_issued` property enforces the fail-closed contract. `IssuerKeyPair` gains `generated_at` epoch and `key_continuity_policy` with an allowlist.

### Audit logger fail-closed

**PR #179** · `src/qwed_new/core/audit.py`

Malformed audit payloads now fail closed. Organization-level audit chains are isolated. SQLite writes use `BEGIN IMMEDIATE` transactions — no partial-write records.

### Proof-path and reasoning corrections

**PRs #177, #180, #161**

Reasoning acceptance requires proof prerequisites. Batch math simplification is separated from the proof path. Symbolic verifier returns `BLOCKED` when no proof exists.

### Agent and executor boundary fixes

**PRs #176, #168**

Unknown agent actions explicitly denied and logged. Secure executor hard-blocked on unrecognized input shapes.

### Schema validation tightening

**PR #160**

`additionalProperties: false` enforcement is now strict.

### Version table

| Artifact             | Previous | This release |
| -------------------- | -------- | ------------ |
| `qwed` (PyPI)        | 5.1.0    | 5.1.1        |
| `qwed_sdk` (Python)  | 5.1.0    | 5.1.1        |
| `@qwed-ai/sdk` (npm) | 5.1.0    | 5.1.1        |
| `qwed` (Rust crate)  | 5.1.0    | 5.1.1        |

```bash theme={null}
pip install qwed==5.1.1
docker pull qwedai/qwed-verification:5.1.1
npm install @qwed-ai/sdk@5.1.1
cargo add qwed@5.1.1
go get github.com/QWED-AI/qwed-verification/sdk-go@v5.1.1
```

***

## v5.1.0 — agent state governance and fail-closed hardening

**Released: April 19, 2026** · [GitHub Release](https://github.com/QWED-AI/qwed-verification/releases/tag/v5.1.0)

> Minor release expanding QWED from action verification into state governance while closing adversarial fail-open gaps identified after v5.0.0. Includes AgentStateGuard plus a focused hardening wave across execution, tool governance, mathematical verification, API semantics, and schema validation.

### New capability

* **AgentStateGuard** — Deterministic state verification with strict structural validation, semantic transition checks, and governed atomic state commits. Extends QWED from action-only verification to state and memory governance. See the [AgentStateGuard guide](/advanced/agent-state-guard) for usage details.

### Fail-closed hardening

* **Legacy `CodeExecutor` hard-blocked** — `CodeExecutor.execute()` now raises a `RuntimeError` on every call, closing the final in-process raw execution path. Migrate any direct imports to `SecureCodeExecutor`. See the [security hardening guide](/advanced/security-hardening#legacy-codeexecutor-hard-blocked) for the migration example.
* **Unknown tools default-denied** — `ToolApprovalSystem` now blocks all unknown tools by default, regardless of heuristic risk score. Tool calls not explicitly allowlisted must be approved through an explicit policy path.
* **Bounded math tolerance** — `verify_math()` rejects oversized, negative, non-finite, and malformed tolerances instead of letting callers weaken correctness checks. See [math engine tolerance bounding](/engines/math#tolerance-bounding) for details.
* **Legacy logic path fails closed** — `verify_logic_rule()` now raises `NotImplementedError` instead of returning `None`. Migrate to `LogicVerifier`.

```python theme={null}
# Before (no longer works)
from qwed_new.core.verifier import VerificationEngine
engine = VerificationEngine()
result = engine.verify_logic_rule(rule="if x > 5 then allow", context={"x": 10})

# After
from qwed_new.core.logic_verifier import LogicVerifier
verifier = LogicVerifier()
result = verifier.verify_conclusion(
    premises=["x is 10", "x > 5 implies allow"],
    conclusion="allow"
)
```

* **Identity sampling rejected** — `verify_identity()` now returns `BLOCKED` with `is_equivalent: false` and `method: "numerical_sampling_rejected"` when numerical sampling matches but no formal proof exists.
* **Ambiguous math API rejected** — `POST /verify/math` now blocks ambiguous implicit-multiplication expressions (e.g., `1/2(3+1)`) with `is_valid: false`, `status: "BLOCKED"`, and `warning: "ambiguous"`.

```json theme={null}
{
  "is_valid": false,
  "result": false,
  "status": "BLOCKED",
  "warning": "ambiguous",
  "message": "Expression may be ambiguous due to implicit multiplication after division",
  "simplified": "..."
}
```

* **Schema uniqueness fail-closed** — `SchemaVerifier` now emits a `uniqueness_validation_error` when `uniqueItems` cannot be proven deterministically, instead of silently passing. See the [schema verifier `uniqueItems`](/engines/schema#uniqueitems--fail-closed-validation) section for details.

```json theme={null}
{
  "path": "$.tags",
  "issue_type": "uniqueness_validation_error",
  "expected": "provably unique items",
  "actual": "uniqueness check could not be completed",
  "message": "uniqueItems could not be verified deterministically: <error details>"
}
```

### Runtime and security

* **Progress-aware doom loop guard** — Added LOOP-004 state-aware replay protection for repeated actions on unchanged state. Requires `pre_action_state_hash` and `state_source` in the agent context.
* Security and infrastructure hardening across configs and CI.
* Stats verifier edge-case coverage expansion.
* CodeQL and cleanup follow-ups.

### Upgrade notes

<Warning>
  Several behaviors changed from ambiguous or silent-pass to explicit failure. Review these before upgrading.
</Warning>

* `CodeExecutor` is no longer usable as a legacy execution path. Migrate any direct imports to `SecureCodeExecutor`.
* Unknown tools now require explicit allowlisting and are no longer auto-approved at low heuristic risk.
* `verify_math()` may return `BLOCKED` for tolerances that exceed the deterministic policy bound.
* `verify_logic_rule()` no longer returns an ambiguous non-result; callers must migrate to `LogicVerifier`.
* Sampling-only `verify_identity()` matches now return `BLOCKED`, not `UNKNOWN`.
* Ambiguous `/verify/math` expressions now return `BLOCKED` with `is_valid: false`.
* `uniqueItems` validation failures are now explicit schema errors instead of silent passes.

### SDK version bumps

* `qwed` (PyPI): `5.0.0` → `5.1.0`
* `qwed_sdk` (Python): `5.0.0` → `5.1.0`
* `@qwed-ai/sdk` (npm): `5.0.0` → `5.1.0`

### Install

```bash theme={null}
pip install qwed==5.1.0
docker pull qwedai/qwed-verification:5.1.0
npm install @qwed-ai/sdk@5.1.0
```

***

## v5.0.0 — enforcement boundary hardening

**Released: April 4, 2026** · [GitHub Release](https://github.com/QWED-AI/qwed-verification/releases/tag/v5.0.0)

> Major release making QWED's verification boundary fail-closed, deterministic about what it proves, and harder to bypass under adversarial conditions. Consolidates 98 commits and 20 merged PRs since v4.0.1, including the full enforcement hardening series.

### Breaking changes

* **`INCONCLUSIVE` is a new verification status** — Natural-language math responses now return `INCONCLUSIVE` when verifying LLM-translated expressions, because the translation step is non-deterministic. Downstream consumers must handle this status alongside `VERIFIED`, `ERROR`, and `BLOCKED`.
* **`BLOCKED` and `UNKNOWN` are explicit outcomes** — These are no longer treated as generic failures. Consumers should map them to distinct UI states or retry logic.
* **`ActionContext` is mandatory for agent verification** — All `verify_action` requests must include `conversation_id` and `step_number`. Requests without them are rejected with `QWED-AGENT-CTX-001`.
* **`security_checks` field removed** — Exfiltration and MCP poison checks are now server-enforced and unconditional. The `security_checks` request field and the TypeScript SDK `checkExfiltration`/`checkMcpPoison` options no longer exist.
* **`/metrics` endpoints require admin authentication** — `GET /metrics` and `GET /metrics/prometheus` now require an authenticated admin or owner role. Update monitoring integrations accordingly.
* **Docker required for stats and consensus verification** — The in-process execution fallbacks have been removed. If Docker is unavailable, `/verify/stats` and `/verify/consensus` return HTTP 503.

### Security

* **Fail-closed verification boundary** — Disabled unsafe in-process execution fallbacks; stats and consensus paths now require the secure Docker sandbox.
* **Logic verifier eval() removed** — The raw `eval()` fallback in logic constraint parsing has been removed. If `SafeEvaluator` is unavailable, the verifier raises a `RuntimeError`.
* **Consensus rate limiting** — `POST /verify/consensus` now enforces per-tenant rate limiting, matching other verification endpoints.
* **Consensus fact self-attestation removed** — The fact engine no longer participates in automatic consensus engine selection, preventing self-referential verification loops.
* **Redis fail-closed** — The sliding window rate limiter now denies requests on Redis errors instead of allowing them.
* **Timing-safe token verification** — Agent token comparison uses `hmac.compare_digest` to prevent timing side-channel attacks.
* **Environment integrity enforcement** — The API server runs `verify_environment_integrity()` at startup before database initialization.

### Trust boundary metadata

API responses now include a `trust_boundary` object that describes exactly what was verified and what was not. This gives you machine-readable transparency into the verification scope.

```json theme={null}
{
  "trust_boundary": {
    "query_interpretation_source": "llm_translation",
    "query_semantics_verified": false,
    "verification_scope": "translated_expression_only",
    "deterministic_expression_evaluation": true,
    "formal_proof": false,
    "overall_status": "INCONCLUSIVE"
  }
}
```

See the [`POST /verify/natural_language`](/api/endpoints#post-verifynatural_language) endpoint reference for full field descriptions.

### Agent hardening

* **Action context mandatory** — All `verify_action` requests must include `conversation_id` and `step_number`. Requests without them are rejected with `QWED-AGENT-CTX-001`.
* **Replay detection** — Reusing a `(conversation_id, step_number)` pair is blocked (`QWED-AGENT-LOOP-002`).
* **Loop detection** — Repeating the same action 3+ consecutive times triggers a denial (`QWED-AGENT-LOOP-003`).
* **In-flight step reservations** — Prevents race conditions when multiple agent calls run concurrently.
* **Budget denial isolation** — Budget-exceeded denials do not consume conversation state, so the agent can retry after the budget resets.
* **Stats exception masking** — The Stats Engine no longer leaks internal exception details to API callers.

### SDK version bumps

* `qwed` (PyPI): `4.0.1` → `5.0.0`
* `qwed_sdk` (Python): `2.1.0-dev` → `5.0.0`
* `@qwed-ai/sdk` (npm): `4.0.1` → `5.0.0`
* TypeScript SDK: Removed `security_checks` from agent verification helpers; `tool_schema` remains.

### Upgrade

```bash theme={null}
pip install qwed==5.0.0
docker pull qwedai/qwed-verification:5.0.0
npm install @qwed-ai/sdk@5.0.0
```

<Warning>
  If you use `POST /verify/natural_language` and check the top-level `status` field, you must now handle `INCONCLUSIVE` as a valid outcome. Use the `trust_boundary` object to inspect the detailed verification breakdown. For fully deterministic results, use `POST /verify/math` directly.
</Warning>

<Warning>
  If you use agent verification, all requests must now include `conversation_id` and `step_number` in the context. See the [agent verification guide](/advanced/agent-verification) for details.
</Warning>

***

## v4.0.5 — stats exception masking

**Released: April 3, 2026** · [GitHub PR #118](https://github.com/QWED-AI/qwed-verification/pull/118)

> Prevents the Stats Engine from leaking internal exception details to API callers. Clients now receive a generic error message when stats code generation fails.

### Security

* **Stats translation error masking** — When stats code generation fails, the API now returns `"Internal verification error"` instead of the raw exception message. This prevents accidental exposure of file paths, API keys, or other sensitive data that may appear in exception text.
* **Server-side logging preserved** — The exception type is still logged server-side for operator debugging, but the log no longer includes the full exception message.

***

## v4.0.4 — fail-closed execution enforcement

**Released: April 3, 2026** · [GitHub PR #117](https://github.com/QWED-AI/qwed-verification/pull/117)

> Removes unsafe in-process execution fallbacks from statistical and consensus verification. All model-generated Python now runs exclusively in the secure Docker sandbox.

### Breaking changes

* **Docker required for stats verification** — The Wasm and restricted Python fallback execution paths have been removed from the Stats Engine. If Docker is unavailable, `/verify/stats` returns HTTP 503 instead of executing code in-process.
* **Consensus Python engine uses Docker** — The Python verification engine within consensus verification now runs through `SecureCodeExecutor` instead of the in-process `CodeExecutor`. If Docker is unavailable during `high` or `maximum` mode, `/verify/consensus` returns HTTP 503.

### Security

* **Fail-closed sandbox gating** — Both the Stats Engine and the Consensus Engine refuse to execute model-generated code when the Docker sandbox is not available, preventing any downgrade to in-process execution.
* **Live Docker health checks** — `SecureCodeExecutor.is_available()` now pings the Docker daemon on each request instead of relying on cached startup state, catching mid-operation Docker failures.

### API changes

* `POST /verify/stats` now returns HTTP 503 when the secure runtime is unavailable, and HTTP 403 when generated code is blocked by security policy.
* `POST /verify/consensus` now returns HTTP 503 when secure execution is required but Docker is unavailable.

***

## v4.0.3 — server-enforced agent security

**Released: April 2, 2026** · [GitHub PR #116](https://github.com/QWED-AI/qwed-verification/pull/116)

> Closes critical verification boundary gaps by enforcing agent security checks server-side, adding rate limiting to consensus verification, and removing unsafe fallback paths.

### Breaking changes

* **`security_checks` removed from agent verification** — Exfiltration and MCP poison checks are now enforced server-side on every request. The `security_checks` request field and the TypeScript SDK `checkExfiltration`/`checkMcpPoison` options have been removed. `tool_schema` remains available to trigger MCP poison inspection.

### Security

* **Logic verifier fail-closed** — The raw `eval()` fallback in logic constraint parsing has been removed. If `SafeEvaluator` is unavailable, the verifier raises an error instead of falling back to unrestricted evaluation.
* **Consensus rate limiting** — The `POST /verify/consensus` endpoint is now rate-limited per API key, matching other verification endpoints.
* **Consensus fact self-attestation removed** — The Fact engine is no longer automatically selected during consensus verification. It produced self-referential results without external context.

***

## v4.0.2 — security cleanup

**Released: April 1, 2026** · [GitHub PR #114](https://github.com/QWED-AI/qwed-verification/pull/114)

> Hardens expression evaluation, improves error handling across the SDK and consensus engine, and removes unused code flagged by CodeQL.

### Security

* **eval() fully eliminated** — SymPy and Z3 expression evaluation now uses a custom AST-walking interpreter instead of `compile()` + `eval()`. The evaluator validates every AST node against a strict allow-list, then interprets the tree directly in a restricted namespace.
* **CodeGuard integration** — Expressions are screened by `CodeGuard` (when available) as a second defense layer between AST validation and evaluation.
* **Keyword unpacking blocked** — Call nodes with `**kwargs`-style keyword unpacking are now rejected during AST validation.

### Improvements

* **Exact SymPy arithmetic** — Numeric literals are coerced to `sympy.Integer` / `sympy.Float` during evaluation, preventing floating-point drift in intermediate math comparisons.
* **Consensus failure recording** — Unexpected errors during async engine aggregation are now logged and recorded as an `EngineResult` entry instead of being silently dropped.
* **Telemetry initialization** — Replaced the `_initialized` flag with `@lru_cache` for one-time cached initialization.
* **SDK import cycle broken** — `qwed_sdk.cache` no longer imports from `qwed_sdk.qwed_local` for terminal colors; it uses `colorama` directly with a plain-text fallback.
* **Integration imports hardened** — Optional framework imports (LangChain, CrewAI, LlamaIndex) now default to `None` explicitly instead of using bare `except: pass`.

### Chores

* Removed unused locals and globals across SDK, core modules, examples, and scripts.
* Replaced module-level `print()` calls in `database.py` with `logger.debug()`.
* Added `ast.Tuple` and `ast.List` to the safe SymPy node type allow-list.

***

## v4.0.1 — Sentinel guard sync

**Released: March 23, 2026** · [GitHub Release](https://github.com/QWED-AI/qwed-verification/releases/tag/v4.0.1) · [PyPI](https://pypi.org/project/qwed/4.0.1/)

> Patch release aligning the TypeScript SDK, backend API schemas, and security guard integrations introduced in v4.0.0.

### New endpoints

* **`POST /verify/process`** — Glass-box reasoning process verifier with IRAC structural compliance and custom milestone validation.
* **Agent Security Checks** — `POST /agents/{id}/verify` now accepts `security_checks: { exfiltration, mcp_poison }` to run `ExfiltrationGuard` and `MCPPoisonGuard` before verification.

### Security fixes

* **Information Disclosure** — Removed raw exception messages from `/verify/rag` error responses; clients receive only `INTERNAL_VERIFICATION_ERROR`.
* **Symbolic Precision** — `RAGVerifyRequest.max_drm_rate` changed from `float | str` → `str` with `field_validator` enforcing Fraction-compatible values.
* **Response Consistency** — RAG error responses now return `"verified": false` matching the success path schema.

### SDK changes (`@qwed-ai/sdk@4.0.1`)

* Added `verifyProcess()` method for IRAC/milestone validation.
* `verifyRAG()` — `maxDrmRate` type changed from `number` to `string`.
* `verifyAgent()` — Payload aligned with backend schema, agent IDs URL-encoded.
* New types: `Process`, `RAG`, `Security` in `VerificationType` enum.

### Tests

* `test_api_phase17_endpoints.py` — covers `/verify/process`, `/verify/rag` exception masking, and agent security check blocking.

```bash theme={null}
pip install qwed==4.0.1
docker pull qwedai/qwed-verification:4.0.1
npm install @qwed-ai/sdk@4.0.1
```

***

## v4.0.0 — Sentinel edition

**Released: March 12, 2026** · [GitHub Release](https://github.com/QWED-AI/qwed-verification/releases/tag/v4.0.0) · [PyPI](https://pypi.org/project/qwed/4.0.0/)

> **147 commits** since v3.0.1 — the largest update in QWED history.

### v4.0.0 patch — provider fallbacks, .env loading, and Gemini stability

* **Native Gemini provider** — Gemini now runs through a dedicated provider with native support for math, logic, stats, fact, and image verification. All API calls enforce a 30-second timeout and automatically strip Markdown code fences from responses.
* **Centralized .env loading** — Environment variables now load in a deterministic priority order: project `.env` first, then `~/.qwed/.env`. This prevents configuration drift between CLI and server contexts.
* **Gemini AST stability** — Fixed intermittent JSON parse failures when Gemini returns code-fenced responses during logic translation.

### Agentic security guards (Phase 17)

A brand-new guard subsystem for securing AI agent tool chains and RAG pipelines:

* **RAGGuard** — Detects prompt injection, data poisoning, and context manipulation in RAG pipelines. IRAC-compliant reporting.
* **ExfiltrationGuard** — Prevents data exfiltration through agent tool calls by analyzing output patterns and destination validation.
* **MCP Poison Guard** — Detects poisoned or tampered MCP tool definitions before agent execution.

All three guards went through **five rounds** of security review via CodeRabbit and SonarCloud.

### New standalone guards

* **SovereigntyGuard** — Enforces data residency policies and local routing rules (GDPR, data localization).
* **ToxicFlowGuard** — Stateful detection of toxic tool-chaining patterns across multi-step agent workflows.
* **SelfInitiatedCoTGuard (S-CoT)** — Verifies self-initiated Chain-of-Thought logic paths for reasoning integrity.

### Process determinism

A new class of deterministic verification:

* **ProcessVerifier** — IRAC/milestone-based process verification with decimal scoring, budget-aware timeouts, and structured compliance reporting. Ensures AI-driven workflows follow deterministic process steps — not just correct answers, but correct *procedures*.

### Critical security fixes

* Replaced direct `eval()` with AST-validated execution (Code Injection Prevention). Further hardened in v4.0.2 with a full AST-walking interpreter.
* Patched critical sandbox escape and namespace mismatch.
* Hardened SymPy input parsing against injection.
* Fixed URL whitespace bypass and protocol wildcard bypass.
* Resolved CVE-2026-24049 (Critical), CVE-2025-8869, and HTTP request smuggling.
* Fixed all 19 Snyk Code findings.
* Secured exception handling across `verify_logic`, `ControlPlane`, `verify_stats`, `agent_tool_call`.

### Docker hardening

* Pinned base image digests with hash-verified requirements
* Non-root user execution with `gosu`/`runuser`
* Automated Docker Hub publishing on release
* SBOM generation (SPDX) and Docker Scout scanning

```bash theme={null}
docker pull qwedai/qwed-verification:4.0.0
```

### CI/CD infrastructure

* **Sentry SDK** — Error tracking and monitoring.
* **CircleCI** — Python matrix testing (3.10, 3.11, 3.12).
* **SonarCloud** — Code quality and coverage.
* **Snyk** — Security scanning with SARIF output.
* **Docker Auto-Publish** — Automated image push on every release.

### Documentation and badges

* OpenSSF Best Practices badge (Silver)
* Snyk security badge and partner attribution
* Docker Hub pulls badge and BuildKit badge
* 11 verification engines across all docs

***

## v3.0.1 — ironclad update

**Released: February 4, 2026** · [GitHub Release](https://github.com/QWED-AI/qwed-verification/releases/tag/v3.0.1)

### Critical security hardening

* **CodeQL Remediation**: Resolved 50+ alerts including ReDoS, Clear-text Logging, and Exception Exposure.
* **Workflow Permissions**: Enforced `permissions: contents: read` across all GitHub Actions to adhere to Least Privilege.
* **PII Protection**: Implemented `redact_pii` logic in all API endpoints and exception handlers.

### Compliance

* **Snyk Attribution**: Added Snyk attribution to README and Documentation footer for Partner Program compliance.

### Bug fixes

* **API Stability**: Fixed unhandled exceptions in `verify_logic` and `agent_tool_call` endpoints.

***

## v2.4.1 — the reasoning engine

**Released: January 20, 2026** · [GitHub Release](https://github.com/QWED-AI/qwed-verification/releases/tag/v2.4.1)

### New features

* **Optimization Engine (`verify_optimization`)**: Added `LogicVerifier` support for Z3's `Optimize` context.
* **Vacuity Checker (`check_vacuity`)**: Added logical proof to detect "Vacuous Truths".

### Enterprise updates

* **Dockerized GitHub Action**: The main `qwed-verification` action now runs in a Docker container.

### Fixes and improvements

* Updated `logic_verifier.py` with additive, non-breaking methods.
* Replaced shell-based `action_entrypoint.sh` with Python handler `action_entrypoint.py`.
