Guardrail Detection

SDK

Passively detect, classify, and audit guardrails in your agent traces — zero runtime enforcement, no changes to existing code.

Traccia's guardrail detection engine runs as an OTel span processor. After every span ends, it inspects span attributes and classifies any guardrail signals into structured GuardrailFinding objects. When the root span ends, it evaluates which guardrail categories are missing given the agent's observed capabilities and writes a GuardrailSummary to the root span.

Zero config — automatic from init()

Guardrail detection is registered automatically when you call traccia.init(). You do not need to register any processor or change any existing code to get Tier B (provider-native) and Tier C (heuristic) signals. Tier A (explicit) requires annotation.

Detection Tiers

Every GuardrailFinding has a source_type and confidence. The three tiers reflect how reliably the SDK can infer that a guardrail exists and whether it fired.

Tiersource_typeconfidenceHow it fires
A — Explicitexplicithigh or mediumDeveloper annotates a function with @observe(as_type="guardrail") or wraps a check in guardrail_span().
B — Provider-nativeprovider_nativehigh or mediumDeterministic structured fields: llm.finish_reason, llm.stop_reason, llm.safety_ratings.
C — HeuristicheuristiclowPattern-matching on tool error messages (denial keywords). Always labeled as heuristic. Does not count as coverage for the missing-guardrail evaluator.

Low confidence does not mean coverage

A Tier C (heuristic) finding appears in detected_categories but does not remove the category from missing_categories. Seeing a possible denial is not the same as having a guardrail. Both signals are preserved so you can investigate.

Tier A — Explicit annotation

Explicit annotation gives you high confidence findings and is the only way to prove a specific guardrail exists in your agent. There are two approaches:

Option 1: guardrail_span() context manager

Recommended for inline checks. Pre-fills all canonical attributes; you only set guardrail.triggered.

agent.py
python
from traccia.guardrails import guardrail_span
with guardrail_span(
"pii_check",
category="pii", # GuardrailCategory value
enforcement_mode="warn", # "block", "warn", "log_only"
) as span:
result = run_pii_scanner(user_input)
span.set_attribute("guardrail.triggered", result.found_pii)
# If triggered=True → finding status = "triggered"
# If triggered=False → finding status = "not_triggered"
# If not set → finding status = "present" (guardrail ran, result unknown)

Option 2: @observe(as_type="guardrail") decorator

For function-level guardrails. If the function returns a bool, guardrail.triggered is set automatically — True means triggered/blocked, False means the guardrail ran and did not fire.

guardrails.py
python
from traccia import observe
INJECTION_KEYWORDS = ["ignore previous", "ignore all", "jailbreak", "act as", "dan mode"]
@observe(
as_type="guardrail",
attributes={
"guardrail.name": "prompt_injection_check",
"guardrail.category": "prompt_injection",
"guardrail.enforcement_mode": "block",
},
)
def check_injection(text: str) -> bool:
"""Returns True (triggered) if injection pattern detected."""
return any(kw in text.lower() for kw in INJECTION_KEYWORDS)
# In your agent:
if check_injection(user_input):
return {"error": "Request blocked: injection attempt detected."}

Auto-triggered from bool return

Pre-setting guardrail.triggered in attributes={} takes precedence and the return value is ignored. For non-bool returns, set guardrail.triggered manually via get_current_span().

Required attributes for high confidence

If any of the three required attributes are missing, confidence is downgraded to medium and a warning is logged to traccia.guardrails.

AttributeRequiredValues
guardrail.namerequiredAny human-readable string
guardrail.categoryrequiredinput_validation, prompt_injection, pii, moderation, tool_permission, output_validation, rate_limit, unknown
guardrail.triggeredrequiredTrue = guardrail fired, False = ran, did not fire
guardrail.enforcement_modeoptionalblock, warn, log_only, unknown
guardrail.policy_idoptionalPolicy version reference for audit trail

Tier B — Provider-native detection (automatic)

No annotation needed. These signals are captured automatically by the LLM instrumentation patches and framework callbacks, then wired to the detector.

ProviderSignalSpan attributeConfidence
OpenAIfinish_reason = "content_filter"llm.finish_reasonhigh
Azure OpenAIfinish_reason = "content_filtered"llm.finish_reasonhigh
Google GenAIfinish_reason = "SAFETY"llm.finish_reasonhigh
Anthropicstop_reason = "content_filter" (requires llm.vendor anthropic or claude)llm.stop_reason + llm.vendorhigh
AnthropicPolicy phrases in error.message (vendor + LLM span signals required)error.message + llm.vendor + llm.model/…medium
Google / LangChainblocked: true or probability: HIGH in safety ratingsllm.safety_ratings (JSON)medium
Any providerResponse incomplete + refusal text in completionllm.response.status + llm.completionmedium

What you need to do

For OpenAI, Anthropic, and the LangChain callback: nothing. Install traccia with the relevant extra (pip install traccia[langchain]) and call init(). The patches and callbacks write these attributes automatically.

