AID-D-009.001: Source-Grounded Claim Verification & Confidence State
Pillars: app, data
Lifecycle: operation
Scope boundary
Owns verification state and calibrated confidence for high-impact factual claims based on policy-approved independent and authoritative sources, preserving contradiction, uncertainty, abstention, and source evidence.
AID-D-009.002: AID-D-009.001 determines the evidence-grounded state of a claim; AID-D-009.002 detects how a contradicted, unverified, inconclusive, or poisoned claim propagates across agents, messages, memory, and delegation. Claim truth state and propagation lineage are separate findings, and peer repetition never upgrades the truth state.
Implementation guidance
- AID-D-009.001-G001: Verify high-impact agent assertions against multiple independent sources and emit a structured verification decision.
- AID-D-009.001-G002: Check claim classes against authoritative source systems before marking them verified.
- AID-D-009.001-G003: Attach confidence and verification state to agent-generated facts so low-confidence assertions are routed for deeper review instead of being treated as verified.
AID-D-009.001-G001
Define authority, independence, freshness, and source population
The signed verification policy names the exact required sources, each source's authority scope and independence group, the minimum supporting groups, and the maximum observation age. Mutually authenticated adapters return signed observations; generic web results cannot become authoritative by choosing a provider label.
Verify and classify every observation
from __future__ import annotations
# File: verification/multi_source.py
import hashlib
import json
import math
import re
from collections import defaultdict
from dataclasses import dataclass
from typing import Literal
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
Verdict = Literal["SUPPORTS", "CONTRADICTS", "ABSTAINS"]
SHA256 = re.compile(r"^[0-9a-f]{64}$")
def _is_sha256(value: object) -> bool:
return isinstance(value, str) and SHA256.fullmatch(value) is not None
@dataclass(frozen=True)
class TrustedSource:
independence_group: str
authority_scopes: frozenset[str]
verification_key: Ed25519PublicKey
@dataclass(frozen=True)
class SignedObservation:
source_id: str
evidence: bytes
signature: bytes
def _valid_observation_shape(value: object) -> bool:
return (
isinstance(value, SignedObservation)
and isinstance(value.source_id, str)
and bool(value.source_id)
and isinstance(value.evidence, bytes)
and bool(value.evidence)
and isinstance(value.signature, bytes)
and bool(value.signature)
)
def _strict_object(pairs: list[tuple[str, object]]) -> dict:
value = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate source-evidence key: " + key)
value[key] = item
return value
def parse_observation(
item: SignedObservation,
*,
trusted_sources: dict[str, TrustedSource],
expected_claim_sha256: str,
expected_scope: str,
evaluated_at_unix: float,
maximum_age_seconds: int,
) -> dict:
if (
not _valid_observation_shape(item)
or not isinstance(trusted_sources, dict)
or not _is_sha256(expected_claim_sha256)
or not isinstance(expected_scope, str)
or not expected_scope
or isinstance(evaluated_at_unix, bool)
or not isinstance(evaluated_at_unix, (int, float))
or not math.isfinite(float(evaluated_at_unix))
or isinstance(maximum_age_seconds, bool)
or not isinstance(maximum_age_seconds, int)
or maximum_age_seconds < 1
):
raise ValueError("observation verification inputs are invalid")
trusted = trusted_sources.get(item.source_id)
if trusted is None or expected_scope not in trusted.authority_scopes:
raise ValueError("source is not authoritative for the requested scope")
trusted.verification_key.verify(item.signature, item.evidence)
value = json.loads(
item.evidence.decode("utf-8", errors="strict"),
object_pairs_hook=_strict_object,
parse_constant=lambda item: (_ for _ in ()).throw(
ValueError("non-finite source value")
),
)
required = {
"schema_version", "source_id", "claim_sha256", "authority_scope",
"observed_at_unix", "valid_until_unix", "verdict", "normalized_value",
}
if not isinstance(value, dict) or set(value) != required:
raise ValueError("source evidence schema mismatch")
observed_at = value["observed_at_unix"]
valid_until = value["valid_until_unix"]
if (
value["schema_version"] != "aidefend.source_observation.v2"
or value["source_id"] != item.source_id
or value["claim_sha256"] != expected_claim_sha256
or value["authority_scope"] != expected_scope
or not isinstance(value["verdict"], str)
or value["verdict"] not in {"SUPPORTS", "CONTRADICTS", "ABSTAINS"}
or isinstance(observed_at, bool)
or not isinstance(observed_at, (int, float))
or isinstance(valid_until, bool)
or not isinstance(valid_until, (int, float))
or not math.isfinite(float(observed_at))
or not math.isfinite(float(valid_until))
):
raise ValueError("source evidence is misbound or malformed")
normalized = value["normalized_value"]
if value["verdict"] == "ABSTAINS":
if normalized is not None:
raise ValueError("abstention must not carry a normalized value")
elif not isinstance(normalized, str) or not normalized:
raise ValueError("support or contradiction lacks a normalized value")
fresh = (
float(observed_at) <= evaluated_at_unix
and evaluated_at_unix - float(observed_at) <= maximum_age_seconds
and float(valid_until) >= evaluated_at_unix
)
return {
**value,
"fresh": fresh,
"independence_group": trusted.independence_group,
"evidence_sha256": hashlib.sha256(item.evidence).hexdigest(),
}
def verify_claim(
observations: list[SignedObservation],
*,
trusted_sources: dict[str, TrustedSource],
required_source_ids: tuple[str, ...],
expected_claim_sha256: str,
expected_scope: str,
evaluated_at_unix: float,
maximum_age_seconds: int,
required_independent_groups: int,
policy_version: str,
) -> dict:
if (
not isinstance(observations, list)
or any(not _valid_observation_shape(item) for item in observations)
):
raise ValueError("source observations are malformed")
if not isinstance(trusted_sources, dict):
raise ValueError("trusted source registry is malformed")
if (
not isinstance(required_source_ids, tuple)
or not required_source_ids
or any(not isinstance(item, str) or not item for item in required_source_ids)
):
raise ValueError("required source identifiers are malformed")
if (
isinstance(required_independent_groups, bool)
or not isinstance(required_independent_groups, int)
or required_independent_groups < 2
or isinstance(maximum_age_seconds, bool)
or not isinstance(maximum_age_seconds, int)
or maximum_age_seconds < 1
or isinstance(evaluated_at_unix, bool)
or not isinstance(evaluated_at_unix, (int, float))
or not math.isfinite(float(evaluated_at_unix))
):
raise ValueError("verification thresholds or evaluation time are invalid")
if (
not isinstance(policy_version, str)
or not policy_version
or not isinstance(expected_scope, str)
or not expected_scope
or not _is_sha256(expected_claim_sha256)
):
raise ValueError("verification policy binding is malformed")
required_sources = set(required_source_ids)
observed_sources = [item.source_id for item in observations]
if (
len(required_sources) != len(required_source_ids)
or len(observed_sources) != len(set(observed_sources))
or set(observed_sources) - required_sources
):
raise ValueError("source population is duplicated or outside policy")
missing = sorted(required_sources - set(observed_sources))
parsed = [
parse_observation(
item,
trusted_sources=trusted_sources,
expected_claim_sha256=expected_claim_sha256,
expected_scope=expected_scope,
evaluated_at_unix=float(evaluated_at_unix),
maximum_age_seconds=maximum_age_seconds,
)
for item in observations
]
fresh = [item for item in parsed if item["fresh"]]
stale = sorted(item["source_id"] for item in parsed if not item["fresh"])
supports = [item for item in fresh if item["verdict"] == "SUPPORTS"]
contradictions = [item for item in fresh if item["verdict"] == "CONTRADICTS"]
abstentions = [item for item in fresh if item["verdict"] == "ABSTAINS"]
support_groups: dict[str, set[str]] = defaultdict(set)
for item in supports:
support_groups[item["independence_group"]].add(item["normalized_value"])
conflicting_support = any(len(values) != 1 for values in support_groups.values())
supported_values = {
next(iter(values)) for values in support_groups.values() if len(values) == 1
}
if contradictions or conflicting_support or len(supported_values) > 1:
decision = "CONTRADICTED"
elif (
not missing
and not stale
and len(support_groups) >= required_independent_groups
and len(supported_values) == 1
):
decision = "VERIFIED"
else:
decision = "INCONCLUSIVE"
return {
"schema_version": "aidefend.multi_source_verification.v2",
"decision": decision,
"claim_sha256": expected_claim_sha256,
"authority_scope": expected_scope,
"policy_version": policy_version,
"required_source_ids": sorted(required_sources),
"missing_source_ids": missing,
"stale_source_ids": stale,
"abstaining_source_ids": sorted(item["source_id"] for item in abstentions),
"supporting_independence_groups": sorted(support_groups),
"contradicting_source_ids": sorted(
item["source_id"] for item in contradictions
),
"observed_values": sorted(supported_values),
"evidence_sha256": sorted(item["evidence_sha256"] for item in parsed),
}Replay and decision semantics
Derive expected_claim_sha256 from the immutable canonical claim, not from an agent-provided digest. Signature failure, malformed evidence, wrong scope, unexpected sources, and duplicate sources are verifier errors; missing, unavailable, stale, abstaining, or insufficient independent sources remain INCONCLUSIVE; a fresh explicit contradiction or conflicting authoritative value is CONTRADICTED. Replay supported, contradicted, stale, correlated-mirror, unavailable, abstaining, temporal-change, wrong-scope, and duplicate-source fixtures under a separate identity.
AID-D-009.001-G002
Read and authenticate the exact authoritative record
Load the endpoint, tenant/resource scope, freshness rule, response bounds, receipt issuer/audience/key, and mTLS material from a signature-verified registry profile. The lookup must bind the response and its signed receipt to the exact claim, tenant, resource, requested as-of instant, and returned version. A transport, schema, receipt, freshness, or binding failure is unavailable evidence, never a verified claim.
# File: verification/authoritative_lookup.py
from __future__ import annotations
import hashlib
import json
import math
import ssl
from datetime import datetime, timezone
from urllib.parse import urlsplit
import httpx
import jwt
def reject_duplicate_keys(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError(f'duplicate authoritative-response key: {key}')
value[key] = item
return value
def canonical(value: dict) -> bytes:
return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()
def lookup(*, profile: dict, claim_id: str, tenant_id: str,
resource_id: str, as_of: str) -> dict:
required_profile = {
'policy_version', 'policy_sha256', 'endpoint', 'connect_timeout_seconds',
'read_timeout_seconds', 'response_max_bytes', 'response_chunk_bytes',
'ca_file', 'client_cert', 'client_key', 'receipt_public_key',
'receipt_issuer', 'receipt_audience', 'maximum_receipt_age_seconds',
}
if not isinstance(profile, dict) or set(profile) != required_profile:
raise ValueError('verified authoritative-source profile schema differs')
for field in ('connect_timeout_seconds', 'read_timeout_seconds', 'maximum_receipt_age_seconds'):
value = profile[field]
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value)) or value <= 0:
raise ValueError(f'invalid signed value: {field}')
maximum = profile['response_max_bytes']
chunk = profile['response_chunk_bytes']
if (isinstance(maximum, bool) or not isinstance(maximum, int) or maximum < 2
or isinstance(chunk, bool) or not isinstance(chunk, int)
or chunk < 1 or chunk > maximum):
raise ValueError('signed response bounds are invalid')
parsed = urlsplit(profile['endpoint'])
if (parsed.scheme != 'https' or not parsed.hostname or parsed.username
or parsed.password or parsed.query or parsed.fragment):
raise ValueError('authoritative endpoint is not one canonical HTTPS URL')
if not all(isinstance(value, str) and value for value in (claim_id, tenant_id, resource_id, as_of)):
raise ValueError('exact claim and resource scope is required')
tls = ssl.create_default_context(cafile=profile['ca_file'])
tls.load_cert_chain(profile['client_cert'], profile['client_key'])
timeout = httpx.Timeout(float(profile['read_timeout_seconds']),
connect=float(profile['connect_timeout_seconds']))
with httpx.Client(verify=tls, timeout=timeout, follow_redirects=False) as client:
with client.stream(
'GET', profile['endpoint'],
params={'claim_id': claim_id, 'tenant_id': tenant_id,
'resource_id': resource_id, 'as_of': as_of},
headers={'Accept': 'application/json', 'Accept-Encoding': 'identity'},
) as response:
if 300 <= response.status_code < 400:
raise RuntimeError('authoritative-source redirect is forbidden')
response.raise_for_status()
if response.headers.get('Content-Encoding', 'identity').lower() != 'identity':
raise RuntimeError('encoded authoritative response is forbidden')
chunks, observed = [], 0
for item in response.iter_bytes(chunk):
observed += len(item)
if observed > maximum:
raise RuntimeError('authoritative response exceeds signed bound')
chunks.append(item)
payload = json.loads(
b''.join(chunks).decode('utf-8', errors='strict'),
object_pairs_hook=reject_duplicate_keys, parse_constant=lambda value: (_ for _ in ()).throw(ValueError('non-finite JSON constant: ' + value)),
)
fields = {'claim_id', 'tenant_id', 'resource_id', 'as_of',
'version', 'value', 'receipt_jws'}
if not isinstance(payload, dict) or set(payload) != fields:
raise RuntimeError('authoritative response schema differs')
if (payload['claim_id'], payload['tenant_id'], payload['resource_id'], payload['as_of']) != (
claim_id, tenant_id, resource_id, as_of) or not payload['version']:
raise RuntimeError('authoritative response scope binding differs')
signed_payload = {key: value for key, value in payload.items() if key != 'receipt_jws'}
payload_sha256 = hashlib.sha256(canonical(signed_payload)).hexdigest()
claims = jwt.decode(
payload['receipt_jws'], profile['receipt_public_key'], algorithms=['EdDSA'],
issuer=profile['receipt_issuer'], audience=profile['receipt_audience'],
options={'require': ['iss', 'aud', 'iat', 'exp', 'jti', 'claim_id',
'tenant_id', 'resource_id', 'as_of', 'version',
'payload_sha256']},
)
if any(claims[field] != signed_payload[field] for field in (
'claim_id', 'tenant_id', 'resource_id', 'as_of', 'version')):
raise RuntimeError('authoritative receipt scope differs')
if claims['payload_sha256'] != payload_sha256:
raise RuntimeError('authoritative receipt digest differs')
now = datetime.now(timezone.utc).timestamp()
if isinstance(claims['iat'], bool) or not isinstance(claims['iat'], int) or not 0 <= now - claims['iat'] <= profile['maximum_receipt_age_seconds']:
raise RuntimeError('authoritative receipt is future-issued or stale')
return {**signed_payload, 'receipt_jti': claims['jti'],
'response_sha256': payload_sha256,
'policy_version': profile['policy_version'],
'policy_sha256': profile['policy_sha256']}
Independent replay
A separate verifier identity rereads the same tenant/resource/as-of record or authenticates the retained immutable response, verifies the receipt independently, and recomputes the typed comparison and freshness decision. Test wrong tenant, wrong resource, stale cache, schema change, missing record, version race, redirect, oversized body, duplicate keys, TLS failure, and receipt misbinding.
AID-D-009.001-G003
Before you begin
Owned by the factual-assurance telemetry team for agent-generated facts that require a calibrated confidence and verification-state envelope for triage.
Production implementation
Validate the immutable claim and model confidence, calibrate only within a supported stratum, validate any upstream source/peer/authoritative receipts by identity/scope/version/freshness/evidence digest, compute VERIFIED, CONTRADICTED, UNVERIFIED or INCONCLUSIVE independently of numeric confidence, and emit the bounded triage annotation. High confidence without valid source evidence remains UNVERIFIED, and no action is approved or blocked.
Independent verification
An independent runner replays the exact inputs and receipts, recomputes calibration and state transition, runs high-confidence-false, low-confidence-supported, stale-receipt, unsupported-stratum and contradictory-evidence fixtures, and reconciles every eligible claim/annotation with the workflow ledger.
Executable verification-state gate
# File: verification/claim_state.py
from __future__ import annotations
import math
def claim_state(
*, calibrated_confidence: float,
verification_decision: str,
verified_threshold: float,
review_threshold: float,
) -> dict:
values = (calibrated_confidence, verified_threshold, review_threshold)
if any(isinstance(value, bool) or not math.isfinite(value) for value in values):
raise ValueError("confidence policy is invalid")
if not 0 <= review_threshold <= verified_threshold <= 1:
raise ValueError("confidence thresholds are inconsistent")
if verification_decision == "VERIFIED" and calibrated_confidence >= verified_threshold:
state = "VERIFIED"
elif calibrated_confidence >= review_threshold:
state = "REVIEW_REQUIRED"
else:
state = "UNVERIFIED"
return {
"state": state,
"confidence": calibrated_confidence,
"verification_decision": verification_decision,
}Only VERIFIED claims may receive a verified label. Preserve the calibration-set digest, verifier receipts, policy version, confidence, and state so an independent evaluator can recompute the transition.
Open-source tools
- LlamaIndex
- LangChain
- Ragas (faithfulness scoring against supplied retrieval context)
- DeepEval (faithfulness scoring against supplied retrieval context)
- spaCy
- sentence-transformers
- Neo4j Community Edition
- jsonschema
- OpenTelemetry
Source-available / open-weight tools
- Vectara HHEM-2.1-Open (Apache-2.0; open-weight)
- Patronus Lynx 8B (CC-BY-NC-4.0; open-weight)
- Patronus Lynx 70B (CC-BY-NC-4.0; open-weight)
- MiniCheck-Flan-T5-Large (MIT; open-weight)
- MiniCheck-RoBERTa-Large (MIT; open-weight)
- MiniCheck-DeBERTa-v3-Large (MIT; open-weight)
- Bespoke-MiniCheck-7B (CC-BY-NC-4.0; open-weight)
Commercial / hosted tools
- Tavily Search API
- Wolfram|Alpha APIs
- Diffbot Knowledge Graph API
- Neo4j Enterprise Edition
- Databricks MLflow 3
Threat mappings
MITRE ATLAS
- AML.T0070 RAG Poisoning
- AML.T0066 Retrieval Content Crafting
- AML.T0067 LLM Trusted Output Components Manipulation
- AML.T0067.000 LLM Trusted Output Components Manipulation: Citations
- AML.T0071 False RAG Entry Injection
- AML.T0062 Discover LLM Hallucinations (verification findings identify unverified hallucinations before downstream persistence or amplification)
- AML.T0080 AI Agent Context Poisoning (verification findings identify poisoned context before persistence)
- AML.T0080.000 AI Agent Context Poisoning: Memory (verification findings identify hallucinated facts before memory admission)
MAESTRO
- Data Poisoning (L2)
- Compromised RAG Pipelines (L2) (verification findings identify poisoned or unsupported facts before retrieval-index admission)
OWASP LLM Top 10 2026
- LLM07:2026 Misinformation
OWASP ML Top 10 2023
- ML09:2023 Output Integrity Attack (verification findings identify fabricated claims before downstream systems treat them as authoritative)
OWASP Top 10 for Agentic Applications 2026
- ASI06:2026 Memory & Context Poisoning (verification findings identify poisoned context before shared-memory admission)
- ASI01:2026 Agent Goal Hijack (poisoned facts redirect agent decision pathways)
NIST Adversarial Machine Learning 2025
- NISTAML.027 Misaligned Outputs
- NISTAML.015 Indirect Prompt Injection
Cisco Integrated AI Security and Safety Framework
- AISubtech-15.1.19 Integrity: Hallucinations / Misinformation (official misinformation crosswalk supports independent factual-claim verification and unverified claim findings)
- AISubtech-15.1.5 Safety Harms and Toxicity: Disinformation (factual claims checked against independently authenticated sources)
- AITech-15.1 Harmful Content (factual claims checked against independently authenticated sources)
- AITech-9.1 Model or Agentic System Manipulation (factual claims checked against independently authenticated sources)
Google Secure AI Framework 2.0 - Risks
- IMO: Insecure Model Output (independent source verification identifies unsupported or contradicted claims)
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.8: LLM hallucinations
- Agents - Core 13.1: Memory Poisoning (verification findings identify poisoned facts before shared-memory admission)
- Agents - Core 13.7: Misaligned & Deceptive Behaviors