Skip to content

Agentic AI-security

This page covers the separate tulip-agents-security package (tulip_security), Tulip's security domain — one of several action domains (refunds, deployments, records) built on the same harness. It lets you build agents that test and assess other AI systems.

In plain terms: you've deployed an AI — a chatbot, an agent with tools, or a model behind an API. Can someone jailbreak it, make it leak data, or trick it into doing something it shouldn't? The module builds an agent that checks, automatically and continuously, and reports only the flaws it can actually prove — a penetration tester for AI that refuses to cry wolf.

How it works, in three steps:

  1. Point Tulip at a target AI — Target.endpoint("https://bot.example/chat").
  2. Run a job — red_team(target) attacks it; assure(target) scores its defenses.
  3. Get evidence — each result is an Evidence finding (the attack worked, here's the proof) or an Abstention (no proof, so no claim). Never a guess.

More precisely: tulip_security builds agents whose subject is another AI system — agents that red-team and assess other AI (continuous monitoring is on the roadmap), and produce a grounded Evidence record or an explicit Abstention, never a hallucinated verdict.

A cybersecurity agent = a target (an AI system) × a job (red-team · assure; monitor is roadmap) × an output (grounded Evidence | Abstention) × being itself trustworthy by construction.

The trust chain A Target, the AI under test, is attacked by red_team or assessed by assure, and each result goes through GSAR grounding in ground_finding. Below the grounding threshold the result becomes an Abstention: the finding is withheld and no claim is made. When it proceeds it becomes an Evidence finding. Verification challenges the finding, and the admission gate, admit, weighs the proposed action against the finding, the verdict and the policy. On allow the action runs. On hold it runs only when admit is given approved_by, naming the person who approved it. On deny it never runs, and no approval overrides a deny. When an AuditTrail is passed to admit, it records every decision, allowed or not. each result below threshold proceed finding + verdict every decision Target the AI under test red_team · assure attack · assess GSAR grounding ground_finding() Abstention withheld · no claim Evidence grounded finding Verification verify() Admission gate admit() · policy Allow action runs Hold runs if approved_by Deny never runs AuditTrail hash-chained A held action runs only when admit() is given approved_by; no approval overrides a deny. Decisions are recorded when an AuditTrail is passed to admit().

Why this, and why now

Companies ship LLM agents — with tools, memory, autonomy, and access to data and actions — into production. That is a new, largely unguarded attack surface: prompt injection (direct and indirect), tool abuse / excessive agency, data exfiltration, memory and RAG poisoning, A2A identity confusion, unknown model and hardware provenance.

Existing tooling either filters I/O without testing it, or tests it and hallucinates findings — the literature is explicit that AI scorers lack grounding. This module's answer is abstain-by-construction grounding (GSAR): no evidence, no claim.

The Target — what you point at

A Target is a uniform handle to the AI system under assessment. One .send() contract over four constructors:

from tulip_security import Target

Target.endpoint("https://bot.example/chat")   # a remote LLM / agent endpoint
Target.agent(my_tulip_agent)                   # an in-process tulip.Agent
Target.a2a(peer_send)                          # an A2A peer's send coroutine
Target.from_callable(fn)                       # any sync/async str -> str

Everything below operates on a Target, so the same probe runs against a black-box endpoint, an agent you built, or an offline stub in CI.

red_team — attack, grounded

red_team runs the OWASP-ASI probe suite (or the smaller owasp-llm one) against a target and grounds each outcome; MITRE ATLAS appears as tags on the findings (Evidence.taxonomy), not as a suite. A probe whose attack landed yields an Evidence finding; an inconclusive one yields an Abstention.

import asyncio
from tulip_security import Target, red_team, is_finding


async def main():
    report = await red_team(Target.endpoint("https://bot.example/chat"), suite="owasp-asi")
    for r in report:
        if is_finding(r):
            print(r.severity.value, r.title, r.taxonomy)   # e.g. LLM01, ASI02, AML.T0054
        else:
            print("abstained:", r.reason)


asyncio.run(main())

The bundled catalogue covers direct and indirect prompt injection, jailbreak, excessive agency / tool misuse, and sensitive-information disclosure — each tagged with the taxonomy a SOC (security operations center) and an AI-assurance reviewer expect.

assure — assess, grounded

Where red_team asks can I break it?, assure asks how well does it hold up? guardrail_coverage runs the suite and grounds a posture finding in the direct observations — INFO when fully hardened, escalating to CRITICAL at 0% coverage, with the taxonomy listing exactly the gaps.

from tulip_security import assure
posture = await assure(target)          # one grounded posture finding per assessment
coverage = posture[0]                   # today assure runs a single assessment: guardrail coverage
# or call guardrail_coverage(target) directly for just that one finding

Each item in that list is a grounded finding, not a compliance attestation. assure never asserts posture it cannot evidence — it abstains. Tulip deliberately does not do compliance attestation; its governance acts on the action itself: the admission gate — enforced policy, a human on the actions that warrant one, and a tamper-evident record.

Secure by default — the floor

An agent that red-teams or assures other AI must itself be trustworthy. governed_agent builds a tulip.Agent with the security spine on by default — GSAR grounding, guardrails (PII / injection / a dangerous-tool denylist; allowlist is opt-in), and a tamper-evident audit trail — and returns it alongside that trail.

from tulip.control import governed_agent

secured = governed_agent(model="openai:gpt-4o-mini", tools=[...])
result = secured.run_sync("...")
assert secured.audit_trail.verify()   # the chain is intact (tamper-evident)

The AuditTrail is an in-memory SHA-256 hash chain: every action commits to the hash before it, so an edit, deletion, or reorder in the middle breaks verify(). Records cut off the end still leave a valid chain; catching that needs verify(expected_head=...) against a head hash pinned outside the trail. That makes it tamper-evident (it detects edits) rather than tamper-proof. It is unsigned by default, so anyone who can write the log could rebuild the whole chain around an edit and verify() would still pass; only verify(expected_head=...) against a head hash anchored somewhere out of their reach would catch it. Give it an Ed25519Signer (AuditTrail(signer=...), which needs pip install "tulip-agents[audit]") and verify(keys=...) then fails if any record is not signed with a key you trust. The signature covers each record's hash when it is written, so it cannot catch a payload that was wrong before it was recorded. It exports as JSONL for shipping to your audit store or a SIEM (a security team's log platform); anyone holding the export can check it with verify_jsonl() and the public keys alone, and write-once retention can harden it further.

Enforce it before it acts

Grounding, verification, and the audit trail make an agent trustworthy; the admission gate makes that trust binding. Run a side-effecting action through admit() (or ctx.actions.execute()). The action fires at once when policy allows it (approve() → ALLOW). When policy holds it (require_human), admit() raises AdmissionError instead of running it; it runs only if you call admit() again naming the person who approved it (admit(approved_by=...)). ctx.actions.execute() takes no approved_by, so a hold there is simply refused. Everything else is denied, and no approval overrides a denial. Every decision — allowed, held, or denied — is recorded when you pass an AuditTrail to admit(trail=...), and AdmissionError is raised whenever the action does not run. That's the line between an agent that could be safe and a runtime that enforces its policy — an action routed through the gate runs only when policy allows it or a named person approves the hold. See SecurityContext.

Regular cyber — classic security operations

This page covers securing AI. The same engine works on classic security operations too — the bundled SOC-analyst factory, the incident-response playbooks (phishing_triage, ransomware_containment, nist_800_61_ir), and read-only cloud-posture auditing are a full worked domain, alongside the general-operations examples elsewhere in the docs. Different target, different taxonomy — same Target + grounded-Evidence contract: grounding is exactly what makes an AI SOC agent's verdicts trustworthy, answering the false-positive pain teams hit with AI-graded triage.

See also: Grounded findings · GSAR typed grounding · Threat scenarios · Cloud-posture agent.