Tier C — Heuristic detection

Disabling Tier C via init()

Heuristics are on by default. Disable without touching processors:

main.py
python
from traccia import init
init(guardrail_heuristics=False)
# Or: TRACCIA_GUARDRAIL_HEURISTICS=false
# Or traccia.toml [instrumentation] guardrail_heuristics = false

Tier A and Tier B are unchanged. If enrichment fails, a warning is logged and traces still export; set logger traccia.guardrails.processor to DEBUG for tracebacks.

When a tool or function span raises an error whose message contains denial-like keywords (permission, denied, unauthorized, forbidden, not allowed), a heuristic tool_permission finding is emitted with low confidence.

Known false-positive error types — excluded automatically

The following error types are excluded from heuristic matching regardless of keywords, because they are infrastructure failures, not policy violations:
TimeoutErrorConnectionErrorOSErrorsocket.timeoutrequests.exceptions.Timeoutrequests.exceptions.ConnectionErrorhttpx.TimeoutExceptionhttpx.ConnectError

Even when a Tier C finding is emitted, it does not count as coverage. The tool_permission category will still appear in missing_categories. This is intentional: "we saw something that might be a guardrail" is not the same as "we have a guardrail."

Missing guardrail evaluator

When the root span ends, the evaluator inspects all spans in the trace, infers the agent's capabilities, and reports which guardrail categories are expected but not detected.

Capability inference rules

CapabilityEvidence in trace
calls_llmAny span with llm.model
handles_user_textAny span with llm.prompt (may be internal — see note below)
produces_user_textAny span with llm.completion
uses_toolsAny span with span.type=tool or agent.tool.name
has_external_actionsTool spans or handoff spans

Required-guardrail matrix

Observed capabilityExpected guardrail categoriesMissing confidence
calls_llm + handles_user_textinput_validation, prompt_injectionmedium
produces_user_textoutput_validation, moderationmedium
uses_toolstool_permissionhigh

Why medium for input_validation/prompt_injection? llm.prompt is present on any LLM call, including batch pipelines over internal documents. We cannot confirm from the trace alone that the prompt is user-provided. Use suppression (below) to opt out for internal agents.

Suppressing false positive missing warnings

If your agent is a batch pipeline or internal-only service where certain guardrail categories genuinely do not apply, set the suppression attribute on any span in the run:

batch_pipeline.py
python
from traccia.guardrails import guardrail_span, ATTR_GUARDRAIL_SUPPRESS_MISSING
from traccia import span
# Option 1: convenience parameter on guardrail_span
with guardrail_span(
"pipeline_root",
category="unknown",
suppress_missing=["prompt_injection", "input_validation"],
):
run_batch_pipeline(documents)
# Option 2: set directly on any span
with span("pipeline_root") as s:
s.set_attribute(
ATTR_GUARDRAIL_SUPPRESS_MISSING,
["prompt_injection", "input_validation"],
)
run_batch_pipeline(documents)

What suppression does and does not do

Suppression removes the specified categories from missing_categories in the summary. It does not affect detected findings or triggered categories. If a finding was actually detected for a suppressed category, it still appears in the trace.

Valid suppression values (GuardrailCategory strings):

input_validationprompt_injectionpiimoderationtool_permissionoutput_validationrate_limitunknown

Output — what appears in traces

All guardrail data is written as span attributes and flows through every configured exporter (OTLP, file, console). No separate API call or storage mechanism is used.

Per span (with guardrail signal)

guardrail.finding.count(int)Number of findings on this span
guardrail.findings(JSON string)Array of GuardrailFinding objects

Root span (aggregated summary)

guardrail.summary(JSON string)Full GuardrailSummary
guardrail.summary.coverage_confidence(string)high / medium / low
guardrail.summary.missing_count(int)Number of missing categories
guardrail.summary.detected_categories(string)Comma-separated categories
guardrail.findings(JSON string)All findings from the run
guardrail.finding.count(int)Total findings in the run

Example GuardrailSummary (root span attribute)

guardrail.summary (root span attribute)
json
{
"detected_categories": ["pii", "moderation"],
"triggered_categories": ["pii"],
"missing_categories": [
{
"category": "input_validation",
"why_required": "Agent makes LLM calls with prompt data (may be user-provided)",
"missing_confidence": "medium",
"evidence_ref": {"attribute_keys": ["calls_llm", "handles_user_text"]}
},
{
"category": "tool_permission",
"why_required": "Agent uses tool/function calls",
"missing_confidence": "high",
"evidence_ref": {"attribute_keys": ["uses_tools"]}
}
],
"coverage_confidence": "high",
"capabilities_observed": ["calls_llm", "handles_user_text", "produces_user_text", "uses_tools"],
"limitations": [
"2 expected guardrail categories not detected in trace."
]
}

What coverage_confidence means

