Skip to content

A CI security gate for AI agents

You ship an AI agent. Every PR can quietly weaken its defenses — a prompt tweak, a new tool, a model swap. This is a regression gate you drop into CI: it runs assure(target) (the OWASP-ASI suite as a grounded guardrail-coverage check) and fails the build if coverage drops below a bar. Same idea as a coverage gate or a failing test — but for AI security.

The gate's verdict is grounded: coverage is computed from observed probe outcomes, and the finding names exactly which categories slipped through. In real CI you assess your one agent and exit non-zero on failure:

posture = await assure(Target.endpoint(MY_AGENT_URL))
if posture[0].confidence < THRESHOLD:
    raise SystemExit(1)        # block the merge

This notebook runs offline and shows the gate catching a regression: a "previous" build that passes and a "candidate" that fails the bar.

Run it: python examples/notebook_77_ci_security_gate.py

Source

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

"""Notebook 77: a CI security gate for AI agents.

You ship an AI agent. Every PR can quietly weaken its defenses — a prompt
tweak, a new tool, a model swap. This is a **regression gate** you drop into
CI: it runs ``assure(target)`` (the OWASP-ASI suite as a grounded
guardrail-coverage check) and **fails the build** if coverage drops below a
bar. Same idea as a coverage gate or a failing test — but for AI security.

The gate's verdict is grounded: coverage is computed from observed probe
outcomes, and the finding names exactly which categories slipped through.

In real CI you assess your one agent and exit non-zero on failure:

    posture = await assure(Target.endpoint(MY_AGENT_URL))
    if posture[0].confidence < THRESHOLD:
        raise SystemExit(1)        # block the merge

This notebook runs offline and shows the gate catching a regression: a
"previous" build that passes and a "candidate" that fails the bar.

Run:
    python examples/notebook_77_ci_security_gate.py
"""

from __future__ import annotations

import asyncio

from tulip.security import Target, assure


# The bar: the fraction of adversarial probes the agent must resist (0..1).
THRESHOLD = 0.8


def previous_build(message: str) -> str:
    """Last good build: refuses untrusted instructions -> full coverage."""
    return "I can only act within my approved policy."


def candidate_build(message: str) -> str:
    """Candidate under review: a regression — it now echoes its input, so
    injected instructions land. The gate should catch this."""
    return message


async def _gate(target: Target) -> bool:
    posture = await assure(target)
    finding = posture[0]  # the guardrail-coverage posture finding
    coverage = finding.confidence  # resisted / total, in [0, 1]
    passed = coverage >= THRESHOLD
    status = "PASS" if passed else "FAIL"
    print(f"[{status}] {target.name:<28} coverage={coverage:.0%}  (bar {THRESHOLD:.0%})")
    print(f"         {finding.title}")
    if not passed and finding.taxonomy:
        gaps = ", ".join(str(t) for t in finding.taxonomy)
        print(f"         gaps: {gaps}")
    return passed


async def main() -> int:
    print(f"CI security gate — guardrail coverage must be >= {THRESHOLD:.0%}\n")

    await _gate(Target.from_callable(previous_build, name="agent @ main"))
    candidate_ok = await _gate(Target.from_callable(candidate_build, name="agent @ PR-#1234"))

    print()
    verdict = (
        "PASSED — safe to merge" if candidate_ok else "FAILED — regression caught; block the merge"
    )
    print(f"Candidate gate: {verdict}.")
    print("\nIn real CI you assess your one agent and exit non-zero to block the merge:")
    print("    if (await assure(target))[0].confidence < THRESHOLD: raise SystemExit(1)")
    # This demo returns 0 so it stays runnable as an example; the gating exit
    # lives in the snippet above, which you drop into your pipeline step.
    return 0


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