Skip to content

fix(interceptor): GENERAL/DATA_QUERY passthrough returns UNVERIFIABLE, not FORWARDED (closes #6)#25

Merged
Rahul Dass (rahuldass19) merged 2 commits into
mainfrom
fix/issue-6-unverifiable-passthrough
May 25, 2026
Merged

fix(interceptor): GENERAL/DATA_QUERY passthrough returns UNVERIFIABLE, not FORWARDED (closes #6)#25
Rahul Dass (rahuldass19) merged 2 commits into
mainfrom
fix/issue-6-unverifiable-passthrough

Conversation

@rahuldass19

@rahuldass19 Rahul Dass (rahuldass19) commented May 25, 2026

Copy link
Copy Markdown
Member

Problem

_route_to_engine() returned verified: True for GENERAL and DATA_QUERY
payloads — the two types with no verification engine behind them. This result
fed directly into _build_verdict(), which then issued a signed JWT attestation
with verdict: forwarded for content that was never actually checked.

An attacker only needed to label a payload payload_type: general to receive
cryptographic endorsement from QWED-A2A.

Changes

schema.py

  • Added VerdictStatus.UNVERIFIABLE — a distinct status for payloads with no
    applicable verification engine

interceptor.py (3 changes)

  • _route_to_engine(): passthrough now returns verified: False,
    status: unverifiable
  • intercept(): new dispatch branch handles UNVERIFIABLE before FORWARDED
  • _build_verdict(): skips JWT signing entirely when status is UNVERIFIABLE
    — issuing a signed JWT for unverified content is a false cryptographic claim

Tests

test_unverifiable_passthrough.py — 7 new regression tests:

  • GENERALUNVERIFIABLE, not FORWARDED
  • DATA_QUERYUNVERIFIABLE, not FORWARDED
  • No attestation_jwt on either
  • engine_used == "passthrough"
  • reason is present and non-empty
  • Sanity: verified FINANCIAL payloads still receive JWT attestation

test_interceptor.py — 5 existing tests updated to reflect new semantics

Acceptance criteria

  • GENERALVerdictStatus.UNVERIFIABLE
  • DATA_QUERYVerdictStatus.UNVERIFIABLE
  • attestation_jwt is None for both
  • verified: True never returned from passthrough path
  • Normal verified paths (FINANCIAL etc.) unaffected
  • 57/57 tests passing, lint clean

Summary by CodeRabbit

  • New Features

    • Introduced UNVERIFIABLE verdict status for verification outcomes.
  • Behavior Changes

    • Payloads without a verification engine now return UNVERIFIABLE (not FORWARDED) and do not include an attestation JWT.
    • JWT signing is skipped for UNVERIFIABLE verdicts; verified payloads retain existing signing behavior.
  • Tests

    • Added and updated tests covering UNVERIFIABLE passthroughs and attestation expectations.
  • Telemetry

    • Added metric tracking for UNVERIFIABLE intercepts.

Review Change Stack

closes #6

Root cause: _route_to_engine() returned verified=True for GENERAL and
DATA_QUERY payloads with no verification engine. _build_verdict() then
issued a signed JWT attestation for content that was never checked.

Changes:
- schema.py: add VerdictStatus.UNVERIFIABLE
- interceptor.py: passthrough returns verified=False + status=unverifiable
- interceptor.py: intercept() dispatches UNVERIFIABLE before FORWARDED check
- interceptor.py: _build_verdict() skips JWT signing when UNVERIFIABLE
  (issuing a JWT for unverified content is a false cryptographic claim)

Tests:
- test_unverifiable_passthrough.py: 7 regression tests covering
  GENERAL and DATA_QUERY passthrough behavior, no-JWT guarantee,
  engine name, reason presence, and sanity check on verified path
- test_interceptor.py: updated 5 existing tests to reflect new semantics
  (signing failure tests moved to FINANCIAL — GENERAL now skips signing)
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3c5e1ea-6fcd-4b3b-9ef7-a09b126b8843

📥 Commits

Reviewing files that changed from the base of the PR and between 298d3f8 and 65487c6.

📒 Files selected for processing (2)
  • src/qwed_a2a/utils/telemetry.py
  • tests/test_unverifiable_passthrough.py

📝 Walkthrough

Walkthrough

Adds a new VerdictStatus.UNVERIFIABLE; interceptor routes passthrough/no-engine payloads (GENERAL, DATA_QUERY) as unverifiable, skips JWT attestation for those verdicts, increments a new telemetry counter, and updates/extends tests to assert UNVERIFIABLE behavior and attestations are omitted.

Changes

UNVERIFIABLE Verdict Implementation

Layer / File(s) Summary
UNVERIFIABLE Verdict Status Schema
src/qwed_a2a/protocol/schema.py
VerdictStatus enum expanded to include UNVERIFIABLE = "unverifiable".
Interceptor UNVERIFIABLE Handling
src/qwed_a2a/interceptor.py
intercept() adds branch for engine_result status "unverifiable" to build a UNVERIFIABLE verdict; _route_to_engine() returns verified=False, engine="passthrough", status="unverifiable" for passthrough/no-engine payloads; _build_verdict() skips JWT signing when status is UNVERIFIABLE (attestation_jwt left None) while preserving existing signing failure behavior for signed verdicts.
Telemetry Accounting
src/qwed_a2a/utils/telemetry.py
Adds InterceptMetrics.total_unverifiable counter, includes it in to_dict(), and increments it when status == "unverifiable".
Existing Test Updates
tests/test_interceptor.py
Refactors tests: expect UNVERIFIABLE for general passthroughs, construct verified financial messages for attestation-error tests, and update trust-boundary expectations.
Comprehensive Regression Test Suite
tests/test_unverifiable_passthrough.py
New tests and fixtures asserting GENERAL/DATA_QUERY return UNVERIFIABLE (no attestation_jwt), report engine_used == "passthrough", include non-empty reason; sanity test confirms FINANCIAL_TRANSACTION still yields FORWARDED with JWT.
Fixture Formatting and Cleanup
tests/conftest.py, tests/test_crypto_signing.py, tests/test_endpoints.py
Reformat line_items fixtures to multi-line dicts, remove unused pytest import, and reformat one assertion to single-line (no behavioural changes).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Interceptor as A2AVerificationInterceptor.intercept
  participant Router as _route_to_engine
  participant Builder as _build_verdict
  participant Crypto as CryptoService

  Client->>Interceptor: send AgentMessage (e.g., GENERAL/DATA_QUERY)
  Interceptor->>Router: determine engine for payload
  Router-->>Interceptor: {engine:"passthrough", verified:false, status:"unverifiable", reason:"no engine"}
  Interceptor->>Builder: build UNVERIFIABLE verdict with details
  Builder->>Crypto: (skips) sign_verdict  --x
  Builder-->>Client: VerificationVerdict(status=UNVERIFIABLE, attestation_jwt=None)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • QWED-AI/qwed-a2a#2: Related prior changes to the A2A verification pipeline and telemetry that this PR builds on.
  • QWED-AI/qwed-a2a#20: Overlapping changes to intercept()/trust-boundary behavior and passthrough handling.

Suggested labels

bug, fail-closed, p0

🐰 A new verdict hops into view, UNVERIFIABLE named,
No engine to sign it, no token claimed.
Passthrough messages now honest and plain,
No attestation—no cloak, no feigned gain.
Thump, hop, tests green—truth unchained.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing passthrough behavior for GENERAL/DATA_QUERY payloads to return UNVERIFIABLE status instead of FORWARDED, with a clear issue reference.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-6-unverifiable-passthrough

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 298d3f8f84

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

if engine_result.get("status") == "unverifiable":
verdict = self._build_verdict(
trace_id=trace_id,
status=VerdictStatus.UNVERIFIABLE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update metrics accounting for UNVERIFIABLE verdicts

Emitting VerdictStatus.UNVERIFIABLE here introduces a new steady-state outcome, but record_intercept only counts forwarded and blocked explicitly and sends every other status to total_errors; in practice, normal GENERAL/DATA_QUERY traffic will now be misclassified as errors, skewing dashboards and any alerting keyed off error rate. Please add explicit handling for unverifiable (ideally a separate counter) before routing production traffic through this branch.

Useful? React with 👍 / 👎.

Comment thread src/qwed_a2a/interceptor.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_unverifiable_passthrough.py (1)

63-73: ⚡ Quick win

Assert DATA_QUERY reason presence to match passthrough contract.

You already enforce a non-empty reason for GENERAL; mirroring that for DATA_QUERY will keep regression coverage symmetric and catch contract drift.

✅ Suggested test addition
     async def test_data_query_returns_unverifiable_not_forwarded(
         self, open_interceptor, data_query_message
     ):
         """DATA_QUERY payload must produce UNVERIFIABLE verdict, not FORWARDED."""
         verdict = await open_interceptor.intercept(
             data_query_message, trace_id="t_data_query_unverifiable"
         )
         assert verdict.status == VerdictStatus.UNVERIFIABLE, (
             f"Expected UNVERIFIABLE, got {verdict.status}. "
             "DATA_QUERY payloads must not be falsely endorsed as verified."
         )
+        assert verdict.reason is not None
+        assert len(verdict.reason) > 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_unverifiable_passthrough.py` around lines 63 - 73, The DATA_QUERY
passthrough test currently only checks the UNVERIFIABLE status, so extend the
existing test in test_data_query_returns_unverifiable_not_forwarded to also
assert that verdict.reason is present and non-empty, matching the GENERAL
passthrough contract. Use the same open_interceptor.intercept flow and keep the
assertion alongside the existing VerdictStatus.UNVERIFIABLE check so regressions
in reason propagation are caught for DATA_QUERY as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_unverifiable_passthrough.py`:
- Around line 63-73: The DATA_QUERY passthrough test currently only checks the
UNVERIFIABLE status, so extend the existing test in
test_data_query_returns_unverifiable_not_forwarded to also assert that
verdict.reason is present and non-empty, matching the GENERAL passthrough
contract. Use the same open_interceptor.intercept flow and keep the assertion
alongside the existing VerdictStatus.UNVERIFIABLE check so regressions in reason
propagation are caught for DATA_QUERY as well.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f18337b2-3ca8-4e44-a375-5c288d714728

📥 Commits

Reviewing files that changed from the base of the PR and between 91f2fc6 and 298d3f8.

📒 Files selected for processing (7)
  • src/qwed_a2a/interceptor.py
  • src/qwed_a2a/protocol/schema.py
  • tests/conftest.py
  • tests/test_crypto_signing.py
  • tests/test_endpoints.py
  • tests/test_interceptor.py
  • tests/test_unverifiable_passthrough.py
💤 Files with no reviewable changes (1)
  • tests/test_crypto_signing.py

…A_QUERY

Codex P2: record_intercept() was routing UNVERIFIABLE status to
total_errors, misclassifying normal GENERAL/DATA_QUERY traffic as
errors and skewing dashboards. Added explicit total_unverifiable
counter with its own branch in record_intercept() and to_dict().

CodeRabbit: Extended test_data_query_returns_unverifiable_not_forwarded
to assert verdict.reason is present and non-empty, matching the GENERAL
passthrough contract for symmetric regression coverage.
@rahuldass19

Copy link
Copy Markdown
Member Author

The Sentry finding is not a valid bug in this context.

The scenario described FINANCIAL_TRANSACTION with enable_financial_verification=False
falling through to UNVERIFIABLE is intentional. If no verification engine ran, no
cryptographic endorsement should be issued regardless of why. Reverting to
FORWARDED + signed JWT when an engine is disabled would mean signing a verdict for
content that was never checked, which is exactly the false attestation pattern this
PR was written to eliminate.

If a downstream consumer needs to distinguish "engine disabled by config" from
"payload type has no engine", that's a separate concern tracked in future issues.
The current behavior is correct per QWED's fail-closed policy.

Sentry suggestion rejected.

@rahuldass19 Rahul Dass (rahuldass19) merged commit 89f5fe0 into main May 25, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant