Introduce an agent.threat.detection.* attribute namespace for recording the outcome of runtime threat-detection evaluations on AI agent spans, in a ruleset-agnostic and vendor-neutral way.
AI agent runtimes already emit spans for operations such as invoke_agent, execute_tool, and model calls under the gen_ai.* semantic conventions. Around those operations, a growing set of runtime security layers evaluates each step: guardrails, tool-call inspectors, MCP-aware proxies, embedded scanners, and policy engines. When one of those layers fires on a span, there is currently no agreed convention for recording which rule matched, which ruleset issued it, how severe the finding is, or what the enforcing layer did.
Today, vendors write ad-hoc attributes such as guardrail.rule, security.flag, or proprietary vendor.x.* keys. Dashboards, alerting pipelines, and SIEM ingestion break across products. Operators cannot answer basic cross-vendor questions like "how many high-severity blocks fired on execute_tool spans this week, across all detection layers?". The gap was raised on semantic-conventions-genai#132, where maintainers and adjacent agentic-observability authors suggested escalating to an OTEP and recommended an agent.* rather than gen_ai.* namespace because detection layers are agent-aware but not always GenAI-API-aware. Related issues #94 (agent workflow grouping), #95 (MCP tool approval), and #89 (agent failure repair) all touch the same surface area: agent-runtime decisions need a stable schema, and the detection slice of that surface is well-scoped enough to standardize first.
This proposal does not standardize any ruleset. It standardizes how any ruleset, open or proprietary, reports its evaluations on an agent span.
A detection layer that evaluates an agent operation sets a small group of attributes on the span (or on a span event, depending on whether the evaluation is the operation itself or a side observation). The shape is the same in both cases.
span.name = execute_tool
gen_ai.operation.name = execute_tool
gen_ai.tool.name = read_url
agent.threat.detection.rule_id = "ATR-2026-00081"
agent.threat.detection.ruleset = "atr@v2.1.3"
agent.threat.detection.severity = "high"
agent.threat.detection.action = "block"
agent.threat.detection.correlation_id = "9c7e0a2f" # optional, opaque
A backend can now answer:
- How many spans with
gen_ai.operation.name = execute_toolwere blocked by any detection layer in the last 24 hours? - Which
rule_idvalues fire most often under whichruleset? - What is the severity distribution of detections, sliced by tenant or by agent framework?
Because the attributes live in the agent.threat.detection.* namespace and not under gen_ai.*, they are composable with the existing GenAI conventions without overlap, and they can also be applied to non-GenAI agent operations (for example MCP server spans that do not invoke an LLM at all).
| Attribute | Type | Requirement | Description |
|---|---|---|---|
agent.threat.detection.rule_id |
string | Conditionally required when a rule matched | Stable identifier of the matched rule, scoped by ruleset. Example: ATR-2026-00081, guardrail-pii-7. |
agent.threat.detection.ruleset |
string | Conditionally required when rule_id is set |
Namespace plus version of the ruleset. Example: atr@v2.1.3, vendor-x@2026.04. |
agent.threat.detection.severity |
string enum | Conditionally required when rule_id is set |
One of low, medium, high, critical. Aligned with common security severity scales. |
agent.threat.detection.action |
string enum | Required when a detection layer evaluated the operation | One of allow, block, warn, ask. Records the enforcing decision. ask covers human-in-the-loop approval flows (see issue #95). |
agent.threat.detection.correlation_id |
string | Optional | Opaque identifier connecting this detection to an upstream evaluation, a related repair span, or a SIEM ticket. Treated as opaque by OpenTelemetry. |
Cardinality is bounded by ruleset size, which is operationally small for production rulesets (today on the order of 10^2 to 10^3). The correlation_id is opaque and is not intended for high-cardinality joins inside a tracing backend.
Two patterns are valid:
- On the existing operation span. A guardrail wrapped around
execute_toolsets the attributes directly on theexecute_toolspan. This is the common case when the detection layer is in-process and the operation does not duplicate. - On a span event or a dedicated detection span. When the detection is asynchronous (for example, an out-of-band scanner that re-reads a tool output) or when multiple rules fire on one operation, each detection is a span event under the operation span, or a child span with the same attributes plus
correlation_idlinking to the parent operation.
Both shapes use the same attribute names. Backends do not need separate ingestion paths.
- No overlap with
gen_ai.*. Agen_ai.operation.name = execute_toolspan keeps all its existing attributes; the threat detection attributes are additive. - No overlap with
error.*. A detection is not an exception. A blocked operation may or may not also seterror.type, at the discretion of the framework. - Composes with
gen_ai.group.*from issue #94 if that proposal is accepted: detections inside a grouped iteration retain their group attributes. - Composes with the repair conventions sketched in issue #89:
correlation_idis the natural join key from a detection span to a downstreamgen_ai.repair.*span.
- A ruleset that mutates rule IDs between versions can confuse aggregation. The
rulesetattribute carrying an explicit version (atr@v2.1.3) is the mitigation. - A detection layer that reports
action = blockbut does not actually block (for example, a shadow-mode evaluator) misleads operators. The recommendation is thatactionreflects the enforcing decision actually applied; shadow-mode evaluations should useaction = allowand rely onrule_idplusseverityto communicate findings.
- Namespace placement. The original issue proposed
gen_ai.threat.detection.*. Reviewer feedback on issue #132 pointed out that some detection layers are agent-aware but not strictly GenAI-API-aware (for example, MCP firewalls that intercept tool calls before any LLM is involved). The proposedagent.*top-level is a small expansion of the conventional namespace surface but is the more accurate home. Mitigation: scopeagent.*strictly to attributes that apply to agent operations regardless of GenAI API surface. - Action enum granularity. Four values cover the common cases (allow, block, warn, ask). A finer-grained enum (for example,
redacted,rewritten) would be more expressive but risks fragmentation across vendors. Mitigation: keep the enum minimal and letrule_idplus rule documentation carry the finer-grained semantics. - Ruleset coupling. Standardizing the attribute shape does not standardize the rulesets themselves. Different rulesets may produce inconsistent severities or overlapping rule IDs. Mitigation:
rulesetis required alongsiderule_id; operators decide how to weigh different sources.
- Ad-hoc vendor attributes. Today, products emit attributes such as
guardrail.rule,security.policy.id, orvendor.x.detection.*. These are not interoperable. error.*repurposing. A detection could be expressed as an error. This loses severity, conflates failures with policy decisions, and breaks dashboards that already treaterror.*as exceptions.- CloudEvents extension. A separate event schema could carry the same fields. Putting them on the operation span keeps the join with
gen_ai.*natural and avoids a second pipeline. gen_ai.threat.*namespace. Discussed in issue #132. Rejected by reviewer feedback because it implies a GenAI API boundary that the detection layer does not always sit behind.
- Should
severityalign directly with CVSS qualitative bands, or remain abstract? Aligning would help SIEM integration; staying abstract leaves room for non-CVE-style rulesets. - Should
action = warnandaction = askbe separate, or merged? They model different operator intents (passive warning vs. active human approval) and the working argument is to keep them separate to support issue #95. - Is
correlation_idbetter expressed as a span link rather than a string attribute?
- Agent Threat Rules (ATR), an MIT-licensed open ruleset at Agent-Threat-Rule/agent-threat-rules, already issues stable
rule_idvalues under theATR-YYYY-NNNNNscheme and ships a CLI scanner. It can populate these attributes today; any other ruleset (proprietary or open) can do the same. - Production deployments referenced in issue #132 include rule packs shipped into microsoft/agent-governance-toolkit#908, cisco-ai-defense/skill-scanner#79, the MISP taxonomy #323, and the OWASP regression harness #74. These are listed as evidence that detection layers with stable rule IDs are already operating in the field; the OTEP does not depend on any of them and works equally well for a vendor whose rule IDs are entirely internal.
- A companion convention for
agent.policy.*covering policy-engine decisions that are not framed as threat detection (for example, rate limits or quota enforcement). - Alignment with the
gen_ai.repair.*direction in issue #89 so that a detection on one span and its remediation on another span are joinable throughcorrelation_id. - A small registry of well-known
rulesetvalues, similar to the existing OTel registry of known instrumentation libraries, to help backends render names consistently without requiring those rulesets to be part of the OpenTelemetry project.