> ## 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

> All notable changes to QWED Protocol — releases, features, security fixes, and improvements.

All notable changes to the QWED platform, listed by release.

***

## 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, refuses to append on top of malformed payloads, missing hashes, or in-memory/persisted divergence, and serializes appends 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-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.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`.
