Skip to content

Verify findings — the SDK that prevents security hallucinations

A Tulip Evidence is a claim. Before you act on it, verify() puts it through an independent skeptic that challenges the evidence and scores confidence. A well-grounded finding survives; an unsupported or fabricated one is refuted — so a hallucinated "critical" never drives a real action.

verify() is framework-agnostic: it takes a Tulip Evidence or a finding-shaped dict produced by any other agent (LangGraph, CrewAI, anything), which is what lets Tulip sit above the stack as the verification layer. The notebook verifies a real grounded finding (survives), a fabricated one with no evidence (refuted), and an external finding that has references but was never grounded (refuted).

Runs fully offline.

Run it: python examples/notebook_78_verify_findings.py

See also: Agentic AI-security · SecurityContext.

Source

#!/usr/bin/env python3
# Copyright 2026 Tulip Labs
# SPDX-License-Identifier: Apache-2.0

"""Notebook 78: verify findings — the SDK that prevents security hallucinations.

A `Evidence` is a *claim*. Before you act on it, `verify()` puts it through an
independent skeptic that challenges the evidence and scores confidence. A
well-grounded finding **survives**; an unsupported or fabricated one is
**refuted** — so a hallucinated "critical" never drives a real action.

`verify()` is framework-agnostic: it takes a Tulip `Evidence` *or* a
finding-shaped dict produced by any other agent (LangGraph/CrewAI/anything),
which is what lets Tulip sit above the stack as the verification layer.

Runs fully offline.

Run:
    python examples/notebook_78_verify_findings.py
"""

from __future__ import annotations

import asyncio

from tulip.security import Target, is_finding, red_team, verify


def _vulnerable_bot(prompt: str) -> str:
    return prompt  # naive bot: obeys injected instructions (see notebook 76)


def _show(label: str, verdict: object) -> None:
    v = verdict  # VerificationResult
    status = "SURVIVES" if v.survives else "REFUTED"  # type: ignore[attr-defined]
    print(f"\n[{status}] {label}  (confidence {v.confidence:.2f})")  # type: ignore[attr-defined]
    for r in v.refutations:  # type: ignore[attr-defined]
        print(f"    - ({r.weight}) {r.reason}")


async def main() -> int:
    print("verify() — does the evidence actually support the claim?")

    # 1. A REAL finding from red-teaming a vulnerable bot — well grounded.
    report = await red_team(Target.from_callable(_vulnerable_bot, name="bot"), suite="owasp-asi")
    real_finding = next(r for r in report if is_finding(r))
    _show(f"real finding: {real_finding.title}", await verify(real_finding))

    # 2. A FABRICATED finding from some other agent — no evidence at all.
    hallucination = {"title": "Host 10.0.0.5 is compromised", "severity": "critical"}
    _show("hallucinated finding (no evidence)", await verify(hallucination))

    # 3. An external finding that has refs but was never grounded.
    ungrounded = {
        "title": "Possible data exfiltration",
        "severity": "high",
        "evidence_refs": ["siem:alert:9931"],
    }
    _show("external finding (ungrounded)", await verify(ungrounded))

    print(
        "\nOnly the grounded finding survives. Tulip refuses to act on a claim whose "
        "evidence it cannot independently stand behind — that is how it prevents "
        "security hallucinations."
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(asyncio.run(main()))