coverage_confidence measures signal quality, not protection breadth. An agent with a single explicit high-confidence guardrail and 3 missing categories will show high coverage confidence. It means what we detected, we detected reliably — but the missing categories indicate the agent may be under-protected.

ValueMeaning
highAll detected findings are explicit or provider-native HIGH confidence.
mediumMix of HIGH and MEDIUM confidence findings.
lowNo findings detected, or all findings are heuristic-only (LOW). Out-of-band guardrails are not visible to tracing.

Hard limits — what cannot be captured

These are fundamental limits of trace-based inference, not implementation gaps. They will not be closed with more heuristics.

Out-of-band guardrails are invisible

API gateways, proxy-level filters, and external validators that run outside the traced process cannot be detected unless they write span attributes into the trace.

Presence ≠ correctness

A guardrail can exist and be misconfigured. Traces show observed outcomes, not policy correctness. A pii_check that never triggers because it always returns False looks identical to a well-functioning one from the trace perspective.

Absence ≠ missing guardrail

guardrail.triggered = False means the guardrail ran and did not fire. No guardrail span at all means either no guardrail exists, or it exists but is out-of-band. The missing-guardrail evaluator always calls this out in limitations.

llm.prompt does not prove user input

Any LLM call sets llm.prompt — including batch pipelines over internal documents. handles_user_text is always MEDIUM confidence because of this. Use suppression to opt out for known internal-only agents.

Prompt injection cannot be inferred from output alone

You cannot tell from the model's response whether a prompt injection check ran. Only an explicit guardrail span proves the check was executed.

Heuristic keywords miss unusual phrasing

Tier C fires on specific denial keywords. A real tool denial with an unusual error message will be missed. An explicit guardrail_span wrapping the tool call is the only reliable alternative.

Complete example: multi-guardrail agent

agent_with_guardrails.py
python
import traccia
from traccia import observe, span
from traccia.guardrails import guardrail_span, ATTR_GUARDRAIL_SUPPRESS_MISSING
from openai import OpenAI
traccia.init(agent_id="content-agent", agent_name="Content Agent", env="production")
INJECTION_KEYWORDS = ["ignore previous", "jailbreak", "act as", "dan mode"]
# --- Tier A: explicit guardrail via decorator (auto-triggered from bool return) ---
@observe(
as_type="guardrail",
attributes={
"guardrail.name": "prompt_injection_check",
"guardrail.category": "prompt_injection",
"guardrail.enforcement_mode": "block",
},
)
def check_injection(text: str) -> bool:
return any(kw in text.lower() for kw in INJECTION_KEYWORDS)
def run(user_input: str) -> str:
with span("agent.run") as root:
root.set_attribute("agent.id", "content-agent")
# Tier A: prompt injection check (bool return sets guardrail.triggered)
if check_injection(user_input):
return "Request blocked: injection attempt detected."
# Tier A: PII check via context manager
with guardrail_span("pii_check", category="pii", enforcement_mode="warn") as gs:
has_pii = scan_for_pii(user_input)
gs.set_attribute("guardrail.triggered", has_pii)
if has_pii:
from traccia.processors.redaction_processor import redact_string
user_input = redact_string(user_input)
# Tier A: input length rate limit
with guardrail_span("input_length", category="rate_limit", enforcement_mode="block") as gs:
too_long = len(user_input) > 4000
gs.set_attribute("guardrail.triggered", too_long)
if too_long:
return "Input too long. Please shorten your request."
# LLM call — Tier B signals (content_filter etc.) captured automatically
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": user_input}],
)
return response.choices[0].message.content or ""
# The root span will contain:
# guardrail.summary.detected_categories: ["prompt_injection", "pii", "rate_limit"]
# guardrail.summary.triggered_categories: (whichever fired)
# guardrail.summary.missing_categories: [output_validation, moderation] (if not added)
# guardrail.summary.coverage_confidence: "high" (all explicit)

OpenAI Agents SDK — automatic guardrail capture

Python Only
The OpenAI Agents SDK has a first-class concept of input/output guardrails. Traccia captures these automatically as Tier A explicit findings when the OpenAI Agents integration is active — no annotation needed.

openai_agents_guardrails.py
python
from traccia import init
from agents import Agent, Runner, input_guardrail, GuardrailFunctionOutput
init() # Automatically captures guardrail spans from the Agents SDK
@input_guardrail
async def safety_check(ctx, agent, input):
# ... your check logic ...
return GuardrailFunctionOutput(output_info=info, tripwire_triggered=is_unsafe)
agent = Agent(
name="Safe Agent",
instructions="...",
input_guardrails=[safety_check],
)
# Traccia captures: agent.span.type=guardrail, agent.guardrail.name, agent.guardrail.triggered
# These become Tier A explicit findings automatically.

In the Traccia dashboard

Trace and agent views surface this data as Guardrail Posture (coverage, findings, missing categories, and optional curation). See the platform guide: Guardrail Posture (platform).

© 2026 Traccia.